Skip to content

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 form crates/pm-bot/…:NNN are 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 and docs/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 — ScannerHooks trait surface + ReadCtx pattern (drives §6 harness design; content moved here from strategy-extension-points.md by commit 803a320f).
  • strategy-pipeline.md — global pipeline order (drove §3 hook-by-hook test placement); removed entirely by commit 803a320f as stale legacy multi-strategy content, no replacement exists.
  • trading-mechanics.md — shared mechanics (post-L-C, no stop-loss subsystem); removed entirely by commit 803a320f for 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.md per 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 ​

  1. 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. Memory feedback_spec_driven_tests.
  2. Production-grade confidence — every Harvester semantic invariant from memory project_harvester_semantics.md has at least one integration test exercising the full pipeline (MarketCache → evaluate_* → Decision).
  3. Детерминизм решений — поток решений directional байт-стабилен между прогонами. Прежний носитель этого утверждения — бит-паритет реплея — снят 2026-08-21 вместе с бэктестом; гарантию держит tests/it/directional_decision_trace_golden.rs.
  4. Liveness observability — every ADR-feature surfaces a tracing event on its observable boundary, asserted in a tracing::Subscriber-based test. Memory feedback_silent_feature_broken.
  5. Single coverage matrix — docs/tests/components-status.md rows for component C-02 Directional ratify each rule-ID with method + artefact.

1.2 Non-goals ​

  1. Line coverage as a primary metric — coverage is measured by rule-ID ratification, not lines.
  2. 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.
  3. Property-based tests beyond invariants already encoded in validate_directional_invariants at crates/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.
  4. Tests for archived strategies (maker, maker-rebate, merge-arb, bucket-arb, single-token-mm). Restoration tracked in backlog items 145..149.
  5. 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 ​

  1. 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.
  2. 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 + jq from production endpoints (G-Q3 (a)). Memory feedback_real_data_tests.
  3. Acceptance = ratified rule-ID + observable artefact. Each проверено row in docs/tests/components-status.md MUST cite (rule-ID, method, artefact: test path or VERIFIED: marker).
  4. 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 memory project_harvester_semantics.md disagree (e.g. legacy HarvesterStrategy = sweep=true, exit=None vs. user spec post_only=true, exit=0.99), the memory wins. The legacy FSD profile is a historical artefact pre-ADR-024.
  5. Liveness is non-optional. Memory feedback_silent_feature_broken forbids ADR-features whose observable boundary is unasserted. Each hook that can emit a Decision::* 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:

  1. Test exists at a deterministic path (crates/pm-bot/tests/directional/<file>.rs or inline mod tests in crates/pm-bot/src/domain/strategies/directional.rs).
  2. Test asserts the rule's invariant against a fixture that cites a production source (Gamma URL + timestamp OR generated by capture).
  3. Components-status row updated: rule-ID + method + artefact path.
  4. 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_sweep at directional.rs:1069.
  • compute_entry_price at directional.rs:1164.
  • round_to_tick at directional.rs:1171.
  • validate_directional_invariants at directional.rs:567.

Fixtures. Real Gamma JSON (§5.1 (1)–(4)) + real CLOB orderbook snapshot

  • optional WsBookDelta sequence for hooks that observe in-flight book updates (on_orderbook_update at directional.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 from validate_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):

#InvariantSource
H1Entry 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
H2Entry price source: ENTRY_PRICE_BASE=best-ask, OFFSET_CENTS=0 — place at best_ask без offsetmemory §2
H3Max fill willingness: 0.98 (DIRECTIONAL_MAX_ENTRY_PRICE=0.98, BOT_MAX_PRICE_CENTS=98)memory §3
H4post_only=false — orders accepted by CLOB, не rejectedmemory §4
H5Exit: limit GTC SELL @ 0.99, placed on first fill (passive sit в book)memory §5
H6DCA 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
H7Pending order TTL = 600 s (PENDING_TTL_SECS=600)memory §7
H8Auto-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 backstopmemory §8
H9No automated stop-loss; only operator emergency-stop via POST /api/local/emergency-stop (ADR-024 L-C; ADR-005 L-C8)memory §9
H10Market filter operationally configured via MARKET_TAG_FILTER (current live: tag 103040 weather); strategy functional на любом binary marketmemory §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:

