Skip to content

Целевой Strategy SDK pm-engine ​

Уровень: target. Источник правды для контракта между Engine и стратегиями (Strategy trait + StrategyContext + Signal lifecycle). Противоречащие документы (ScannerHooks из legacy pm-bot, прямые вызовы OrderClient из стратегий, ADR-025 как самостоятельный источник без интеграции в unify) подлежат удалению/redirect во второй итерации.

Контекст и scope ​

Что покрывает. Strategy trait, StrategyContext (хуки + helpers), Signal lifecycle, инвариант mode-agnostic стратегии, дизайн restoration алгоритмов как layers поверх SDK.

Что не покрывает.

  • Где enforce'ятся RISK-LIMITS / kill switch — см. risk-and-emergency.md.
  • Внутреннюю архитектуру Engine — см. engine.md.
  • DDL signals/orders/positions/events — см. storage.md.
  • Бизнес-логику directional/Harvester — docs/strategies/directional-prd.md + docs/strategies/directional.md.

Якорь ​

project_canonical_vision A1 (directional/Harvester — единственная активная), A2 (восстановление через layers поверх extension API), A4 (stop-loss переработан через config-driven форму). Источник: ADR-025 (PROPOSED) + openspec spec order-router-and-sdk + design «Стратегический SDK».

Принципы SDK ​

  1. Единый путь live/probe. Код стратегии не содержит ветвления по режиму. Live и реальный денежный probe используют один OrderClient; ограничения probe принадлежат внешним предохранителям.
  2. Декларативный intent. Стратегия не вызывает OrderClient напрямую. Она добавляет Signal и связанный OrderIntent в выходной StrategyEventContext. Engine фиксирует сигнал и проводит intent через EmergencyGuard → OrderRouter → OrderClient.
  3. Изолированная ветвь события. Стратегия получает свой StrategyEventContext, разделяет исходную Arc<EventRevision> с другими стратегиями только для чтения и возвращает новую типизированную стадию. Полный контракт определён в event-context.md.

В целевой архитектуре имя StrategyContext остаётся за исторической сервисной поверхностью только до миграции. Чтобы не смешивать зависимости стратегии с данными конкретного решения, целевые роли называются StrategyServices и StrategyEventContext соответственно. 4. Изоляция per-strategy. Один файл-стратегия = одна реализация trait'а. Никаких cross-strategy if-веток. Расширение = новый файл (strategies/binary_arb/, strategies/bucket_arb/, …) без правок в Engine. Принцип feedback_strategy_isolation. 5. Hooks-driven. Engine вызывает стратегию через хуки на конкретные события (market data, tick, order event, position change, closure). Стратегия не делает busy-loop, не держит фоновый таск, не вызывает Engine inversely вне хелперов context'а. 6. Typed config per strategy. Каждая стратегия декларирует свой Config struct с named typed knobs. Никакого universal condition engine, никакого composable enum Condition, никакого evaluate() over generic tree. Form schema во фронтенде — strategy-specific keys с showWhen. Альтернатива (composable conditions из аудита pm-orchestrator) рассмотрена и отвергнута по KISS. См. memory rule feedback_strategy_specific_typed_config + design D14. 7. Static registry для регистрации. Strategy trait + static registry; добавление стратегии = новый файл + регистрация. Альтернатива enum StrategyMode с разветвлением в core отвергнута. См. design D12. 8. FSM per strategy — typed enum. States + transitions — typed enum внутри стратегии. SDK даёт transition!(from, to, reason) макро, которое в одной SQLite-транзакции с domain mutation пишет events.StrategyFsmTransition. Generic typestate / trait-based FSM библиотеки отвергнуты. См. design D13.

Strategy trait ​

rust
pub trait Strategy: Send + Sync {
    fn name(&self) -> &str;

