Тема
Directional/Harvester Test Coverage Architecture
ИСТОРИЧЕСКИЙ ДОКУМЕНТ. Сохранён как снимок прежней тестовой архитектуры; replay, runtime simulation и standalone capture из него больше не являются действующими поверхностями. Текущие контракты находятся в
openspec/specs/, а трассировка реальных ордеров остаётся частью live-телеметрии.
pm-engine cutover (2026-07-22): the directional/Harvester strategy now lives in
crates/pm-engine/src/strategies/directional/(pm-bot was removed). Code anchors below of the formcrates/pm-bot/…:NNNare historical — they reference the pre-migration layout and are kept as-is rather than blindly remapped (line-exact anchors do not survive the hexagonal re-layout). The test taxonomy here remains valid; for the current engine test map see test-coverage-matrix.md anddocs/tests/components-status.md(referenced throughout this historical document; no longer present in the repository — not replaced by an equivalent path).
Status: living reference. Initial draft 2026-05-13 (Phase G2 of the directional test coverage plan; first revision post-L-C cleanup).
Companion documents:
- polymarket-data-model.md — production data cardinality (drives §5 edge-case dimensions).
- ../target/extension-points.md —
ScannerHookstrait surface +ReadCtxpattern (drives §6 harness design; content moved here fromstrategy-extension-points.mdby commit803a320f). strategy-pipeline.md— global pipeline order (drove §3 hook-by-hook test placement); removed entirely by commit803a320fas stale legacy multi-strategy content, no replacement exists.trading-mechanics.md— shared mechanics (post-L-C, no stop-loss subsystem); removed entirely by commit803a320ffor asserting the retired "4 active strategies" model, no replacement exists.ADR-024(directional-first architecture) — strategic context, L-C commit hashes, emergency-stop migration.ADR-005(Local-Bot / dashboard integration) — Local-Bot API contract for the emergency-stop endpoint.- удалённый
../operations/capture-runbook.md— исторический runbook записи потока рынков и ADR-009; вся поверхность снята 2026-08-22. - ../strategies/directional-harvester.md — unified PRD+FSD (canonical rule source; объединяет ранее раздельные
directional-prd.mdиdirectional.mdper SSOT-конвергенция I-4 2026-05-20). - Memory
project_harvester_semantics.md— authoritative Harvester spec (10 invariants, synced 2026-05-14 to live VPS truth; stop-loss removed per ADR-024). - Audit reports (scratch workspace, not part of the published site or the repository — cleared after the task per project policy; kept here only as a filename record, not a working link):
.tmp/audit-reports/I-directional-rule-map.md,J-directional-pricing-knobs.md,K-knob-liveness.md,L-knob-verification.md,M-stop-loss-removal-scope.md.
This document is the single source of truth for how directional/Harvester is tested. It does NOT enumerate test bodies; it enumerates categories, acceptance contracts, fixture strategy, batch order. G3 seeds fixtures against §5; G4 implements tests against §6 in batched commits.
1. Goals & Non-Goals
1.1 Goals
- Spec-driven coverage — every test traces to a rule-ID in PRD/FSD/memory or to an invariant in
docs/architecture/polymarket-data-model.md§3/§5. Memoryfeedback_spec_driven_tests. - Production-grade confidence — every Harvester semantic invariant from memory
project_harvester_semantics.mdhas at least one integration test exercising the full pipeline (MarketCache→evaluate_*→Decision). - Детерминизм решений — поток решений directional байт-стабилен между прогонами. Прежний носитель этого утверждения — бит-паритет реплея — снят 2026-08-21 вместе с бэктестом; гарантию держит
tests/it/directional_decision_trace_golden.rs. - Liveness observability — every ADR-feature surfaces a tracing event on its observable boundary, asserted in a
tracing::Subscriber-based test. Memoryfeedback_silent_feature_broken. - Single coverage matrix —
docs/tests/components-status.mdrows for componentC-02 Directionalratify each rule-ID with method + artefact.
1.2 Non-goals
- Line coverage as a primary metric — coverage is measured by rule-ID ratification, not lines.
- Performance benchmarks with hard gates (this phase) — G-Q4 defers threshold enforcement. Bench scaffolding is designed in §3.3 so it can be activated later without test-file restructuring.
- Property-based tests beyond invariants already encoded in
validate_directional_invariantsatcrates/pm-bot/src/domain/strategies/directional.rs:567— property testing is in scope for §3.5 edge-case dimensions only if a targeted fixture cannot reasonably enumerate the space. - Tests for archived strategies (
maker,maker-rebate,merge-arb,bucket-arb,single-token-mm). Restoration tracked in backlog items 145..149. - Re-introducing stop-loss tests — stop-loss removed per ADR-024 L-C1..C5 (commits 66e9569..940fad3). Any future stop-loss work is a new strategy, not a directional regression.
2. Coverage philosophy
2.1 Five rules
- Rules drive tests, never the reverse. A test that has no PRD/FSD/memory line behind it must either (a) get a rule-ID added to the relevant spec document or (b) be deleted. Memory
feedback_spec_driven_tests. - Real-fixture preference. Synthetic stubs are acceptable only when:
- The behaviour is purely arithmetic (
compute_sweep,round_to_tick,compute_entry_price). - Real-API replay would inflate the fixture size beyond ~10 KiB without incremental coverage gain. Otherwise: Gamma JSON + CLOB orderbook frames captured via
curl + jqfrom production endpoints (G-Q3 (a)). Memoryfeedback_real_data_tests.
- The behaviour is purely arithmetic (
- Acceptance = ratified rule-ID + observable artefact. Each
провереноrow indocs/tests/components-status.mdMUST cite (rule-ID, method, artefact: test path orVERIFIED:marker). - Harvester semantics is authoritative. When the unified PRD+FSD profile table at
docs/strategies/directional-harvester.md(Current Behavior §Жизненный цикл) and the user-authoritative spec in memoryproject_harvester_semantics.mddisagree (e.g. legacyHarvesterStrategy = sweep=true, exit=Nonevs. user specpost_only=true, exit=0.99), the memory wins. The legacy FSD profile is a historical artefact pre-ADR-024. - Liveness is non-optional. Memory
feedback_silent_feature_brokenforbids ADR-features whose observable boundary is unasserted. Each hook that can emit aDecision::*MUST have one liveness test exercising the tracing event.
2.2 Definition of Done for a rule-ID
A rule-ID is проверено when all four hold:
- Test exists at a deterministic path (
crates/pm-bot/tests/directional/<file>.rsor inlinemod testsincrates/pm-bot/src/domain/strategies/directional.rs). - Test asserts the rule's invariant against a fixture that cites a production source (Gamma URL + timestamp OR generated by capture).
- Components-status row updated: rule-ID + method + artefact path.
VERIFIED: <rule-id> @ YYYY-MM-DD by <method>marker at the implementation site (per components-status §0.4 grep contract).
3. Test categories (six, per G-Q1)
3.1 Functional correctness
Scope. Every rule-ID extracted from directional-harvester.md (unified PRD+FSD) and (post-L-C) the surviving subset of strategy.md collector rules that directly bear on directional admission / decision emission.
Test type. Integration preferred (full ScannerHooks invocation against a fixture-driven MarketCache). Unit acceptable as a fallback for pure functions:
compute_sweepatdirectional.rs:1069.compute_entry_priceatdirectional.rs:1164.round_to_tickatdirectional.rs:1171.validate_directional_invariantsatdirectional.rs:567.
Fixtures. Real Gamma JSON (§5.1 (1)–(4)) + real CLOB orderbook snapshot
- optional
WsBookDeltasequence for hooks that observe in-flight book updates (on_orderbook_updateatdirectional.rs:3920).
Acceptance. Rule-ID ratified проверено in components-status with artefact path.
Refresh sites post-L-C. All rule-IDs in the R-DIR-* and R-INV-* families from audit report I §2 stand. The following lines from I §2 are retired with L-C and MUST NOT be reinstated:
R-DIR-017(stop-loss emit) — removed.R-HARVESTER-STOP-LOSS— removed.R-INV-STOP-LT-ENTRY— removed (invariant block deleted fromvalidate_directional_invariants).
3.2 Harvester semantic invariants
Scope. Each of the ten invariants in memory project_harvester_semantics.md (synced 2026-05-14 to live VPS truth + user verbal directive 2026-05-13):
| # | Invariant | Source |
|---|---|---|
| H1 | Entry trigger: best_ask ∈ [0.95, 0.98] — limit BUY emitted when in band. Within band, cheapest-first preference per OpenSpec capability directional-entry-buffer (ratified 2026-05-14, openspec/specs/directional-entry-buffer/spec.md): per-tick warmup buffer + stable sort by best_ask ascending + greedy budget/concurrent-slot bounded emit; scope strictly periodic-discovery. Evidence: components-status §C-02.A.B. | memory §1 + OpenSpec directional-entry-buffer |
| H2 | Entry price source: ENTRY_PRICE_BASE=best-ask, OFFSET_CENTS=0 — place at best_ask без offset | memory §2 |
| H3 | Max fill willingness: 0.98 (DIRECTIONAL_MAX_ENTRY_PRICE=0.98, BOT_MAX_PRICE_CENTS=98) | memory §3 |
| H4 | post_only=false — orders accepted by CLOB, не rejected | memory §4 |
| H5 | Exit: limit GTC SELL @ 0.99, placed on first fill (passive sit в book) | memory §5 |
| H6 | DCA enabled; on each fill emit additional buy within BUDGET_PER_MARKET=$5.0 + MIN_SIZE_SHARES=5 caps; recompute exit on total accumulated size (RECOMPUTE_EXIT_ON_FILL=true). Overshoot semantics (OQ-2 closure 2026-05-14): DCA size = (remaining_budget / target_buy_price).floor() at directional.rs:4095-4096; .floor() prevents size overshoot above remaining_budget. Sub-tick fractional residuals silently dropped (no roll-into-exit). Micro-edge test deferred — see backlog #152. | memory §6 |
| H7 | Pending order TTL = 600 s (PENDING_TTL_SECS=600) | memory §7 |
| H8 | Auto-redeem hybrid — WS-event-driven primary via on_market_resolved + 10-min periodic backstop in evaluate_periodic (const REDEEM_BACKSTOP_SECS=600); idempotent by position-removal contract; neg_risk=true conservative default in backstop | memory §8 |
| H9 | No automated stop-loss; only operator emergency-stop via POST /api/local/emergency-stop (ADR-024 L-C; ADR-005 L-C8) | memory §9 |
| H10 | Market filter operationally configured via MARKET_TAG_FILTER (current live: tag 103040 weather); strategy functional на любом binary market | memory §10 |
Test type. Integration on a full Harvester preset config (see §5.1 (7)) replayed through scanner hooks. Each invariant gets one dedicated test asserting an observable: an emitted Decision shape, a tracing event, or absence thereof (e.g. H9 requires that nothing automatic fires sell @ <0.99 when book drops).
Acceptance. Invariant assertion ratified проверено. For H9, the acceptance is a negative test: drive a synthetic book collapse fixture through the Harvester preset and assert that no Decision::PlaceOrder with side=SELL is emitted unless price >= exit_price. For H4 (post_only=false), assert that emitted Decision::PlaceOrder does NOT carry post_only=true flag regardless of orderbook state. For H8 (auto-redeem cadence) the OQ-1 resolution (2026-05-14) is hybrid: the WS-event-driven primary path remains via Strategy::on_market_resolved and a 600 s periodic backstop in evaluate_periodic catches WS-missed resolutions. Three sub-tests in crates/pm-bot/tests/directional_harvester_invariants.rs (h8_redeem_ws_primary_path_emits_and_clears, h8_redeem_periodic_backstop_catches_ws_missed, h8_redeem_idempotent_ws_then_backstop) ratify the contract.
3.3 Latency-sensitive (informational, no thresholds per G-Q4)
Scope. Hot-loop functions whose timing affects live-trading PnL but whose thresholds are user-deferred:
| Function | Site | Bench file (G4 batch G) |
|---|---|---|
on_orderbook_update | directional.rs:3920 | benches/on_orderbook_update.rs |
evaluate_signal | directional.rs:1305 | benches/evaluate_signal.rs |
evaluate_periodic | directional.rs:4321 | benches/evaluate_periodic.rs |
rtds_filter_set (pm-bot-era name; the FilterSet mechanism it benched was removed from pm_sdk::rtds by rtds-transport-remediation, 2026-09 — no current equivalent) | directional.rs:4943 | benches/rtds_filter_set.rs |
parse_market_with_content_hash | discovery layer (BUG-04 closed) | already exists |
Test type. criterion bench. Structure compiles and reports under cargo bench -p pm-bot --no-run in debug; release build only required when threshold gates are activated (out-of-scope per G-Q4).
Acceptance. Bench file compiles + cargo bench --no-run succeeds. No hard gate.
Note. Pre-existing latency reference: audit report H (directional perf baseline) — captures the pre-L-C numbers; post-L-C re-measurement is a follow-up after G4 batch G lands. Not a blocker for components-status ratification.
3.4 Replay-determinism
Scope. Directional-specific replay invariants on top of the generic R-BT-LA-NO-FUTURE-METADATA and SHA-256 parity contracts already enforced by tests/replay_strict_temporal_order.rs and tests/capture_replay_parity.rs.
Cross-cutting scope clarification (OQ-6 closure 2026-05-14): R-OBS-* (observability, docs/tests/strategy.md §17) and R-ERR-* (error handling, §19) rules are infrastructure-level / cross-cutting respectively and are orthogonal to the directional test plan. They belong to their respective component rows in docs/tests/components-status.md (e.g. C-01 WS, C-03 DB), not to C-02 Directional. Only R-BT-LA-NO-FUTURE-METADATA from strategy.md is in scope here.
Additional invariants:
- R-REPLAY-DECISION-SEQ (new): same input frames → byte-identical
Decisionstream (not just frame hash).capture_replay_parity.rstoday asserts hash of raw capture output, not the strategy's emitted decisions. A directional drift would not be caught. - R-REPLAY-PERIODIC-COUNT: replay of a session of duration T with
BOT_FALLBACK_SCAN_SECS = Sfires exactlyfloor(T/S)periodic ticks (zero-interval handled separately byreplay_periodic_interval_zero_means_every_frame.rs). - R-REPLAY-PRESET-IDEMPOTENT: replaying the same capture under the same operator-config preset twice produces identical decisions (extends
replay_uses_operator_config_not_default.rswhich currently only asserts the config path, not decision equality).
Test type. Integration. Run pm_analytics::replay::run_with_params as a library twice on the same capture, diff decision vectors.
Acceptance. Byte-equivalent decision stream across two replays.
3.5 Edge cases
Scope. Production data-cardinality dimensions (docs/architecture/polymarket-data-model.md §3 + §5). Directional must handle each correctly per its rule (admit or skip) without panic.
| Dimension | Production share | Required behaviour | Rule-ID |
|---|---|---|---|
tick=0.001 markets | 25 % | Quantise entry price to 0.001 step | R-EDGE-TICK-001 |
| Over/Under labels | 8 % | Strategy treats outcomes positionally; label opaque | R-EDGE-LABEL-OVERUNDER |
| Team-vs-team labels | 8 % | Same positional contract | R-EDGE-LABEL-TEAM |
| Odd/Even labels | 2 % | Same positional contract | R-EDGE-LABEL-ODDEVEN |
neg_risk=true markets | 63.5 % | Filtered upstream by MarketCache::MarketTypeFilter::Standard; directional MUST NOT receive them. Liveness gap: assert filter applied | R-EDGE-NEGRISK-001 |
Reversed outcome order ["No","Yes"] | 0 % observed in 1 000-sample | Positional contract still holds; invariant unchecked | R-EDGE-REVERSE-OUTCOME |
Empty clobTokenIds (closed markets) | n/a | Discovery layer rejects (covered by closed test | |
gamma_parse_market_rejection_logging); directional never sees them | R-EDGE-EMPTY-TOKENS | ||
| Empty orderbook (no asks) | rare but live | Skip — t_sweep_empty_book_returns_none + t_skip_when_ask_missing already cover | R-EDGE-EMPTY-BOOK |
| 1-token / 3-token "binary" market (defensive) | 0 % observed | market_to_input MUST reject (binary gate) | R-DIR-002 (refined) |
Test type. One fixture per dimension (Gamma JSON + matching CLOB book snapshot where applicable). For R-EDGE-NEGRISK-001 the test is a boundary test: pass a neg_risk=true market into the MarketCache and assert directional's evaluate_periodic never enumerates it.
Acceptance. Per-dimension fixture test exists and passes; rule-ID ratified.
3.6 Liveness boundary signals
Scope. Each ADR-feature observable boundary per memory feedback_silent_feature_broken:
| Signal | Boundary | Test ID |
|---|---|---|
Each Decision::* emission → tracing::info! with strategy="directional" + decision-type + cid | directional.rs:1533, 2687, 3835, 5045-5062 | R-LIVENESS-DECISION |
rtds_filter_set emits non-empty FilterSet for active markets (pm-bot-era mechanism; FilterSet no longer exists in pm_sdk::rtds after rtds-transport-remediation — current admission is TrackedTokens::is_tracked at the pm-engine seam, not a client-side filter) | directional.rs:4943 | R-LIVENESS-RTDS (already covered by rtds_hook_e2e.rs — keep) |
| Periodic scan tick produces an observable event (decision OR explicit no-op log) | directional.rs:4321 (decision path) + evaluate_periodic no-op branch | R-LIVENESS-PERIODIC |
| Auto-redeem emit when resolution event seen for filled position | directional.rs:4261 on_market_resolved | R-LIVENESS-REDEEM (already covered by t_on_market_resolved_emits_redeem_and_clears_position) |
Emergency-stop endpoint sets atomic flag + logs OPERATOR EMERGENCY STOP … line | crates/pm-bot/src/io/local_api/handlers.rs:1270 emergency_stop, mod.rs:467 route registration, mod.rs:158 AtomicBool projection | R-LIVENESS-ESTOP (new post-L-C8/C9) |
| WS-freeze / heartbeat hook is observable from local API metrics tick | local_api/metrics_loop.rs:237 | R-LIVENESS-WS-FREEZE (already covered indirectly) |
Test type. tracing-test (already a dev-dep) Subscriber capture in an integration test. The test asserts that a specific event with structured fields fires when the hook is invoked.
Acceptance. Each event in the table has a corresponding test asserting event presence with at least the field set (strategy, cid where applicable). For R-LIVENESS-ESTOP, the test issues a POST /api/local/emergency-stop against the test server and asserts:
- The AtomicBool at
local_api/mod.rs:158flips. - The tracing line at
handlers.rs:1347fires. - The cancel-all report at
handlers.rs:1326includes the operator string"operator: /api/local/emergency-stop".
4. Updated rule-ID inventory (post-L-C, 2026-05-13)
4.1 Delta from G1 (audit report I §2)
| Action | Count | Notes |
|---|---|---|
| Retired (stop-loss removal) | 3 | R-DIR-017, R-HARVESTER-STOP-LOSS, R-INV-STOP-LT-ENTRY |
| Added (emergency-stop migration) | 3 | R-ESTOP-IDEMPOTENT, R-ESTOP-AUDIT-RECORD, R-LIVENESS-ESTOP |
| Added (replay refinement) | 3 | R-REPLAY-DECISION-SEQ, R-REPLAY-PERIODIC-COUNT, R-REPLAY-PRESET-IDEMPOTENT |
| Added (edge-case label expansion) | 2 | R-EDGE-LABEL-TEAM, R-EDGE-LABEL-ODDEVEN |
| Refined | 1 | R-DIR-002 binary gate sharpened |
Net total post-L-C: 43 − 3 + 8 = 48 directional-relevant rule-IDs.
4.2 New rule-IDs
| Rule-ID | Source | Description | Implementation site | Test placement (G4) |
|---|---|---|---|---|
| R-ESTOP-IDEMPOTENT | ADR-005 + L-C8 | Repeated POST /api/local/emergency-stop is idempotent (flag stays set, no double cancel-all crash) | local_api/handlers.rs:1270-1350 | tests/directional/liveness_emergency_stop.rs |
| R-ESTOP-AUDIT-RECORD | ADR-005 + L-C8 | Emergency-stop trigger records timestamp + source string in EmergencyStopRecord at local_api/mod.rs:230 | local_api/mod.rs:221-260 | same file |
| R-LIVENESS-ESTOP | feedback_silent_feature_broken | Endpoint emits tracing event + cancel-all report | handlers.rs:1326-1347 | same file |
| СНЯТО R-REPLAY-DECISION-SEQ | strategy.md §16 (refined) | Two replays of same capture → byte-identical decision vector | application/backtest/engine.rs + directional hooks | tests/directional/replay_decision_sequence_equality.rs |
| СНЯТО R-REPLAY-PERIODIC-COUNT | strategy.md §15 | Replay fires exactly floor(T/S) periodic ticks for session duration T and BOT_FALLBACK_SCAN_SECS=S | directional.rs:4321 + replay loop | tests/directional/replay_periodic_tick_count.rs |
| СНЯТО R-REPLAY-PRESET-IDEMPOTENT | L-track replay confidence | Same capture + same operator-config preset → identical decisions | pm_analytics::replay::run_with_params | extend replay_uses_operator_config_not_default.rs |
| R-EDGE-LABEL-TEAM | data-model §3.2 | Team-vs-team label markets handled positionally | directional.rs:3195 | tests/directional/edge_label_team.rs |
| R-EDGE-LABEL-ODDEVEN | data-model §3.2 | Odd/Even label markets handled positionally | same | tests/directional/edge_label_oddeven.rs |
4.3 Coverage of new and existing rules by already-closed backlog items
The following tests are reusable as ratification artefacts (no rewrite):
| Closed item | File | Ratifies |
|---|---|---|
| #01 | tests/gamma_end_date_parsing.rs | discovery layer end-date parsing (precondition for MAX_HOURS_TO_EXPIRY) |
| #03 | tests/gamma_pagination.rs | discovery pagination (precondition for directional admission) |
| #05 | tests/gamma_parse_market_rejection_logging.rs | R-EDGE-EMPTY-TOKENS (closed-market rejection) |
| BUG-04 | tests/parse_market_source_updated_at.rs | updatedAt propagation (used by staleness checks) |
| #140 (DONE) | tests/replay_strict_temporal_order.rs | R-BT-LA-NO-FUTURE-METADATA (precondition for §3.4 invariants) |
| F4 contract | tests/strategy_extension_contract.rs | ScannerHooks trait surface (precondition for §3.2, §3.6) |
| L-C8/L-C9 | (TBD) | R-ESTOP-* (will be ratified by G4 batch F) |
| #144 | (open) F5 coverage gap — per-rule decomposition tracker; this document is its deliverable |
4.4 Rule-IDs covered today
Per audit report I §5, ~22 rules fully covered + ~12 partially before L-C. Post-L-C, three rule-IDs are retired (above) and eight are new. The post-L-C provisional coverage matrix:
| Status | Count | Comment |
|---|---|---|
| Fully covered (test exists, rule ratified or ratifiable) | ~22 | Re-validate against components-status during G4 |
| Partially covered (test exists but missing real-fixture or liveness assertion) | ~12 | Targets of G4 batch B/C/F |
| Uncovered (no test) | ~14 | Targets of G4 batch C/D/F |
Critical uncovered cluster (per audit I §5 + new rule-IDs):
- R-HARVESTER-* integration as a coherent 7-point unit (H1..H7) — no single end-to-end test exists. G4 batch C primary deliverable.
- R-EDGE-LABEL-* — three label dimensions (Over/Under, team, Odd/Even) have zero tests. G4 batch D.
- R-LIVENESS-DECISION — no test asserts tracing events per Decision. G4 batch F.
- R-EDGE-NEGRISK-001 — boundary unverified at evaluator level.
- R-DIR-002 — 2-token gate untested with synthetic 1- or 3-token input.
- R-ESTOP-* — three new rules from L-C8/L-C9, zero tests today.
- R-REPLAY-DECISION-SEQ — gap explicitly called out in audit I §3.4.
5. Fixture strategy (drives G3)
5.1 Fixture taxonomy
| # | Fixture | Purpose | Acquisition |
|---|---|---|---|
| 1 | gamma/event-binary-standard.json | Single binary market, neg_risk=false, ["Yes","No"] outcomes, tick=0.01 | `curl gamma-api/events?... |
| 2 | gamma/event-binary-negrisk.json | Single binary market, neg_risk=true | same query, filter |
| 3 | gamma/event-overunder.json | ["Over","Under"] outcome labels | `curl ... |
| 4 | gamma/event-team-matchup.json | Team-vs-team labels (e.g. ["Yankees","Red Sox"]) | sports events tag |
| 5 | gamma/event-oddeven.json | Odd/Even labels | targeted Gamma query |
| 6 | gamma/event-tick-0001.json + clob/book-tick-0001.json | min_tick_size=0.001 market with live book | Gamma + CLOB pair |
| 7 | gamma/event-weather-harvester.json + clob/book-weather.json + capture session | Weather event (tag 103040), neg_risk=true, 7–11 child markets; full Harvester preset capture | curl gamma-api/events?tag_id=103040&active=true + pm-bot --dry-run --capture |
| 8 | clob/book-binary-active.json | Generic active book snapshot, used by §3.1 functional fixtures | curl clob/book?token_id=… |
| 9 | clob/book-empty-asks.json | Book with empty asks side | hand-edited from (8) — synthetic acceptable for negative test |
| 10 | capture/harvester-full-cycle/ | Full session: REST Gamma + WsBook deltas + WsUser partial fills + resolution | pm-bot --dry-run --capture --capture-path .tmp/captures/harvester-full-cycle |
| 11 | capture/periodic-tick-determinism/ | Session of known duration T with config BOT_FALLBACK_SCAN_SECS=S | dry-run capture, fixed duration |
| 12 | harvester/preset-canonical.json | Mirror of live harvester profile from config.db (audit J §4) | pm-cli strategy export --label harvester |
Fixtures (1)–(9) are static JSON. Fixtures (10)–(11) are directories of *.jsonl.zstd (удалённый формат pm-bot-эры и его снятого runbook). Fixture (12) is a SQLite-style JSON snapshot of the strategies.definition row.
5.2 Fixture acquisition method (per G-Q3 (a))
# Single-market Gamma fixture
curl -s 'https://gamma-api.polymarket.com/events?tag_id=103040&limit=1&active=true' \
| jq '.[0]' \
> crates/pm-bot/tests/fixtures/gamma/event-weather-harvester.json
# Matched CLOB book
TOKEN=$(jq -r '.markets[0].clobTokenIds[0]' crates/pm-bot/tests/fixtures/gamma/event-weather-harvester.json)
curl -s "https://clob.polymarket.com/book?token_id=${TOKEN}" \
| jq '.' \
> crates/pm-bot/tests/fixtures/clob/book-weather.json
# Capture session
cargo run -p pm-bot --release -- \
--dry-run --capture \
--capture-path .tmp/captures/harvester-full-cycle \
--env env/.env.harvester-test
# After session: cp -r .tmp/captures/harvester-full-cycle/<id>/ \
# crates/pm-bot/tests/fixtures/capture/harvester-full-cycle/No pip, no non-stdlib deps — memory feedback_no_python_deps. jq is acceptable system tooling.
5.3 Storage layout
crates/pm-bot/tests/fixtures/
├── gamma/ # Gamma /events JSON snapshots (§5.1 #1..#5,#7)
│ ├── event-binary-standard.json
│ ├── event-binary-negrisk.json
│ ├── event-overunder.json
│ ├── event-team-matchup.json
│ ├── event-oddeven.json
│ ├── event-tick-0001.json
│ └── event-weather-harvester.json
├── clob/ # CLOB orderbook snapshots (§5.1 #6,#8,#9)
│ ├── book-binary-active.json
│ ├── book-tick-0001.json
│ ├── book-empty-asks.json
│ └── book-weather.json
├── capture/ # Capture sessions (§5.1 #10,#11) + existing frozen-v1/
│ ├── frozen-v1/ # existing (e2e_directional_replay.rs)
│ ├── harvester-full-cycle/
│ └── periodic-tick-determinism/
├── harvester/ # Preset configs (§5.1 #12)
│ └── preset-canonical.json
└── ws_traffic.jsonl # existing (ws_load_test.rs)The gamma/, clob/, capture/ subdirectories already exist; only harvester/ is new.
5.4 Versioning
- Filename date suffix: capture-day fixtures use ISO date in the filename:
event-weather-2026-05-13.json. Sliding-window refreshes drop the prior file in the same commit. Captures that pin a specific market resolution outcome use a fixed slug + date and are not refreshed (e.g.event-weather-2026-05-13-cyyz-resolved-yes.json). - Capture directory
README.md: each capture session directory undercapture/carries a one-pageREADME.mddocumenting:- source command line,
- operator-config preset (
harvester/preset-canonical.jsonat capture time), - market slugs included,
- session duration,
- expected outcome counts (for sanity checks in §3.4 tests).
- Fixture changelog: appendix at the bottom of this document (Change log §10) — one bullet per capture refresh, immutable history.
5.5 Fixture inventory (current as of G3, 2026-05-13)
Acquired from live gamma-api.polymarket.com and clob.polymarket.com on 2026-05-13. Harvester preset copied from local config.db (mirror of VPS harvester-live instance, verified knob-set identical via G1.7 audit). Capture sessions deferred to G4 — see crates/pm-bot/tests/fixtures/capture/README.md for acquisition recipe.
| ID | File | Size | Source / market | Key fields for tests | Notes |
|---|---|---|---|---|---|
| F-G1 | gamma/event-singleton-binary.json | 8 KB | event 23792, slug gta-vi-released-before-june-2026, 1 market, Yes/No | id, markets[0].clobTokenIds, markets[0].outcomes, markets[0].orderPriceMinTickSize=0.001 | Baseline binary; neg_risk field absent (singleton non-neg-risk events omit it) — tests must default-handle None |
| F-G2 | gamma/event-weather-harvester.json | 36 KB | event 128661, slug highest-temperature-in-nyc-on-december-30, tag 103040, 7 buckets, negRiskMarketID set | All markets[*].clobTokenIds, negRiskMarketID, submitted_by, conditionIds | Harvester primary target; multi-bucket neg-risk weather; covers R-HARVESTER-* rule family |
| F-G3 | gamma/market-non-yes-no-labels.json | 6 KB | market will-bitcoin-hit-80k-or-100k-first-272, outcomes ["80k","100k"], tick 0.001, closed | outcomes, clobTokenIds, orderPriceMinTickSize | Label-axis edge case (no Yes/No); from closed-markets pool (no active non-Yes/No markets currently on Polymarket) |
| F-G4 | gamma/market-tick-0001.json | 7 KB | market will-bitcoin-hit-1m-before-gta-vi-872, tick 0.001, Yes/No, neg_risk=false | orderPriceMinTickSize, clobTokenIds | Tick edge case; standalone (not intersecting weather — no tick=0.001 weather markets in current pool) |
| F-C1 | clob/book-binary-active.json | 2 KB | orderbook for F-G1 token, 6 bids × 55 asks, tick 0.001 | bids, asks, tick_size, last_trade_price, neg_risk=false | Wide-book active baseline; covers R-DIR-PRICE-* |
| F-C2 | clob/book-thin-tick-0001.json | 3 KB | orderbook for F-G4 token, 42 bids × 33 asks, tick 0.001 | bids, asks, min_order_size=5 | Thin-book scenario for stop-loss bid-drain and reprice logic |
| F-C3 | clob/book-absent.json | 60 B | 404 response for closed F-G3 token | {"error":"No orderbook exists for the requested token id"} | Negative case for orderbook fetch (closed/illiquid path); tests must handle HTTP 404 |
| F-C4 | clob/prices-history-binary-24h.json | 4 KB | F-G1 token, 1d interval, fidelity 10, 144 points | history[].t, history[].p | Replay reference for price-series tests; not a substitute for capture session but covers single-asset trajectory shape |
| F-H1 | harvester/harvester-canonical-preset.json | 2 KB | local config.db strategy harvester, 50 knobs | All BOT_*, DIRECTIONAL_*, MARKET_* knobs | Source of truth for config_test.rs preset-loading tests; mirror of VPS harvester-live instance |
| F-CAP-1 | (deferred) | — | Harvester dry-run, happy path | SESSION.json, rest_gamma.jsonl.zst, ws_market.jsonl.zst | G4 — acquisition recipe in tests/fixtures/capture/README.md |
| F-CAP-2 | (deferred) | — | Harvester dry-run, stop-loss path | as above | G4 — acquisition recipe in tests/fixtures/capture/README.md |
Acquisition method: live curl against public Polymarket endpoints, no authentication required, no PII or wallet keys in any fixture (grep-verified). Total committed fixture size: ~70 KB (excluding the deferred capture sessions).
Known sample gaps (acceptable for G4 to surface as backlog items if test rules cannot be exercised with current fixtures):
- No
neg_risk=truebinary singleton (current Polymarket pool: all neg-risk events are multi-bucket). - No tick=0.001 weather/neg-risk intersection (pool sampled 500 active markets; no overlap found).
- F-C3 is a 404 error body, not an empty
{bids:[],asks:[]}orderbook — tests for the latter must construct it synthetically (acceptable perfeedback_real_data_tests: edge case from real API behaviour).
6. Test implementation strategy (drives G4)
6.1 Test file organisation
A new directory crates/pm-bot/tests/directional/ contains all post-L-C directional integration tests. Inline unit tests stay in directional.rs::mod tests.
crates/pm-bot/tests/directional/
├── functional/
│ ├── r_dir_002_binary_gate.rs
│ ├── r_dir_014_partial_fill_no_dca.rs
│ └── ... (one per uncovered R-DIR-*)
├── harvester/
│ ├── h1_entry_band.rs
│ ├── h2_exit_at_099.rs
│ ├── h3_dca_recompute.rs
│ ├── h4_realtime_reactivity.rs
│ ├── h6_no_auto_stop_loss.rs # negative test
│ └── h7_tag_filter.rs
├── edge/
│ ├── label_overunder.rs
│ ├── label_team.rs
│ ├── label_oddeven.rs
│ ├── negrisk_boundary.rs
│ ├── reverse_outcome.rs
│ └── tick_0001.rs # mirror of inline unit; extra integration
├── replay/
│ ├── decision_sequence_equality.rs
│ ├── periodic_tick_count.rs
│ └── preset_idempotent.rs
├── liveness/
│ ├── decision_emission.rs
│ ├── periodic_observable.rs
│ └── emergency_stop.rs
└── common/
└── mod.rs # harness helpers (see §6.2)Bench files live separately under crates/pm-bot/benches/ (Cargo convention). G4 batch G adds them.
6.2 Harness / helpers
A test-only helper module crates/pm-bot/tests/directional/common/mod.rs provides:
load_gamma_fixture(name) -> RestGammaResponse— read + parse JSON.load_clob_book(name) -> WsBookSnapshot— same.load_capture(name) -> CaptureSession— open capture directory.load_harvester_preset() -> DirectionalConfig— parseharvester/preset-canonical.json.build_market_cache(filter, gamma_events) -> MarketCache— construct a populated cache.run_replay_collecting_decisions(capture, config) -> Vec<Decision>— callpm_analytics::replay::run_with_paramsand capture emitted decisions via a test-only sink. Used for §3.4.subscribe_tracing() -> TracingHandle—tracing-testsetup with field assertion helpers for §3.6.mock_clob_server() -> wiremock::MockServer— stand-upwiremock(dev-dep) for outbound HTTP calls (already used elsewhere).
The helper module is #[cfg(test)]-only and never published.
6.3 Acceptance criteria mapping (per rule-ID)
| Rule-ID | Test file | Test fn name | Fixture | Assertion type |
|---|---|---|---|---|
| R-DIR-002 | functional/r_dir_002_binary_gate.rs | rejects_one_token_market / rejects_three_token_market | synthetic in-test (1/3 tokens) | functional |
| R-DIR-014 (non-DCA path) | functional/r_dir_014_partial_fill_no_dca.rs | partial_fill_tracks_without_emit | clob/book-binary-active.json | functional |
| H1..H10 | harvester/h{1..10}_*.rs | descriptive | fixture (7) + (10) + (12) | invariant |
| H9 (negative) | harvester/h9_no_auto_stop_loss.rs | book_collapse_does_not_emit_sell | synthetic book-decay sequence | negative |
| H4 (post_only=false) | harvester/h4_post_only_false.rs | decision_carries_post_only_false | fixture (12) | flag assertion |
| H8 (auto-redeem) | harvester/h8_auto_redeem.rs | (deferred pending OQ-1 resolution) | (11) | cadence |
| R-EDGE-LABEL-* | edge/label_*.rs | directional_admits_<label>_positionally | (3)/(4)/(5) | functional |
| R-EDGE-LABEL-TEAM (client_id safety, OQ-5 closure 2026-05-14) | edge/label_team.rs (extension) | team_label_client_id_parses_via_prefix_match | (4) | parsing safety |
client_id safety note (OQ-5 closure 2026-05-14): Outcome labels пасуются verbatim from Gamma и могут содержать spaces / colons / Unicode (team names: «Kansas City Chiefs», «Montréal Canadiens»). client_id builds as directional:entry:<cid>:<outcome> (directional.rs:1502-1504); after_execute uses starts_with prefix-match (directional.rs:4603-4663) — parsing safe regardless of embedded special chars. Unverified: CLOB API server-side charset acceptance for non-ASCII outcome labels — see backlog #151. | R-EDGE-NEGRISK-001 | edge/negrisk_boundary.rs | marketcache_filters_negrisk_before_directional | (2) | boundary | | R-EDGE-REVERSE-OUTCOME | edge/reverse_outcome.rs | reversed_outcome_order_still_positional | synthetic mutated copy of (1) | invariant | | R-REPLAY-DECISION-SEQ | replay/decision_sequence_equality.rs | two_replays_produce_equal_decisions | (10) | byte-equal | | R-REPLAY-PERIODIC-COUNT | replay/periodic_tick_count.rs | replay_fires_floor_t_over_s_periodic_ticks | (11) | count | | R-REPLAY-PRESET-IDEMPOTENT | extend tests/replay_uses_operator_config_not_default.rs | additional fn | existing | byte-equal | | R-LIVENESS-DECISION | liveness/decision_emission.rs | place_order_emits_tracing_event | (1) + (8) | tracing capture | | R-LIVENESS-PERIODIC | liveness/periodic_observable.rs | noop_tick_emits_log | (11) | tracing capture | | R-ESTOP-IDEMPOTENT + R-ESTOP-AUDIT-RECORD + R-LIVENESS-ESTOP | liveness/emergency_stop.rs | three fns | n/a (HTTP) | endpoint + tracing |
6.4 Implementation batches (drives G4 commits)
Each batch is a separate commit. Batch order chosen so that earlier batches unlock later (e.g. harness ships before its consumers).
| Batch | Scope | Depends on | Estimated commits |
|---|---|---|---|
| A | tests/directional/common/mod.rs harness + first fixture acquisition (§5.1 #1 + #8) | — | 1–2 |
| B | Functional gap fills (R-DIR-002, R-DIR-014 non-DCA path, plus refinements caught during G4 reading) | A | 1–2 |
| C | Harvester invariants H1..H10 (integration tests on capture (10) + preset (12); H8 deferred pending OQ-1 resolution) | A + §5.1 (10) seeded | 2–3 |
| D | Edge cases (label_overunder, label_team, label_oddeven, negrisk_boundary, reverse_outcome) | A + §5.1 (2)(3)(4)(5) seeded | 2 |
| E | Replay-determinism (decision_sequence_equality, periodic_tick_count, preset_idempotent) | A + §5.1 (10)(11) seeded | 1–2 |
| F | Liveness (decision_emission, periodic_observable, emergency_stop) | A; emergency_stop depends on L-C8 (already landed) | 1–2 |
| G | Benches (benches/*.rs) — no thresholds | A (harness re-used) | 1 |
Total expected commits in G4: 9–14.
6.5 Components-status update protocol
For each batch:
- Land the test commit(s).
- In the same series (next commit), update
docs/tests/components-status.mdrow forC-02 Directionalwith rule-IDs newly ratified. - Insert
// VERIFIED: <rule-id> @ YYYY-MM-DD by <method>at the implementation site referenced in §6.3. - Append a Change log entry to this document (§10).
Per memory feedback_no_silent_backlog, items that cannot be closed in the batch (e.g. unresolved spec question) must produce a backlog item under docs/tests/backlog/ before the batch is reported done.
7. Coverage acceptance metric
A directional release is test-complete when all hold:
- Every rule-ID enumerated in §4 has status
провереноindocs/tests/components-status.md(rowC-02 Directional). - Every Harvester invariant H1..H10 in §3.2 has at least one ratifying test under
tests/directional/harvester/. - Every liveness signal in §3.6 captured by
tracing-testand asserted. - Every edge-case dimension in §3.5 has at least one fixture test.
- Every bench file in §3.3 compiles under
cargo bench -p pm-bot --no-run(thresholds optional per G-Q4).
The measurable metric is the rule-ID coverage matrix in components-status — not lines or branches.
8. Open questions / UNRESOLVED for G3/G4
| # | Question | Owner of resolution |
|---|---|---|
| 1 | Auto-redeem 10-min cadence (memory project_harvester_semantics implies 10-min poll, but on_market_resolved fires immediately on WS resolution per audit J GAP-2). Is the desired behaviour event-driven (current) or polled (per spec)? Bears on whether R-LIVENESS-REDEEM stays as-is or extends to assert a delay window. | user |
These items are not G2 blockers — they are inputs G3/G4 will resolve via direct user query (memory feedback_questions_not_actions) or by deferring specific tests to a follow-up backlog item per memory feedback_no_silent_backlog.
9. References
ADR-024directional-first architecture- ADR-025 strategy extension points (PROPOSED) — ../target/extension-points.md
- ADR-009 и его
capture-runbook.mdсняты 2026-08-22 вместе с replay и паритетными воротами. ADR-005pm-bot dashboard integration (L-C8/L-C9 emergency-stop endpoint)- polymarket-data-model.md (§3 cardinality, §5 invariants)
- ../target/extension-points.md (F3 + F5 ReadCtx; moved here from
strategy-extension-points.md) strategy-pipeline.md— removed entirely by commit803a320f, no replacement exists.- directional-harvester.md (unified PRD+FSD)
- Memory
project_harvester_semantics.md(10-invariant spec, synced 2026-05-14 to live VPS truth) - Memory
feedback_spec_driven_tests,feedback_real_data_tests,feedback_silent_feature_broken,feedback_no_silent_backlog,feedback_no_python_deps,feedback_questions_not_actions. - Audit reports (scratch workspace, cleared after the task — filename record only, not a working link):
I-directional-rule-map.md,J-directional-pricing-knobs.md,K-knob-liveness.md,L-knob-verification.md,M-stop-loss-removal-scope.md.
10. Change log
- 2026-05-14 (OQ-2..OQ-7 resolution batch) — Closed 5 open questions docs-only + 2 backlog items spawned. OQ-2 (b): DCA overshoot —
.floor()prevents size overshoot, sub-tick residuals dropped, §3.2 H6 row updated + backlog #152 spawned. OQ-3 (c): emergency-stop resume — restart-only by user design 2026-05-14, NO/clear-emergencyendpoint, R-ESTOP-* asserts permanent halt, ADR-024 Consequences + ADR-005 API surface note updated. OQ-4 (e): live probe 412/0 tick=0.001 weather markets — already resolved. OQ-5 (b): client_id parsing safety viastarts_withprefix-match, §6.3 note added + backlog #151 spawned for CLOB charset verification. OQ-6 (b): R-OBS-* / R-ERR-* orthogonal to directional plan, §3.4 clarification added. OQ-7 (e): F5 ReadCtx confirmed stable atdirectional.rs:4938-4941. Memoryproject_harvester_semanticsextended with emergency-stop semantics appendix. Audit source:.tmp/audit-reports/N-oq-resolution.md. - 2026-05-14 (H8 OQ-1 hybrid) — Resolved OQ-1: H8 auto-redeem cadence implemented as hybrid (WS primary via
on_market_resolved- 10-min periodic backstop in
evaluate_periodic, constREDEEM_BACKSTOP_SECS=600atcrates/pm-bot/src/domain/strategies/directional.rs). Idempotency falls out of position-removal contract (both paths drop position after redeem emit; no tombstone needed). Conservativeneg_risk=truedefault in backstop. Three H8 sub-tests re-enabled incrates/pm-bot/tests/directional_harvester_invariants.rs(was a single#[ignore]d placeholder); §3.2 H8 row and acceptance notes updated; memoryproject_harvester_semanticsitem 8 updated to hybrid semantics.
- 10-min periodic backstop in
- 2026-05-14 — Synced H-invariants under authoritative VPS+user spec: fixes pre-existing staleness in §3.2 (post_only was
true→ corrected tofalse; entry band was 0.97–0.98 → corrected to 0.95–0.98); expanded H1..H7 → H1..H10 to cover entry price source (H2), max fill knob (H3), pending TTL (H7), auto-redeem cadence (H8), and operationally-configured market filter (H10). L-C10 (commit7902b61) removed stop-loss item but did not refresh remaining items under user's 2026-05-13 correction — this update closes that orchestrator gap. Sources: user verbal directive 2026-05-13 + live VPSharvesterprofile probe in G1.7 K-knob-liveness audit. §6.3 mapping, §6.4 batch C scope, §7 acceptance criteria all bumped from H1..H7 → H1..H10. - 2026-05-13 (G3) — Fixture seeding. Acquired 9 real-data fixtures via live
gamma-api.polymarket.com+clob.polymarket.comcapture (4 Gamma JSON, 4 CLOB JSON, 1 Harvester preset). Added §5.5 fixture inventory table. Capture sessions F-CAP-1/F-CAP-2 deferred to G4 with acquisition recipe documented atcrates/pm-bot/tests/fixtures/capture/README.md. Known sample gaps recorded (no neg-risk singleton, no tick=0.001 × weather overlap, F-C3 is 404 not empty book) — acceptable perfeedback_real_data_tests. - 2026-05-13 — Initial G2 draft. Built on post-L-C cleaned scope (HEAD
7902b61). Retired stop-loss rule-IDs (R-DIR-017, R-HARVESTER-STOP-LOSS, R-INV-STOP-LT-ENTRY); added emergency-stop rule-IDs (R-ESTOP-IDEMPOTENT, R-ESTOP-AUDIT-RECORD, R-LIVENESS-ESTOP) per L-C8/L-C9; refined replay determinism to three sub-rules; added label-axis edge cases (team, odd/even) per data-model §3.2. Total rule-ID inventory: 48 directional-relevant rules. Six implementation batches A..G enumerated for G4.