FunctionSiteBench file (G4 batch G)
on_orderbook_updatedirectional.rs:3920benches/on_orderbook_update.rs
evaluate_signaldirectional.rs:1305benches/evaluate_signal.rs
evaluate_periodicdirectional.rs:4321benches/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:4943benches/rtds_filter_set.rs
parse_market_with_content_hashdiscovery 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:

  1. R-REPLAY-DECISION-SEQ (new): same input frames → byte-identical Decision stream (not just frame hash). capture_replay_parity.rs today asserts hash of raw capture output, not the strategy's emitted decisions. A directional drift would not be caught.
  2. R-REPLAY-PERIODIC-COUNT: replay of a session of duration T with BOT_FALLBACK_SCAN_SECS = S fires exactly floor(T/S) periodic ticks (zero-interval handled separately by replay_periodic_interval_zero_means_every_frame.rs).
  3. 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.rs which 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.

DimensionProduction shareRequired behaviourRule-ID
tick=0.001 markets25 %Quantise entry price to 0.001 stepR-EDGE-TICK-001
Over/Under labels8 %Strategy treats outcomes positionally; label opaqueR-EDGE-LABEL-OVERUNDER
Team-vs-team labels8 %Same positional contractR-EDGE-LABEL-TEAM
Odd/Even labels2 %Same positional contractR-EDGE-LABEL-ODDEVEN
neg_risk=true markets63.5 %Filtered upstream by MarketCache::MarketTypeFilter::Standard; directional MUST NOT receive them. Liveness gap: assert filter appliedR-EDGE-NEGRISK-001
Reversed outcome order ["No","Yes"]0 % observed in 1 000-samplePositional contract still holds; invariant uncheckedR-EDGE-REVERSE-OUTCOME
Empty clobTokenIds (closed markets)n/aDiscovery layer rejects (covered by closed test
gamma_parse_market_rejection_logging); directional never sees themR-EDGE-EMPTY-TOKENS
Empty orderbook (no asks)rare but liveSkip — t_sweep_empty_book_returns_none + t_skip_when_ask_missing already coverR-EDGE-EMPTY-BOOK
1-token / 3-token "binary" market (defensive)0 % observedmarket_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:

SignalBoundaryTest ID
Each Decision::* emission → tracing::info! with strategy="directional" + decision-type + ciddirectional.rs:1533, 2687, 3835, 5045-5062R-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:4943R-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 branchR-LIVENESS-PERIODIC
Auto-redeem emit when resolution event seen for filled positiondirectional.rs:4261 on_market_resolvedR-LIVENESS-REDEEM (already covered by t_on_market_resolved_emits_redeem_and_clears_position)
Emergency-stop endpoint sets atomic flag + logs OPERATOR EMERGENCY STOP … linecrates/pm-bot/src/io/local_api/handlers.rs:1270 emergency_stop, mod.rs:467 route registration, mod.rs:158 AtomicBool projectionR-LIVENESS-ESTOP (new post-L-C8/C9)
WS-freeze / heartbeat hook is observable from local API metrics ticklocal_api/metrics_loop.rs:237R-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:

  1. The AtomicBool at local_api/mod.rs:158 flips.
  2. The tracing line at handlers.rs:1347 fires.
  3. The cancel-all report at handlers.rs:1326 includes 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) ​

ActionCountNotes
Retired (stop-loss removal)3R-DIR-017, R-HARVESTER-STOP-LOSS, R-INV-STOP-LT-ENTRY
Added (emergency-stop migration)3R-ESTOP-IDEMPOTENT, R-ESTOP-AUDIT-RECORD, R-LIVENESS-ESTOP
Added (replay refinement)3R-REPLAY-DECISION-SEQ, R-REPLAY-PERIODIC-COUNT, R-REPLAY-PRESET-IDEMPOTENT
Added (edge-case label expansion)2R-EDGE-LABEL-TEAM, R-EDGE-LABEL-ODDEVEN
Refined1R-DIR-002 binary gate sharpened

Net total post-L-C: 43 − 3 + 8 = 48 directional-relevant rule-IDs.

4.2 New rule-IDs ​