    // Event hooks — все опциональны, default = no-op.
    fn on_market_data(&mut self, snapshot: MarketSnapshot, ctx: &mut StrategyContext) {}
    fn on_tick(&mut self, now: LogicalTime, ctx: &mut StrategyContext) {}
    fn on_order_event(&mut self, event: OrderEvent, ctx: &mut StrategyContext) {}
    fn on_position_change(&mut self, position: PositionSnapshot, reason: ChangeReason, ctx: &mut StrategyContext) {}
    fn on_closure(&mut self, market_id: ConditionId, reason: ClosureReason, ctx: &mut StrategyContext) {}
}
  • on_market_data: реактивный вход при изменении книги (book/price_change через unified OrderbookStore).
  • on_tick: периодический скан (heartbeat от LifecycleFSM, фиксированный интервал из config).
  • on_order_event: реакция на ack/fill/cancel/reject/expired от OrderClient.
  • on_position_change: позиция изменилась (открыта/обновлена/закрыта/redeemed).
  • on_closure: рынок закрылся (per-outcome closure, см. outcome-tradability-detection).

StrategyContext — helpers + emit ​

Emission helpers ​

МетодНазначение
ctx.emit_signal(Signal) -> SignalUidРегистрирует Signal через SignalRegistry (txn: signals + events.SignalEmitted)
ctx.emit_order(OrderIntent)Декларативный intent → EmergencyGuard → OrderRouter
ctx.cancel_order(OrderUid, reason)Отмена ордера через OrderRouter (тот же writer)
ctx.close_signal(SignalUid, reason)Явное закрытие сигнала (Signal → COMPLETED)
ctx.supersede_signal(old: SignalUid, new: Signal) -> SignalUidЗамена сигнала: old → SUPERSEDED, орфан-ордера cancel'ятся, new → EMITTED
transition!(from, to, reason) (макро)Persists events.StrategyFsmTransition атомарно с domain mutation; используется внутри hooks стратегии при смене своей typed-enum фазы
ctx.load_fsm_phase(SignalUid) -> Option<Phase>Recovery FSM-фазы на bootstrap; стратегия знает свой Phase-тип, SDK хранит serde_json::Value

Read helpers (read-only views) ​

МетодВозвращает
ctx.positions_for_signal(SignalUid)Все позиции, привязанные к signal_id (из PositionRegistry write-through cache)
ctx.orders_for_signal(SignalUid)Все ордера signal'а (live + терминальные за окно retention)
ctx.open_positions(strategy)Все открытые позиции стратегии
ctx.orderbook(token_id) -> &OrderbookSnapshotRead-only snapshot unified OrderbookStore (ADR-022)

Policy helpers (ENTRY-POLICY / EXECUTION) ​

Помогают стратегии решать «когда / по какой цене эмитить». Возвращают decision, не блокируют поток.

HelperDecisionИсточник правил
ctx.entry_window_check(market) -> EntryWindowDecision::{Open, Closed}per-strategy finish_type ∈ {EndDate, GstOffset { offset_seconds }, GstLocalMidnight} + Option<u64> boundsЯкорь A9, capability outcome-tradability-detection
ctx.debounce_gate(signal_key) -> DebounceDecision::{Armed, Suppressed}armed-state per-key в config.db knobsADR-030
ctx.stop_loss_eval(position) -> StopLossDecision::{Hold, Exit}threshold/cooldown/debounce из config.dbADR-027 directional-stop-loss + A4

Эти решения не дают reject в OrderRouter — они формируют поток intents до эмиссии. Если decision = «не эмитить» — стратегия просто не вызывает emit_order. Принцип см. risk-and-emergency.md.

Sizing helper (RISK-LIMITS-aware) ​

ctx.compute_size(intent_draft) -> SizingDecision::{Size(shares), Skip(reason)} — рассчитывает размер ордера с учётом всех 3 canonical RISK-LIMITS (shares-per-market, USD-per-market, total budget + reinvest). Размер НЕ зависит от баланса кошелька: относительного лимита в системе нет. Если интент-черновик не помещается ни в один из лимитов — возвращает Skip(reason). Помогает стратегии не эмитить заведомо-rejected intent (оптимизация; реальный enforcement всё равно в OrderRouter).

Signal lifecycle ​