Rule-IDSourceDescriptionImplementation siteTest placement (G4)
R-ESTOP-IDEMPOTENTADR-005 + L-C8Repeated POST /api/local/emergency-stop is idempotent (flag stays set, no double cancel-all crash)local_api/handlers.rs:1270-1350tests/directional/liveness_emergency_stop.rs
R-ESTOP-AUDIT-RECORDADR-005 + L-C8Emergency-stop trigger records timestamp + source string in EmergencyStopRecord at local_api/mod.rs:230local_api/mod.rs:221-260same file
R-LIVENESS-ESTOPfeedback_silent_feature_brokenEndpoint emits tracing event + cancel-all reporthandlers.rs:1326-1347same file
СНЯТО R-REPLAY-DECISION-SEQstrategy.md §16 (refined)Two replays of same capture → byte-identical decision vectorapplication/backtest/engine.rs + directional hookstests/directional/replay_decision_sequence_equality.rs
СНЯТО R-REPLAY-PERIODIC-COUNTstrategy.md §15Replay fires exactly floor(T/S) periodic ticks for session duration T and BOT_FALLBACK_SCAN_SECS=Sdirectional.rs:4321 + replay looptests/directional/replay_periodic_tick_count.rs
СНЯТО R-REPLAY-PRESET-IDEMPOTENTL-track replay confidenceSame capture + same operator-config preset → identical decisionspm_analytics::replay::run_with_paramsextend replay_uses_operator_config_not_default.rs
R-EDGE-LABEL-TEAMdata-model §3.2Team-vs-team label markets handled positionallydirectional.rs:3195tests/directional/edge_label_team.rs
R-EDGE-LABEL-ODDEVENdata-model §3.2Odd/Even label markets handled positionallysametests/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 itemFileRatifies
#01tests/gamma_end_date_parsing.rsdiscovery layer end-date parsing (precondition for MAX_HOURS_TO_EXPIRY)
#03tests/gamma_pagination.rsdiscovery pagination (precondition for directional admission)
#05tests/gamma_parse_market_rejection_logging.rsR-EDGE-EMPTY-TOKENS (closed-market rejection)
BUG-04tests/parse_market_source_updated_at.rsupdatedAt propagation (used by staleness checks)
#140 (DONE)tests/replay_strict_temporal_order.rsR-BT-LA-NO-FUTURE-METADATA (precondition for §3.4 invariants)
F4 contracttests/strategy_extension_contract.rsScannerHooks 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:

StatusCountComment
Fully covered (test exists, rule ratified or ratifiable)~22Re-validate against components-status during G4
Partially covered (test exists but missing real-fixture or liveness assertion)~12Targets of G4 batch B/C/F
Uncovered (no test)~14Targets of G4 batch C/D/F

Critical uncovered cluster (per audit I §5 + new rule-IDs):

  1. R-HARVESTER-* integration as a coherent 7-point unit (H1..H7) — no single end-to-end test exists. G4 batch C primary deliverable.
  2. R-EDGE-LABEL-* — three label dimensions (Over/Under, team, Odd/Even) have zero tests. G4 batch D.
  3. R-LIVENESS-DECISION — no test asserts tracing events per Decision. G4 batch F.
  4. R-EDGE-NEGRISK-001 — boundary unverified at evaluator level.
  5. R-DIR-002 — 2-token gate untested with synthetic 1- or 3-token input.
  6. R-ESTOP-* — three new rules from L-C8/L-C9, zero tests today.
  7. R-REPLAY-DECISION-SEQ — gap explicitly called out in audit I §3.4.

5. Fixture strategy (drives G3) ​

5.1 Fixture taxonomy ​

#FixturePurposeAcquisition
1gamma/event-binary-standard.jsonSingle binary market, neg_risk=false, ["Yes","No"] outcomes, tick=0.01`curl gamma-api/events?...
2gamma/event-binary-negrisk.jsonSingle binary market, neg_risk=truesame query, filter
3gamma/event-overunder.json["Over","Under"] outcome labels`curl ...
4gamma/event-team-matchup.jsonTeam-vs-team labels (e.g. ["Yankees","Red Sox"])sports events tag
5gamma/event-oddeven.jsonOdd/Even labelstargeted Gamma query
6gamma/event-tick-0001.json + clob/book-tick-0001.jsonmin_tick_size=0.001 market with live bookGamma + CLOB pair
7gamma/event-weather-harvester.json + clob/book-weather.json + capture sessionWeather event (tag 103040), neg_risk=true, 7–11 child markets; full Harvester preset capturecurl gamma-api/events?tag_id=103040&active=true + pm-bot --dry-run --capture
8clob/book-binary-active.jsonGeneric active book snapshot, used by §3.1 functional fixturescurl clob/book?token_id=…
9clob/book-empty-asks.jsonBook with empty asks sidehand-edited from (8) — synthetic acceptable for negative test
10capture/harvester-full-cycle/Full session: REST Gamma + WsBook deltas + WsUser partial fills + resolutionpm-bot --dry-run --capture --capture-path .tmp/captures/harvester-full-cycle
11capture/periodic-tick-determinism/Session of known duration T with config BOT_FALLBACK_SCAN_SECS=Sdry-run capture, fixed duration
12harvester/preset-canonical.jsonMirror 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 under capture/ carries a one-page README.md documenting:
    • source command line,
    • operator-config preset (harvester/preset-canonical.json at 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.

IDFileSizeSource / marketKey fields for testsNotes
F-G1gamma/event-singleton-binary.json8 KBevent 23792, slug gta-vi-released-before-june-2026, 1 market, Yes/Noid, markets[0].clobTokenIds, markets[0].outcomes, markets[0].orderPriceMinTickSize=0.001Baseline binary; neg_risk field absent (singleton non-neg-risk events omit it) — tests must default-handle None
F-G2gamma/event-weather-harvester.json36 KBevent 128661, slug highest-temperature-in-nyc-on-december-30, tag 103040, 7 buckets, negRiskMarketID setAll markets[*].clobTokenIds, negRiskMarketID, submitted_by, conditionIdsHarvester primary target; multi-bucket neg-risk weather; covers R-HARVESTER-* rule family
F-G3gamma/market-non-yes-no-labels.json6 KBmarket will-bitcoin-hit-80k-or-100k-first-272, outcomes ["80k","100k"], tick 0.001, closedoutcomes, clobTokenIds, orderPriceMinTickSizeLabel-axis edge case (no Yes/No); from closed-markets pool (no active non-Yes/No markets currently on Polymarket)
F-G4gamma/market-tick-0001.json7 KBmarket will-bitcoin-hit-1m-before-gta-vi-872, tick 0.001, Yes/No, neg_risk=falseorderPriceMinTickSize, clobTokenIdsTick edge case; standalone (not intersecting weather — no tick=0.001 weather markets in current pool)
F-C1clob/book-binary-active.json2 KBorderbook for F-G1 token, 6 bids × 55 asks, tick 0.001bids, asks, tick_size, last_trade_price, neg_risk=falseWide-book active baseline; covers R-DIR-PRICE-*
F-C2clob/book-thin-tick-0001.json3 KBorderbook for F-G4 token, 42 bids × 33 asks, tick 0.001bids, asks, min_order_size=5Thin-book scenario for stop-loss bid-drain and reprice logic
F-C3clob/book-absent.json60 B404 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-C4clob/prices-history-binary-24h.json4 KBF-G1 token, 1d interval, fidelity 10, 144 pointshistory[].t, history[].pReplay reference for price-series tests; not a substitute for capture session but covers single-asset trajectory shape
F-H1harvester/harvester-canonical-preset.json2 KBlocal config.db strategy harvester, 50 knobsAll BOT_*, DIRECTIONAL_*, MARKET_* knobsSource of truth for config_test.rs preset-loading tests; mirror of VPS harvester-live instance
F-CAP-1(deferred)—Harvester dry-run, happy pathSESSION.json, rest_gamma.jsonl.zst, ws_market.jsonl.zstG4 — acquisition recipe in tests/fixtures/capture/README.md
F-CAP-2(deferred)—Harvester dry-run, stop-loss pathas aboveG4 — 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=true binary 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 per feedback_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:

  1. load_gamma_fixture(name) -> RestGammaResponse — read + parse JSON.
  2. load_clob_book(name) -> WsBookSnapshot — same.
  3. load_capture(name) -> CaptureSession — open capture directory.
  4. load_harvester_preset() -> DirectionalConfig — parse harvester/preset-canonical.json.
  5. build_market_cache(filter, gamma_events) -> MarketCache — construct a populated cache.
  6. run_replay_collecting_decisions(capture, config) -> Vec<Decision> — call pm_analytics::replay::run_with_params and capture emitted decisions via a test-only sink. Used for §3.4.
  7. subscribe_tracing() -> TracingHandle — tracing-test setup with field assertion helpers for §3.6.
  8. mock_clob_server() -> wiremock::MockServer — stand-up wiremock (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-IDTest fileTest fn nameFixtureAssertion type
R-DIR-002functional/r_dir_002_binary_gate.rsrejects_one_token_market / rejects_three_token_marketsynthetic in-test (1/3 tokens)functional
R-DIR-014 (non-DCA path)functional/r_dir_014_partial_fill_no_dca.rspartial_fill_tracks_without_emitclob/book-binary-active.jsonfunctional
H1..H10harvester/h{1..10}_*.rsdescriptivefixture (7) + (10) + (12)invariant
H9 (negative)harvester/h9_no_auto_stop_loss.rsbook_collapse_does_not_emit_sellsynthetic book-decay sequencenegative
H4 (post_only=false)harvester/h4_post_only_false.rsdecision_carries_post_only_falsefixture (12)flag assertion
H8 (auto-redeem)harvester/h8_auto_redeem.rs(deferred pending OQ-1 resolution)(11)cadence
R-EDGE-LABEL-*edge/label_*.rsdirectional_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).

BatchScopeDepends onEstimated commits
Atests/directional/common/mod.rs harness + first fixture acquisition (§5.1 #1 + #8)—1–2
BFunctional gap fills (R-DIR-002, R-DIR-014 non-DCA path, plus refinements caught during G4 reading)A1–2
CHarvester invariants H1..H10 (integration tests on capture (10) + preset (12); H8 deferred pending OQ-1 resolution)A + §5.1 (10) seeded2–3
DEdge cases (label_overunder, label_team, label_oddeven, negrisk_boundary, reverse_outcome)A + §5.1 (2)(3)(4)(5) seeded2
EReplay-determinism (decision_sequence_equality, periodic_tick_count, preset_idempotent)A + §5.1 (10)(11) seeded1–2
FLiveness (decision_emission, periodic_observable, emergency_stop)A; emergency_stop depends on L-C8 (already landed)1–2
GBenches (benches/*.rs) — no thresholdsA (harness re-used)1

Total expected commits in G4: 9–14.

6.5 Components-status update protocol ​

For each batch:

  1. Land the test commit(s).
  2. In the same series (next commit), update docs/tests/components-status.md row for C-02 Directional with rule-IDs newly ratified.
  3. Insert // VERIFIED: <rule-id> @ YYYY-MM-DD by <method> at the implementation site referenced in §6.3.
  4. 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:

  1. Every rule-ID enumerated in §4 has status проверено in docs/tests/components-status.md (row C-02 Directional).
  2. Every Harvester invariant H1..H10 in §3.2 has at least one ratifying test under tests/directional/harvester/.
  3. Every liveness signal in §3.6 captured by tracing-test and asserted.
  4. Every edge-case dimension in §3.5 has at least one fixture test.
  5. 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 ​

#QuestionOwner of resolution
1Auto-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-024 directional-first architecture
  • ADR-025 strategy extension points (PROPOSED) — ../target/extension-points.md
  • ADR-009 и его capture-runbook.md сняты 2026-08-22 вместе с replay и паритетными воротами.
  • ADR-005 pm-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 commit 803a320f, 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-emergency endpoint, 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 via starts_with prefix-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 at directional.rs:4938-4941. Memory project_harvester_semantics extended 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, const REDEEM_BACKSTOP_SECS=600 at crates/pm-bot/src/domain/strategies/directional.rs). Idempotency falls out of position-removal contract (both paths drop position after redeem emit; no tombstone needed). Conservative neg_risk=true default in backstop. Three H8 sub-tests re-enabled in crates/pm-bot/tests/directional_harvester_invariants.rs (was a single #[ignore]d placeholder); §3.2 H8 row and acceptance notes updated; memory project_harvester_semantics item 8 updated to hybrid semantics.
  • 2026-05-14 — Synced H-invariants under authoritative VPS+user spec: fixes pre-existing staleness in §3.2 (post_only was true → corrected to false; 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 (commit 7902b61) 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 VPS harvester profile 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.com capture (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 at crates/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 per feedback_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.