EMITTED ─────► EXECUTING ─────► COMPLETED
   │                                ▲
   │                                │
   ├──► REJECTED                    │
   │     (нет дочерних entities)    │
   │                                │
   └──► SUPERSEDED ─────────────────┘
         (новый сигнал заменяет)
  • EMITTED — после ctx.emit_signal(...).
  • EXECUTING — на первом ассоциированном OrderPlaced.
  • COMPLETED — auto, когда все связанные orders в терминальном статусе И все связанные positions в CLOSED|REDEEMED. Также через явный ctx.close_signal(...).
  • REJECTED — если ни один OrderIntent не прошёл (например, kill switch активен).
  • SUPERSEDED — через ctx.supersede_signal(...). Орфан-ордера cancel'ятся OrderRouter'ом.

Полная трассировка signal → orders → positions → events — один JOIN по signal_id (см. storage.md). Используется в pm-cli signal show и в дашборд-странице /trace/<signal_uid>.

Пример — directional/Harvester (single active per ADR-024) ​

Алгоритм переписывается с нуля по PRD/FSD на чистый SDK:

rust
impl Strategy for HarvesterStrategy {
    fn on_market_data(&mut self, snap: MarketSnapshot, ctx: &mut StrategyContext) {
        // 1. ENTRY-POLICY: entry window
        if ctx.entry_window_check(&snap.market).is_closed() { return; }

        // 2. ENTRY-POLICY: debounce
        if ctx.debounce_gate(snap.signal_key()).is_suppressed() { return; }

        // 3. Compute intent draft, RISK-LIMITS-aware
        let intent_draft = OrderIntent::buy(snap.token_id, best_ask, target_size);
        let SizingDecision::Size(shares) = ctx.compute_size(&intent_draft) else { return };

        // 4. Emit signal + order
        let signal_uid = ctx.emit_signal(Signal::entry(snap.market.condition_id, …));
        ctx.emit_order(intent_draft.with_signal(signal_uid).with_size(shares));
    }

    fn on_order_event(&mut self, ev: OrderEvent, ctx: &mut StrategyContext) {
        if let OrderEvent::Fill(fill) = ev {
            // GTC@0.99 exit per directional spec
            let exit_intent = OrderIntent::sell_gtc(fill.token_id, price_milli(990), fill.size_shares);
            ctx.emit_order(exit_intent.with_signal(fill.signal_uid));
        }
    }

    fn on_position_change(&mut self, pos: PositionSnapshot, _r: ChangeReason, ctx: &mut StrategyContext) {
        // EXECUTION: stop-loss exit mechanic
        if let StopLossDecision::Exit = ctx.stop_loss_eval(&pos) {
            ctx.emit_order(OrderIntent::fok_sell(pos.token_id, pos.best_bid, pos.size_shares)
                           .with_signal(pos.signal_uid));
        }
    }
}

Бизнес-семантика (entry [0.95, 0.98] best-ask, DCA $5/маркет, GTC@$0.99 exit, stop-loss с config-driven knobs, redeem cycle через Phase J state machine) — из project_harvester_semantics, docs/strategies/directional-harvester.md (раздел «Redeem cycle (pm-engine Phase J)»). SDK не диктует семантику; он диктует контракт.

RedemptionPort (Phase J1) ​

Hexagonal port для on-chain CTF redeem operations. Граница между domain (strategy / RedemptionService) и pm-sdk on-chain adapters.

Trait ​

rust
#[async_trait]
pub trait RedemptionPort: Send + Sync + 'static {
    async fn redeem(&self, request: RedemptionRequest) -> Result<RedemptionAck, RedemptionError>;
}

RedemptionRequest несёт owner_address, condition_id, neg_risk flag + (для neg-risk) yes_token_id / no_token_id. RedemptionAck содержит tx_hash + submitted_at.

Implementations ​

  • LiveRedemptionAdapter — pm-sdk redeem_standard / redeem_neg_risk через alloy provider.
  • TestRedemptionAdapter — test-only двойник с явно заданным результатом; в production target не входит.

RedemptionError ​

  • Transient — retryable (network, RPC, gas issues).
  • Terminal — non-retryable (zero balance, invalid token id, already redeemed).
  • InvalidRequest — engine/strategy bug (fail-fast).

RedemptionService маппит Transient на retry-цикл с backoff из knobs, Terminal / InvalidRequest — на immediate mark_redeem_failed. См. docs/strategies/directional-harvester.md раздел «Redeem cycle».

Restoration алгоритмов как layers ​

Согласно якорю A2: будущие стратегии (binary-arb, bucket-arb, single-token-mm, maker_rebate, merge_arb) восстанавливаются как отдельные файлы в strategies/<name>/, реализующие Strategy trait. Core (engine/, ports/, sdk/) не изменяется при добавлении новой стратегии — это и есть смысл «extension points».

Acceptance gate ADR-025: первая успешно восстановленная стратегия валидирует достаточность SDK. До этого SDK считается provisional; если первая restoration требует расширения trait'а или нового helper'а — это нормальный delta, не нарушение принципа.

Restoration roadmap: docs/restoration-backlog/ + docs/tests/backlog/restore-*-as-layer.md (seeded в pivot plan F6).

Что SDK НЕ даёт стратегии ​

  • Доступа к OrderClient напрямую (всё через ctx.emit_order → OrderRouter).
  • Доступа к factual runtime mode (live / probe) — by design, mode-agnostic invariant; simulated execution mode не существует.
  • Прямого write в signals/orders/positions/events (single-writer гейт ast-grep в CI).
  • Доступа к kill switch reset / EmergencyGuard mutate (operator-only через pm-cli).
  • Long-running async task'ов вне хуков (нет background spawn).

Decision rationale (растворённые ADR) ​

  • Per ADR-024 (ACCEPTED) — directional-first: единственная активная стратегия в active scope. SDK проектируется так, чтобы восстановление было аддитивным (новый файл стратегии, реализующий Strategy trait, core не меняется).
  • Per ADR-025 (PROPOSED) — Strategy extension points: формализация Strategy trait + StrategyContext + Signal lifecycle. Acceptance gate = первая restoration validates SDK достаточность. Этот SSOT-документ — целевая форма ADR-025.
  • Per design «Стратегический SDK» (unify-orders-positions-signals-events/design.md): mode-agnostic invariant (стратегия не знает про режим), декларативный intent (никаких прямых OrderClient вызовов), полная переписанная directional/Harvester на чистый SDK по PRD/FSD.
  • Per spec order-router-and-sdk (формальные requirement'ы «Strategy Emits Declarative Intent via Context» + «Strategy code is mode-agnostic» + «ENTRY-POLICY and EXECUTION Logic Lives in Strategy»).
  • Per strategy-isolation-design (ACCEPTED, исторический ADR) — принцип «один файл = одна стратегия, никаких cross-strategy if-веток» сохраняется и усиливается.
  • Per ADR-027 directional-stop-loss + якорь A4: stop-loss как EXECUTION helper в StrategyContext, не RISK-LIMIT. Knobs в config.db.
  • Per ADR-030 (DEFERRED) — entry debounce: в текущем pm-engine не реализован. Будущий возврат оформляется отдельным ENTRY-POLICY extension, а не считается существующим helper'ом.

Cross-references ​

  • Engine — кто вызывает хуки и принимает emit'ы: engine.md.
  • Кто проверяет 3 canonical RISK-LIMITS после emit_order: risk-and-emergency.md.
  • Где живут strategy knobs (config.db): storage.md + memory feedback_strategy_config_via_ui.
  • SSE-каналы, через которые наблюдается signal lifecycle: wire-contract.md.
  • Бизнес-семантика directional/Harvester: docs/strategies/directional-prd.md, docs/strategies/directional.md.
  • Restoration backlog: docs/restoration-backlog/, docs/tests/backlog/restore-*-as-layer.md.
  • Memory: project_canonical_vision A1+A2, feedback_strategy_isolation, project_harvester_semantics.
  • ADR: docs/adr/adr-024-directional-first-architecture.md, docs/adr/adr-025-strategy-extension-points.md, docs/adr/strategy-isolation-design.md.