From 398f734393452013b4976eca9c05fe8120e0b98d Mon Sep 17 00:00:00 2001 From: Greg Konush <12027037+gregkonush@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:01:27 -0700 Subject: [PATCH 1/2] refactor(torghut): normalize scheduler pipeline modules --- .../torghut/app/trading/scheduler/pipeline.py | 15 +- .../scheduler/pipeline_modules/__init__.py | 136 +-- .../scheduler/pipeline_modules/contexts.py | 358 ++++++++ ...emethodspart3.py => decision_lifecycle.py} | 441 ++++----- ...ipelinemethodspart7.py => llm_outcomes.py} | 499 ++++------- ...gpipelinemethodspart6.py => llm_review.py} | 756 +++++++--------- .../part_01_statements_158.py | 196 ---- .../part_09_tradingpipeline.py | 187 ---- ...ngpipelinemethodspart1.py => run_cycle.py} | 615 +++++++------ ...pelinemethodspart5.py => runtime_gates.py} | 390 +++----- .../scheduler/pipeline_modules/shared.py | 591 ++++++++++++ ...nemethodspart2.py => signal_processing.py} | 454 +++++----- ...nemethodspart4.py => submission_policy.py} | 628 ++++++------- .../scheduler/pipeline_modules/support.py | 845 ++++++++++++++++++ .../pipeline_modules/trading_pipeline.py | 28 + .../torghut/app/trading/scheduler/safety.py | 15 +- .../torghut/app/trading/scheduler/state.py | 7 + .../torghut/app/trading/scheduler/state.pyi | 7 +- .../scheduler/submission_preparation.py | 15 +- .../__init__.py | 78 +- .../direct_submission.py | 572 ++++++++++++ .../part_01_statements_74.py | 80 -- ...plepipelinesubmissionpreparationmixinme.py | 461 ---------- ...rationmixinme.py => quote_routeability.py} | 725 +++++++-------- .../quote_routeability_values.py | 273 ++++++ ...npreparationmixinme.py => quote_sizing.py} | 506 ++++++----- .../submission_preparation_modules/shared.py | 145 +++ .../trading/scheduler/target_plan_helpers.py | 68 ++ .../trading/scheduler/target_plan_helpers.pyi | 39 + .../test_trading_pipeline_dspy_gate_a.py | 47 +- .../test_trading_pipeline_dspy_gate_b.py | 31 +- .../test_trading_pipeline_dspy_gate_c.py | 66 +- .../test_trading_pipeline_dspy_gate_d.py | 31 +- .../test_trading_pipeline_execution_llm_a.py | 35 +- .../test_trading_pipeline_execution_llm_b.py | 37 +- ...est_trading_pipeline_external_targets_a.py | 31 +- ...est_trading_pipeline_external_targets_c.py | 39 +- ...est_trading_pipeline_external_targets_d.py | 58 +- .../test_trading_pipeline_live_regime_a.py | 45 +- .../test_trading_pipeline_live_regime_b.py | 34 +- ...ing_pipeline_materialized_target_plan_a.py | 56 +- ...ing_pipeline_materialized_target_plan_b.py | 41 +- ...ing_pipeline_materialized_target_plan_c.py | 40 +- ..._trading_pipeline_position_projection_a.py | 34 +- ..._trading_pipeline_position_projection_b.py | 39 +- .../test_trading_pipeline_probe_exits_a.py | 37 +- .../test_trading_pipeline_probe_exits_b.py | 40 +- .../test_trading_pipeline_quote_outcome.py | 34 +- ...test_trading_pipeline_route_execution_a.py | 39 +- ...test_trading_pipeline_route_execution_b.py | 41 +- ...t_trading_pipeline_target_plan_source_a.py | 61 +- ...t_trading_pipeline_target_plan_source_b.py | 29 +- ...st_trading_pipeline_warmup_submission_a.py | 65 +- ...st_trading_pipeline_warmup_submission_b.py | 44 +- 54 files changed, 6141 insertions(+), 4043 deletions(-) create mode 100644 services/torghut/app/trading/scheduler/pipeline_modules/contexts.py rename services/torghut/app/trading/scheduler/pipeline_modules/{part_04_tradingpipelinemethodspart3.py => decision_lifecycle.py} (67%) rename services/torghut/app/trading/scheduler/pipeline_modules/{part_08_tradingpipelinemethodspart7.py => llm_outcomes.py} (60%) rename services/torghut/app/trading/scheduler/pipeline_modules/{part_07_tradingpipelinemethodspart6.py => llm_review.py} (56%) delete mode 100644 services/torghut/app/trading/scheduler/pipeline_modules/part_01_statements_158.py delete mode 100644 services/torghut/app/trading/scheduler/pipeline_modules/part_09_tradingpipeline.py rename services/torghut/app/trading/scheduler/pipeline_modules/{part_02_tradingpipelinemethodspart1.py => run_cycle.py} (64%) rename services/torghut/app/trading/scheduler/pipeline_modules/{part_06_tradingpipelinemethodspart5.py => runtime_gates.py} (71%) create mode 100644 services/torghut/app/trading/scheduler/pipeline_modules/shared.py rename services/torghut/app/trading/scheduler/pipeline_modules/{part_03_tradingpipelinemethodspart2.py => signal_processing.py} (74%) rename services/torghut/app/trading/scheduler/pipeline_modules/{part_05_tradingpipelinemethodspart4.py => submission_policy.py} (60%) create mode 100644 services/torghut/app/trading/scheduler/pipeline_modules/support.py create mode 100644 services/torghut/app/trading/scheduler/pipeline_modules/trading_pipeline.py create mode 100644 services/torghut/app/trading/scheduler/submission_preparation_modules/direct_submission.py delete mode 100644 services/torghut/app/trading/scheduler/submission_preparation_modules/part_01_statements_74.py delete mode 100644 services/torghut/app/trading/scheduler/submission_preparation_modules/part_04_simplepipelinesubmissionpreparationmixinme.py rename services/torghut/app/trading/scheduler/submission_preparation_modules/{part_03_simplepipelinesubmissionpreparationmixinme.py => quote_routeability.py} (54%) create mode 100644 services/torghut/app/trading/scheduler/submission_preparation_modules/quote_routeability_values.py rename services/torghut/app/trading/scheduler/submission_preparation_modules/{part_02_simplepipelinesubmissionpreparationmixinme.py => quote_sizing.py} (65%) create mode 100644 services/torghut/app/trading/scheduler/submission_preparation_modules/shared.py diff --git a/services/torghut/app/trading/scheduler/pipeline.py b/services/torghut/app/trading/scheduler/pipeline.py index 07ad86292e..75cb89b310 100644 --- a/services/torghut/app/trading/scheduler/pipeline.py +++ b/services/torghut/app/trading/scheduler/pipeline.py @@ -1,14 +1,7 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false +"""Public trading pipeline import surface.""" + from __future__ import annotations -from importlib import import_module as _import_module -import sys as _sys +from .pipeline_modules import TradingPipeline -_module_name = __name__ -_parent_name, _, _module_attr = _module_name.rpartition(".") -_impl = _import_module("app.trading.scheduler.pipeline_modules") -globals().update(_impl.__dict__) -_sys.modules[_module_name] = _impl -_parent = _sys.modules.get(_parent_name) -if _parent is not None: - setattr(_parent, _module_attr, _impl) +__all__ = ["TradingPipeline"] diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/__init__.py b/services/torghut/app/trading/scheduler/pipeline_modules/__init__.py index d1505ace58..9206c9c07c 100644 --- a/services/torghut/app/trading/scheduler/pipeline_modules/__init__.py +++ b/services/torghut/app/trading/scheduler/pipeline_modules/__init__.py @@ -1,121 +1,25 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false -from __future__ import annotations - -from importlib import import_module as __compat_import_module__ -import logging as __compat_logging__ -import sys as __compat_sys__ -import types as __compat_types__ - -__compat_part_modules__: list[__compat_types__.ModuleType] = [] - - -class __CompatModule__(__compat_types__.ModuleType): - def __setattr__(self, name: str, value: object) -> None: - super().__setattr__(name, value) - for module in __compat_part_modules__: - module.__dict__[name] = value - - -def __compat_export__(module: __compat_types__.ModuleType) -> None: - for name, value in module.__dict__.items(): - if name.startswith("__"): - continue - globals()[name] = value - +"""Semantic modules for the trading scheduler pipeline.""" -__compat_module__ = __compat_import_module__(f"{__name__}.part_01_statements_158") -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) - -__compat_module__ = __compat_import_module__( - f"{__name__}.part_02_tradingpipelinemethodspart1" -) -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) - -__compat_module__ = __compat_import_module__( - f"{__name__}.part_03_tradingpipelinemethodspart2" -) -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) - -__compat_module__ = __compat_import_module__( - f"{__name__}.part_04_tradingpipelinemethodspart3" -) -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) - -__compat_module__ = __compat_import_module__( - f"{__name__}.part_05_tradingpipelinemethodspart4" -) -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) - -__compat_module__ = __compat_import_module__( - f"{__name__}.part_06_tradingpipelinemethodspart5" -) -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) - -__compat_module__ = __compat_import_module__( - f"{__name__}.part_07_tradingpipelinemethodspart6" -) -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) - -__compat_module__ = __compat_import_module__( - f"{__name__}.part_08_tradingpipelinemethodspart7" -) -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) +from __future__ import annotations -__compat_module__ = __compat_import_module__(f"{__name__}.part_09_tradingpipeline") -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) +from .decision_lifecycle import TradingPipelineDecisionLifecycleMixin +from .llm_outcomes import TradingPipelineReviewOutcomeMixin +from .llm_review import TradingPipelineReviewMixin +from .run_cycle import TradingPipelineRunCycleMixin +from .runtime_gates import TradingPipelineRuntimeGatesMixin +from .shared import TradingPipelineBase +from .signal_processing import TradingPipelineSignalProcessingMixin +from .submission_policy import TradingPipelineSubmissionPolicyMixin +from .trading_pipeline import TradingPipeline -__compat_sys__.modules[__name__].__class__ = __CompatModule__ -logger = __compat_logging__.getLogger(__name__.removesuffix("_modules")) -for __compat_loaded_module__ in globals().get("__compat_part_modules__", ()): - __compat_loaded_module__.__dict__["logger"] = logger __all__ = [ - name - for name in globals() - if not name.startswith("__") and not name.startswith("_CompatModule") + "TradingPipeline", + "TradingPipelineBase", + "TradingPipelineDecisionLifecycleMixin", + "TradingPipelineReviewMixin", + "TradingPipelineReviewOutcomeMixin", + "TradingPipelineRunCycleMixin", + "TradingPipelineRuntimeGatesMixin", + "TradingPipelineSignalProcessingMixin", + "TradingPipelineSubmissionPolicyMixin", ] -del __compat_module__ diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/contexts.py b/services/torghut/app/trading/scheduler/pipeline_modules/contexts.py new file mode 100644 index 0000000000..20e5f87675 --- /dev/null +++ b/services/torghut/app/trading/scheduler/pipeline_modules/contexts.py @@ -0,0 +1,358 @@ +"""Typed internal contexts for scheduler pipeline stages.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date, datetime +from decimal import Decimal +from typing import TYPE_CHECKING, Any, Optional + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + + from sqlalchemy.orm import Session + + from ....models import Strategy, TradeDecision + from ...ingest import SignalBatch + from ...llm import LLMReviewEngine + from ...llm.schema import ( + MarketContextBundle, + MarketSnapshot as LLMMarketSnapshot, + PortfolioSnapshot, + RecentDecisionSummary, + ) + from ...models import StrategyDecision + from ...prices import MarketSnapshot + + +def _empty_symbol_allowlist() -> set[str]: + return set() + + +@dataclass(frozen=True) +class TradingPipelineRuntimeDependencies: + alpaca_client: Any + order_firewall: Any + ingestor: Any + decision_engine: Any + risk_engine: Any + executor: Any + execution_adapter: Any + reconciler: Any + universe_resolver: Any + state: Any + account_label: str + session_factory: Any + llm_review_engine: Any | None = None + price_fetcher: Any | None = None + strategy_catalog: Any | None = None + execution_policy: Any | None = None + order_feed_ingestor: Any | None = None + + @classmethod + def from_legacy_call( + cls, + args: tuple[Any, ...], + kwargs: dict[str, Any], + *, + default_session_factory: Any, + ) -> TradingPipelineRuntimeDependencies: + names = ( + "alpaca_client", + "order_firewall", + "ingestor", + "decision_engine", + "risk_engine", + "executor", + "execution_adapter", + "reconciler", + "universe_resolver", + "state", + "account_label", + "session_factory", + "llm_review_engine", + "price_fetcher", + "strategy_catalog", + "execution_policy", + "order_feed_ingestor", + ) + values = dict(kwargs) + for name, value in zip(names, args, strict=False): + if name in values: + raise TypeError(f"multiple values for argument {name!r}") + values[name] = value + values.setdefault("session_factory", default_session_factory) + missing = [name for name in names[:11] if name not in values] + if missing: + missing_list = ", ".join(missing) + raise TypeError(f"missing required pipeline dependencies: {missing_list}") + return cls( + alpaca_client=values["alpaca_client"], + order_firewall=values["order_firewall"], + ingestor=values["ingestor"], + decision_engine=values["decision_engine"], + risk_engine=values["risk_engine"], + executor=values["executor"], + execution_adapter=values["execution_adapter"], + reconciler=values["reconciler"], + universe_resolver=values["universe_resolver"], + state=values["state"], + account_label=str(values["account_label"]), + session_factory=values["session_factory"], + llm_review_engine=values.get("llm_review_engine"), + price_fetcher=values.get("price_fetcher"), + strategy_catalog=values.get("strategy_catalog"), + execution_policy=values.get("execution_policy"), + order_feed_ingestor=values.get("order_feed_ingestor"), + ) + + +@dataclass(frozen=True) +class SessionWarmupWindow: + session_day: date + start: datetime + end: datetime + limit: int + max_seconds: int + max_signals: int + + +@dataclass(frozen=True) +class AllocationDecisionContext: + session: Session + strategies: list[Strategy] + account: dict[str, str] + positions: list[dict[str, Any]] + allowed_symbols: set[str] + + +@dataclass(frozen=True) +class BatchSignalProcessingContext: + session: Session + batch: SignalBatch + strategies: list[Strategy] + account_snapshot: Any + account: dict[str, str] + positions: list[dict[str, Any]] + allowed_symbols: set[str] + + +@dataclass(frozen=True) +class PositionTagContext: + symbol_exposures: Mapping[str, Mapping[str, Any]] + signed_position_qty: Decimal + position_qty: Decimal + side: str + + +@dataclass(frozen=True) +class StrategyPositionTagRequest: + position: dict[str, Any] + strategy_id: str + exposure: Mapping[str, Any] + qty: Decimal + side: str + session_open: datetime + split_from_aggregate: bool = False + + +@dataclass(frozen=True) +class StrategyPositionExposureUpdate: + symbol: str + strategy_id: str + signed_qty: Decimal + filled_qty: Decimal + side: str + execution_created_at: datetime + avg_fill_price: Optional[Decimal] + + +@dataclass(frozen=True) +class DecisionSubmissionContext: + session: Session + decision_row: TradeDecision + strategy: Strategy + account: dict[str, str] + positions: list[dict[str, Any]] + symbol_allowlist: set[str] = field(default_factory=_empty_symbol_allowlist) + + +@dataclass(frozen=True) +class LiveSubmissionGateInputs: + session: Session | None = None + hypothesis_summary: Mapping[str, Any] | None = None + empirical_jobs_status: Mapping[str, Any] | None = None + dspy_runtime_status: Mapping[str, Any] | None = None + quant_health_status: Mapping[str, Any] | None = None + + +@dataclass(frozen=True) +class DecisionBlockRequest: + session: Session + decision: StrategyDecision + decision_row: TradeDecision + reason: str + submission_stage: str + capital_stage: str | None = None + extra_metadata: Mapping[str, Any] | None = None + severity: str = "warning" + + +@dataclass(frozen=True) +class DecisionRejectionRequest: + session: Session + decision: StrategyDecision + decision_row: TradeDecision + reasons: list[str] + log_template: str + + +@dataclass(frozen=True) +class DomainTelemetryEvent: + event_name: str + severity: str + decision: StrategyDecision | None = None + decision_row: TradeDecision | None = None + execution: Any | None = None + reason_codes: Sequence[str] | None = None + extra_properties: Mapping[str, Any] | None = None + + +@dataclass(frozen=True) +class ExecutionPolicyRequest: + context: DecisionSubmissionContext + decision: StrategyDecision + snapshot: Optional[MarketSnapshot] + + +@dataclass(frozen=True) +class RiskVerdictRequest: + context: DecisionSubmissionContext + decision: StrategyDecision + execution_advisor: Mapping[str, Any] | None + + +@dataclass(frozen=True) +class LLMReviewContext: + session: Session + decision_row: TradeDecision + account: dict[str, str] + positions: list[dict[str, Any]] + + +@dataclass(frozen=True) +class LLMRuntimeBlockRequest: + context: LLMReviewContext + decision: StrategyDecision + reason: str + reject_reason: str + risk_flags: list[str] + response_payload_extra: dict[str, Any] | None = None + policy_resolution: dict[str, Any] | None = None + + +@dataclass(frozen=True) +class LLMPolicyReviewRequest: + context: LLMReviewContext + decision: StrategyDecision + guardrails: Any + policy_resolution: dict[str, Any] + engine: LLMReviewEngine | None = None + + +@dataclass(frozen=True) +class LLMRuntimeReviewResult: + engine: LLMReviewEngine | None + block: tuple[StrategyDecision, Optional[str]] | None = None + + +@dataclass(frozen=True) +class MarketContextBlockRequest: + context: LLMReviewContext + decision: StrategyDecision + guardrails: Any + policy_resolution: dict[str, Any] + market_context: Optional[MarketContextBundle] + market_context_error: Optional[str] + + +@dataclass(frozen=True) +class LLMUnavailableRequest: + context: LLMReviewContext + decision: StrategyDecision + reason: str + shadow_mode: bool + effective_fail_mode: str | None = None + risk_flags: list[str] | None = None + market_context: Optional[MarketContextBundle] = None + reject_reason: str | None = None + response_payload_extra: dict[str, Any] | None = None + policy_resolution: dict[str, Any] | None = None + + +@dataclass(frozen=True) +class LLMReviewErrorRequest: + context: LLMReviewContext + decision: StrategyDecision + guardrails: Any + policy_resolution: dict[str, Any] + engine: LLMReviewEngine + request_json: dict[str, Any] + error: Exception + + +@dataclass(frozen=True) +class LLMReviewRunRequest: + context: LLMReviewContext + decision: StrategyDecision + guardrails: Any + policy_resolution: dict[str, Any] + engine: LLMReviewEngine + request_json: dict[str, Any] + + +@dataclass(frozen=True) +class LLMReviewInputs: + portfolio_snapshot: PortfolioSnapshot + market_snapshot: Optional[LLMMarketSnapshot] + market_context: Optional[MarketContextBundle] + market_context_error: Optional[str] + recent_decisions: list[RecentDecisionSummary] + + +@dataclass(frozen=True) +class LLMReviewRecord: + session: Session + decision_row: TradeDecision + model: str + prompt_version: str + request_json: dict[str, Any] + response_json: dict[str, Any] + verdict: str + confidence: Optional[float] + adjusted_qty: Optional[Decimal] + adjusted_order_type: Optional[str] + rationale: Optional[str] + risk_flags: list[str] + tokens_prompt: Optional[int] + tokens_completion: Optional[int] + + +@dataclass(frozen=True) +class OrderSubmissionRequest: + session: Session + execution_client: Any + decision: StrategyDecision + decision_row: TradeDecision + selected_adapter_name: str + retry_delays: list[int] + + +@dataclass(frozen=True) +class ExecutionFallbackRequest: + session: Session + decision: StrategyDecision + decision_row: TradeDecision + execution: Any + selected_adapter_name: str + actual_adapter_name: str diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/part_04_tradingpipelinemethodspart3.py b/services/torghut/app/trading/scheduler/pipeline_modules/decision_lifecycle.py similarity index 67% rename from services/torghut/app/trading/scheduler/pipeline_modules/part_04_tradingpipelinemethodspart3.py rename to services/torghut/app/trading/scheduler/pipeline_modules/decision_lifecycle.py index cb50c0accf..984907be77 100644 --- a/services/torghut/app/trading/scheduler/pipeline_modules/part_04_tradingpipelinemethodspart3.py +++ b/services/torghut/app/trading/scheduler/pipeline_modules/decision_lifecycle.py @@ -1,168 +1,72 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false """Trading pipeline implementation.""" from __future__ import annotations -import hashlib -import json -import inspect import logging import os -from collections.abc import Callable, Mapping -from datetime import date, datetime, timedelta, timezone +from collections.abc import Mapping +from datetime import datetime, timezone from decimal import Decimal -from pathlib import Path -from typing import Any, Optional, Sequence, cast +from typing import Any, Optional from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session -from ....alpaca_client import TorghutAlpacaClient from ....config import settings -from ....db import SessionLocal from ....models import ( Execution, - LLMDecisionReview, - PositionSnapshot, - RejectedSignalOutcomeEvent, Strategy, TradeDecision, coerce_json_payload, ) -from ....observability import capture_posthog_event -from ....snapshots import snapshot_account_and_positions -from ....strategies import StrategyCatalog -from ...autonomy.phase_manifest_contract import AUTONOMY_PHASE_ORDER -from ...decisions import DecisionEngine from ...empirical_jobs import build_empirical_jobs_status -from ...execution import OrderExecutor -from ...execution_adapters import ExecutionAdapter -from ...execution_policy import ExecutionPolicy -from ...feature_quality import ( - REASON_STALENESS, - FeatureQualityThresholds, - evaluate_feature_batch_quality, -) -from ...features import extract_executable_price, optional_decimal, payload_value -from ...firewall import OrderFirewall, OrderFirewallBlocked -from ...ingest import ClickHouseSignalIngestor, SignalBatch -from ...lean_lanes import LeanLaneManager -from ...llm import LLMReviewEngine, apply_policy +from ...ingest import SignalBatch from ...llm.dspy_programs.runtime import ( DSPyReviewRuntime, DSPyRuntimeUnsupportedStateError, ) -from ...llm.guardrails import evaluate_llm_guardrails -from ...llm.policy import allowed_order_types -from ...llm.schema import MarketContextBundle -from ...llm.schema import MarketSnapshot as LLMMarketSnapshot -from ...market_context import ( - MarketContextClient, - MarketContextStatus, - evaluate_market_context, -) -from ...market_context_domains import ( - active_market_context_domain_states, - active_market_context_reasons, -) -from ...models import SignalEnvelope, StrategyDecision -from ...order_feed import OrderFeedIngestor -from ...paper_route_evidence import ( - PAPER_ROUTE_ACCOUNT_PRE_SESSION_READINESS_SECONDS, - PAPER_ROUTE_ACCOUNT_START_SNAPSHOT_AFTER_START_GRACE_SECONDS, -) +from ...models import StrategyDecision from ...portfolio import ( AllocationResult, - PortfolioSizingResult, - allocator_from_settings, - sizer_from_settings, -) -from ...prices import ClickHousePriceFetcher, MarketSnapshot, PriceFetcher -from ...quote_quality import ( - QuoteQualityPolicy, - QuoteQualityStatus, - SignalQuoteQualityTracker, - assess_signal_quote_quality, -) -from ...quantity_rules import ( - min_qty_for_symbol, - quantize_qty_for_symbol, - resolve_quantity_resolution, ) -from ...reconcile import Reconciler -from ...regime_hmm import ( - HMMRegimeContext, - resolve_hmm_context, - resolve_regime_context_authority_reason, -) -from ...risk import RiskEngine -from ...session_context import regular_session_open_utc_for -from ...tca import derive_adaptive_execution_policy from ...time_source import trading_now -from ...universe import UniverseResolver from ...submission_council import ( build_hypothesis_runtime_summary, build_live_submission_gate_payload, build_submission_gate_market_context_status, load_quant_evidence_status, ) -from ..pipeline_helpers import ( - _allocator_rejection_reasons, - _apply_projected_position_decision, - _attach_dspy_lineage, - _autonomy_gate_report_is_saturated_fail_sentinel, - _build_committee_veto_alignment_payload, - _build_llm_policy_resolution, - _build_portfolio_snapshot, - _classify_llm_error, - _clone_positions, - _coerce_bool, - _coerce_json, - _coerce_runtime_uncertainty_gate_action, - _coerce_strategy_symbols, - _committee_trace_has_veto, - _extract_json_error_payload, - _format_order_submit_rejection, - _hash_payload, - _is_runtime_risk_increasing_entry, - _llm_guardrail_controls_snapshot, - _load_recent_decisions, - _normalize_rollout_stage, - _optional_decimal, - _optional_int, - _price_snapshot_payload, - _project_open_orders_onto_positions, - _resolve_decision_regime_label_with_source, - _resolve_llm_review_error_reject_reason, - _resolve_llm_unavailable_reject_reason, - _resolve_signal_regime, - _select_strictest_runtime_uncertainty_gate, - _uncertainty_gate_staleness_reason, -) from ..safety import ( - _FRESH_TAIL_NO_SIGNAL_REASONS, - _is_market_session_open, - _latch_signal_continuity_alert_state, - _record_signal_continuity_recovery_cycle, - _signal_bootstrap_grace_active, - _signal_tail_is_fresh, + FRESH_TAIL_NO_SIGNAL_REASONS, + is_market_session_open, + latch_signal_continuity_alert_state, + signal_bootstrap_grace_active, + signal_tail_is_fresh, ) from ..state import ( - RuntimeUncertaintyGate, - RuntimeUncertaintyGateAction, - TradingState, - _normalize_reason_metric, + normalize_reason_metric, ) -# ruff: noqa: F401,F403,F405,F821,F821,F821 +from .contexts import ( + AllocationDecisionContext, + DecisionBlockRequest, + DecisionSubmissionContext, + DomainTelemetryEvent, + ExecutionPolicyRequest, + LiveSubmissionGateInputs, + RiskVerdictRequest, +) +from .shared import TradingPipelineBase +from .support import ( + apply_projected_position_decision, + coerce_json, + coerce_strategy_symbols, +) -from .part_01_statements_158 import * -from .part_02_tradingpipelinemethodspart1 import * -from .part_03_tradingpipelinemethodspart2 import * +logger = logging.getLogger(__name__) -class _TradingPipelineMethodsPart3: +class TradingPipelineDecisionLifecycleMixin(TradingPipelineBase): def _position_qty_for_symbol( self, positions: list[dict[str, Any]], @@ -205,12 +109,8 @@ def _sell_inventory_context( def _apply_allocation_results( self, *, - session: Session, + context: AllocationDecisionContext, allocation_results: list[AllocationResult], - strategies: list[Strategy], - account: dict[str, str], - positions: list[dict[str, Any]], - allowed_symbols: set[str], ) -> None: for allocation_result in allocation_results: self.state.metrics.record_allocator_result(allocation_result) @@ -218,15 +118,13 @@ def _apply_allocation_results( self.state.metrics.decisions_total += 1 try: submitted_decision = self._handle_decision( - session, + context, decision, - strategies, - account, - positions, - allowed_symbols, ) if submitted_decision is not None: - _apply_projected_position_decision(positions, submitted_decision) + apply_projected_position_decision( + context.positions, submitted_decision + ) except Exception: logger.exception( "Decision handling failed strategy_id=%s symbol=%s timeframe=%s", @@ -269,8 +167,8 @@ def record_no_signal_batch(self, batch: SignalBatch) -> None: self.state.last_ingest_window_end = batch.query_end self.state.last_ingest_reason = batch.no_signal_reason reason = batch.no_signal_reason - normalized_reason = _normalize_reason_metric(reason) - market_session_open = self._is_market_session_open() + normalized_reason = normalize_reason_metric(reason) + market_session_open = self.is_market_session_open() self.state.market_session_open = market_session_open self.state.metrics.market_session_open = 1 if market_session_open else 0 if batch.signal_lag_seconds is not None: @@ -314,7 +212,7 @@ def record_no_signal_batch(self, batch: SignalBatch) -> None: self.state.metrics.record_signal_expected_staleness(normalized_reason) if actionable and streak_threshold_met: - _latch_signal_continuity_alert_state(self.state, normalized_reason) + latch_signal_continuity_alert_state(self.state, normalized_reason) self.state.metrics.record_signal_staleness_alert(reason) logger.warning( "Signal continuity alert: reason=%s consecutive_no_signal=%s lag_seconds=%s market_session_open=%s", @@ -324,7 +222,7 @@ def record_no_signal_batch(self, batch: SignalBatch) -> None: market_session_open, ) elif actionable and lag_threshold_met: - _latch_signal_continuity_alert_state(self.state, normalized_reason) + latch_signal_continuity_alert_state(self.state, normalized_reason) self.state.metrics.record_signal_staleness_alert(reason) logger.warning( "Signal freshness alert: reason=%s lag_seconds=%s market_session_open=%s", @@ -333,7 +231,7 @@ def record_no_signal_batch(self, batch: SignalBatch) -> None: market_session_open, ) elif actionable and self.state.signal_continuity_alert_active: - _latch_signal_continuity_alert_state(self.state, normalized_reason) + latch_signal_continuity_alert_state(self.state, normalized_reason) elif not actionable and (streak_threshold_met or lag_threshold_met): logger.info( "Signal continuity observed as expected staleness reason=%s lag_seconds=%s market_session_open=%s", @@ -351,13 +249,13 @@ def _is_actionable_no_signal_reason( ) -> bool: if reason == "cursor_ahead_of_stream": return True - if reason in _FRESH_TAIL_NO_SIGNAL_REASONS: - if _signal_bootstrap_grace_active( + if reason in FRESH_TAIL_NO_SIGNAL_REASONS: + if signal_bootstrap_grace_active( self.state, grace_seconds=settings.trading_signal_bootstrap_grace_seconds, ): return False - if _signal_tail_is_fresh( + if signal_tail_is_fresh( reason, signal_lag_seconds, stale_lag_seconds=settings.trading_signal_stale_lag_alert_seconds, @@ -370,9 +268,12 @@ def _is_actionable_no_signal_reason( ) return reason not in expected_market_closed_reasons + def is_market_session_open(self, now: datetime | None = None) -> bool: + return self._is_market_session_open(now) + def _is_market_session_open(self, now: datetime | None = None) -> bool: trading_client = getattr(self.alpaca_client, "trading", None) - return _is_market_session_open(trading_client, now=now) + return is_market_session_open(trading_client, now=now) def reconcile(self) -> int: with self.session_factory() as session: @@ -383,86 +284,74 @@ def reconcile(self) -> int: def _handle_decision( self, - session: Session, - decision: StrategyDecision, - strategies: list[Strategy], - account: dict[str, str], - positions: list[dict[str, Any]], - allowed_symbols: set[str], + context: AllocationDecisionContext | Any, + decision: StrategyDecision | None = None, + *legacy_args: Any, ) -> StrategyDecision | None: + context, decision = self._handle_decision_request( + context, + decision, + legacy_args, + ) decision_row: Optional[TradeDecision] = None try: strategy_context = self._resolve_strategy_context( decision=decision, - strategies=strategies, - allowed_symbols=allowed_symbols, + strategies=context.strategies, + allowed_symbols=context.allowed_symbols, ) if strategy_context is None: return strategy, symbol_allowlist = strategy_context decision_row = self._ensure_pending_decision_row( - session=session, + session=context.session, decision=decision, strategy=strategy, ) if decision_row is None: return - prepared = self._prepare_decision_for_submission( - session=session, - decision=decision, + submission_context = DecisionSubmissionContext( + session=context.session, decision_row=decision_row, strategy=strategy, - account=account, - positions=positions, + account=context.account, + positions=context.positions, + symbol_allowlist=symbol_allowlist, ) - if prepared is None: - return - decision, snapshot = prepared - - policy_stage = self._evaluate_execution_policy_outcome( - session=session, - decision=decision, - decision_row=decision_row, - strategy=strategy, - positions=positions, - snapshot=snapshot, + policy_stage = self._prepare_decision_policy_stage( + context=submission_context, decision=decision ) if policy_stage is None: return decision, policy_outcome = policy_stage - if not self._passes_risk_verdict( - session=session, - decision=decision, - decision_row=decision_row, - strategy=strategy, - account=account, - positions=positions, - symbol_allowlist=symbol_allowlist, - execution_advisor=policy_outcome.advisor_metadata, - ): - return - if not self._is_trading_submission_allowed( - session=session, - decision=decision, - decision_row=decision_row, + if ( + not self._passes_risk_verdict( + RiskVerdictRequest( + context=submission_context, + decision=decision, + execution_advisor=policy_outcome.advisor_metadata, + ) + ) + or not self._is_trading_submission_allowed( + session=context.session, + decision=decision, + decision_row=decision_row, + ) + or not self._submit_decision_execution( + session=context.session, + decision=decision, + decision_row=decision_row, + policy_outcome=policy_outcome, + ) ): - return - - submitted = self._submit_decision_execution( - session=session, - decision=decision, - decision_row=decision_row, - policy_outcome=policy_outcome, - ) - if not submitted: return None return decision except Exception as exc: try: - session.rollback() + context.session.rollback() except Exception: logger.exception( "Decision handler rollback failed strategy_id=%s symbol=%s", @@ -481,7 +370,7 @@ def _handle_decision( self.state.metrics.record_decision_rejection_reasons([reason_code]) self.state.metrics.record_decision_state("rejected") self.executor.mark_rejected( - session, + context.session, decision_row, reason_code, metadata_update=self._decision_lifecycle_metadata( @@ -490,6 +379,51 @@ def _handle_decision( ) return None + @staticmethod + def _handle_decision_request( + context: AllocationDecisionContext | Any, + decision: StrategyDecision | None, + legacy_args: tuple[Any, ...], + ) -> tuple[AllocationDecisionContext, StrategyDecision]: + if isinstance(context, AllocationDecisionContext): + if decision is None: + raise TypeError("decision is required") + return context, decision + if decision is None or len(legacy_args) != 4: + raise TypeError("legacy _handle_decision call requires 6 arguments") + strategies, account, positions, allowed_symbols = legacy_args + return ( + AllocationDecisionContext( + session=context, + strategies=strategies, + account=account, + positions=positions, + allowed_symbols=allowed_symbols, + ), + decision, + ) + + def _prepare_decision_policy_stage( + self, + *, + context: DecisionSubmissionContext, + decision: StrategyDecision, + ) -> tuple[StrategyDecision, Any] | None: + prepared = self._prepare_decision_for_submission( + context=context, + decision=decision, + ) + if prepared is None: + return None + decision, snapshot = prepared + return self._evaluate_execution_policy_outcome( + ExecutionPolicyRequest( + context=context, + decision=decision, + snapshot=snapshot, + ) + ) + def _resolve_strategy_context( self, *, @@ -503,7 +437,7 @@ def _resolve_strategy_context( if strategy is None: return None - strategy_symbols = _coerce_strategy_symbols(strategy.universe_symbols) + strategy_symbols = coerce_strategy_symbols(strategy.universe_symbols) if strategy_symbols and allowed_symbols: return strategy, strategy_symbols & allowed_symbols if strategy_symbols: @@ -549,27 +483,27 @@ def _dspy_runtime_gate_status(self) -> dict[str, object]: def _live_submission_gate( self, *, - session: Session | None = None, - hypothesis_summary: Mapping[str, Any] | None = None, - empirical_jobs_status: Mapping[str, Any] | None = None, - dspy_runtime_status: Mapping[str, Any] | None = None, - quant_health_status: Mapping[str, Any] | None = None, + inputs: LiveSubmissionGateInputs | None = None, + **legacy_inputs: Any, ) -> dict[str, object]: + if inputs is None and legacy_inputs: + inputs = LiveSubmissionGateInputs(**legacy_inputs) + inputs = inputs or LiveSubmissionGateInputs() if ( - session is None - and hypothesis_summary is None - and empirical_jobs_status is None - and dspy_runtime_status is None - and quant_health_status is None + inputs.session is None + and inputs.hypothesis_summary is None + and inputs.empirical_jobs_status is None + and inputs.dspy_runtime_status is None + and inputs.quant_health_status is None and self._last_live_submission_gate is not None ): return dict(self._last_live_submission_gate) - summary = hypothesis_summary - if summary is None and session is not None: + summary = inputs.hypothesis_summary + if summary is None and inputs.session is not None: try: summary = build_hypothesis_runtime_summary( - session, + inputs.session, state=self.state, market_context_status=build_submission_gate_market_context_status( self.state @@ -586,11 +520,11 @@ def _live_submission_gate( }, } - empirical_status = empirical_jobs_status - if empirical_status is None and session is not None: + empirical_status = inputs.empirical_jobs_status + if empirical_status is None and inputs.session is not None: try: empirical_status = build_empirical_jobs_status( - session=session, + session=inputs.session, stale_after_seconds=settings.trading_empirical_job_stale_after_seconds, ) except Exception as exc: # pragma: no cover - additive runtime safety @@ -600,7 +534,7 @@ def _live_submission_gate( "message": f"empirical job status unavailable: {type(exc).__name__}", } - quant_status = quant_health_status + quant_status = inputs.quant_health_status if quant_status is None: quant_status = load_quant_evidence_status(account_label=self.account_label) @@ -608,10 +542,10 @@ def _live_submission_gate( self.state, hypothesis_summary=summary, empirical_jobs_status=empirical_status, - dspy_runtime_status=dspy_runtime_status, + dspy_runtime_status=inputs.dspy_runtime_status, quant_health_status=quant_status, quant_account_label=self.account_label, - session=session, + session=inputs.session, ) self._last_live_submission_gate = dict(gate) return gate @@ -659,36 +593,42 @@ def _decision_lifecycle_metadata( def _block_decision_submission( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - reason: str, - submission_stage: str, - capital_stage: str | None = None, - extra_metadata: Mapping[str, Any] | None = None, - severity: str = "warning", + request: DecisionBlockRequest | None = None, + **legacy_kwargs: Any, ) -> None: + if request is None: + request = DecisionBlockRequest( + session=legacy_kwargs["session"], + decision=legacy_kwargs["decision"], + decision_row=legacy_kwargs["decision_row"], + reason=legacy_kwargs["reason"], + submission_stage=legacy_kwargs["submission_stage"], + capital_stage=legacy_kwargs.get("capital_stage"), + extra_metadata=legacy_kwargs.get("extra_metadata"), + severity=legacy_kwargs.get("severity", "warning"), + ) metadata = self._decision_lifecycle_metadata( - submission_stage=submission_stage, - capital_stage=capital_stage, - extra=extra_metadata, + submission_stage=request.submission_stage, + capital_stage=request.capital_stage, + extra=request.extra_metadata, ) - self.state.metrics.record_submission_block(reason) + self.state.metrics.record_submission_block(request.reason) self.state.metrics.record_decision_state("blocked") self.executor.mark_blocked( - session, - decision_row, - reason, + request.session, + request.decision_row, + request.reason, metadata_update=metadata, ) self._emit_domain_telemetry( - event_name="torghut.decision.blocked", - severity=severity, - decision=decision, - decision_row=decision_row, - reason_codes=[reason], - extra_properties={"decision_status": "blocked"}, + DomainTelemetryEvent( + event_name="torghut.decision.blocked", + severity=request.severity, + decision=request.decision, + decision_row=request.decision_row, + reason_codes=[request.reason], + extra_properties={"decision_status": "blocked"}, + ) ) def _ensure_pending_decision_row( @@ -721,7 +661,7 @@ def _ensure_pending_decision_row( resolved_status = ( str(execution_status or "submitted").strip() or "submitted" ) - decision_json = _coerce_json(decision_row.decision_json) + decision_json = coerce_json(decision_row.decision_json) decision_json["submission_stage"] = "execution_backfilled" decision_json["control_plane_snapshot"] = coerce_json_payload( self._submission_control_plane_snapshot() @@ -761,16 +701,18 @@ def _expire_stale_planned_decision( return False self.state.metrics.planned_decisions_stale_total += 1 self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason="stale_planned_cleanup", - submission_stage="blocked_stale_planned_cleanup", - extra_metadata={ - "stale_planned_age_seconds": age_seconds, - "planned_decision_timeout_seconds": timeout_seconds, - }, - severity="error", + DecisionBlockRequest( + session=session, + decision=decision, + decision_row=decision_row, + reason="stale_planned_cleanup", + submission_stage="blocked_stale_planned_cleanup", + extra_metadata={ + "stale_planned_age_seconds": age_seconds, + "planned_decision_timeout_seconds": timeout_seconds, + }, + severity="error", + ) ) logger.error( "Recovered stale planned decision decision_id=%s strategy_id=%s symbol=%s age_seconds=%s timeout_seconds=%s", @@ -781,6 +723,3 @@ def _expire_stale_planned_decision( timeout_seconds, ) return True - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/part_08_tradingpipelinemethodspart7.py b/services/torghut/app/trading/scheduler/pipeline_modules/llm_outcomes.py similarity index 60% rename from services/torghut/app/trading/scheduler/pipeline_modules/part_08_tradingpipelinemethodspart7.py rename to services/torghut/app/trading/scheduler/pipeline_modules/llm_outcomes.py index 2fa4f4baad..087bde4c85 100644 --- a/services/torghut/app/trading/scheduler/pipeline_modules/part_08_tradingpipelinemethodspart7.py +++ b/services/torghut/app/trading/scheduler/pipeline_modules/llm_outcomes.py @@ -1,64 +1,29 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false """Trading pipeline implementation.""" from __future__ import annotations -import hashlib -import json -import inspect import logging -import os -from collections.abc import Callable, Mapping -from datetime import date, datetime, timedelta, timezone +from collections.abc import Mapping +from datetime import datetime, timedelta, timezone from decimal import Decimal -from pathlib import Path -from typing import Any, Optional, Sequence, cast +from typing import Any, Optional, cast -from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session -from ....alpaca_client import TorghutAlpacaClient from ....config import settings -from ....db import SessionLocal from ....models import ( - Execution, LLMDecisionReview, - PositionSnapshot, - RejectedSignalOutcomeEvent, - Strategy, TradeDecision, coerce_json_payload, ) -from ....observability import capture_posthog_event from ....snapshots import snapshot_account_and_positions -from ....strategies import StrategyCatalog -from ...autonomy.phase_manifest_contract import AUTONOMY_PHASE_ORDER -from ...decisions import DecisionEngine -from ...empirical_jobs import build_empirical_jobs_status -from ...execution import OrderExecutor -from ...execution_adapters import ExecutionAdapter -from ...execution_policy import ExecutionPolicy -from ...feature_quality import ( - REASON_STALENESS, - FeatureQualityThresholds, - evaluate_feature_batch_quality, -) -from ...features import extract_executable_price, optional_decimal, payload_value -from ...firewall import OrderFirewall, OrderFirewallBlocked -from ...ingest import ClickHouseSignalIngestor, SignalBatch -from ...lean_lanes import LeanLaneManager -from ...llm import LLMReviewEngine, apply_policy from ...llm.dspy_programs.runtime import ( - DSPyReviewRuntime, DSPyRuntimeUnsupportedStateError, ) -from ...llm.guardrails import evaluate_llm_guardrails from ...llm.policy import allowed_order_types from ...llm.schema import MarketContextBundle from ...llm.schema import MarketSnapshot as LLMMarketSnapshot from ...market_context import ( - MarketContextClient, MarketContextStatus, evaluate_market_context, ) @@ -67,121 +32,46 @@ active_market_context_reasons, ) from ...models import SignalEnvelope, StrategyDecision -from ...order_feed import OrderFeedIngestor -from ...paper_route_evidence import ( - PAPER_ROUTE_ACCOUNT_PRE_SESSION_READINESS_SECONDS, - PAPER_ROUTE_ACCOUNT_START_SNAPSHOT_AFTER_START_GRACE_SECONDS, -) -from ...portfolio import ( - AllocationResult, - PortfolioSizingResult, - allocator_from_settings, - sizer_from_settings, -) -from ...prices import ClickHousePriceFetcher, MarketSnapshot, PriceFetcher -from ...quote_quality import ( - QuoteQualityPolicy, - QuoteQualityStatus, - SignalQuoteQualityTracker, - assess_signal_quote_quality, -) -from ...quantity_rules import ( - min_qty_for_symbol, - quantize_qty_for_symbol, - resolve_quantity_resolution, -) -from ...reconcile import Reconciler -from ...regime_hmm import ( - HMMRegimeContext, - resolve_hmm_context, - resolve_regime_context_authority_reason, -) -from ...risk import RiskEngine -from ...session_context import regular_session_open_utc_for -from ...tca import derive_adaptive_execution_policy +from ...prices import MarketSnapshot from ...time_source import trading_now -from ...universe import UniverseResolver -from ...submission_council import ( - build_hypothesis_runtime_summary, - build_live_submission_gate_payload, - build_submission_gate_market_context_status, - load_quant_evidence_status, -) -from ..pipeline_helpers import ( - _allocator_rejection_reasons, - _apply_projected_position_decision, - _attach_dspy_lineage, - _autonomy_gate_report_is_saturated_fail_sentinel, - _build_committee_veto_alignment_payload, - _build_llm_policy_resolution, - _build_portfolio_snapshot, - _classify_llm_error, - _clone_positions, - _coerce_bool, - _coerce_json, - _coerce_runtime_uncertainty_gate_action, - _coerce_strategy_symbols, - _committee_trace_has_veto, - _extract_json_error_payload, - _format_order_submit_rejection, - _hash_payload, - _is_runtime_risk_increasing_entry, - _llm_guardrail_controls_snapshot, - _load_recent_decisions, - _normalize_rollout_stage, - _optional_decimal, - _optional_int, - _price_snapshot_payload, - _project_open_orders_onto_positions, - _resolve_decision_regime_label_with_source, - _resolve_llm_review_error_reject_reason, - _resolve_llm_unavailable_reject_reason, - _resolve_signal_regime, - _select_strictest_runtime_uncertainty_gate, - _uncertainty_gate_staleness_reason, -) -from ..safety import ( - _FRESH_TAIL_NO_SIGNAL_REASONS, - _is_market_session_open, - _latch_signal_continuity_alert_state, - _record_signal_continuity_recovery_cycle, - _signal_bootstrap_grace_active, - _signal_tail_is_fresh, +from ..pipeline_helpers import build_llm_policy_resolution + +from .contexts import ( + LLMReviewErrorRequest, + LLMReviewRecord, + LLMUnavailableRequest, + MarketContextBlockRequest, ) -from ..state import ( - RuntimeUncertaintyGate, - RuntimeUncertaintyGateAction, - TradingState, - _normalize_reason_metric, +from .shared import TradingPipelineBase +from .support import ( + attach_dspy_lineage, + build_committee_veto_alignment_payload, + build_portfolio_snapshot, + classify_llm_error, + coerce_json, + committee_trace_has_veto, + hash_payload, + llm_guardrail_controls_snapshot, + load_recent_decisions, + normalize_rollout_stage, + optional_decimal, + optional_int, + price_snapshot_payload, + resolve_llm_review_error_reject_reason, + resolve_llm_unavailable_reject_reason, ) -# ruff: noqa: F401,F403,F405,F821,F821,F821 +logger = logging.getLogger(__name__) -from .part_01_statements_158 import * -from .part_02_tradingpipelinemethodspart1 import * -from .part_03_tradingpipelinemethodspart2 import * -from .part_04_tradingpipelinemethodspart3 import * -from .part_05_tradingpipelinemethodspart4 import * -from .part_06_tradingpipelinemethodspart5 import * -from .part_07_tradingpipelinemethodspart6 import * - -class _TradingPipelineMethodsPart7: +class TradingPipelineReviewOutcomeMixin(TradingPipelineBase): def _maybe_handle_market_context_block( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - account: dict[str, str], - positions: list[dict[str, Any]], - guardrails: Any, - policy_resolution: dict[str, Any], - market_context: Optional[MarketContextBundle], - market_context_error: Optional[str], + request: MarketContextBlockRequest, ) -> tuple[StrategyDecision, Optional[str]] | None: + market_context = request.market_context market_context_status = evaluate_market_context(market_context) - if market_context_error is not None: + if request.market_context_error is not None: market_context_status = MarketContextStatus( allow_llm=False, reason="market_context_fetch_error", @@ -192,7 +82,7 @@ def _maybe_handle_market_context_block( self.state.metrics.llm_market_context_block_total += 1 market_context_shadow_mode = ( - guardrails.shadow_mode + request.guardrails.shadow_mode or settings.trading_market_context_fail_mode == "shadow_only" ) self.state.metrics.record_market_context_result( @@ -200,24 +90,23 @@ def _maybe_handle_market_context_block( shadow_mode=market_context_shadow_mode, ) return self._handle_llm_unavailable( - session, - decision, - decision_row, - account, - positions, - reason=market_context_status.reason or "market_context_unavailable", - reject_reason="market_context_block", - shadow_mode=market_context_shadow_mode, - effective_fail_mode=guardrails.effective_fail_mode, - risk_flags=market_context_status.risk_flags, - market_context=market_context, - response_payload_extra={ - "market_context": { - "reason": market_context_status.reason, - "risk_flags": list(market_context_status.risk_flags), - } - }, - policy_resolution=policy_resolution, + LLMUnavailableRequest( + context=request.context, + decision=request.decision, + reason=market_context_status.reason or "market_context_unavailable", + reject_reason="market_context_block", + shadow_mode=market_context_shadow_mode, + effective_fail_mode=request.guardrails.effective_fail_mode, + risk_flags=market_context_status.risk_flags, + market_context=market_context, + response_payload_extra={ + "market_context": { + "reason": market_context_status.reason, + "risk_flags": list(market_context_status.risk_flags), + } + }, + policy_resolution=request.policy_resolution, + ) ) def _record_market_context_observation( @@ -309,15 +198,15 @@ def _build_llm_response_json( if guardrails.reasons: response_json["mrm_guardrails"] = list(guardrails.reasons) response_json["policy_resolution"] = policy_resolution - response_json["guardrail_controls"] = _llm_guardrail_controls_snapshot() - committee_veto = _committee_trace_has_veto(response_json) + response_json["guardrail_controls"] = llm_guardrail_controls_snapshot() + committee_veto = committee_trace_has_veto(response_json) response_json["committee_veto_alignment"] = ( - _build_committee_veto_alignment_payload( + build_committee_veto_alignment_payload( committee_veto=committee_veto, deterministic_veto=policy_outcome.verdict == "veto", ) ) - _attach_dspy_lineage( + attach_dspy_lineage( response_json, artifact_source="runtime_review", ) @@ -337,7 +226,7 @@ def _record_llm_committee_metrics(self, response_json: Mapping[str, Any]) -> Non self.state.metrics.record_llm_committee_member( role=str(role), verdict=str(role_data.get("verdict", "unknown")), - latency_ms=_optional_int(role_data.get("latency_ms")), + latency_ms=optional_int(role_data.get("latency_ms")), schema_error=bool(role_data.get("schema_error", False)), ) @@ -379,7 +268,7 @@ def _finalize_llm_review_outcome( policy_outcome: Any, guardrails: Any, ) -> tuple[StrategyDecision, Optional[str]]: - committee_veto = _committee_trace_has_veto(outcome.response_json) + committee_veto = committee_trace_has_veto(outcome.response_json) if committee_veto: self.state.metrics.record_llm_committee_veto_alignment( committee_veto=True, @@ -396,27 +285,24 @@ def _finalize_llm_review_outcome( def _handle_llm_review_error( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - guardrails: Any, - policy_resolution: dict[str, Any], - engine: LLMReviewEngine, - request_json: dict[str, Any], - error: Exception, + request: LLMReviewErrorRequest, ) -> tuple[StrategyDecision, Optional[str]]: + decision = request.decision + request_json = request.request_json + policy_resolution = request.policy_resolution self.state.metrics.llm_error_total += 1 - unsupported_state_error = isinstance(error, DSPyRuntimeUnsupportedStateError) + unsupported_state_error = isinstance( + request.error, DSPyRuntimeUnsupportedStateError + ) if not unsupported_state_error: - engine.circuit_breaker.record_error() + request.engine.circuit_breaker.record_error() if unsupported_state_error: - policy_resolution = _build_llm_policy_resolution( - rollout_stage=guardrails.rollout_stage, + policy_resolution = build_llm_policy_resolution( + rollout_stage=request.guardrails.rollout_stage, effective_fail_mode="veto", - guardrail_reasons=guardrails.reasons, + guardrail_reasons=request.guardrails.reasons, ) - error_label = _classify_llm_error(error) + error_label = classify_llm_error(request.error) if error_label == "llm_response_not_json": self.state.metrics.llm_parse_error_total += 1 elif error_label == "llm_response_invalid": @@ -425,47 +311,50 @@ def _handle_llm_review_error( fallback = ( "veto" if unsupported_state_error - else self._resolve_llm_fallback(guardrails.effective_fail_mode) + else self._resolve_llm_fallback(request.guardrails.effective_fail_mode) ) effective_verdict = "veto" if fallback == "veto" else "approve" if not request_json: request_json = {"decision": decision.model_dump(mode="json")} response_json: dict[str, Any] = { - "error": str(error), + "error": str(request.error), "fallback": fallback, "effective_verdict": effective_verdict, "policy_resolution": policy_resolution, - "guardrail_controls": _llm_guardrail_controls_snapshot(), + "guardrail_controls": llm_guardrail_controls_snapshot(), "advisory_only": True, } - if guardrails.reasons: - response_json["mrm_guardrails"] = list(guardrails.reasons) - response_json["request_hash"] = _hash_payload(request_json) - response_json["response_hash"] = _hash_payload(response_json) + if request.guardrails.reasons: + response_json["mrm_guardrails"] = list(request.guardrails.reasons) + response_json["request_hash"] = hash_payload(request_json) + response_json["response_hash"] = hash_payload(response_json) self._persist_llm_review( - session=session, - decision_row=decision_row, - model=self._llm_runtime_model_identifier(), - prompt_version=self._llm_runtime_prompt_identifier(), - request_json=request_json, - response_json=response_json, - verdict="error", - confidence=None, - adjusted_qty=None, - adjusted_order_type=None, - rationale=f"llm_error_{fallback}", - risk_flags=[type(error).__name__] + list(guardrails.reasons), - tokens_prompt=None, - tokens_completion=None, + LLMReviewRecord( + session=request.context.session, + decision_row=request.context.decision_row, + model=self._llm_runtime_model_identifier(), + prompt_version=self._llm_runtime_prompt_identifier(), + request_json=request_json, + response_json=response_json, + verdict="error", + confidence=None, + adjusted_qty=None, + adjusted_order_type=None, + rationale=f"llm_error_{fallback}", + risk_flags=[type(request.error).__name__] + + list(request.guardrails.reasons), + tokens_prompt=None, + tokens_completion=None, + ) ) if unsupported_state_error: logger.warning( "Unsupported DSPy runtime state; vetoing decision_id=%s error=%s", - decision_row.id, - error, + request.context.decision_row.id, + request.error, ) return decision, "llm_unavailable_dspy_runtime_unsupported_state" - if guardrails.shadow_mode: + if request.guardrails.shadow_mode: self.state.metrics.llm_shadow_total += 1 if not settings.llm_shadow_mode: self.state.metrics.llm_guardrail_shadow_total += 1 @@ -473,58 +362,50 @@ def _handle_llm_review_error( if fallback == "veto": logger.warning( "LLM review failed; vetoing decision_id=%s error=%s", - decision_row.id, - error, + request.context.decision_row.id, + request.error, ) - return decision, _resolve_llm_review_error_reject_reason(error) + return decision, resolve_llm_review_error_reject_reason(request.error) logger.warning( "LLM review failed; pass-through decision_id=%s error=%s", - decision_row.id, - error, + request.context.decision_row.id, + request.error, ) return decision, None def _handle_llm_unavailable( self, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - account: dict[str, str], - positions: list[dict[str, Any]], - reason: str, - shadow_mode: bool, - effective_fail_mode: Optional[str] = None, - risk_flags: Optional[list[str]] = None, - market_context: Optional[MarketContextBundle] = None, - reject_reason: Optional[str] = None, - response_payload_extra: Optional[dict[str, Any]] = None, - policy_resolution: Optional[dict[str, Any]] = None, + request: LLMUnavailableRequest, ) -> tuple[StrategyDecision, Optional[str]]: - fallback = self._resolve_llm_fallback(effective_fail_mode) + context = request.context + decision = request.decision + fallback = self._resolve_llm_fallback(request.effective_fail_mode) effective_verdict = "veto" if fallback == "veto" else "approve" reject_reason = ( - _resolve_llm_unavailable_reject_reason(reason) - if fallback == "veto" and not shadow_mode + resolve_llm_unavailable_reject_reason(request.reason) + if fallback == "veto" and not request.shadow_mode else None ) self.state.metrics.record_llm_unavailable( - reason=reason, reject_reason=reject_reason + reason=request.reason, reject_reason=reject_reason + ) + portfolio_snapshot = build_portfolio_snapshot( + context.account, context.positions ) - portfolio_snapshot = _build_portfolio_snapshot(account, positions) market_snapshot = self._build_market_snapshot(decision) - recent_decisions = _load_recent_decisions( - session, + recent_decisions = load_recent_decisions( + context.session, decision.strategy_id, decision.symbol, ) if self.llm_review_engine is not None: request_payload = self.llm_review_engine.build_request( decision=decision, - account=account, - positions=positions, + account=context.account, + positions=context.positions, portfolio=portfolio_snapshot, market=market_snapshot, - market_context=market_context, + market_context=request.market_context, recent_decisions=recent_decisions, ).model_dump(mode="json") else: @@ -534,14 +415,14 @@ def _handle_llm_unavailable( "market": market_snapshot.model_dump(mode="json") if market_snapshot is not None else None, - "market_context": market_context.model_dump(mode="json") - if market_context is not None + "market_context": request.market_context.model_dump(mode="json") + if request.market_context is not None else None, "recent_decisions": [ summary.model_dump(mode="json") for summary in recent_decisions ], - "account": account, - "positions": positions, + "account": context.account, + "positions": context.positions, "policy": { "adjustment_allowed": settings.llm_adjustment_allowed, "min_qty_multiplier": str(settings.llm_min_qty_multiplier), @@ -554,60 +435,64 @@ def _handle_llm_unavailable( "prompt_version": f"dspy:{settings.llm_dspy_signature_version}", } response_payload = { - "error": reason, + "error": request.reason, "fallback": fallback, "effective_verdict": effective_verdict, "reject_reason": reject_reason, - "policy_resolution": policy_resolution - or _build_llm_policy_resolution( - rollout_stage=_normalize_rollout_stage(settings.llm_rollout_stage), + "policy_resolution": request.policy_resolution + or build_llm_policy_resolution( + rollout_stage=normalize_rollout_stage(settings.llm_rollout_stage), effective_fail_mode=fallback, - guardrail_reasons=risk_flags or [], + guardrail_reasons=request.risk_flags or [], ), "advisory_only": True, } - if response_payload_extra: - response_payload.update(response_payload_extra) + if request.response_payload_extra: + response_payload.update(request.response_payload_extra) decision_metadata_update: dict[str, Any] = {} - if response_payload_extra: - llm_runtime_payload = response_payload_extra.get("llm_runtime") + if request.response_payload_extra: + llm_runtime_payload = request.response_payload_extra.get("llm_runtime") if isinstance(llm_runtime_payload, Mapping): decision_metadata_update["llm_runtime"] = dict( cast(Mapping[str, Any], llm_runtime_payload) ) - market_context_payload = response_payload_extra.get("market_context") + market_context_payload = request.response_payload_extra.get( + "market_context" + ) if isinstance(market_context_payload, Mapping): decision_metadata_update["market_context"] = dict( cast(Mapping[str, Any], market_context_payload) ) if decision_metadata_update: self.executor.update_decision_json( - session, - decision_row, + context.session, + context.decision_row, decision_metadata_update, ) - response_payload["request_hash"] = _hash_payload(request_payload) - response_payload["response_hash"] = _hash_payload(response_payload) + response_payload["request_hash"] = hash_payload(request_payload) + response_payload["response_hash"] = hash_payload(response_payload) self._persist_llm_review( - session=session, - decision_row=decision_row, - model=self._llm_runtime_model_identifier(), - prompt_version=self._llm_runtime_prompt_identifier(), - request_json=request_payload, - response_json={ - **response_payload, - "guardrail_controls": _llm_guardrail_controls_snapshot(), - }, - verdict="error", - confidence=None, - adjusted_qty=None, - adjusted_order_type=None, - rationale=reason, - risk_flags=[reason] + (risk_flags or []), - tokens_prompt=None, - tokens_completion=None, + LLMReviewRecord( + session=context.session, + decision_row=context.decision_row, + model=self._llm_runtime_model_identifier(), + prompt_version=self._llm_runtime_prompt_identifier(), + request_json=request_payload, + response_json={ + **response_payload, + "guardrail_controls": llm_guardrail_controls_snapshot(), + }, + verdict="error", + confidence=None, + adjusted_qty=None, + adjusted_order_type=None, + rationale=request.reason, + risk_flags=[request.reason] + (request.risk_flags or []), + tokens_prompt=None, + tokens_completion=None, + ) ) - if shadow_mode: + if request.shadow_mode: self.state.metrics.llm_shadow_total += 1 if not settings.llm_shadow_mode: self.state.metrics.llm_guardrail_shadow_total += 1 @@ -641,8 +526,8 @@ def _build_market_snapshot( snapshot = MarketSnapshot( symbol=decision.symbol, as_of=decision.event_ts, - price=_optional_decimal(price), - spread=_optional_decimal(spread), + price=optional_decimal(price), + spread=optional_decimal(spread), source=source, ) else: @@ -683,7 +568,7 @@ def _resolve_pre_llm_executability_reject( caps = output_mapping.get("caps") per_symbol_cap = None if isinstance(caps, Mapping): - per_symbol_cap = _optional_decimal( + per_symbol_cap = optional_decimal( cast(Mapping[str, Any], caps).get("per_symbol") ) if limiting_constraint == "symbol_capacity_exhausted" or ( @@ -692,8 +577,8 @@ def _resolve_pre_llm_executability_reject( return "symbol_capacity_exhausted" fractional_allowed = output_mapping.get("fractional_allowed") - final_qty = _optional_decimal(output_mapping.get("final_qty")) - min_executable_qty = _optional_decimal(output_mapping.get("min_executable_qty")) + final_qty = optional_decimal(output_mapping.get("final_qty")) + min_executable_qty = optional_decimal(output_mapping.get("min_executable_qty")) if ( fractional_allowed is False and final_qty is not None @@ -722,7 +607,7 @@ def _ensure_decision_price( updated_params = dict(decision.params) if signal_price is None: updated_params["price"] = snapshot.price - updated_params["price_snapshot"] = _price_snapshot_payload(snapshot) + updated_params["price_snapshot"] = price_snapshot_payload(snapshot) if snapshot.spread is not None and "spread" not in updated_params: updated_params["spread"] = snapshot.spread return decision.model_copy(update={"params": updated_params}), snapshot @@ -768,24 +653,11 @@ def _get_account_snapshot(self, session: Session): @staticmethod def _persist_llm_review( - session: Session, - decision_row: TradeDecision, - model: str, - prompt_version: str, - request_json: dict[str, Any], - response_json: dict[str, Any], - verdict: str, - confidence: Optional[float], - adjusted_qty: Optional[Decimal], - adjusted_order_type: Optional[str], - rationale: Optional[str], - risk_flags: list[str], - tokens_prompt: Optional[int], - tokens_completion: Optional[int], + record: LLMReviewRecord, ) -> None: - request_payload = coerce_json_payload(request_json) - response_payload_json = dict(response_json) - _attach_dspy_lineage( + request_payload = coerce_json_payload(record.request_json) + response_payload_json = dict(record.response_json) + attach_dspy_lineage( response_payload_json, artifact_source="runtime_persisted_review", ) @@ -793,30 +665,32 @@ def _persist_llm_review( response_payload_json.get("committee_veto_alignment"), Mapping ): response_payload_json["committee_veto_alignment"] = ( - _build_committee_veto_alignment_payload( - committee_veto=_committee_trace_has_veto(response_payload_json), - deterministic_veto=verdict == "veto", + build_committee_veto_alignment_payload( + committee_veto=committee_trace_has_veto(response_payload_json), + deterministic_veto=record.verdict == "veto", ) ) response_payload = coerce_json_payload(response_payload_json) - risk_payload = coerce_json_payload(risk_flags) + risk_payload = coerce_json_payload(record.risk_flags) review = LLMDecisionReview( - trade_decision_id=decision_row.id, - model=model, - prompt_version=prompt_version, + trade_decision_id=record.decision_row.id, + model=record.model, + prompt_version=record.prompt_version, input_json=request_payload, response_json=response_payload, - verdict=verdict, - confidence=Decimal(str(confidence)) if confidence is not None else None, - adjusted_qty=adjusted_qty, - adjusted_order_type=adjusted_order_type, - rationale=rationale, + verdict=record.verdict, + confidence=Decimal(str(record.confidence)) + if record.confidence is not None + else None, + adjusted_qty=record.adjusted_qty, + adjusted_order_type=record.adjusted_order_type, + rationale=record.rationale, risk_flags=risk_payload, - tokens_prompt=tokens_prompt, - tokens_completion=tokens_completion, + tokens_prompt=record.tokens_prompt, + tokens_completion=record.tokens_completion, ) - session.add(review) - session.commit() + record.session.add(review) + record.session.commit() @staticmethod def _persist_llm_adjusted_decision( @@ -824,13 +698,10 @@ def _persist_llm_adjusted_decision( decision_row: TradeDecision, decision: StrategyDecision, ) -> None: - decision_json = _coerce_json(decision_row.decision_json) + decision_json = coerce_json(decision_row.decision_json) decision_json["llm_adjusted_decision"] = coerce_json_payload( decision.model_dump(mode="json") ) decision_row.decision_json = decision_json session.add(decision_row) session.commit() - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/part_07_tradingpipelinemethodspart6.py b/services/torghut/app/trading/scheduler/pipeline_modules/llm_review.py similarity index 56% rename from services/torghut/app/trading/scheduler/pipeline_modules/part_07_tradingpipelinemethodspart6.py rename to services/torghut/app/trading/scheduler/pipeline_modules/llm_review.py index 903b336e8e..cbe0176b7a 100644 --- a/services/torghut/app/trading/scheduler/pipeline_modules/part_07_tradingpipelinemethodspart6.py +++ b/services/torghut/app/trading/scheduler/pipeline_modules/llm_review.py @@ -1,171 +1,67 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false """Trading pipeline implementation.""" from __future__ import annotations +import logging import hashlib import json -import inspect -import logging -import os -from collections.abc import Callable, Mapping -from datetime import date, datetime, timedelta, timezone +from collections.abc import Mapping +from datetime import datetime, timezone from decimal import Decimal -from pathlib import Path from typing import Any, Optional, Sequence, cast from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session -from ....alpaca_client import TorghutAlpacaClient from ....config import settings -from ....db import SessionLocal from ....models import ( - Execution, - LLMDecisionReview, - PositionSnapshot, - RejectedSignalOutcomeEvent, Strategy, - TradeDecision, - coerce_json_payload, ) -from ....observability import capture_posthog_event -from ....snapshots import snapshot_account_and_positions -from ....strategies import StrategyCatalog -from ...autonomy.phase_manifest_contract import AUTONOMY_PHASE_ORDER -from ...decisions import DecisionEngine -from ...empirical_jobs import build_empirical_jobs_status -from ...execution import OrderExecutor -from ...execution_adapters import ExecutionAdapter -from ...execution_policy import ExecutionPolicy -from ...feature_quality import ( - REASON_STALENESS, - FeatureQualityThresholds, - evaluate_feature_batch_quality, -) -from ...features import extract_executable_price, optional_decimal, payload_value -from ...firewall import OrderFirewall, OrderFirewallBlocked -from ...ingest import ClickHouseSignalIngestor, SignalBatch -from ...lean_lanes import LeanLaneManager +from ...firewall import OrderFirewall from ...llm import LLMReviewEngine, apply_policy from ...llm.dspy_programs.runtime import ( DSPyReviewRuntime, - DSPyRuntimeUnsupportedStateError, ) from ...llm.guardrails import evaluate_llm_guardrails -from ...llm.policy import allowed_order_types -from ...llm.schema import MarketContextBundle -from ...llm.schema import MarketSnapshot as LLMMarketSnapshot -from ...market_context import ( - MarketContextClient, - MarketContextStatus, - evaluate_market_context, -) -from ...market_context_domains import ( - active_market_context_domain_states, - active_market_context_reasons, -) -from ...models import SignalEnvelope, StrategyDecision -from ...order_feed import OrderFeedIngestor -from ...paper_route_evidence import ( - PAPER_ROUTE_ACCOUNT_PRE_SESSION_READINESS_SECONDS, - PAPER_ROUTE_ACCOUNT_START_SNAPSHOT_AFTER_START_GRACE_SECONDS, -) +from ...models import StrategyDecision from ...portfolio import ( - AllocationResult, PortfolioSizingResult, - allocator_from_settings, sizer_from_settings, ) -from ...prices import ClickHousePriceFetcher, MarketSnapshot, PriceFetcher -from ...quote_quality import ( - QuoteQualityPolicy, - QuoteQualityStatus, - SignalQuoteQualityTracker, - assess_signal_quote_quality, -) from ...quantity_rules import ( min_qty_for_symbol, quantize_qty_for_symbol, resolve_quantity_resolution, ) -from ...reconcile import Reconciler -from ...regime_hmm import ( - HMMRegimeContext, - resolve_hmm_context, - resolve_regime_context_authority_reason, +from ..pipeline_helpers import build_llm_policy_resolution + +from .contexts import ( + LLMPolicyReviewRequest, + LLMReviewContext, + LLMReviewErrorRequest, + LLMReviewInputs, + LLMReviewRecord, + LLMReviewRunRequest, + LLMRuntimeReviewResult, + LLMRuntimeBlockRequest, + LLMUnavailableRequest, + MarketContextBlockRequest, ) -from ...risk import RiskEngine -from ...session_context import regular_session_open_utc_for -from ...tca import derive_adaptive_execution_policy -from ...time_source import trading_now -from ...universe import UniverseResolver -from ...submission_council import ( - build_hypothesis_runtime_summary, - build_live_submission_gate_payload, - build_submission_gate_market_context_status, - load_quant_evidence_status, +from .shared import ( + TradingPipelineBase, + RUNTIME_REGIME_CONFIDENCE_DEFAULT_THRESHOLDS, ) -from ..pipeline_helpers import ( - _allocator_rejection_reasons, - _apply_projected_position_decision, - _attach_dspy_lineage, - _autonomy_gate_report_is_saturated_fail_sentinel, - _build_committee_veto_alignment_payload, - _build_llm_policy_resolution, - _build_portfolio_snapshot, - _classify_llm_error, - _clone_positions, - _coerce_bool, - _coerce_json, - _coerce_runtime_uncertainty_gate_action, - _coerce_strategy_symbols, - _committee_trace_has_veto, - _extract_json_error_payload, - _format_order_submit_rejection, - _hash_payload, - _is_runtime_risk_increasing_entry, - _llm_guardrail_controls_snapshot, - _load_recent_decisions, - _normalize_rollout_stage, - _optional_decimal, - _optional_int, - _price_snapshot_payload, - _project_open_orders_onto_positions, - _resolve_decision_regime_label_with_source, - _resolve_llm_review_error_reject_reason, - _resolve_llm_unavailable_reject_reason, - _resolve_signal_regime, - _select_strictest_runtime_uncertainty_gate, - _uncertainty_gate_staleness_reason, +from .support import ( + build_portfolio_snapshot, + is_runtime_risk_increasing_entry, + load_recent_decisions, + optional_decimal, ) -from ..safety import ( - _FRESH_TAIL_NO_SIGNAL_REASONS, - _is_market_session_open, - _latch_signal_continuity_alert_state, - _record_signal_continuity_recovery_cycle, - _signal_bootstrap_grace_active, - _signal_tail_is_fresh, -) -from ..state import ( - RuntimeUncertaintyGate, - RuntimeUncertaintyGateAction, - TradingState, - _normalize_reason_metric, -) - -# ruff: noqa: F401,F403,F405,F821,F821,F821 -from .part_01_statements_158 import * -from .part_02_tradingpipelinemethodspart1 import * -from .part_03_tradingpipelinemethodspart2 import * -from .part_04_tradingpipelinemethodspart3 import * -from .part_05_tradingpipelinemethodspart4 import * -from .part_06_tradingpipelinemethodspart5 import * +logger = logging.getLogger(__name__) -class _TradingPipelineMethodsPart6: +class TradingPipelineReviewMixin(TradingPipelineBase): def _resolve_regime_confidence_thresholds( self, entropy_band: str, @@ -176,7 +72,7 @@ def _resolve_regime_confidence_thresholds( abstain_threshold, ) = settings.trading_runtime_regime_confidence_thresholds_by_entropy_band.get( normalized_entropy_band, - _RUNTIME_REGIME_CONFIDENCE_DEFAULT_THRESHOLDS, + RUNTIME_REGIME_CONFIDENCE_DEFAULT_THRESHOLDS, ) decimal_degrade_threshold = Decimal(str(degrade_threshold)) decimal_abstain_threshold = Decimal(str(abstain_threshold)) @@ -193,7 +89,7 @@ def _apply_runtime_uncertainty_gate( uncertainty_gate, regime_gate, gate = ( self._resolve_runtime_uncertainty_gate_components(decision) ) - risk_increasing_entry = _is_runtime_risk_increasing_entry(decision, positions) + risk_increasing_entry = is_runtime_risk_increasing_entry(decision, positions) payload: dict[str, Any] = { "action": gate.action, "source": gate.source, @@ -356,7 +252,7 @@ def _apply_portfolio_sizing( account: dict[str, str], positions: list[dict[str, Any]], ) -> PortfolioSizingResult: - equity = _optional_decimal(account.get("equity")) + equity = optional_decimal(account.get("equity")) sizer = sizer_from_settings(strategy, equity) return sizer.size(decision, account=account, positions=positions) @@ -367,17 +263,14 @@ def _load_strategies(session: Session) -> list[Strategy]: def _apply_llm_review( self, - session: Session, + context: LLMReviewContext, decision: StrategyDecision, - decision_row: TradeDecision, - account: dict[str, str], - positions: list[dict[str, Any]], ) -> tuple[StrategyDecision, Optional[str]]: if not settings.llm_enabled: return decision, None guardrails = evaluate_llm_guardrails() - policy_resolution = _build_llm_policy_resolution( + policy_resolution = build_llm_policy_resolution( rollout_stage=guardrails.rollout_stage, effective_fail_mode=guardrails.effective_fail_mode, guardrail_reasons=guardrails.reasons, @@ -385,110 +278,40 @@ def _apply_llm_review( self._record_llm_policy_resolution_metrics(policy_resolution) engine: LLMReviewEngine | None = None - if settings.llm_dspy_runtime_mode == "active": - gate_allowed, dspy_live_gate_reasons = settings.llm_dspy_live_runtime_gate() - if not gate_allowed: - reject_reason, runtime_subtype = self._classify_dspy_live_runtime_block( - dspy_live_gate_reasons - ) - return self._handle_llm_dspy_live_runtime_block( - session=session, - decision=decision, - decision_row=decision_row, - account=account, - positions=positions, - reason="llm_dspy_live_runtime_gate_blocked", - reject_reason=reject_reason, - risk_flags=list(dspy_live_gate_reasons), - response_payload_extra={ - "llm_runtime": { - "reject_reason": reject_reason, - "subtype": runtime_subtype, - "error": "llm_dspy_live_runtime_gate_blocked", - "primary_reason": ( - dspy_live_gate_reasons[0] - if dspy_live_gate_reasons - else "llm_dspy_live_runtime_gate_blocked" - ), - } - }, - policy_resolution=_build_llm_policy_resolution( - rollout_stage=guardrails.rollout_stage, - effective_fail_mode="veto", - guardrail_reasons=tuple(guardrails.reasons) - + tuple(dspy_live_gate_reasons), - ), - ) - - engine = self.llm_review_engine or LLMReviewEngine() - - dspy_runtime = getattr(engine, "dspy_runtime", None) - if isinstance(dspy_runtime, DSPyReviewRuntime): - dspy_live_ready, dspy_live_readiness_reasons = ( - dspy_runtime.evaluate_live_readiness() - ) - else: - dspy_live_ready, dspy_live_readiness_reasons = ( - DSPyReviewRuntime.from_settings().evaluate_live_readiness() - ) - - if not dspy_live_ready: - reject_reason, runtime_subtype = self._classify_dspy_live_runtime_block( - dspy_live_readiness_reasons - ) - return self._handle_llm_dspy_live_runtime_block( - session=session, - decision=decision, - decision_row=decision_row, - account=account, - positions=positions, - reason="llm_dspy_live_runtime_gate_blocked", - reject_reason=reject_reason, - risk_flags=list(dspy_live_readiness_reasons), - response_payload_extra={ - "llm_runtime": { - "reject_reason": reject_reason, - "subtype": runtime_subtype, - "error": "llm_dspy_live_runtime_gate_blocked", - "primary_reason": ( - dspy_live_readiness_reasons[0] - if dspy_live_readiness_reasons - else "llm_dspy_live_runtime_gate_blocked" - ), - } - }, - policy_resolution=_build_llm_policy_resolution( - rollout_stage=guardrails.rollout_stage, - effective_fail_mode="veto", - guardrail_reasons=tuple(guardrails.reasons) - + tuple(dspy_live_readiness_reasons), - ), - ) + runtime_review = self._resolve_active_dspy_runtime_review( + LLMPolicyReviewRequest( + context=context, + decision=decision, + guardrails=guardrails, + policy_resolution=policy_resolution, + ) + ) + if runtime_review.block is not None: + return runtime_review.block + engine = runtime_review.engine guardrail_block = self._handle_llm_guardrail_block( - session=session, - decision=decision, - decision_row=decision_row, - account=account, - positions=positions, - guardrails=guardrails, - policy_resolution=policy_resolution, + LLMPolicyReviewRequest( + context=context, + decision=decision, + guardrails=guardrails, + policy_resolution=policy_resolution, + ) ) if guardrail_block is not None: return guardrail_block if engine is None: - engine = self.llm_review_engine or LLMReviewEngine() + engine = cast(LLMReviewEngine, self.llm_review_engine or LLMReviewEngine()) circuit_open = self._handle_llm_circuit_open( - session=session, - decision=decision, - decision_row=decision_row, - account=account, - positions=positions, - guardrails=guardrails, - policy_resolution=policy_resolution, - engine=engine, + LLMPolicyReviewRequest( + context=context, + decision=decision, + guardrails=guardrails, + policy_resolution=policy_resolution, + engine=engine, + ) ) if circuit_open is not None: return circuit_open @@ -496,26 +319,26 @@ def _apply_llm_review( request_json: dict[str, Any] = {} try: return self._run_llm_review_request( - session=session, - decision=decision, - decision_row=decision_row, - account=account, - positions=positions, - guardrails=guardrails, - policy_resolution=policy_resolution, - engine=engine, - request_json=request_json, + LLMReviewRunRequest( + context=context, + decision=decision, + guardrails=guardrails, + policy_resolution=policy_resolution, + engine=engine, + request_json=request_json, + ) ) except Exception as exc: return self._handle_llm_review_error( - session=session, - decision=decision, - decision_row=decision_row, - guardrails=guardrails, - policy_resolution=policy_resolution, - engine=engine, - request_json=request_json, - error=exc, + LLMReviewErrorRequest( + context=context, + decision=decision, + guardrails=guardrails, + policy_resolution=policy_resolution, + engine=engine, + request_json=request_json, + error=exc, + ) ) def _record_llm_policy_resolution_metrics( @@ -532,102 +355,141 @@ def _record_llm_policy_resolution_metrics( if bool(policy_resolution.get("fail_mode_violation_active")): self.state.metrics.llm_fail_mode_override_total += 1 + def _resolve_active_dspy_runtime_review( + self, + request: LLMPolicyReviewRequest, + ) -> LLMRuntimeReviewResult: + if settings.llm_dspy_runtime_mode != "active": + return LLMRuntimeReviewResult(engine=request.engine) + gate_allowed, gate_reasons = settings.llm_dspy_live_runtime_gate() + if not gate_allowed: + return LLMRuntimeReviewResult( + engine=None, + block=self._handle_dspy_runtime_readiness_block(request, gate_reasons), + ) + + engine = request.engine or self.llm_review_engine or LLMReviewEngine() + dspy_runtime = getattr(engine, "dspy_runtime", None) + if isinstance(dspy_runtime, DSPyReviewRuntime): + ready, readiness_reasons = dspy_runtime.evaluate_live_readiness() + else: + ready, readiness_reasons = ( + DSPyReviewRuntime.from_settings().evaluate_live_readiness() + ) + if ready: + return LLMRuntimeReviewResult(engine=engine) + return LLMRuntimeReviewResult( + engine=engine, + block=self._handle_dspy_runtime_readiness_block( + request, + readiness_reasons, + ), + ) + + def _handle_dspy_runtime_readiness_block( + self, + request: LLMPolicyReviewRequest, + reasons: Sequence[str], + ) -> tuple[StrategyDecision, Optional[str]]: + reject_reason, runtime_subtype = self._classify_dspy_live_runtime_block(reasons) + response_payload_extra = { + "llm_runtime": { + "reject_reason": reject_reason, + "subtype": runtime_subtype, + "error": "llm_dspy_live_runtime_gate_blocked", + "primary_reason": ( + reasons[0] if reasons else "llm_dspy_live_runtime_gate_blocked" + ), + } + } + guardrails = request.guardrails + return self._handle_llm_dspy_live_runtime_block( + LLMRuntimeBlockRequest( + context=request.context, + decision=request.decision, + reason="llm_dspy_live_runtime_gate_blocked", + reject_reason=reject_reason, + risk_flags=list(reasons), + response_payload_extra=response_payload_extra, + policy_resolution=build_llm_policy_resolution( + rollout_stage=guardrails.rollout_stage, + effective_fail_mode="veto", + guardrail_reasons=tuple(guardrails.reasons) + tuple(reasons), + ), + ), + ) + def _handle_llm_guardrail_block( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - account: dict[str, str], - positions: list[dict[str, Any]], - guardrails: Any, - policy_resolution: dict[str, Any], + request: LLMPolicyReviewRequest, ) -> tuple[StrategyDecision, Optional[str]] | None: + guardrails = request.guardrails if guardrails.allow_requests: return None self.state.metrics.llm_guardrail_block_total += 1 return self._handle_llm_unavailable( - session, - decision, - decision_row, - account, - positions, - reason="llm_guardrail_blocked", - shadow_mode=True, - effective_fail_mode=guardrails.effective_fail_mode, - risk_flags=list(guardrails.reasons), - market_context=None, - policy_resolution=policy_resolution, + LLMUnavailableRequest( + context=request.context, + decision=request.decision, + reason="llm_guardrail_blocked", + shadow_mode=True, + effective_fail_mode=guardrails.effective_fail_mode, + risk_flags=list(guardrails.reasons), + market_context=None, + policy_resolution=request.policy_resolution, + ) ) def _handle_llm_circuit_open( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - account: dict[str, str], - positions: list[dict[str, Any]], - guardrails: Any, - policy_resolution: dict[str, Any], - engine: LLMReviewEngine, + request: LLMPolicyReviewRequest, ) -> tuple[StrategyDecision, Optional[str]] | None: + guardrails = request.guardrails + engine = cast(LLMReviewEngine, request.engine) if not engine.circuit_breaker.is_open(): return None self.state.metrics.llm_circuit_open_total += 1 return self._handle_llm_unavailable( - session, - decision, - decision_row, - account, - positions, - reason="llm_circuit_open", - shadow_mode=guardrails.shadow_mode, - effective_fail_mode=guardrails.effective_fail_mode, - market_context=None, - policy_resolution=policy_resolution, + LLMUnavailableRequest( + context=request.context, + decision=request.decision, + reason="llm_circuit_open", + shadow_mode=guardrails.shadow_mode, + effective_fail_mode=guardrails.effective_fail_mode, + market_context=None, + policy_resolution=request.policy_resolution, + ) ) def _handle_llm_dspy_live_runtime_block( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - account: dict[str, str], - positions: list[dict[str, Any]], - reason: str, - reject_reason: str, - risk_flags: list[str], - response_payload_extra: Optional[dict[str, Any]] = None, - policy_resolution: Optional[dict[str, Any]] = None, + request: LLMRuntimeBlockRequest, ) -> tuple[StrategyDecision, Optional[str]]: block_fail_mode = settings.llm_dspy_live_runtime_block_fail_mode - if reject_reason.startswith("llm_runtime_fallback"): + if request.reject_reason.startswith("llm_runtime_fallback"): self.state.metrics.llm_runtime_fallback_total += 1 effective_fail_mode = "veto" if block_fail_mode == "veto" else "pass_through" - passthrough_decision = decision + passthrough_decision = request.decision if block_fail_mode == "pass_through_reduced_size": passthrough_decision = self._degrade_llm_runtime_block_qty( - decision=decision, - positions=positions, - reason=reason, - risk_flags=risk_flags, + decision=request.decision, + positions=request.context.positions, + reason=request.reason, + risk_flags=request.risk_flags, ) return self._handle_llm_unavailable( - session, - passthrough_decision, - decision_row, - account, - positions, - reason=reason, - reject_reason=reject_reason, - shadow_mode=False, - effective_fail_mode=effective_fail_mode, - risk_flags=risk_flags, - market_context=None, - response_payload_extra=response_payload_extra, - policy_resolution=policy_resolution, + LLMUnavailableRequest( + context=request.context, + decision=passthrough_decision, + reason=request.reason, + reject_reason=request.reject_reason, + shadow_mode=False, + effective_fail_mode=effective_fail_mode, + risk_flags=request.risk_flags, + market_context=None, + response_payload_extra=request.response_payload_extra, + policy_resolution=request.policy_resolution, + ) ) @staticmethod @@ -761,26 +623,101 @@ def _degrade_llm_runtime_block_qty( def _run_llm_review_request( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - account: dict[str, str], - positions: list[dict[str, Any]], - guardrails: Any, - policy_resolution: dict[str, Any], - engine: LLMReviewEngine, - request_json: dict[str, Any], + request: LLMReviewRunRequest, ) -> tuple[StrategyDecision, Optional[str]]: + context = request.context + decision = request.decision + guardrails = request.guardrails + engine = request.engine + request_json = request.request_json + pre_llm_reject_reason = self._record_pre_llm_executability_reject(decision) + if pre_llm_reject_reason is not None: + return decision, pre_llm_reject_reason + + self.state.metrics.llm_requests_total += 1 + inputs = self._llm_review_inputs(request) + market_context_block = self._maybe_handle_market_context_block( + MarketContextBlockRequest( + context=context, + decision=decision, + guardrails=guardrails, + policy_resolution=request.policy_resolution, + market_context=inputs.market_context, + market_context_error=inputs.market_context_error, + ) + ) + if market_context_block is not None: + return market_context_block + + llm_request = engine.build_request( + decision, + context.account, + context.positions, + inputs.portfolio_snapshot, + inputs.market_snapshot, + inputs.market_context, + inputs.recent_decisions, + adjustment_allowed=guardrails.adjustment_allowed, + ) + request_json.update(llm_request.model_dump(mode="json")) + outcome = engine.review( + decision, + context.account, + context.positions, + request=llm_request, + portfolio=inputs.portfolio_snapshot, + market=inputs.market_snapshot, + market_context=inputs.market_context, + recent_decisions=inputs.recent_decisions, + ) + runtime_fallback = self._handle_llm_runtime_fallback(request, outcome) + if runtime_fallback is not None: + return runtime_fallback + self._record_llm_verdict_counter(outcome.response.verdict) + policy_outcome = apply_policy( + decision, + outcome.response, + adjustment_allowed=guardrails.adjustment_allowed, + ) + response_json = self._build_llm_response_json( + outcome=outcome, + policy_outcome=policy_outcome, + guardrails=guardrails, + policy_resolution=request.policy_resolution, + ) + self._persist_successful_llm_review( + request=request, + outcome=outcome, + policy_outcome=policy_outcome, + response_json=response_json, + ) + engine.circuit_breaker.record_success() + return self._finalize_llm_review_outcome( + decision=decision, + outcome=outcome, + policy_outcome=policy_outcome, + guardrails=guardrails, + ) + + def _record_pre_llm_executability_reject( + self, + decision: StrategyDecision, + ) -> str | None: pre_llm_reject_reason = self._resolve_pre_llm_executability_reject(decision) if pre_llm_reject_reason == "symbol_capacity_exhausted": self.state.metrics.pre_llm_capacity_reject_total += 1 - return decision, pre_llm_reject_reason + return pre_llm_reject_reason if pre_llm_reject_reason == "qty_below_min": self.state.metrics.pre_llm_qty_below_min_total += 1 - return decision, pre_llm_reject_reason + return pre_llm_reject_reason + return None - self.state.metrics.llm_requests_total += 1 + def _llm_review_inputs( + self, + request: LLMReviewRunRequest, + ) -> LLMReviewInputs: + decision = request.decision + context = request.context market_context, market_context_error = self._fetch_market_context( decision.symbol, as_of=decision.event_ts, @@ -792,67 +729,43 @@ def _run_llm_review_request( ) if market_context_error is not None: self.state.metrics.llm_market_context_error_total += 1 - - portfolio_snapshot = _build_portfolio_snapshot(account, positions) - market_snapshot = self._build_market_snapshot(decision) - recent_decisions = _load_recent_decisions( - session, - decision.strategy_id, - decision.symbol, - ) - market_context_block = self._maybe_handle_market_context_block( - session=session, - decision=decision, - decision_row=decision_row, - account=account, - positions=positions, - guardrails=guardrails, - policy_resolution=policy_resolution, + return LLMReviewInputs( + portfolio_snapshot=build_portfolio_snapshot( + context.account, + context.positions, + ), + market_snapshot=self._build_market_snapshot(decision), market_context=market_context, market_context_error=market_context_error, + recent_decisions=load_recent_decisions( + context.session, + decision.strategy_id, + decision.symbol, + ), ) - if market_context_block is not None: - return market_context_block - request = engine.build_request( - decision, - account, - positions, - portfolio_snapshot, - market_snapshot, - market_context, - recent_decisions, - adjustment_allowed=guardrails.adjustment_allowed, + def _handle_llm_runtime_fallback( + self, + request: LLMReviewRunRequest, + outcome: Any, + ) -> tuple[StrategyDecision, Optional[str]] | None: + if outcome.runtime_fallback is None: + return None + self.executor.update_decision_json( + request.context.session, + request.context.decision_row, + {"llm_runtime": outcome.runtime_fallback}, ) - request_json.update(request.model_dump(mode="json")) - outcome = engine.review( - decision, - account, - positions, - request=request, - portfolio=portfolio_snapshot, - market=market_snapshot, - market_context=market_context, - recent_decisions=recent_decisions, + runtime_error = str( + outcome.runtime_fallback.get("error") or "dspy_runtime_error" ) - if outcome.runtime_fallback is not None: - self.executor.update_decision_json( - session, - decision_row, - {"llm_runtime": outcome.runtime_fallback}, - ) - runtime_error = str( - outcome.runtime_fallback.get("error") or "dspy_runtime_error" - ) - runtime_subtype = str( - outcome.runtime_fallback.get("subtype") or "dspy_runtime_error" - ) - return self._handle_llm_dspy_live_runtime_block( - session=session, - decision=decision, - decision_row=decision_row, - account=account, - positions=positions, + runtime_subtype = str( + outcome.runtime_fallback.get("subtype") or "dspy_runtime_error" + ) + return self._handle_llm_dspy_live_runtime_block( + LLMRuntimeBlockRequest( + context=request.context, + decision=request.decision, reason=runtime_error, reject_reason=str( outcome.runtime_fallback.get("reject_reason") @@ -863,50 +776,41 @@ def _run_llm_review_request( "llm_runtime": outcome.runtime_fallback, "dspy": outcome.response_json.get("dspy"), }, - policy_resolution=policy_resolution, + policy_resolution=request.policy_resolution, ) - self._record_llm_verdict_counter(outcome.response.verdict) - policy_outcome = apply_policy( - decision, - outcome.response, - adjustment_allowed=guardrails.adjustment_allowed, - ) - response_json = self._build_llm_response_json( - outcome=outcome, - policy_outcome=policy_outcome, - guardrails=guardrails, - policy_resolution=policy_resolution, ) + + def _persist_successful_llm_review( + self, + *, + request: LLMReviewRunRequest, + outcome: Any, + policy_outcome: Any, + response_json: dict[str, Any], + ) -> None: + context = request.context self._record_llm_committee_metrics(response_json) self._record_llm_token_metrics(outcome) adjusted_qty, adjusted_order_type = self._apply_llm_policy_verdict( - session=session, - decision_row=decision_row, + session=context.session, + decision_row=context.decision_row, policy_outcome=policy_outcome, ) self._persist_llm_review( - session=session, - decision_row=decision_row, - model=outcome.model, - prompt_version=outcome.prompt_version, - request_json=outcome.request_json, - response_json=response_json, - verdict=policy_outcome.verdict, - confidence=outcome.response.confidence, - adjusted_qty=adjusted_qty, - adjusted_order_type=adjusted_order_type, - rationale=outcome.response.rationale, - risk_flags=outcome.response.risk_flags, - tokens_prompt=outcome.tokens_prompt, - tokens_completion=outcome.tokens_completion, - ) - engine.circuit_breaker.record_success() - return self._finalize_llm_review_outcome( - decision=decision, - outcome=outcome, - policy_outcome=policy_outcome, - guardrails=guardrails, + LLMReviewRecord( + session=context.session, + decision_row=context.decision_row, + model=outcome.model, + prompt_version=outcome.prompt_version, + request_json=outcome.request_json, + response_json=response_json, + verdict=policy_outcome.verdict, + confidence=outcome.response.confidence, + adjusted_qty=adjusted_qty, + adjusted_order_type=adjusted_order_type, + rationale=outcome.response.rationale, + risk_flags=outcome.response.risk_flags, + tokens_prompt=outcome.tokens_prompt, + tokens_completion=outcome.tokens_completion, + ) ) - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/part_01_statements_158.py b/services/torghut/app/trading/scheduler/pipeline_modules/part_01_statements_158.py deleted file mode 100644 index ed55889eaf..0000000000 --- a/services/torghut/app/trading/scheduler/pipeline_modules/part_01_statements_158.py +++ /dev/null @@ -1,196 +0,0 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false -"""Trading pipeline implementation.""" - -from __future__ import annotations - -import hashlib -import json -import inspect -import logging -import os -from collections.abc import Callable, Mapping -from datetime import date, datetime, timedelta, timezone -from decimal import Decimal -from pathlib import Path -from typing import Any, Optional, Sequence, cast - -from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session - -from ....alpaca_client import TorghutAlpacaClient -from ....config import settings -from ....db import SessionLocal -from ....models import ( - Execution, - LLMDecisionReview, - PositionSnapshot, - RejectedSignalOutcomeEvent, - Strategy, - TradeDecision, - coerce_json_payload, -) -from ....observability import capture_posthog_event -from ....snapshots import snapshot_account_and_positions -from ....strategies import StrategyCatalog -from ...autonomy.phase_manifest_contract import AUTONOMY_PHASE_ORDER -from ...decisions import DecisionEngine -from ...empirical_jobs import build_empirical_jobs_status -from ...execution import OrderExecutor -from ...execution_adapters import ExecutionAdapter -from ...execution_policy import ExecutionPolicy -from ...feature_quality import ( - REASON_STALENESS, - FeatureQualityThresholds, - evaluate_feature_batch_quality, -) -from ...features import extract_executable_price, optional_decimal, payload_value -from ...firewall import OrderFirewall, OrderFirewallBlocked -from ...ingest import ClickHouseSignalIngestor, SignalBatch -from ...lean_lanes import LeanLaneManager -from ...llm import LLMReviewEngine, apply_policy -from ...llm.dspy_programs.runtime import ( - DSPyReviewRuntime, - DSPyRuntimeUnsupportedStateError, -) -from ...llm.guardrails import evaluate_llm_guardrails -from ...llm.policy import allowed_order_types -from ...llm.schema import MarketContextBundle -from ...llm.schema import MarketSnapshot as LLMMarketSnapshot -from ...market_context import ( - MarketContextClient, - MarketContextStatus, - evaluate_market_context, -) -from ...market_context_domains import ( - active_market_context_domain_states, - active_market_context_reasons, -) -from ...models import SignalEnvelope, StrategyDecision -from ...order_feed import OrderFeedIngestor -from ...paper_route_evidence import ( - PAPER_ROUTE_ACCOUNT_PRE_SESSION_READINESS_SECONDS, - PAPER_ROUTE_ACCOUNT_START_SNAPSHOT_AFTER_START_GRACE_SECONDS, -) -from ...portfolio import ( - AllocationResult, - PortfolioSizingResult, - allocator_from_settings, - sizer_from_settings, -) -from ...prices import ClickHousePriceFetcher, MarketSnapshot, PriceFetcher -from ...quote_quality import ( - QuoteQualityPolicy, - QuoteQualityStatus, - SignalQuoteQualityTracker, - assess_signal_quote_quality, -) -from ...quantity_rules import ( - min_qty_for_symbol, - quantize_qty_for_symbol, - resolve_quantity_resolution, -) -from ...reconcile import Reconciler -from ...regime_hmm import ( - HMMRegimeContext, - resolve_hmm_context, - resolve_regime_context_authority_reason, -) -from ...risk import RiskEngine -from ...session_context import regular_session_open_utc_for -from ...tca import derive_adaptive_execution_policy -from ...time_source import trading_now -from ...universe import UniverseResolver -from ...submission_council import ( - build_hypothesis_runtime_summary, - build_live_submission_gate_payload, - build_submission_gate_market_context_status, - load_quant_evidence_status, -) -from ..pipeline_helpers import ( - _allocator_rejection_reasons, - _apply_projected_position_decision, - _attach_dspy_lineage, - _autonomy_gate_report_is_saturated_fail_sentinel, - _build_committee_veto_alignment_payload, - _build_llm_policy_resolution, - _build_portfolio_snapshot, - _classify_llm_error, - _clone_positions, - _coerce_bool, - _coerce_json, - _coerce_runtime_uncertainty_gate_action, - _coerce_strategy_symbols, - _committee_trace_has_veto, - _extract_json_error_payload, - _format_order_submit_rejection, - _hash_payload, - _is_runtime_risk_increasing_entry, - _llm_guardrail_controls_snapshot, - _load_recent_decisions, - _normalize_rollout_stage, - _optional_decimal, - _optional_int, - _price_snapshot_payload, - _project_open_orders_onto_positions, - _resolve_decision_regime_label_with_source, - _resolve_llm_review_error_reject_reason, - _resolve_llm_unavailable_reject_reason, - _resolve_signal_regime, - _select_strictest_runtime_uncertainty_gate, - _uncertainty_gate_staleness_reason, -) -from ..safety import ( - _FRESH_TAIL_NO_SIGNAL_REASONS, - _is_market_session_open, - _latch_signal_continuity_alert_state, - _record_signal_continuity_recovery_cycle, - _signal_bootstrap_grace_active, - _signal_tail_is_fresh, -) -from ..state import ( - RuntimeUncertaintyGate, - RuntimeUncertaintyGateAction, - TradingState, - _normalize_reason_metric, -) - -# ruff: noqa: F401,F403,F405,F821,F821,F821 - - -logger = logging.getLogger(__name__) - -_REJECTED_SIGNAL_OUTCOME_FOLLOWUP_HORIZON = timedelta(minutes=5) - -_REJECTED_SIGNAL_OUTCOME_LABEL_LIMIT = 25 - -_AUTONOMY_PHASE_ORDER: tuple[str, ...] = AUTONOMY_PHASE_ORDER - -_RUNTIME_UNCERTAINTY_DEGRADE_QTY_MULTIPLIER = Decimal("0.50") - -_RUNTIME_UNCERTAINTY_DEGRADE_MAX_PARTICIPATION_RATE = Decimal("0.05") - -_RUNTIME_UNCERTAINTY_DEGRADE_MIN_EXECUTION_SECONDS = 120 - -_RUNTIME_REGIME_CONFIDENCE_DEFAULT_THRESHOLDS = (Decimal("0.75"), Decimal("0.55")) - -_STRATEGY_POSITION_TAG_TOLERANCE = Decimal("0.0001") - -_STRATEGY_POSITION_TAG_LOOKBACK = timedelta(days=7) - - -def _normalized_symbol(symbol: object) -> str: - return str(symbol or "").strip().upper() - - -def _aware_utc(value: datetime) -> datetime: - if value.tzinfo is None: - return value.replace(tzinfo=timezone.utc) - return value.astimezone(timezone.utc) - - -class _TradingPipelineFieldsPart1: - """Orchestrate ingest -> decide -> risk -> execute for one cycle.""" - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/part_09_tradingpipeline.py b/services/torghut/app/trading/scheduler/pipeline_modules/part_09_tradingpipeline.py deleted file mode 100644 index eb78640d2c..0000000000 --- a/services/torghut/app/trading/scheduler/pipeline_modules/part_09_tradingpipeline.py +++ /dev/null @@ -1,187 +0,0 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false -"""Trading pipeline implementation.""" - -from __future__ import annotations - -import hashlib -import json -import inspect -import logging -import os -from collections.abc import Callable, Mapping -from datetime import date, datetime, timedelta, timezone -from decimal import Decimal -from pathlib import Path -from typing import Any, Optional, Sequence, cast - -from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session - -from ....alpaca_client import TorghutAlpacaClient -from ....config import settings -from ....db import SessionLocal -from ....models import ( - Execution, - LLMDecisionReview, - PositionSnapshot, - RejectedSignalOutcomeEvent, - Strategy, - TradeDecision, - coerce_json_payload, -) -from ....observability import capture_posthog_event -from ....snapshots import snapshot_account_and_positions -from ....strategies import StrategyCatalog -from ...autonomy.phase_manifest_contract import AUTONOMY_PHASE_ORDER -from ...decisions import DecisionEngine -from ...empirical_jobs import build_empirical_jobs_status -from ...execution import OrderExecutor -from ...execution_adapters import ExecutionAdapter -from ...execution_policy import ExecutionPolicy -from ...feature_quality import ( - REASON_STALENESS, - FeatureQualityThresholds, - evaluate_feature_batch_quality, -) -from ...features import extract_executable_price, optional_decimal, payload_value -from ...firewall import OrderFirewall, OrderFirewallBlocked -from ...ingest import ClickHouseSignalIngestor, SignalBatch -from ...lean_lanes import LeanLaneManager -from ...llm import LLMReviewEngine, apply_policy -from ...llm.dspy_programs.runtime import ( - DSPyReviewRuntime, - DSPyRuntimeUnsupportedStateError, -) -from ...llm.guardrails import evaluate_llm_guardrails -from ...llm.policy import allowed_order_types -from ...llm.schema import MarketContextBundle -from ...llm.schema import MarketSnapshot as LLMMarketSnapshot -from ...market_context import ( - MarketContextClient, - MarketContextStatus, - evaluate_market_context, -) -from ...market_context_domains import ( - active_market_context_domain_states, - active_market_context_reasons, -) -from ...models import SignalEnvelope, StrategyDecision -from ...order_feed import OrderFeedIngestor -from ...paper_route_evidence import ( - PAPER_ROUTE_ACCOUNT_PRE_SESSION_READINESS_SECONDS, - PAPER_ROUTE_ACCOUNT_START_SNAPSHOT_AFTER_START_GRACE_SECONDS, -) -from ...portfolio import ( - AllocationResult, - PortfolioSizingResult, - allocator_from_settings, - sizer_from_settings, -) -from ...prices import ClickHousePriceFetcher, MarketSnapshot, PriceFetcher -from ...quote_quality import ( - QuoteQualityPolicy, - QuoteQualityStatus, - SignalQuoteQualityTracker, - assess_signal_quote_quality, -) -from ...quantity_rules import ( - min_qty_for_symbol, - quantize_qty_for_symbol, - resolve_quantity_resolution, -) -from ...reconcile import Reconciler -from ...regime_hmm import ( - HMMRegimeContext, - resolve_hmm_context, - resolve_regime_context_authority_reason, -) -from ...risk import RiskEngine -from ...session_context import regular_session_open_utc_for -from ...tca import derive_adaptive_execution_policy -from ...time_source import trading_now -from ...universe import UniverseResolver -from ...submission_council import ( - build_hypothesis_runtime_summary, - build_live_submission_gate_payload, - build_submission_gate_market_context_status, - load_quant_evidence_status, -) -from ..pipeline_helpers import ( - _allocator_rejection_reasons, - _apply_projected_position_decision, - _attach_dspy_lineage, - _autonomy_gate_report_is_saturated_fail_sentinel, - _build_committee_veto_alignment_payload, - _build_llm_policy_resolution, - _build_portfolio_snapshot, - _classify_llm_error, - _clone_positions, - _coerce_bool, - _coerce_json, - _coerce_runtime_uncertainty_gate_action, - _coerce_strategy_symbols, - _committee_trace_has_veto, - _extract_json_error_payload, - _format_order_submit_rejection, - _hash_payload, - _is_runtime_risk_increasing_entry, - _llm_guardrail_controls_snapshot, - _load_recent_decisions, - _normalize_rollout_stage, - _optional_decimal, - _optional_int, - _price_snapshot_payload, - _project_open_orders_onto_positions, - _resolve_decision_regime_label_with_source, - _resolve_llm_review_error_reject_reason, - _resolve_llm_unavailable_reject_reason, - _resolve_signal_regime, - _select_strictest_runtime_uncertainty_gate, - _uncertainty_gate_staleness_reason, -) -from ..safety import ( - _FRESH_TAIL_NO_SIGNAL_REASONS, - _is_market_session_open, - _latch_signal_continuity_alert_state, - _record_signal_continuity_recovery_cycle, - _signal_bootstrap_grace_active, - _signal_tail_is_fresh, -) -from ..state import ( - RuntimeUncertaintyGate, - RuntimeUncertaintyGateAction, - TradingState, - _normalize_reason_metric, -) - -# ruff: noqa: F401,F403,F405,F821,F821,F821 - -from .part_01_statements_158 import * -from .part_02_tradingpipelinemethodspart1 import * -from .part_03_tradingpipelinemethodspart2 import * -from .part_04_tradingpipelinemethodspart3 import * -from .part_05_tradingpipelinemethodspart4 import * -from .part_06_tradingpipelinemethodspart5 import * -from .part_07_tradingpipelinemethodspart6 import * -from .part_08_tradingpipelinemethodspart7 import * - - -class TradingPipeline( - _TradingPipelineFieldsPart1, - _TradingPipelineMethodsPart1, - _TradingPipelineMethodsPart2, - _TradingPipelineMethodsPart3, - _TradingPipelineMethodsPart4, - _TradingPipelineMethodsPart5, - _TradingPipelineMethodsPart6, - _TradingPipelineMethodsPart7, - object, -): - pass - - -__all__ = ["TradingPipeline"] - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/part_02_tradingpipelinemethodspart1.py b/services/torghut/app/trading/scheduler/pipeline_modules/run_cycle.py similarity index 64% rename from services/torghut/app/trading/scheduler/pipeline_modules/part_02_tradingpipelinemethodspart1.py rename to services/torghut/app/trading/scheduler/pipeline_modules/run_cycle.py index 3b470dcba9..2f3e5041ee 100644 --- a/services/torghut/app/trading/scheduler/pipeline_modules/part_02_tradingpipelinemethodspart1.py +++ b/services/torghut/app/trading/scheduler/pipeline_modules/run_cycle.py @@ -1,209 +1,117 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false """Trading pipeline implementation.""" from __future__ import annotations -import hashlib -import json -import inspect import logging -import os -from collections.abc import Callable, Mapping +from collections.abc import Mapping from datetime import date, datetime, timedelta, timezone from decimal import Decimal -from pathlib import Path from typing import Any, Optional, Sequence, cast from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session -from ....alpaca_client import TorghutAlpacaClient from ....config import settings from ....db import SessionLocal from ....models import ( Execution, - LLMDecisionReview, PositionSnapshot, - RejectedSignalOutcomeEvent, Strategy, TradeDecision, - coerce_json_payload, ) -from ....observability import capture_posthog_event -from ....snapshots import snapshot_account_and_positions -from ....strategies import StrategyCatalog -from ...autonomy.phase_manifest_contract import AUTONOMY_PHASE_ORDER -from ...decisions import DecisionEngine -from ...empirical_jobs import build_empirical_jobs_status -from ...execution import OrderExecutor -from ...execution_adapters import ExecutionAdapter from ...execution_policy import ExecutionPolicy from ...feature_quality import ( REASON_STALENESS, FeatureQualityThresholds, evaluate_feature_batch_quality, ) -from ...features import extract_executable_price, optional_decimal, payload_value -from ...firewall import OrderFirewall, OrderFirewallBlocked -from ...ingest import ClickHouseSignalIngestor, SignalBatch +from ...ingest import SignalBatch from ...lean_lanes import LeanLaneManager -from ...llm import LLMReviewEngine, apply_policy -from ...llm.dspy_programs.runtime import ( - DSPyReviewRuntime, - DSPyRuntimeUnsupportedStateError, -) -from ...llm.guardrails import evaluate_llm_guardrails -from ...llm.policy import allowed_order_types -from ...llm.schema import MarketContextBundle -from ...llm.schema import MarketSnapshot as LLMMarketSnapshot from ...market_context import ( MarketContextClient, - MarketContextStatus, - evaluate_market_context, -) -from ...market_context_domains import ( - active_market_context_domain_states, - active_market_context_reasons, ) -from ...models import SignalEnvelope, StrategyDecision +from ...models import SignalEnvelope from ...order_feed import OrderFeedIngestor from ...paper_route_evidence import ( PAPER_ROUTE_ACCOUNT_PRE_SESSION_READINESS_SECONDS, PAPER_ROUTE_ACCOUNT_START_SNAPSHOT_AFTER_START_GRACE_SECONDS, ) -from ...portfolio import ( - AllocationResult, - PortfolioSizingResult, - allocator_from_settings, - sizer_from_settings, -) -from ...prices import ClickHousePriceFetcher, MarketSnapshot, PriceFetcher +from ...prices import ClickHousePriceFetcher from ...quote_quality import ( QuoteQualityPolicy, - QuoteQualityStatus, SignalQuoteQualityTracker, - assess_signal_quote_quality, -) -from ...quantity_rules import ( - min_qty_for_symbol, - quantize_qty_for_symbol, - resolve_quantity_resolution, -) -from ...reconcile import Reconciler -from ...regime_hmm import ( - HMMRegimeContext, - resolve_hmm_context, - resolve_regime_context_authority_reason, ) -from ...risk import RiskEngine from ...session_context import regular_session_open_utc_for -from ...tca import derive_adaptive_execution_policy from ...time_source import trading_now -from ...universe import UniverseResolver -from ...submission_council import ( - build_hypothesis_runtime_summary, - build_live_submission_gate_payload, - build_submission_gate_market_context_status, - load_quant_evidence_status, +from ..safety import ( + latch_signal_continuity_alert_state, + record_signal_continuity_recovery_cycle, ) -from ..pipeline_helpers import ( - _allocator_rejection_reasons, - _apply_projected_position_decision, - _attach_dspy_lineage, - _autonomy_gate_report_is_saturated_fail_sentinel, - _build_committee_veto_alignment_payload, - _build_llm_policy_resolution, - _build_portfolio_snapshot, - _classify_llm_error, - _clone_positions, - _coerce_bool, - _coerce_json, - _coerce_runtime_uncertainty_gate_action, - _coerce_strategy_symbols, - _committee_trace_has_veto, - _extract_json_error_payload, - _format_order_submit_rejection, - _hash_payload, - _is_runtime_risk_increasing_entry, - _llm_guardrail_controls_snapshot, - _load_recent_decisions, - _normalize_rollout_stage, - _optional_decimal, - _optional_int, - _price_snapshot_payload, - _project_open_orders_onto_positions, - _resolve_decision_regime_label_with_source, - _resolve_llm_review_error_reject_reason, - _resolve_llm_unavailable_reject_reason, - _resolve_signal_regime, - _select_strictest_runtime_uncertainty_gate, - _uncertainty_gate_staleness_reason, + + +from .contexts import ( + BatchSignalProcessingContext, + SessionWarmupWindow, + StrategyPositionExposureUpdate, + TradingPipelineRuntimeDependencies, ) -from ..safety import ( - _FRESH_TAIL_NO_SIGNAL_REASONS, - _is_market_session_open, - _latch_signal_continuity_alert_state, - _record_signal_continuity_recovery_cycle, - _signal_bootstrap_grace_active, - _signal_tail_is_fresh, +from .shared import ( + TradingPipelineBase, + STRATEGY_POSITION_TAG_LOOKBACK, + aware_utc, + normalized_symbol, + same_side_position_exposure, ) -from ..state import ( - RuntimeUncertaintyGate, - RuntimeUncertaintyGateAction, - TradingState, - _normalize_reason_metric, +from .support import ( + clone_positions, + coerce_strategy_symbols, + optional_decimal, + project_open_orders_onto_positions, ) -# ruff: noqa: F401,F403,F405,F821,F821,F821 +logger = logging.getLogger(__name__) -from .part_01_statements_158 import * - -class _TradingPipelineMethodsPart1: +class TradingPipelineRunCycleMixin(TradingPipelineBase): def __init__( self, - alpaca_client: TorghutAlpacaClient, - order_firewall: OrderFirewall, - ingestor: ClickHouseSignalIngestor, - decision_engine: DecisionEngine, - risk_engine: RiskEngine, - executor: OrderExecutor, - execution_adapter: ExecutionAdapter, - reconciler: Reconciler, - universe_resolver: UniverseResolver, - state: TradingState, - account_label: str, - session_factory: Callable[[], Session] = SessionLocal, - llm_review_engine: Optional[LLMReviewEngine] = None, - price_fetcher: Optional[PriceFetcher] = None, - strategy_catalog: StrategyCatalog | None = None, - execution_policy: Optional[ExecutionPolicy] = None, - order_feed_ingestor: OrderFeedIngestor | None = None, + dependencies: TradingPipelineRuntimeDependencies | None = None, + *legacy_args: Any, + **legacy_kwargs: Any, ) -> None: - self.alpaca_client = alpaca_client - self.order_firewall = order_firewall - self.ingestor = ingestor - self.decision_engine = decision_engine - self.risk_engine = risk_engine - self.executor = executor - self.execution_adapter = execution_adapter - self.reconciler = reconciler - self.universe_resolver = universe_resolver - self.state = state - self.account_label = account_label - self.session_factory = session_factory - self.price_fetcher = price_fetcher or ClickHousePriceFetcher() + dependencies = ( + dependencies + or TradingPipelineRuntimeDependencies.from_legacy_call( + legacy_args, + legacy_kwargs, + default_session_factory=SessionLocal, + ) + ) + self.alpaca_client = dependencies.alpaca_client + self.order_firewall = dependencies.order_firewall + self.ingestor = dependencies.ingestor + self.decision_engine = dependencies.decision_engine + self.risk_engine = dependencies.risk_engine + self.executor = dependencies.executor + self.execution_adapter = dependencies.execution_adapter + self.reconciler = dependencies.reconciler + self.universe_resolver = dependencies.universe_resolver + self.state = dependencies.state + self.account_label = dependencies.account_label + self.session_factory = dependencies.session_factory + self.price_fetcher = dependencies.price_fetcher or ClickHousePriceFetcher() if self.decision_engine.price_fetcher is None: self.decision_engine.price_fetcher = self.price_fetcher self._snapshot_cache = None self._snapshot_cached_at: Optional[datetime] = None - self.strategy_catalog = strategy_catalog - self.execution_policy = execution_policy or ExecutionPolicy() - self.order_feed_ingestor = order_feed_ingestor or OrderFeedIngestor() + self.strategy_catalog = dependencies.strategy_catalog + self.execution_policy = dependencies.execution_policy or ExecutionPolicy() + self.order_feed_ingestor = ( + dependencies.order_feed_ingestor or OrderFeedIngestor() + ) self.market_context_client = MarketContextClient() self.lean_lane_manager = LeanLaneManager() - self.llm_review_engine = llm_review_engine + self.llm_review_engine = dependencies.llm_review_engine self._last_live_submission_gate: dict[str, object] | None = None self._signal_quote_quality = SignalQuoteQualityTracker( policy=QuoteQualityPolicy( @@ -249,13 +157,15 @@ def run_once(self) -> None: ): return self._process_batch_signals( - session=session, - batch=batch, - strategies=strategies, - account_snapshot=account_snapshot, - account=account, - positions=positions, - allowed_symbols=allowed_symbols, + context=BatchSignalProcessingContext( + session=session, + batch=batch, + strategies=strategies, + account_snapshot=account_snapshot, + account=account, + positions=positions, + allowed_symbols=allowed_symbols, + ) ) self.ingestor.commit_cursor(session, batch) @@ -281,6 +191,34 @@ def _warm_session_context_from_open( if not callable(fetch_with_reason) or not callable(get_cursor): return + window = self._resolve_session_warmup_window(session, get_cursor) + if window is None: + return + warmup_batch = self._fetch_session_warmup_batch(fetch_with_reason, window) + if warmup_batch is None: + return + warmed = self._warm_session_context_signals( + warmup_batch, + strategies=strategies, + allowed_symbols=allowed_symbols, + ) + self._session_context_warmup_day = window.session_day + logger.info( + "Session context warmup complete account=%s start=%s end=%s limit=%s signals=%s max_seconds=%s max_signals=%s", + self.account_label, + window.start.isoformat(), + window.end.isoformat(), + window.limit, + warmed, + window.max_seconds, + window.max_signals, + ) + + def _resolve_session_warmup_window( + self, + session: Session, + get_cursor: Any, + ) -> SessionWarmupWindow | None: now = trading_now(account_label=self.account_label).astimezone(timezone.utc) session_open = regular_session_open_utc_for(now) session_day = session_open.date() @@ -326,24 +264,44 @@ def _warm_session_context_from_open( 1, int(settings.trading_session_context_warmup_signal_limit), ) - warmup_limit = min(warmup_signal_limit, max_warmup_signals) + return SessionWarmupWindow( + session_day=session_day, + start=warmup_start, + end=warmup_end, + limit=min(warmup_signal_limit, max_warmup_signals), + max_seconds=max_warmup_seconds, + max_signals=max_warmup_signals, + ) + + def _fetch_session_warmup_batch( + self, + fetch_with_reason: Any, + window: SessionWarmupWindow, + ) -> SignalBatch | None: try: - warmup_batch = cast( + return cast( SignalBatch, fetch_with_reason( - start=warmup_start, - end=warmup_end, - limit=warmup_limit, + start=window.start, + end=window.end, + limit=window.limit, ), ) except Exception: logger.exception( "Failed to fetch session context warmup signals start=%s end=%s", - warmup_start.isoformat(), - warmup_end.isoformat(), + window.start.isoformat(), + window.end.isoformat(), ) - return + return None + def _warm_session_context_signals( + self, + warmup_batch: SignalBatch, + *, + strategies: Sequence[Strategy] | None, + allowed_symbols: set[str] | None, + ) -> int: relevant_symbols = self._relevant_signal_symbols( strategies=strategies, allowed_symbols=allowed_symbols, @@ -352,7 +310,7 @@ def _warm_session_context_from_open( for signal in warmup_batch.signals: if ( relevant_symbols - and _normalized_symbol(signal.symbol) not in relevant_symbols + and normalized_symbol(signal.symbol) not in relevant_symbols ): continue try: @@ -367,17 +325,7 @@ def _warm_session_context_from_open( signal.event_ts, exc_info=True, ) - self._session_context_warmup_day = session_day - logger.info( - "Session context warmup complete account=%s start=%s end=%s limit=%s signals=%s max_seconds=%s max_signals=%s", - self.account_label, - warmup_start.isoformat(), - warmup_end.isoformat(), - warmup_limit, - warmed, - max_warmup_seconds, - max_warmup_signals, - ) + return warmed def _capture_runtime_window_account_snapshot_if_due(self, session: Session) -> None: if not ( @@ -447,7 +395,7 @@ def _prepare_batch_for_decisions( *, quality_signals: list[SignalEnvelope], ) -> bool: - market_session_open = self._is_market_session_open() + market_session_open = self.is_market_session_open() self.state.market_session_open = market_session_open self.state.metrics.market_session_open = 1 if market_session_open else 0 if not batch.signals: @@ -548,7 +496,7 @@ def _prepare_batch_for_decisions( self.state.last_signal_continuity_state = "signals_present" self.state.last_signal_continuity_reason = None self.state.last_signal_continuity_actionable = False - _record_signal_continuity_recovery_cycle( + record_signal_continuity_recovery_cycle( self.state, required_recovery_cycles=max( 1, int(settings.trading_signal_continuity_recovery_cycles) @@ -572,7 +520,7 @@ def _quality_gate_signals( filtered = [ signal for signal in signals - if _normalized_symbol(signal.symbol) in relevant_symbols + if normalized_symbol(signal.symbol) in relevant_symbols ] return filtered @@ -585,22 +533,22 @@ def _relevant_signal_symbols( if strategies is None: return set() normalized_allowed_symbols = { - _normalized_symbol(symbol) + normalized_symbol(symbol) for symbol in ( allowed_symbols if allowed_symbols is not None else self.universe_resolver.get_resolution().symbols ) - if _normalized_symbol(symbol) + if normalized_symbol(symbol) } relevant_symbols: set[str] = set() for strategy in strategies: if not strategy.enabled: continue strategy_symbols = { - _normalized_symbol(symbol) - for symbol in _coerce_strategy_symbols(strategy.universe_symbols) - if _normalized_symbol(symbol) + normalized_symbol(symbol) + for symbol in coerce_strategy_symbols(strategy.universe_symbols) + if normalized_symbol(symbol) } if strategy_symbols and normalized_allowed_symbols: relevant_symbols.update(strategy_symbols & normalized_allowed_symbols) @@ -619,7 +567,7 @@ def _build_run_context( "cash": str(account_snapshot.cash), "buying_power": str(account_snapshot.buying_power), } - snapshot_positions = _clone_positions(account_snapshot.positions) + snapshot_positions = clone_positions(account_snapshot.positions) positions = self._resolve_execution_context_positions( snapshot_positions, session=session, @@ -662,7 +610,7 @@ def _build_run_context( "universe_source_unavailable" ) self.state.metrics.record_universe_fail_safe_block(universe_reason) - _latch_signal_continuity_alert_state( + latch_signal_continuity_alert_state( self.state, "universe_source_unavailable" ) self.state.last_error = ( @@ -683,45 +631,61 @@ def _resolve_execution_context_positions( *, session: Session | None = None, ) -> list[dict[str, Any]]: - normalized_positions = _clone_positions(snapshot_positions) + normalized_positions = self._seed_execution_context_positions( + snapshot_positions + ) + self._project_open_orders_into_positions(normalized_positions) + if session is not None: + normalized_positions = self._attach_current_session_strategy_position_tags( + session, + normalized_positions, + ) + return normalized_positions + + def _seed_execution_context_positions( + self, + snapshot_positions: list[dict[str, Any]], + ) -> list[dict[str, Any]]: + fallback_positions = clone_positions(snapshot_positions) seed_snapshot = getattr(self.execution_adapter, "seed_positions_snapshot", None) - if callable(seed_snapshot): - try: - seed_snapshot(_clone_positions(snapshot_positions)) - except Exception as exc: - logger.warning( - "Failed to seed simulation execution positions account=%s error=%s", - self.account_label, - exc, - ) - else: - list_positions = getattr(self.execution_adapter, "list_positions", None) - if callable(list_positions): - try: - seeded_positions = list_positions() - except Exception as exc: - logger.warning( - "Failed to read simulation execution positions account=%s error=%s", - self.account_label, - exc, - ) - else: - if isinstance(seeded_positions, list): - normalized_positions: list[dict[str, Any]] = [] - for raw_position in cast(list[Any], seeded_positions): - if not isinstance(raw_position, Mapping): - continue - normalized_positions.append( - { - str(key): value - for key, value in cast( - Mapping[object, Any], raw_position - ).items() - } - ) + if not callable(seed_snapshot): + return fallback_positions + try: + seed_snapshot(clone_positions(snapshot_positions)) + except Exception as exc: + logger.warning( + "Failed to seed simulation execution positions account=%s error=%s", + self.account_label, + exc, + ) + return fallback_positions + list_positions = getattr(self.execution_adapter, "list_positions", None) + if not callable(list_positions): + return fallback_positions + try: + seeded_positions = list_positions() + except Exception as exc: + logger.warning( + "Failed to read simulation execution positions account=%s error=%s", + self.account_label, + exc, + ) + return fallback_positions + if not isinstance(seeded_positions, list): + return fallback_positions + return [ + {str(key): value for key, value in cast(Mapping[object, Any], item).items()} + for item in cast(list[Any], seeded_positions) + if isinstance(item, Mapping) + ] + + def _project_open_orders_into_positions( + self, + normalized_positions: list[dict[str, Any]], + ) -> None: projected_open_orders = self._resolve_execution_context_open_orders() if projected_open_orders: - projected_count = _project_open_orders_onto_positions( + projected_count = project_open_orders_onto_positions( normalized_positions, projected_open_orders, ) @@ -731,12 +695,6 @@ def _resolve_execution_context_positions( self.account_label, projected_count, ) - if session is not None: - normalized_positions = self._attach_current_session_strategy_position_tags( - session, - normalized_positions, - ) - return normalized_positions def _attach_current_session_strategy_position_tags( self, @@ -748,9 +706,31 @@ def _attach_current_session_strategy_position_tags( session_open = regular_session_open_utc_for( trading_now(account_label=self.account_label).astimezone(timezone.utc) ) - lookback_start = session_open - _STRATEGY_POSITION_TAG_LOOKBACK + lookback_start = session_open - STRATEGY_POSITION_TAG_LOOKBACK + rows = self._load_strategy_position_tag_rows(session, lookback_start) + if rows is None: + return positions + exposures = self._build_strategy_position_exposures(rows, session_open) + if not exposures: + return positions + tagged_positions: list[dict[str, Any]] = [] + for position in positions: + tagged_positions.extend( + self._attach_strategy_position_tags( + position, + exposures=exposures, + session_open=session_open, + ) + ) + return tagged_positions + + def _load_strategy_position_tag_rows( + self, + session: Session, + lookback_start: datetime, + ) -> Sequence[Any] | None: try: - rows = session.execute( + return session.execute( select(Execution, TradeDecision) .join(TradeDecision, Execution.trade_decision_id == TradeDecision.id) .where( @@ -766,99 +746,112 @@ def _attach_current_session_strategy_position_tags( "Failed to resolve strategy position tags account=%s", self.account_label, ) - return positions + return None + def _build_strategy_position_exposures( + self, + rows: Sequence[Any], + session_open: datetime, + ) -> dict[str, dict[str, dict[str, Any]]]: exposures: dict[str, dict[str, dict[str, Any]]] = {} for execution, decision_row in rows: - symbol = _normalized_symbol(execution.symbol or decision_row.symbol) - strategy_id = str(decision_row.strategy_id) - if not symbol or not strategy_id: + update = self._strategy_position_exposure_update(execution, decision_row) + if update is None: continue - execution_created_at = _aware_utc(execution.created_at) - filled_qty = _optional_decimal(execution.filled_qty) - if filled_qty is None or filled_qty <= 0: - continue - side = str(execution.side or "").strip().lower() - if side not in {"buy", "sell"}: - continue - signed_qty = filled_qty if side == "buy" else -filled_qty - strategy_exposures = exposures.setdefault(symbol, {}) - exposure = strategy_exposures.setdefault( - strategy_id, - { - "qty": Decimal("0"), - "buy_qty": Decimal("0"), - "buy_notional": Decimal("0"), - "session_qty": Decimal("0"), - "latest_execution_at": None, - "earliest_execution_at": None, - }, + self._record_strategy_position_exposure( + exposures, + update=update, + session_open=session_open, ) - exposure["qty"] = cast(Decimal, exposure["qty"]) + signed_qty - if execution_created_at >= session_open: - exposure["session_qty"] = ( - cast(Decimal, exposure["session_qty"]) + signed_qty - ) - avg_fill_price = _optional_decimal(execution.avg_fill_price) - if side == "buy" and avg_fill_price is not None and avg_fill_price > 0: - exposure["buy_qty"] = cast(Decimal, exposure["buy_qty"]) + filled_qty - exposure["buy_notional"] = cast( - Decimal, - exposure["buy_notional"], - ) + (filled_qty * avg_fill_price) - earliest_execution_at = exposure.get("earliest_execution_at") - if ( - earliest_execution_at is None - or execution_created_at < earliest_execution_at - ): - exposure["earliest_execution_at"] = execution_created_at - latest_execution_at = exposure.get("latest_execution_at") - if ( - latest_execution_at is None - or execution_created_at > latest_execution_at - ): - exposure["latest_execution_at"] = execution_created_at - - if not exposures: - return positions - - tagged_positions: list[dict[str, Any]] = [] - for position in positions: - tagged_positions.extend( - self._attach_strategy_position_tags( - position, - exposures=exposures, - session_open=session_open, - ) - ) - return tagged_positions + return exposures @staticmethod - def _same_side_position_exposure( - position_qty: Decimal, - exposure_qty: Decimal, - ) -> bool: - if position_qty == 0 or exposure_qty == 0: - return False - return (position_qty > 0 and exposure_qty > 0) or ( - position_qty < 0 and exposure_qty < 0 + def _strategy_position_exposure_update( + execution: Any, + decision_row: Any, + ) -> StrategyPositionExposureUpdate | None: + symbol = normalized_symbol(execution.symbol or decision_row.symbol) + strategy_id = str(decision_row.strategy_id) + if not symbol or not strategy_id: + return None + filled_qty = optional_decimal(execution.filled_qty) + if filled_qty is None or filled_qty <= 0: + return None + side = str(execution.side or "").strip().lower() + if side not in {"buy", "sell"}: + return None + signed_qty = filled_qty if side == "buy" else -filled_qty + return StrategyPositionExposureUpdate( + symbol=symbol, + strategy_id=strategy_id, + signed_qty=signed_qty, + filled_qty=filled_qty, + side=side, + execution_created_at=aware_utc(execution.created_at), + avg_fill_price=optional_decimal(execution.avg_fill_price), ) @staticmethod - def _attach_strategy_position_tag( - position: dict[str, Any], + def _empty_strategy_position_exposure() -> dict[str, Any]: + return { + "qty": Decimal("0"), + "buy_qty": Decimal("0"), + "buy_notional": Decimal("0"), + "session_qty": Decimal("0"), + "latest_execution_at": None, + "earliest_execution_at": None, + } + + @staticmethod + def _record_strategy_position_exposure( + exposures: dict[str, dict[str, dict[str, Any]]], *, - exposures: Mapping[str, Mapping[str, Mapping[str, Any]]], + update: StrategyPositionExposureUpdate, session_open: datetime, - ) -> dict[str, Any]: - tagged_positions = TradingPipeline._attach_strategy_position_tags( - position, - exposures=exposures, - session_open=session_open, + ) -> None: + strategy_exposures = exposures.setdefault(update.symbol, {}) + exposure = strategy_exposures.setdefault( + update.strategy_id, + TradingPipelineRunCycleMixin._empty_strategy_position_exposure(), + ) + exposure["qty"] = cast(Decimal, exposure["qty"]) + update.signed_qty + if update.execution_created_at >= session_open: + exposure["session_qty"] = ( + cast(Decimal, exposure["session_qty"]) + update.signed_qty + ) + if ( + update.side == "buy" + and update.avg_fill_price is not None + and update.avg_fill_price > 0 + ): + exposure["buy_qty"] = cast(Decimal, exposure["buy_qty"]) + update.filled_qty + exposure["buy_notional"] = cast( + Decimal, + exposure["buy_notional"], + ) + (update.filled_qty * update.avg_fill_price) + TradingPipelineRunCycleMixin._record_position_exposure_window( + exposure, + update.execution_created_at, ) - if len(tagged_positions) == 1: - return tagged_positions[0] - return position + @staticmethod + def _record_position_exposure_window( + exposure: dict[str, Any], + execution_created_at: datetime, + ) -> None: + earliest_execution_at = exposure.get("earliest_execution_at") + if ( + earliest_execution_at is None + or execution_created_at < earliest_execution_at + ): + exposure["earliest_execution_at"] = execution_created_at + latest_execution_at = exposure.get("latest_execution_at") + if latest_execution_at is None or execution_created_at > latest_execution_at: + exposure["latest_execution_at"] = execution_created_at -__all__ = [name for name in globals() if not name.startswith("__")] + @staticmethod + def same_side_position_exposure( + position_qty: Decimal, + exposure_qty: Decimal, + ) -> bool: + return same_side_position_exposure(position_qty, exposure_qty) diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/part_06_tradingpipelinemethodspart5.py b/services/torghut/app/trading/scheduler/pipeline_modules/runtime_gates.py similarity index 71% rename from services/torghut/app/trading/scheduler/pipeline_modules/part_06_tradingpipelinemethodspart5.py rename to services/torghut/app/trading/scheduler/pipeline_modules/runtime_gates.py index ce532266da..6f79ca162c 100644 --- a/services/torghut/app/trading/scheduler/pipeline_modules/part_06_tradingpipelinemethodspart5.py +++ b/services/torghut/app/trading/scheduler/pipeline_modules/runtime_gates.py @@ -1,168 +1,55 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false """Trading pipeline implementation.""" from __future__ import annotations -import hashlib -import json -import inspect import logging -import os -from collections.abc import Callable, Mapping -from datetime import date, datetime, timedelta, timezone +import json +from collections.abc import Mapping from decimal import Decimal from pathlib import Path -from typing import Any, Optional, Sequence, cast +from typing import Any, cast -from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session -from ....alpaca_client import TorghutAlpacaClient from ....config import settings -from ....db import SessionLocal from ....models import ( - Execution, - LLMDecisionReview, - PositionSnapshot, - RejectedSignalOutcomeEvent, - Strategy, TradeDecision, - coerce_json_payload, -) -from ....observability import capture_posthog_event -from ....snapshots import snapshot_account_and_positions -from ....strategies import StrategyCatalog -from ...autonomy.phase_manifest_contract import AUTONOMY_PHASE_ORDER -from ...decisions import DecisionEngine -from ...empirical_jobs import build_empirical_jobs_status -from ...execution import OrderExecutor -from ...execution_adapters import ExecutionAdapter -from ...execution_policy import ExecutionPolicy -from ...feature_quality import ( - REASON_STALENESS, - FeatureQualityThresholds, - evaluate_feature_batch_quality, -) -from ...features import extract_executable_price, optional_decimal, payload_value -from ...firewall import OrderFirewall, OrderFirewallBlocked -from ...ingest import ClickHouseSignalIngestor, SignalBatch -from ...lean_lanes import LeanLaneManager -from ...llm import LLMReviewEngine, apply_policy -from ...llm.dspy_programs.runtime import ( - DSPyReviewRuntime, - DSPyRuntimeUnsupportedStateError, -) -from ...llm.guardrails import evaluate_llm_guardrails -from ...llm.policy import allowed_order_types -from ...llm.schema import MarketContextBundle -from ...llm.schema import MarketSnapshot as LLMMarketSnapshot -from ...market_context import ( - MarketContextClient, - MarketContextStatus, - evaluate_market_context, -) -from ...market_context_domains import ( - active_market_context_domain_states, - active_market_context_reasons, -) -from ...models import SignalEnvelope, StrategyDecision -from ...order_feed import OrderFeedIngestor -from ...paper_route_evidence import ( - PAPER_ROUTE_ACCOUNT_PRE_SESSION_READINESS_SECONDS, - PAPER_ROUTE_ACCOUNT_START_SNAPSHOT_AFTER_START_GRACE_SECONDS, ) -from ...portfolio import ( - AllocationResult, - PortfolioSizingResult, - allocator_from_settings, - sizer_from_settings, -) -from ...prices import ClickHousePriceFetcher, MarketSnapshot, PriceFetcher -from ...quote_quality import ( - QuoteQualityPolicy, - QuoteQualityStatus, - SignalQuoteQualityTracker, - assess_signal_quote_quality, -) -from ...quantity_rules import ( - min_qty_for_symbol, - quantize_qty_for_symbol, - resolve_quantity_resolution, -) -from ...reconcile import Reconciler +from ...firewall import OrderFirewallBlocked +from ...models import StrategyDecision from ...regime_hmm import ( HMMRegimeContext, resolve_hmm_context, resolve_regime_context_authority_reason, ) -from ...risk import RiskEngine -from ...session_context import regular_session_open_utc_for -from ...tca import derive_adaptive_execution_policy -from ...time_source import trading_now -from ...universe import UniverseResolver -from ...submission_council import ( - build_hypothesis_runtime_summary, - build_live_submission_gate_payload, - build_submission_gate_market_context_status, - load_quant_evidence_status, -) -from ..pipeline_helpers import ( - _allocator_rejection_reasons, - _apply_projected_position_decision, - _attach_dspy_lineage, - _autonomy_gate_report_is_saturated_fail_sentinel, - _build_committee_veto_alignment_payload, - _build_llm_policy_resolution, - _build_portfolio_snapshot, - _classify_llm_error, - _clone_positions, - _coerce_bool, - _coerce_json, - _coerce_runtime_uncertainty_gate_action, - _coerce_strategy_symbols, - _committee_trace_has_veto, - _extract_json_error_payload, - _extract_top_regime_posterior_probability, - _format_order_submit_rejection, - _hash_payload, - _is_runtime_risk_increasing_entry, - _llm_guardrail_controls_snapshot, - _load_recent_decisions, - _normalize_rollout_stage, - _optional_decimal, - _optional_int, - _price_snapshot_payload, - _project_open_orders_onto_positions, - _resolve_decision_regime_label_with_source, - _resolve_llm_review_error_reject_reason, - _resolve_llm_unavailable_reject_reason, - _resolve_signal_regime, - _select_strictest_runtime_uncertainty_gate, - _uncertainty_gate_staleness_reason, -) -from ..safety import ( - _FRESH_TAIL_NO_SIGNAL_REASONS, - _is_market_session_open, - _latch_signal_continuity_alert_state, - _record_signal_continuity_recovery_cycle, - _signal_bootstrap_grace_active, - _signal_tail_is_fresh, -) from ..state import ( RuntimeUncertaintyGate, RuntimeUncertaintyGateAction, - TradingState, - _normalize_reason_metric, ) -# ruff: noqa: F401,F403,F405,F821,F821,F821 - -from .part_01_statements_158 import * -from .part_02_tradingpipelinemethodspart1 import * -from .part_03_tradingpipelinemethodspart2 import * -from .part_04_tradingpipelinemethodspart3 import * -from .part_05_tradingpipelinemethodspart4 import * +from .contexts import ( + DomainTelemetryEvent, + ExecutionFallbackRequest, + OrderSubmissionRequest, +) +from .shared import ( + TradingPipelineBase, + RUNTIME_UNCERTAINTY_DEGRADE_MAX_PARTICIPATION_RATE, + RUNTIME_UNCERTAINTY_DEGRADE_MIN_EXECUTION_SECONDS, + RUNTIME_UNCERTAINTY_DEGRADE_QTY_MULTIPLIER, +) +from .support import ( + autonomy_gate_report_is_saturated_fail_sentinel, + coerce_bool, + coerce_runtime_uncertainty_gate_action, + extract_json_error_payload, + extract_top_regime_posterior_probability, + format_order_submit_rejection, + optional_decimal, + resolve_decision_regime_label_with_source, + select_strictest_runtime_uncertainty_gate, + uncertainty_gate_staleness_reason, +) def _execution_quantity_resolution_mismatched( @@ -172,7 +59,8 @@ def _execution_quantity_resolution_mismatched( decision_sizing = decision.params.get("sizing") if not isinstance(decision_sizing, Mapping): return False - decision_resolution = decision_sizing.get("quantity_resolution") + decision_sizing_map = cast(Mapping[str, Any], decision_sizing) + decision_resolution = decision_sizing_map.get("quantity_resolution") if not isinstance(decision_resolution, Mapping): return False decision_resolution_map = cast(Mapping[str, Any], decision_resolution) @@ -190,14 +78,14 @@ def _runtime_uncertainty_gate_from_payload( payload: Mapping[str, Any], action_key: str, ) -> RuntimeUncertaintyGate | None: - staleness_reason = _uncertainty_gate_staleness_reason(source, payload) + staleness_reason = uncertainty_gate_staleness_reason(source, payload) if staleness_reason is not None: return RuntimeUncertaintyGate( action="abstain", source=f"{source}_stale", reason=staleness_reason, ) - action = _coerce_runtime_uncertainty_gate_action(payload.get(action_key)) + action = coerce_runtime_uncertainty_gate_action(payload.get(action_key)) if action is None: return None return RuntimeUncertaintyGate(action=action, source=source) @@ -233,7 +121,7 @@ def _runtime_uncertainty_gate_from_autonomy_report( def _runtime_uncertainty_gate_from_autonomy_payload( gate_map: Mapping[str, Any], ) -> RuntimeUncertaintyGate: - staleness_reason = _uncertainty_gate_staleness_reason( + staleness_reason = uncertainty_gate_staleness_reason( "autonomy_gate_report", gate_map ) if staleness_reason is not None: @@ -242,7 +130,7 @@ def _runtime_uncertainty_gate_from_autonomy_payload( source="autonomy_gate_report_stale", reason=staleness_reason, ) - gate_action = _coerce_runtime_uncertainty_gate_action( + gate_action = coerce_runtime_uncertainty_gate_action( gate_map.get("uncertainty_gate_action") ) if gate_action is None: @@ -258,14 +146,14 @@ def _runtime_uncertainty_gate_from_autonomy_action( gate_action: RuntimeUncertaintyGateAction, gate_map: Mapping[str, Any], ) -> RuntimeUncertaintyGate: - coverage_error = _optional_decimal(gate_map.get("coverage_error")) - shift_score = _optional_decimal(gate_map.get("shift_score")) - conformal_interval_width = _optional_decimal( + coverage_error = optional_decimal(gate_map.get("coverage_error")) + shift_score = optional_decimal(gate_map.get("shift_score")) + conformal_interval_width = optional_decimal( gate_map.get("conformal_interval_width") ) source = "autonomy_gate_report" reason = None - if _autonomy_gate_report_is_saturated_fail_sentinel( + if autonomy_gate_report_is_saturated_fail_sentinel( action=gate_action, coverage_error=coverage_error, shift_score=shift_score, @@ -297,7 +185,7 @@ def _runtime_regime_gate_from_decision_payload( reason="decision_regime_gate_unparseable", ) gate_map = cast(Mapping[str, Any], regime_gate) - gate_action = _coerce_runtime_uncertainty_gate_action(gate_map.get("action")) + gate_action = coerce_runtime_uncertainty_gate_action(gate_map.get("action")) if gate_action is None: return RuntimeUncertaintyGate( action="abstain", @@ -310,7 +198,7 @@ def _runtime_regime_gate_from_decision_payload( source="decision_regime_gate", regime_action_source="decision_regime_gate", regime_label=_optional_stripped_text(gate_map.get("regime_label")), - regime_stale=_coerce_bool(gate_map.get("regime_stale")), + regime_stale=coerce_bool(gate_map.get("regime_stale")), reason=_optional_stripped_text(gate_map.get("reason")), ) @@ -319,7 +207,7 @@ def _runtime_regime_gate_from_decision_label( decision: StrategyDecision, ) -> RuntimeUncertaintyGate: regime_label, regime_source, regime_fallback = ( - _resolve_decision_regime_label_with_source(decision) + resolve_decision_regime_label_with_source(decision) ) if regime_label: return RuntimeUncertaintyGate( @@ -382,7 +270,10 @@ def _optional_stripped_text(value: object) -> str | None: return str(value).strip() -class _TradingPipelineMethodsPart5: +logger = logging.getLogger(__name__) + + +class TradingPipelineRuntimeGatesMixin(TradingPipelineBase): def _maybe_record_lean_strategy_shadow( self, *, @@ -447,80 +338,85 @@ def _maybe_record_lean_strategy_shadow( def _submit_order_with_handling( self, - *, - session: Session, - execution_client: Any, - decision: StrategyDecision, - decision_row: TradeDecision, - selected_adapter_name: str, - retry_delays: list[int], + request: OrderSubmissionRequest | None = None, + **legacy_kwargs: Any, ) -> tuple[Any | None, bool]: + request = self._order_submission_request(request, legacy_kwargs) try: - retry_delays_seconds = [float(delay) for delay in retry_delays] + retry_delays_seconds = [float(delay) for delay in request.retry_delays] execution = self.executor.submit_order( - session, - execution_client, - decision, - decision_row, + request.session, + request.execution_client, + request.decision, + request.decision_row, self.account_label, - execution_expected_adapter=selected_adapter_name, + execution_expected_adapter=request.selected_adapter_name, retry_delays=retry_delays_seconds, ) return execution, False except OrderFirewallBlocked as exc: return self._handle_order_firewall_block( - session=session, - decision=decision, - decision_row=decision_row, - selected_adapter_name=selected_adapter_name, + request=request, exc=exc, ) except Exception as exc: return self._handle_order_submit_exception( - session=session, - decision=decision, - decision_row=decision_row, - selected_adapter_name=selected_adapter_name, + request=request, exc=exc, ) + @staticmethod + def _order_submission_request( + request: OrderSubmissionRequest | None, + legacy_kwargs: Mapping[str, Any], + ) -> OrderSubmissionRequest: + if request is not None: + return request + return OrderSubmissionRequest( + session=legacy_kwargs["session"], + execution_client=legacy_kwargs["execution_client"], + decision=legacy_kwargs["decision"], + decision_row=legacy_kwargs["decision_row"], + selected_adapter_name=legacy_kwargs["selected_adapter_name"], + retry_delays=legacy_kwargs["retry_delays"], + ) + def _handle_order_firewall_block( self, *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - selected_adapter_name: str, + request: OrderSubmissionRequest, exc: OrderFirewallBlocked, ) -> tuple[None, bool]: self.state.metrics.orders_rejected_total += 1 self.state.metrics.record_decision_state("rejected") self.state.metrics.record_execution_submit_result( status="rejected", - adapter=selected_adapter_name, + adapter=request.selected_adapter_name, ) self.state.metrics.record_decision_rejection_reasons([str(exc)]) self.executor.mark_rejected( - session, - decision_row, + request.session, + request.decision_row, str(exc), metadata_update=self._decision_lifecycle_metadata( submission_stage="rejected_submit" ), ) self._emit_domain_telemetry( - event_name="torghut.execution.rejected", - severity="warning", - decision=decision, - decision_row=decision_row, - reason_codes=[str(exc)], - extra_properties={"rejection_type": "firewall_blocked"}, + DomainTelemetryEvent( + event_name="torghut.execution.rejected", + severity="warning", + decision=request.decision, + decision_row=request.decision_row, + reason_codes=[str(exc)], + extra_properties={"rejection_type": "firewall_blocked"}, + ) ) logger.warning( "Order blocked by firewall strategy_id=%s decision_id=%s symbol=%s reason=%s", - decision.strategy_id, - decision_row.id, - decision.symbol, + request.decision.strategy_id, + request.decision_row.id, + request.decision.symbol, exc, ) return None, True @@ -528,10 +424,7 @@ def _handle_order_firewall_block( def _handle_order_submit_exception( self, *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - selected_adapter_name: str, + request: OrderSubmissionRequest, exc: Exception, ) -> tuple[None, bool]: self.state.metrics.orders_rejected_total += 1 @@ -539,13 +432,13 @@ def _handle_order_submit_exception( self.state.metrics.record_decision_rejection_reasons( [f"order_submit_error_{type(exc).__name__}"] ) - payload = _extract_json_error_payload(exc) or {} - self._record_local_pre_submit_rejection_metrics(decision, payload) - self._cancel_conflicting_precheck_order(decision_row, payload) - reason = _format_order_submit_rejection(exc) + payload = extract_json_error_payload(exc) or {} + self._record_local_pre_submit_rejection_metrics(request.decision, payload) + self._cancel_conflicting_precheck_order(request.decision_row, payload) + reason = format_order_submit_rejection(exc) self.executor.mark_rejected( - session, - decision_row, + request.session, + request.decision_row, reason, metadata_update=self._decision_lifecycle_metadata( submission_stage="rejected_submit", @@ -554,21 +447,23 @@ def _handle_order_submit_exception( ) self.state.metrics.record_execution_submit_result( status="rejected", - adapter=selected_adapter_name, + adapter=request.selected_adapter_name, ) self._emit_domain_telemetry( - event_name="torghut.execution.rejected", - severity="error", - decision=decision, - decision_row=decision_row, - reason_codes=[reason], - extra_properties={"rejection_type": "submit_failed"}, + DomainTelemetryEvent( + event_name="torghut.execution.rejected", + severity="error", + decision=request.decision, + decision_row=request.decision_row, + reason_codes=[reason], + extra_properties={"rejection_type": "submit_failed"}, + ) ) logger.warning( "Order submission failed strategy_id=%s decision_id=%s symbol=%s error=%s payload=%s", - decision.strategy_id, - decision_row.id, - decision.symbol, + request.decision.strategy_id, + request.decision_row.id, + request.decision.symbol, exc, payload, ) @@ -671,43 +566,41 @@ def _record_simulation_position_state( def _handle_execution_fallback( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - execution: Any, - selected_adapter_name: str, - actual_adapter_name: str, + request: ExecutionFallbackRequest, ) -> None: - if actual_adapter_name == selected_adapter_name: + if request.actual_adapter_name == request.selected_adapter_name: return - fallback_reason = execution.execution_fallback_reason + fallback_reason = request.execution.execution_fallback_reason self.state.metrics.record_execution_fallback( - expected_adapter=selected_adapter_name, - actual_adapter=actual_adapter_name, + expected_adapter=request.selected_adapter_name, + actual_adapter=request.actual_adapter_name, fallback_reason=fallback_reason or "adaptive_fallback", ) self._emit_domain_telemetry( - event_name="torghut.execution.fallback", - severity="warning", - decision=decision, - decision_row=decision_row, - execution=execution, - reason_codes=[fallback_reason or "adaptive_fallback"], - extra_properties={ - "execution_expected_adapter": selected_adapter_name, - "execution_actual_adapter": actual_adapter_name, - }, + DomainTelemetryEvent( + event_name="torghut.execution.fallback", + severity="warning", + decision=request.decision, + decision_row=request.decision_row, + execution=request.execution, + reason_codes=[fallback_reason or "adaptive_fallback"], + extra_properties={ + "execution_expected_adapter": request.selected_adapter_name, + "execution_actual_adapter": request.actual_adapter_name, + }, + ) + ) + self._evaluate_lean_canary_guard( + request.session, symbol=request.decision.symbol ) - self._evaluate_lean_canary_guard(session, symbol=decision.symbol) self.executor.update_decision_params( - session, - decision_row, + request.session, + request.decision_row, { "execution_adapter": { - "selected": selected_adapter_name, - "actual": actual_adapter_name, - "symbol": decision.symbol, + "selected": request.selected_adapter_name, + "actual": request.actual_adapter_name, + "symbol": request.decision.symbol, } }, ) @@ -740,7 +633,7 @@ def _resolve_runtime_uncertainty_gate_components( ) -> tuple[RuntimeUncertaintyGate, RuntimeUncertaintyGate, RuntimeUncertaintyGate]: uncertainty_gate = self._resolve_runtime_uncertainty_gate_from_inputs(decision) regime_gate = self._resolve_runtime_regime_gate(decision) - combined_gate = _select_strictest_runtime_uncertainty_gate( + combined_gate = select_strictest_runtime_uncertainty_gate( [uncertainty_gate, regime_gate] ) return uncertainty_gate, regime_gate, combined_gate @@ -758,12 +651,12 @@ def _resolve_runtime_uncertainty_degrade_profile( ) -> tuple[Decimal, Decimal, int]: regime_label = regime_gate.regime_label if regime_label is None: - regime_label, _, _ = _resolve_decision_regime_label_with_source(decision) + regime_label, _, _ = resolve_decision_regime_label_with_source(decision) regime_key = str(regime_label).strip().lower() if regime_label else "" - qty_multiplier = _RUNTIME_UNCERTAINTY_DEGRADE_QTY_MULTIPLIER - max_participation_rate = _RUNTIME_UNCERTAINTY_DEGRADE_MAX_PARTICIPATION_RATE - min_execution_seconds = _RUNTIME_UNCERTAINTY_DEGRADE_MIN_EXECUTION_SECONDS + qty_multiplier = RUNTIME_UNCERTAINTY_DEGRADE_QTY_MULTIPLIER + max_participation_rate = RUNTIME_UNCERTAINTY_DEGRADE_MAX_PARTICIPATION_RATE + min_execution_seconds = RUNTIME_UNCERTAINTY_DEGRADE_MIN_EXECUTION_SECONDS if regime_key: configured_qty_multiplier = settings.trading_runtime_uncertainty_degrade_qty_multipliers_by_regime.get( @@ -805,14 +698,14 @@ def _resolve_runtime_uncertainty_gate_from_inputs( action="degrade", source="uncertainty_input_missing" ) ) - return _select_strictest_runtime_uncertainty_gate(candidates) + return select_strictest_runtime_uncertainty_gate(candidates) def _append_direct_runtime_uncertainty_candidate( self, params: Mapping[str, Any], candidates: list[RuntimeUncertaintyGate], ) -> None: - direct_action = _coerce_runtime_uncertainty_gate_action( + direct_action = coerce_runtime_uncertainty_gate_action( params.get("uncertainty_gate_action") ) if direct_action is not None: @@ -905,7 +798,7 @@ def _resolve_runtime_regime_gate_from_hmm( reason="regime_hmm_parse_error", ) - regime_label, _, regime_fallback = _resolve_decision_regime_label_with_source( + regime_label, _, regime_fallback = resolve_decision_regime_label_with_source( decision ) regime_stale = bool( @@ -954,7 +847,7 @@ def _resolve_runtime_regime_confidence_gate( regime_context: HMMRegimeContext, regime_label: str | None, ) -> RuntimeUncertaintyGate | None: - top_posterior_probability = _extract_top_regime_posterior_probability( + top_posterior_probability = extract_top_regime_posterior_probability( regime_context.posterior ) if top_posterior_probability is None: @@ -982,6 +875,3 @@ def _resolve_runtime_regime_confidence_gate( reason="regime_hmm_confidence_is_uncertain", ) return None - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/shared.py b/services/torghut/app/trading/scheduler/pipeline_modules/shared.py new file mode 100644 index 0000000000..c0efaccc50 --- /dev/null +++ b/services/torghut/app/trading/scheduler/pipeline_modules/shared.py @@ -0,0 +1,591 @@ +"""Trading pipeline implementation.""" + +from __future__ import annotations + +import logging +from collections.abc import Callable +from datetime import datetime, timedelta, timezone +from decimal import Decimal +from typing import Any + + +from ...autonomy.phase_manifest_contract import AUTONOMY_PHASE_ORDER + + +logger = logging.getLogger(__name__) + +REJECTED_SIGNAL_OUTCOME_FOLLOWUP_HORIZON = timedelta(minutes=5) + +REJECTED_SIGNAL_OUTCOME_LABEL_LIMIT = 25 + +AUTONOMY_PHASE_ORDER_VALUES: tuple[str, ...] = AUTONOMY_PHASE_ORDER + +RUNTIME_UNCERTAINTY_DEGRADE_QTY_MULTIPLIER = Decimal("0.50") + +RUNTIME_UNCERTAINTY_DEGRADE_MAX_PARTICIPATION_RATE = Decimal("0.05") + +RUNTIME_UNCERTAINTY_DEGRADE_MIN_EXECUTION_SECONDS = 120 + +RUNTIME_REGIME_CONFIDENCE_DEFAULT_THRESHOLDS = (Decimal("0.75"), Decimal("0.55")) + +STRATEGY_POSITION_TAG_TOLERANCE = Decimal("0.0001") + +STRATEGY_POSITION_TAG_LOOKBACK = timedelta(days=7) + + +def normalized_symbol(symbol: object) -> str: + return str(symbol or "").strip().upper() + + +def aware_utc(value: datetime) -> datetime: + if value.tzinfo is None: + return value.replace(tzinfo=timezone.utc) + return value.astimezone(timezone.utc) + + +def same_side_position_exposure(position_qty: Decimal, exposure_qty: Decimal) -> bool: + if position_qty == 0 or exposure_qty == 0: + return False + return (position_qty > 0 and exposure_qty > 0) or ( + position_qty < 0 and exposure_qty < 0 + ) + + +class TradingPipelineBase: + """Typed contract shared by scheduler pipeline mixins.""" + + alpaca_client: Any + order_firewall: Any + ingestor: Any + decision_engine: Any + risk_engine: Any + executor: Any + execution_adapter: Any + reconciler: Any + universe_resolver: Any + state: Any + account_label: str + session_factory: Callable[[], Any] + price_fetcher: Any + strategy_catalog: Any + execution_policy: Any + order_feed_ingestor: Any + market_context_client: Any + lean_lane_manager: Any + llm_review_engine: Any + _snapshot_cache: Any + _snapshot_cached_at: Any + _last_live_submission_gate: Any + _signal_quote_quality: Any + _session_context_warmup_day: Any + _runtime_window_account_snapshot_day: Any + + def _append_autonomy_gate_report_uncertainty_candidate( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _append_direct_runtime_uncertainty_candidate( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _append_forecast_audit_uncertainty_candidate( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _append_runtime_payload_uncertainty_candidate( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _apply_allocation_results(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _apply_bounded_collection_exit_window_audit( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _apply_bounded_collection_target_sizing_audit( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _apply_llm_policy_verdict(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _apply_llm_review(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _apply_portfolio_sizing(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _apply_quote_lookup_diagnostic_reason(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _apply_runtime_uncertainty_gate(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _apply_simple_projected_buying_power(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _apply_simple_projected_position(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _attach_current_session_strategy_position_tags( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _attach_strategy_position_tag(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _attach_strategy_position_tags(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _block_decision_submission(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _bounded_collection_exit_window_elapsed(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _bounded_collection_exit_window_guarded_decision( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _bounded_collection_target_notional_sized_decision( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _bounded_collection_target_sizing_payload( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _bounded_degraded_qty(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _build_llm_response_json(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _build_market_snapshot(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _build_rejected_signal_outcome_payload(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _build_run_context(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _cancel_conflicting_precheck_order(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _capture_runtime_window_account_snapshot_if_due( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _classify_dspy_live_runtime_block(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _decision_has_executable_quote_payload(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _decision_lifecycle_metadata(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _decision_quote_snapshot_for_target_sizing( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _decision_row_age_seconds(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _degrade_llm_runtime_block_qty(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _degrade_runtime_uncertainty_gate_decision( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _dspy_runtime_gate_status(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _emit_domain_telemetry(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _ensure_decision_price(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _ensure_pending_decision_row(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _ensure_signal_executable_price(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _evaluate_execution_policy_outcome(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _evaluate_lean_canary_guard(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _evaluate_signal_decisions(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _execution_client_for_symbol(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _execution_client_name(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _expire_stale_planned_decision(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _feature_quality_failure_payload(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _fetch_market_context(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _finalize_llm_review_outcome(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _get_account_snapshot(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _handle_decision(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _handle_execution_fallback(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _handle_llm_circuit_open(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _handle_llm_dspy_live_runtime_block(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _handle_llm_guardrail_block(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _handle_llm_review_error(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _handle_llm_unavailable(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _handle_order_firewall_block(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _handle_order_submit_exception(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _ingest_order_feed(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _is_actionable_no_signal_reason(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _is_trading_submission_allowed(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _label_mature_rejected_signal_outcome_events( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _live_submission_gate(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _llm_runtime_model_identifier(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _llm_runtime_prompt_identifier(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _load_strategies(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _map_submit_exception(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _maybe_handle_market_context_block(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _maybe_record_lean_strategy_shadow(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _normalize_target_plan_action(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _paper_route_decision_requires_executable_quote( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _paper_route_params_with_quote_snapshot(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _paper_route_price_snapshot_payload(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _paper_route_quote_routeability(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _paper_route_quote_routeability_payload(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _paper_route_quote_routeability_retry_metadata( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _active_bounded_paper_route_target_window( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _paper_route_probe_exit_metadata(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _paper_route_probe_context(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _paper_route_probe_capped_decision(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _paper_route_retry_transition(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _profitability_proof_floor(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _proof_floor_submission_block_reason(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _proof_floor_symbol_block_reason(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _align_prechecked_paper_route_probe_cap(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _trading_now(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _paper_route_target_plan_source_mismatch( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _paper_route_target_quantity_resolution(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _paper_route_target_sizing_price(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _passes_risk_verdict(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _persist_llm_adjusted_decision(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _persist_llm_review(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _persist_rejected_signal_outcome_event(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _persist_runtime_uncertainty_gate_payload( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _position_qty_for_symbol(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _prepare_batch_for_decisions(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _prepare_decision_for_submission(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _prepare_run_once(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _process_batch_signals(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _quality_gate_signals(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _recheck_runtime_uncertainty_gate_after_llm( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _record_decision_rejection(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_ingest_window(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_lean_shadow_from_execution(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_llm_committee_metrics(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_llm_policy_resolution_metrics(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_llm_token_metrics(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_llm_verdict_counter(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_local_pre_submit_rejection_metrics( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _record_market_context_observation(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_quantity_resolution_metrics(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_rejected_signal_outcome_event(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_runtime_uncertainty_gate_result(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _record_simulation_position_state(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _reject_submit(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _rejected_signal_outcome_event_id(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _relevant_signal_symbols(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _reopen_rejected_paper_route_quote_routeability_decision( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _resolve_execution_context_open_orders(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _resolve_execution_context_positions(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _resolve_llm_fallback(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _resolve_pre_llm_executability_reject(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _resolve_regime_confidence_thresholds(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _resolve_runtime_regime_confidence_gate(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _resolve_runtime_regime_gate(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _resolve_runtime_regime_gate_from_hmm(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _resolve_runtime_uncertainty_degrade_profile( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _resolve_runtime_uncertainty_gate(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _resolve_runtime_uncertainty_gate_components( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _resolve_runtime_uncertainty_gate_from_inputs( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _resolve_strategy_context(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _run_llm_review_request(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def same_side_position_exposure(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _sell_inventory_context(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _should_degrade_runtime_uncertainty_fail( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _should_degrade_runtime_uncertainty_fail_from_payload( + self, *args: Any, **kwargs: Any + ) -> Any: + raise NotImplementedError + + def _simple_shortability_reason(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _split_strategy_position_tags(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _strategy_tagged_position(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _submission_capital_stage(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _submission_control_plane_snapshot(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _submit_decision_execution(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _submit_order_with_handling(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _sync_lean_observability(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _target_plan_action_for_symbol(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def _warm_session_context_from_open(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def is_market_session_open(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def reconcile(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def record_no_signal_batch(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError + + def run_once(self, *args: Any, **kwargs: Any) -> Any: + raise NotImplementedError diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/part_03_tradingpipelinemethodspart2.py b/services/torghut/app/trading/scheduler/pipeline_modules/signal_processing.py similarity index 74% rename from services/torghut/app/trading/scheduler/pipeline_modules/part_03_tradingpipelinemethodspart2.py rename to services/torghut/app/trading/scheduler/pipeline_modules/signal_processing.py index 223f11d72c..e4407b37e8 100644 --- a/services/torghut/app/trading/scheduler/pipeline_modules/part_03_tradingpipelinemethodspart2.py +++ b/services/torghut/app/trading/scheduler/pipeline_modules/signal_processing.py @@ -1,167 +1,80 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false """Trading pipeline implementation.""" from __future__ import annotations +import logging import hashlib import json import inspect -import logging -import os -from collections.abc import Callable, Mapping -from datetime import date, datetime, timedelta, timezone +from collections.abc import Mapping +from datetime import datetime, timedelta, timezone from decimal import Decimal -from pathlib import Path -from typing import Any, Optional, Sequence, cast +from typing import Any, cast from sqlalchemy import select from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session -from ....alpaca_client import TorghutAlpacaClient from ....config import settings -from ....db import SessionLocal from ....models import ( - Execution, - LLMDecisionReview, - PositionSnapshot, RejectedSignalOutcomeEvent, Strategy, - TradeDecision, - coerce_json_payload, -) -from ....observability import capture_posthog_event -from ....snapshots import snapshot_account_and_positions -from ....strategies import StrategyCatalog -from ...autonomy.phase_manifest_contract import AUTONOMY_PHASE_ORDER -from ...decisions import DecisionEngine -from ...empirical_jobs import build_empirical_jobs_status -from ...execution import OrderExecutor -from ...execution_adapters import ExecutionAdapter -from ...execution_policy import ExecutionPolicy -from ...feature_quality import ( - REASON_STALENESS, - FeatureQualityThresholds, - evaluate_feature_batch_quality, ) from ...features import extract_executable_price, optional_decimal, payload_value -from ...firewall import OrderFirewall, OrderFirewallBlocked -from ...ingest import ClickHouseSignalIngestor, SignalBatch -from ...lean_lanes import LeanLaneManager -from ...llm import LLMReviewEngine, apply_policy -from ...llm.dspy_programs.runtime import ( - DSPyReviewRuntime, - DSPyRuntimeUnsupportedStateError, -) -from ...llm.guardrails import evaluate_llm_guardrails -from ...llm.policy import allowed_order_types -from ...llm.schema import MarketContextBundle -from ...llm.schema import MarketSnapshot as LLMMarketSnapshot -from ...market_context import ( - MarketContextClient, - MarketContextStatus, - evaluate_market_context, -) -from ...market_context_domains import ( - active_market_context_domain_states, - active_market_context_reasons, -) +from ...ingest import SignalBatch from ...models import SignalEnvelope, StrategyDecision -from ...order_feed import OrderFeedIngestor -from ...paper_route_evidence import ( - PAPER_ROUTE_ACCOUNT_PRE_SESSION_READINESS_SECONDS, - PAPER_ROUTE_ACCOUNT_START_SNAPSHOT_AFTER_START_GRACE_SECONDS, -) from ...portfolio import ( - AllocationResult, - PortfolioSizingResult, allocator_from_settings, - sizer_from_settings, ) -from ...prices import ClickHousePriceFetcher, MarketSnapshot, PriceFetcher from ...quote_quality import ( - QuoteQualityPolicy, QuoteQualityStatus, - SignalQuoteQualityTracker, assess_signal_quote_quality, ) from ...quantity_rules import ( - min_qty_for_symbol, - quantize_qty_for_symbol, resolve_quantity_resolution, ) -from ...reconcile import Reconciler -from ...regime_hmm import ( - HMMRegimeContext, - resolve_hmm_context, - resolve_regime_context_authority_reason, +from .contexts import ( + AllocationDecisionContext, + BatchSignalProcessingContext, + PositionTagContext, + StrategyPositionTagRequest, ) -from ...risk import RiskEngine -from ...session_context import regular_session_open_utc_for -from ...tca import derive_adaptive_execution_policy -from ...time_source import trading_now -from ...universe import UniverseResolver -from ...submission_council import ( - build_hypothesis_runtime_summary, - build_live_submission_gate_payload, - build_submission_gate_market_context_status, - load_quant_evidence_status, -) -from ..pipeline_helpers import ( - _allocator_rejection_reasons, - _apply_projected_position_decision, - _attach_dspy_lineage, - _autonomy_gate_report_is_saturated_fail_sentinel, - _build_committee_veto_alignment_payload, - _build_llm_policy_resolution, - _build_portfolio_snapshot, - _classify_llm_error, - _clone_positions, - _coerce_bool, - _coerce_json, - _coerce_runtime_uncertainty_gate_action, - _coerce_strategy_symbols, - _committee_trace_has_veto, - _extract_json_error_payload, - _format_order_submit_rejection, - _hash_payload, - _is_runtime_risk_increasing_entry, - _llm_guardrail_controls_snapshot, - _load_recent_decisions, - _normalize_rollout_stage, - _optional_decimal, - _optional_int, - _price_snapshot_payload, - _project_open_orders_onto_positions, - _resolve_decision_regime_label_with_source, - _resolve_llm_review_error_reject_reason, - _resolve_llm_unavailable_reject_reason, - _resolve_signal_regime, - _select_strictest_runtime_uncertainty_gate, - _uncertainty_gate_staleness_reason, -) -from ..safety import ( - _FRESH_TAIL_NO_SIGNAL_REASONS, - _is_market_session_open, - _latch_signal_continuity_alert_state, - _record_signal_continuity_recovery_cycle, - _signal_bootstrap_grace_active, - _signal_tail_is_fresh, + +from .shared import ( + TradingPipelineBase, + REJECTED_SIGNAL_OUTCOME_FOLLOWUP_HORIZON, + REJECTED_SIGNAL_OUTCOME_LABEL_LIMIT, + STRATEGY_POSITION_TAG_LOOKBACK, + STRATEGY_POSITION_TAG_TOLERANCE, + normalized_symbol, + same_side_position_exposure, ) -from ..state import ( - RuntimeUncertaintyGate, - RuntimeUncertaintyGateAction, - TradingState, - _normalize_reason_metric, +from .support import ( + optional_decimal as scheduler_optional_decimal, + resolve_signal_regime, ) -# ruff: noqa: F401,F403,F405,F821,F821,F821 +logger = logging.getLogger(__name__) -from .part_01_statements_158 import * -from .part_02_tradingpipelinemethodspart1 import * +class TradingPipelineSignalProcessingMixin(TradingPipelineBase): + @staticmethod + def _attach_strategy_position_tag( + position: dict[str, Any], + *, + exposures: Mapping[str, Mapping[str, Mapping[str, Any]]], + session_open: datetime, + ) -> dict[str, Any]: + tagged_positions = ( + TradingPipelineSignalProcessingMixin._attach_strategy_position_tags( + position, + exposures=exposures, + session_open=session_open, + ) + ) + if len(tagged_positions) == 1: + return tagged_positions[0] + return position -class _TradingPipelineMethodsPart2: @staticmethod def _attach_strategy_position_tags( position: dict[str, Any], @@ -171,68 +84,85 @@ def _attach_strategy_position_tags( ) -> list[dict[str, Any]]: if str(position.get("strategy_id") or "").strip(): return [position] - symbol = _normalized_symbol(position.get("symbol")) - if not symbol: - return [position] - symbol_exposures = exposures.get(symbol) - if not symbol_exposures: - return [position] - raw_qty = ( - position.get("qty") - or position.get("quantity") - or position.get("qty_available") - or "0" + tag_context = TradingPipelineSignalProcessingMixin._position_tag_context( + position, exposures ) - raw_position_qty = _optional_decimal(raw_qty) - if raw_position_qty is None or raw_position_qty == 0: + if tag_context is None: return [position] - side = str(position.get("side") or "").strip().lower() - signed_position_qty = ( - -abs(raw_position_qty) - if side == "short" or raw_position_qty < 0 - else raw_position_qty - ) - position_qty = abs(raw_position_qty) - if signed_position_qty < 0: - side = "short" - elif side not in {"long", "short"}: - side = "long" same_side_exposures = [ (strategy_id, exposure) - for strategy_id, exposure in symbol_exposures.items() - if TradingPipeline._same_side_position_exposure( - signed_position_qty, + for strategy_id, exposure in tag_context.symbol_exposures.items() + if same_side_position_exposure( + tag_context.signed_position_qty, cast(Decimal, exposure.get("qty") or Decimal("0")), ) ] if len(same_side_exposures) != 1: - split_positions = TradingPipeline._split_strategy_position_tags( - position, - same_side_exposures=same_side_exposures, - signed_position_qty=signed_position_qty, - session_open=session_open, + split_positions = ( + TradingPipelineSignalProcessingMixin._split_strategy_position_tags( + position, + same_side_exposures=same_side_exposures, + signed_position_qty=tag_context.signed_position_qty, + session_open=session_open, + ) ) return split_positions or [position] strategy_id, exposure = same_side_exposures[0] exposure_qty = cast(Decimal, exposure.get("qty") or Decimal("0")) if ( - abs(abs(exposure_qty) - abs(signed_position_qty)) - > _STRATEGY_POSITION_TAG_TOLERANCE + abs(abs(exposure_qty) - abs(tag_context.signed_position_qty)) + > STRATEGY_POSITION_TAG_TOLERANCE ): return [position] return [ - TradingPipeline._strategy_tagged_position( - position, - strategy_id=strategy_id, - exposure=exposure, - qty=position_qty, - side=side, - session_open=session_open, + TradingPipelineSignalProcessingMixin._strategy_tagged_position( + StrategyPositionTagRequest( + position=position, + strategy_id=strategy_id, + exposure=exposure, + qty=tag_context.position_qty, + side=tag_context.side, + session_open=session_open, + ) ) ] + @staticmethod + def _position_tag_context( + position: dict[str, Any], + exposures: Mapping[str, Mapping[str, Mapping[str, Any]]], + ) -> PositionTagContext | None: + symbol = normalized_symbol(position.get("symbol")) + symbol_exposures = exposures.get(symbol) if symbol else None + if not symbol_exposures: + return None + raw_qty = ( + position.get("qty") + or position.get("quantity") + or position.get("qty_available") + or "0" + ) + raw_position_qty = scheduler_optional_decimal(raw_qty) + if raw_position_qty is None or raw_position_qty == 0: + return None + side = str(position.get("side") or "").strip().lower() + signed_position_qty = ( + -abs(raw_position_qty) + if side == "short" or raw_position_qty < 0 + else raw_position_qty + ) + normalized_side = "short" if signed_position_qty < 0 else side + if normalized_side not in {"long", "short"}: + normalized_side = "long" + return PositionTagContext( + symbol_exposures=symbol_exposures, + signed_position_qty=signed_position_qty, + position_qty=abs(raw_position_qty), + side=normalized_side, + ) + @staticmethod def _split_strategy_position_tags( position: dict[str, Any], @@ -252,7 +182,7 @@ def _split_strategy_position_tags( ) if ( abs(abs(exposure_total) - abs(signed_position_qty)) - > _STRATEGY_POSITION_TAG_TOLERANCE + > STRATEGY_POSITION_TAG_TOLERANCE ): return [] side = "short" if signed_position_qty < 0 else "long" @@ -262,65 +192,62 @@ def _split_strategy_position_tags( if exposure_qty == 0: continue split_positions.append( - TradingPipeline._strategy_tagged_position( - position, - strategy_id=strategy_id, - exposure=exposure, - qty=abs(exposure_qty), - side=side, - session_open=session_open, - split_from_aggregate=True, + TradingPipelineSignalProcessingMixin._strategy_tagged_position( + StrategyPositionTagRequest( + position=position, + strategy_id=strategy_id, + exposure=exposure, + qty=abs(exposure_qty), + side=side, + session_open=session_open, + split_from_aggregate=True, + ) ) ) return split_positions @staticmethod def _strategy_tagged_position( - position: dict[str, Any], - *, - strategy_id: str, - exposure: Mapping[str, Any], - qty: Decimal, - side: str, - session_open: datetime, - split_from_aggregate: bool = False, + request: StrategyPositionTagRequest, ) -> dict[str, Any]: - tagged = dict(position) - tagged["strategy_id"] = strategy_id - tagged["qty"] = str(qty) - tagged["side"] = side or "long" - earliest_execution_at = exposure.get("earliest_execution_at") + tagged = dict(request.position) + tagged["strategy_id"] = request.strategy_id + tagged["qty"] = str(request.qty) + tagged["side"] = request.side or "long" + earliest_execution_at = request.exposure.get("earliest_execution_at") stale_position = ( isinstance(earliest_execution_at, datetime) - and earliest_execution_at < session_open + and earliest_execution_at < request.session_open ) tagged["strategy_position_source"] = ( "open_exposure_filled_executions" if stale_position else "current_session_filled_executions" ) - tagged["strategy_position_session_open"] = session_open.isoformat() + tagged["strategy_position_session_open"] = request.session_open.isoformat() if stale_position and isinstance(earliest_execution_at, datetime): tagged["strategy_position_stale_session_repair"] = True tagged["strategy_position_lookback_start"] = ( - session_open - _STRATEGY_POSITION_TAG_LOOKBACK + request.session_open - STRATEGY_POSITION_TAG_LOOKBACK ).isoformat() tagged["strategy_position_earliest_execution_at"] = ( earliest_execution_at.isoformat() ) - if split_from_aggregate: + if request.split_from_aggregate: tagged["strategy_position_split_from_aggregate"] = True - latest_execution_at = exposure.get("latest_execution_at") + latest_execution_at = request.exposure.get("latest_execution_at") if isinstance(latest_execution_at, datetime): tagged["strategy_position_latest_execution_at"] = ( latest_execution_at.isoformat() ) - buy_qty = cast(Decimal, exposure.get("buy_qty") or Decimal("0")) - buy_notional = cast(Decimal, exposure.get("buy_notional") or Decimal("0")) + buy_qty = cast(Decimal, request.exposure.get("buy_qty") or Decimal("0")) + buy_notional = cast( + Decimal, request.exposure.get("buy_notional") or Decimal("0") + ) if ( buy_qty > 0 and buy_notional > 0 - and not _optional_decimal(tagged.get("avg_entry_price")) + and not scheduler_optional_decimal(tagged.get("avg_entry_price")) ): tagged["avg_entry_price"] = str(buy_notional / buy_qty) return tagged @@ -365,46 +292,42 @@ def _resolve_execution_context_open_orders(self) -> list[dict[str, Any]]: def _process_batch_signals( self, *, - session: Session, - batch: SignalBatch, - strategies: list[Strategy], - account_snapshot: Any, - account: dict[str, str], - positions: list[dict[str, Any]], - allowed_symbols: set[str], + context: BatchSignalProcessingContext, ) -> None: - allocator = allocator_from_settings(account_snapshot.equity) + allocator = allocator_from_settings(context.account_snapshot.equity) relevant_symbols = self._relevant_signal_symbols( - strategies=strategies, - allowed_symbols=allowed_symbols, + strategies=context.strategies, + allowed_symbols=context.allowed_symbols, ) - for signal in batch.signals: + for signal in context.batch.signals: if ( relevant_symbols - and _normalized_symbol(signal.symbol) not in relevant_symbols + and normalized_symbol(signal.symbol) not in relevant_symbols ): continue decisions = self._evaluate_signal_decisions( signal, - strategies, - equity=account_snapshot.equity, - positions=positions, + context.strategies, + equity=context.account_snapshot.equity, + positions=context.positions, ) if not decisions: continue allocation_results = allocator.allocate( decisions, - account=account, - positions=positions, - regime_label=_resolve_signal_regime(signal), + account=context.account, + positions=context.positions, + regime_label=resolve_signal_regime(signal), ) self._apply_allocation_results( - session=session, + context=AllocationDecisionContext( + session=context.session, + strategies=context.strategies, + account=context.account, + positions=context.positions, + allowed_symbols=context.allowed_symbols, + ), allocation_results=allocation_results, - strategies=strategies, - account=account, - positions=positions, - allowed_symbols=allowed_symbols, ) def _evaluate_signal_decisions( @@ -568,10 +491,12 @@ def _persist_rejected_signal_outcome_event( reject_reason=str( event_payload.get("reject_reason") or "unknown" ), - spread_bps=_optional_decimal( + spread_bps=scheduler_optional_decimal( event_payload.get("spread_bps") ), - jump_bps=_optional_decimal(event_payload.get("jump_bps")), + jump_bps=scheduler_optional_decimal( + event_payload.get("jump_bps") + ), outcome_label_status=str( event_payload.get("outcome_label_status") or "pending" ), @@ -591,10 +516,12 @@ def _persist_rejected_signal_outcome_event( existing.reject_reason = str( event_payload.get("reject_reason") or existing.reject_reason ) - existing.spread_bps = _optional_decimal( + existing.spread_bps = scheduler_optional_decimal( event_payload.get("spread_bps") ) - existing.jump_bps = _optional_decimal(event_payload.get("jump_bps")) + existing.jump_bps = scheduler_optional_decimal( + event_payload.get("jump_bps") + ) existing.event_payload_json = dict(event_payload) session.commit() except (SQLAlchemyError, ValueError): @@ -608,8 +535,8 @@ def _label_mature_rejected_signal_outcome_events( self, *, now: datetime | None = None, - limit: int = _REJECTED_SIGNAL_OUTCOME_LABEL_LIMIT, - followup_horizon: timedelta = _REJECTED_SIGNAL_OUTCOME_FOLLOWUP_HORIZON, + limit: int = REJECTED_SIGNAL_OUTCOME_LABEL_LIMIT, + followup_horizon: timedelta = REJECTED_SIGNAL_OUTCOME_FOLLOWUP_HORIZON, ) -> None: resolved_now = now or datetime.now(timezone.utc) mature_before = resolved_now - followup_horizon @@ -656,6 +583,28 @@ def _build_rejected_signal_outcome_payload( row: RejectedSignalOutcomeEvent, followup_horizon: timedelta, ) -> dict[str, Any] | None: + event_payload = self._rejected_signal_event_payload(row) + entry_signal = self._rejected_signal_envelope(row, event_payload) + snapshots = self._rejected_signal_snapshots(entry_signal, followup_horizon) + if snapshots is None: + return None + entry_snapshot, followup_snapshot = snapshots + route_metrics = self._rejected_signal_route_metrics( + entry_snapshot, + followup_snapshot, + followup_horizon, + ) + return self._rejected_signal_outcome_payload( + row=row, + event_payload=event_payload, + entry_snapshot=entry_snapshot, + route_metrics=route_metrics, + ) + + @staticmethod + def _rejected_signal_event_payload( + row: RejectedSignalOutcomeEvent, + ) -> dict[str, Any]: event_payload: dict[str, Any] = {} raw_event_payload = row.event_payload_json if isinstance(raw_event_payload, Mapping): @@ -663,6 +612,13 @@ def _build_rejected_signal_outcome_payload( str(key): value for key, value in cast(Mapping[object, Any], raw_event_payload).items() } + return event_payload + + @staticmethod + def _rejected_signal_envelope( + row: RejectedSignalOutcomeEvent, + event_payload: Mapping[str, Any], + ) -> SignalEnvelope: signal_payload = event_payload.get("signal_payload") if not isinstance(signal_payload, Mapping): signal_payload = {} @@ -681,8 +637,15 @@ def _build_rejected_signal_outcome_payload( timeframe=row.timeframe, seq=seq, ) + return entry_signal + + def _rejected_signal_snapshots( + self, + entry_signal: SignalEnvelope, + followup_horizon: timedelta, + ) -> tuple[Any, Any] | None: followup_signal = entry_signal.model_copy( - update={"event_ts": event_ts + followup_horizon} + update={"event_ts": entry_signal.event_ts + followup_horizon} ) entry_snapshot = self.price_fetcher.fetch_market_snapshot(entry_signal) followup_snapshot = self.price_fetcher.fetch_market_snapshot(followup_signal) @@ -696,6 +659,14 @@ def _build_rejected_signal_outcome_payload( or entry_snapshot.ask is None ): return None + return entry_snapshot, followup_snapshot + + @staticmethod + def _rejected_signal_route_metrics( + entry_snapshot: Any, + followup_snapshot: Any, + followup_horizon: timedelta, + ) -> dict[str, Any]: counterfactual_return = ( followup_snapshot.price - entry_snapshot.price ) / entry_snapshot.price @@ -717,6 +688,22 @@ def _build_rejected_signal_outcome_payload( "half_spread_cost": str(half_spread_cost), "horizon_seconds": str(int(followup_horizon.total_seconds())), } + return { + "counterfactual_return": counterfactual_return, + "post_cost_net_pnl": post_cost_net_pnl, + "route_tca": route_tca, + } + + @staticmethod + def _rejected_signal_outcome_payload( + *, + row: RejectedSignalOutcomeEvent, + event_payload: Mapping[str, Any], + entry_snapshot: Any, + route_metrics: Mapping[str, Any], + ) -> dict[str, Any]: + post_cost_net_pnl = cast(Decimal, route_metrics["post_cost_net_pnl"]) + route_tca = cast(Mapping[str, Any], route_metrics["route_tca"]) return { "schema_version": "torghut.rejected-signal-outcome.v1", "label_status": "labeled", @@ -728,19 +715,19 @@ def _build_rejected_signal_outcome_payload( "execution_signature": event_payload.get("execution_signature"), "feedback_shape_key": event_payload.get("feedback_shape_key"), "feedback_risk_profile_key": event_payload.get("feedback_risk_profile_key"), - "counterfactual_return": str(counterfactual_return), + "counterfactual_return": str(route_metrics["counterfactual_return"]), "route_tca": route_tca, "post_cost_net_pnl": str(post_cost_net_pnl), "executable_quote": { "bid": str(entry_snapshot.bid), "ask": str(entry_snapshot.ask), - "spread": str(entry_spread), + "spread": str(route_tca["entry_spread"]), "source": entry_snapshot.source, "as_of": entry_snapshot.as_of.isoformat(), }, "objective_scorecard": { "net_pnl_per_day": str(post_cost_net_pnl), - "counterfactual_return": str(counterfactual_return), + "counterfactual_return": str(route_metrics["counterfactual_return"]), "post_cost_net_pnl": str(post_cost_net_pnl), "active_day_ratio": "1", "positive_day_ratio": "1" if post_cost_net_pnl > 0 else "0", @@ -894,6 +881,3 @@ def _record_quantity_resolution_metrics( outcome=outcome, reason=cast(str | None, resolution_payload.get("reason")), ) - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/part_05_tradingpipelinemethodspart4.py b/services/torghut/app/trading/scheduler/pipeline_modules/submission_policy.py similarity index 60% rename from services/torghut/app/trading/scheduler/pipeline_modules/part_05_tradingpipelinemethodspart4.py rename to services/torghut/app/trading/scheduler/pipeline_modules/submission_policy.py index b8ac67a559..b2762e5972 100644 --- a/services/torghut/app/trading/scheduler/pipeline_modules/part_05_tradingpipelinemethodspart4.py +++ b/services/torghut/app/trading/scheduler/pipeline_modules/submission_policy.py @@ -1,188 +1,69 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false """Trading pipeline implementation.""" from __future__ import annotations -import hashlib -import json -import inspect import logging -import os -from collections.abc import Callable, Mapping -from datetime import date, datetime, timedelta, timezone -from decimal import Decimal -from pathlib import Path -from typing import Any, Optional, Sequence, cast - -from sqlalchemy import select -from sqlalchemy.exc import SQLAlchemyError +from collections.abc import Mapping +from typing import Any, Optional, cast + from sqlalchemy.orm import Session -from ....alpaca_client import TorghutAlpacaClient from ....config import settings -from ....db import SessionLocal from ....models import ( - Execution, - LLMDecisionReview, - PositionSnapshot, - RejectedSignalOutcomeEvent, - Strategy, TradeDecision, - coerce_json_payload, ) from ....observability import capture_posthog_event -from ....snapshots import snapshot_account_and_positions -from ....strategies import StrategyCatalog -from ...autonomy.phase_manifest_contract import AUTONOMY_PHASE_ORDER -from ...decisions import DecisionEngine -from ...empirical_jobs import build_empirical_jobs_status -from ...execution import OrderExecutor -from ...execution_adapters import ExecutionAdapter -from ...execution_policy import ExecutionPolicy -from ...feature_quality import ( - REASON_STALENESS, - FeatureQualityThresholds, - evaluate_feature_batch_quality, -) -from ...features import extract_executable_price, optional_decimal, payload_value -from ...firewall import OrderFirewall, OrderFirewallBlocked -from ...ingest import ClickHouseSignalIngestor, SignalBatch -from ...lean_lanes import LeanLaneManager -from ...llm import LLMReviewEngine, apply_policy -from ...llm.dspy_programs.runtime import ( - DSPyReviewRuntime, - DSPyRuntimeUnsupportedStateError, -) -from ...llm.guardrails import evaluate_llm_guardrails -from ...llm.policy import allowed_order_types -from ...llm.schema import MarketContextBundle -from ...llm.schema import MarketSnapshot as LLMMarketSnapshot -from ...market_context import ( - MarketContextClient, - MarketContextStatus, - evaluate_market_context, -) -from ...market_context_domains import ( - active_market_context_domain_states, - active_market_context_reasons, -) -from ...models import SignalEnvelope, StrategyDecision -from ...order_feed import OrderFeedIngestor -from ...paper_route_evidence import ( - PAPER_ROUTE_ACCOUNT_PRE_SESSION_READINESS_SECONDS, - PAPER_ROUTE_ACCOUNT_START_SNAPSHOT_AFTER_START_GRACE_SECONDS, -) -from ...portfolio import ( - AllocationResult, - PortfolioSizingResult, - allocator_from_settings, - sizer_from_settings, -) -from ...prices import ClickHousePriceFetcher, MarketSnapshot, PriceFetcher -from ...quote_quality import ( - QuoteQualityPolicy, - QuoteQualityStatus, - SignalQuoteQualityTracker, - assess_signal_quote_quality, -) -from ...quantity_rules import ( - min_qty_for_symbol, - quantize_qty_for_symbol, - resolve_quantity_resolution, -) -from ...reconcile import Reconciler -from ...regime_hmm import ( - HMMRegimeContext, - resolve_hmm_context, - resolve_regime_context_authority_reason, -) -from ...risk import RiskEngine -from ...session_context import regular_session_open_utc_for +from ...models import StrategyDecision +from ...prices import MarketSnapshot from ...tca import derive_adaptive_execution_policy -from ...time_source import trading_now -from ...universe import UniverseResolver -from ...submission_council import ( - build_hypothesis_runtime_summary, - build_live_submission_gate_payload, - build_submission_gate_market_context_status, - load_quant_evidence_status, -) -from ..pipeline_helpers import ( - _allocator_rejection_reasons, - _apply_projected_position_decision, - _attach_dspy_lineage, - _autonomy_gate_report_is_saturated_fail_sentinel, - _build_committee_veto_alignment_payload, - _build_llm_policy_resolution, - _build_portfolio_snapshot, - _classify_llm_error, - _clone_positions, - _coerce_bool, - _coerce_json, - _coerce_runtime_uncertainty_gate_action, - _coerce_strategy_symbols, - _committee_trace_has_veto, - _extract_json_error_payload, - _format_order_submit_rejection, - _hash_payload, - _is_runtime_risk_increasing_entry, - _llm_guardrail_controls_snapshot, - _load_recent_decisions, - _normalize_rollout_stage, - _optional_decimal, - _optional_int, - _price_snapshot_payload, - _project_open_orders_onto_positions, - _resolve_decision_regime_label_with_source, - _resolve_llm_review_error_reject_reason, - _resolve_llm_unavailable_reject_reason, - _resolve_signal_regime, - _select_strictest_runtime_uncertainty_gate, - _uncertainty_gate_staleness_reason, -) -from ..safety import ( - _FRESH_TAIL_NO_SIGNAL_REASONS, - _is_market_session_open, - _latch_signal_continuity_alert_state, - _record_signal_continuity_recovery_cycle, - _signal_bootstrap_grace_active, - _signal_tail_is_fresh, -) from ..state import ( RuntimeUncertaintyGate, RuntimeUncertaintyGateAction, - TradingState, - _normalize_reason_metric, ) -# ruff: noqa: F401,F403,F405,F821,F821,F821 +from .contexts import ( + DecisionBlockRequest, + DecisionRejectionRequest, + DecisionSubmissionContext, + DomainTelemetryEvent, + ExecutionPolicyRequest, + ExecutionFallbackRequest, + LLMReviewContext, + LiveSubmissionGateInputs, + OrderSubmissionRequest, + RiskVerdictRequest, +) +from .shared import TradingPipelineBase +from .support import ( + allocator_rejection_reasons, + autonomy_gate_report_is_saturated_fail_sentinel, + coerce_json, + coerce_runtime_uncertainty_gate_action, + is_runtime_risk_increasing_entry, + optional_decimal, + optional_int, + resolve_decision_regime_label_with_source, +) -from .part_01_statements_158 import * -from .part_02_tradingpipelinemethodspart1 import * -from .part_03_tradingpipelinemethodspart2 import * -from .part_04_tradingpipelinemethodspart3 import * +logger = logging.getLogger(__name__) -class _TradingPipelineMethodsPart4: +class TradingPipelineSubmissionPolicyMixin(TradingPipelineBase): def _prepare_decision_for_submission( self, *, - session: Session, + context: DecisionSubmissionContext, decision: StrategyDecision, - decision_row: TradeDecision, - strategy: Strategy, - account: dict[str, str], - positions: list[dict[str, Any]], ) -> tuple[StrategyDecision, Optional[MarketSnapshot]] | None: - allocator_rejection = _allocator_rejection_reasons(decision) + allocator_rejection = allocator_rejection_reasons(decision) if allocator_rejection: self._record_decision_rejection( - session=session, - decision=decision, - decision_row=decision_row, - reasons=allocator_rejection, - log_template=( - "Decision rejected by allocator strategy_id=%s symbol=%s reason=%s" + DecisionRejectionRequest( + context.session, + decision, + context.decision_row, + allocator_rejection, + "Decision rejected by allocator strategy_id=%s symbol=%s reason=%s", ), ) return None @@ -196,38 +77,42 @@ def _prepare_decision_for_submission( decision.model_dump(mode="json").get("params", {}), ) self.executor.update_decision_params( - session, decision_row, price_params_update + context.session, context.decision_row, price_params_update ) sizing_result = self._apply_portfolio_sizing( - decision, strategy, account, positions + decision, context.strategy, context.account, context.positions ) decision = sizing_result.decision sizing_params = decision.model_dump(mode="json").get("params", {}) - self.executor.sync_decision_state(session, decision_row, decision) + self.executor.sync_decision_state( + context.session, context.decision_row, decision + ) if isinstance(sizing_params, Mapping) and "portfolio_sizing" in sizing_params: self.executor.update_decision_params( - session, decision_row, cast(Mapping[str, Any], sizing_params) + context.session, + context.decision_row, + cast(Mapping[str, Any], sizing_params), ) if not sizing_result.approved: self._record_decision_rejection( - session=session, - decision=decision, - decision_row=decision_row, - reasons=sizing_result.reasons, - log_template=( - "Decision rejected by portfolio sizing strategy_id=%s symbol=%s reason=%s" + DecisionRejectionRequest( + context.session, + decision, + context.decision_row, + sizing_result.reasons, + "Decision rejected by portfolio sizing strategy_id=%s symbol=%s reason=%s", ), ) return None decision, gate_payload, gate_rejection = self._apply_runtime_uncertainty_gate( - decision, positions=positions + decision, positions=context.positions ) self._persist_runtime_uncertainty_gate_payload( - session=session, + session=context.session, decision=decision, - decision_row=decision_row, + decision_row=context.decision_row, gate_payload=gate_payload, ) if gate_rejection: @@ -236,39 +121,45 @@ def _prepare_decision_for_submission( gate_rejection=gate_rejection, ) self._record_decision_rejection( - session=session, - decision=decision, - decision_row=decision_row, - reasons=[gate_rejection], - log_template=( - "Decision rejected by execution gate strategy_id=%s symbol=%s reason=%s" + DecisionRejectionRequest( + context.session, + decision, + context.decision_row, + [gate_rejection], + "Decision rejected by execution gate strategy_id=%s symbol=%s reason=%s", ), ) return None - decision, llm_reject_reason = self._apply_llm_review( - session, decision, decision_row, account, positions + llm_context = LLMReviewContext( + session=context.session, + decision_row=context.decision_row, + account=context.account, + positions=context.positions, + ) + decision, llm_reject_reason = self._apply_llm_review(llm_context, decision) + self.executor.sync_decision_state( + context.session, context.decision_row, decision ) - self.executor.sync_decision_state(session, decision_row, decision) if llm_reject_reason: self._record_runtime_uncertainty_gate_result( gate_payload=gate_payload, gate_rejection=None, ) self._record_decision_rejection( - session=session, - decision=decision, - decision_row=decision_row, - reasons=[llm_reject_reason], - log_template="Decision rejected by llm review strategy_id=%s symbol=%s reason=%s", + DecisionRejectionRequest( + context.session, + decision, + context.decision_row, + [llm_reject_reason], + "Decision rejected by llm review strategy_id=%s symbol=%s reason=%s", + ), ) return None gate_rejection = self._recheck_runtime_uncertainty_gate_after_llm( - session=session, + context=context, decision=decision, - decision_row=decision_row, - positions=positions, gate_payload=gate_payload, ) if gate_rejection: @@ -277,12 +168,12 @@ def _prepare_decision_for_submission( gate_rejection=gate_rejection, ) self._record_decision_rejection( - session=session, - decision=decision, - decision_row=decision_row, - reasons=[gate_rejection], - log_template=( - "Decision rejected by execution gate strategy_id=%s symbol=%s reason=%s" + DecisionRejectionRequest( + context.session, + decision, + context.decision_row, + [gate_rejection], + "Decision rejected by execution gate strategy_id=%s symbol=%s reason=%s", ), ) return None @@ -398,7 +289,7 @@ def _should_degrade_runtime_uncertainty_fail( ) -> bool: if uncertainty_gate.source != "autonomy_gate_report": return False - return _autonomy_gate_report_is_saturated_fail_sentinel( + return autonomy_gate_report_is_saturated_fail_sentinel( action=gate.action, coverage_error=uncertainty_gate.coverage_error, shift_score=uncertainty_gate.shift_score, @@ -414,14 +305,14 @@ def _should_degrade_runtime_uncertainty_fail_from_payload( != "autonomy_gate_report" ): return False - action = _coerce_runtime_uncertainty_gate_action(gate_payload.get("action")) + action = coerce_runtime_uncertainty_gate_action(gate_payload.get("action")) if action is None: return False - return _autonomy_gate_report_is_saturated_fail_sentinel( + return autonomy_gate_report_is_saturated_fail_sentinel( action=action, - coverage_error=_optional_decimal(gate_payload.get("coverage_error")), - shift_score=_optional_decimal(gate_payload.get("shift_score")), - conformal_interval_width=_optional_decimal( + coverage_error=optional_decimal(gate_payload.get("coverage_error")), + shift_score=optional_decimal(gate_payload.get("shift_score")), + conformal_interval_width=optional_decimal( gate_payload.get("conformal_interval_width") ), ) @@ -443,14 +334,14 @@ def _degrade_runtime_uncertainty_gate_decision( decision=decision, regime_gate=regime_gate, ) - allocator = _coerce_json(params.get("allocator")) - current_override = _optional_decimal( + allocator = coerce_json(params.get("allocator")) + current_override = optional_decimal( allocator.get("max_participation_rate_override") ) if current_override is None or current_override > max_participation_rate: allocator["max_participation_rate_override"] = str(max_participation_rate) params["allocator"] = allocator - execution_seconds = _optional_int(params.get("execution_seconds")) + execution_seconds = optional_int(params.get("execution_seconds")) if execution_seconds is None or execution_seconds < min_execution_seconds: params["execution_seconds"] = min_execution_seconds @@ -472,10 +363,8 @@ def _degrade_runtime_uncertainty_gate_decision( def _recheck_runtime_uncertainty_gate_after_llm( self, *, - session: Session, + context: DecisionSubmissionContext, decision: StrategyDecision, - decision_row: TradeDecision, - positions: list[dict[str, Any]], gate_payload: dict[str, Any], ) -> str | None: gate_action = str(gate_payload.get("action") or "pass").strip().lower() @@ -489,23 +378,25 @@ def _recheck_runtime_uncertainty_gate_after_llm( gate_payload["entry_blocked"] = False gate_payload["block_reason"] = None self._persist_runtime_uncertainty_gate_payload( - session=session, + session=context.session, decision=decision, - decision_row=decision_row, + decision_row=context.decision_row, gate_payload=gate_payload, ) return None if gate_action not in {"abstain", "fail"}: return None - risk_increasing_entry = _is_runtime_risk_increasing_entry(decision, positions) + risk_increasing_entry = is_runtime_risk_increasing_entry( + decision, context.positions + ) gate_payload["risk_increasing_entry"] = risk_increasing_entry if not risk_increasing_entry: gate_payload["entry_blocked"] = False gate_payload["block_reason"] = None self._persist_runtime_uncertainty_gate_payload( - session=session, + session=context.session, decision=decision, - decision_row=decision_row, + decision_row=context.decision_row, gate_payload=gate_payload, ) return None @@ -517,139 +408,152 @@ def _recheck_runtime_uncertainty_gate_after_llm( gate_payload["entry_blocked"] = True gate_payload["block_reason"] = reason self._persist_runtime_uncertainty_gate_payload( - session=session, + session=context.session, decision=decision, - decision_row=decision_row, + decision_row=context.decision_row, gate_payload=gate_payload, ) return reason def _record_decision_rejection( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - reasons: list[str], - log_template: str, + request: DecisionRejectionRequest | None = None, + **legacy_kwargs: Any, ) -> None: + request = self._decision_rejection_request(request, legacy_kwargs) + reasons = request.reasons if not reasons: return self.state.metrics.orders_rejected_total += 1 self.state.metrics.record_decision_rejection_reasons(reasons) self.state.metrics.record_decision_state("rejected") for reason in reasons: - logger.info(log_template, decision.strategy_id, decision.symbol, reason) + logger.info( + request.log_template, + request.decision.strategy_id, + request.decision.symbol, + reason, + ) self.executor.mark_rejected( - session, - decision_row, + request.session, + request.decision_row, ";".join(reasons), metadata_update=self._decision_lifecycle_metadata( submission_stage="rejected_pre_submit" ), ) self._emit_domain_telemetry( - event_name="torghut.decision.blocked", - severity="warning", - decision=decision, - decision_row=decision_row, - reason_codes=reasons, - extra_properties={"decision_status": "rejected"}, + DomainTelemetryEvent( + event_name="torghut.decision.blocked", + severity="warning", + decision=request.decision, + decision_row=request.decision_row, + reason_codes=reasons, + extra_properties={"decision_status": "rejected"}, + ) + ) + + @staticmethod + def _decision_rejection_request( + request: DecisionRejectionRequest | None, + legacy_kwargs: Mapping[str, Any], + ) -> DecisionRejectionRequest: + if request is not None: + return request + return DecisionRejectionRequest( + session=legacy_kwargs["session"], + decision=legacy_kwargs["decision"], + decision_row=legacy_kwargs["decision_row"], + reasons=legacy_kwargs["reasons"], + log_template=legacy_kwargs["log_template"], ) def _emit_domain_telemetry( self, - *, - event_name: str, - severity: str, - decision: StrategyDecision | None = None, - decision_row: TradeDecision | None = None, - execution: Any | None = None, - reason_codes: Sequence[str] | None = None, - extra_properties: Mapping[str, Any] | None = None, + event: DomainTelemetryEvent, ) -> None: properties: dict[str, Any] = { "account_label": self.account_label, "trading_mode": settings.trading_mode, } - if decision is not None: + if event.decision is not None: properties.update( { - "strategy_id": decision.strategy_id, - "symbol": decision.symbol, - "timeframe": decision.timeframe, - "decision_action": decision.action, + "strategy_id": event.decision.strategy_id, + "symbol": event.decision.symbol, + "timeframe": event.decision.timeframe, + "decision_action": event.decision.action, } ) - if decision_row is not None: - properties["trade_decision_id"] = str(decision_row.id) - properties["decision_hash"] = decision_row.decision_hash - properties["decision_status"] = decision_row.status - if execution is not None: - properties["execution_id"] = str(getattr(execution, "id", "")) - properties["execution_status"] = str(getattr(execution, "status", "")) + if event.decision_row is not None: + properties["trade_decision_id"] = str(event.decision_row.id) + properties["decision_hash"] = event.decision_row.decision_hash + properties["decision_status"] = event.decision_row.status + if event.execution is not None: + properties["execution_id"] = str(getattr(event.execution, "id", "")) + properties["execution_status"] = str(getattr(event.execution, "status", "")) properties["execution_correlation_id"] = str( - getattr(execution, "execution_correlation_id", "") or "" + getattr(event.execution, "execution_correlation_id", "") or "" ) properties["execution_idempotency_key"] = str( - getattr(execution, "execution_idempotency_key", "") or "" + getattr(event.execution, "execution_idempotency_key", "") or "" ) properties["execution_fallback_reason"] = str( - getattr(execution, "execution_fallback_reason", "") or "" + getattr(event.execution, "execution_fallback_reason", "") or "" ) - if reason_codes: + if event.reason_codes: properties["reason_codes"] = sorted( - {str(reason).strip() for reason in reason_codes if str(reason).strip()} + { + str(reason).strip() + for reason in event.reason_codes + if str(reason).strip() + } ) - if extra_properties: + if event.extra_properties: properties.update( - {str(key): value for key, value in extra_properties.items()} + {str(key): value for key, value in event.extra_properties.items()} ) emitted, drop_reason = capture_posthog_event( - event_name, - severity=severity, + event.event_name, + severity=event.severity, distinct_id=f"torghut-{self.account_label}", properties=properties, ) self.state.metrics.record_domain_telemetry( - event_name=event_name, + event_name=event.event_name, emitted=emitted, drop_reason=drop_reason, ) def _evaluate_execution_policy_outcome( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - strategy: Strategy, - positions: list[dict[str, Any]], - snapshot: Optional[MarketSnapshot], + request: ExecutionPolicyRequest, ) -> tuple[StrategyDecision, Any] | None: + context = request.context + decision = request.decision regime_label, regime_source, regime_fallback = ( - _resolve_decision_regime_label_with_source(decision) + resolve_decision_regime_label_with_source(decision) ) self.state.metrics.record_decision_regime_resolution( source=regime_source, fallback_reason=regime_fallback, ) adaptive_policy = derive_adaptive_execution_policy( - session, + context.session, symbol=decision.symbol, regime_label=regime_label, ) policy_outcome = self.execution_policy.evaluate( decision, - strategy=strategy, - positions=positions, - market_snapshot=snapshot, + strategy=context.strategy, + positions=context.positions, + market_snapshot=request.snapshot, kill_switch_enabled=self.order_firewall.status().kill_switch_enabled, adaptive_policy=adaptive_policy, ) decision = policy_outcome.decision self.executor.update_decision_params( - session, decision_row, policy_outcome.params_update() + context.session, context.decision_row, policy_outcome.params_update() ) self.state.metrics.record_execution_advisor_result( policy_outcome.advisor_metadata @@ -660,49 +564,46 @@ def _evaluate_execution_policy_outcome( policy_outcome.adaptive is not None and policy_outcome.adaptive.applied ), ) - self.executor.sync_decision_state(session, decision_row, decision) + self.executor.sync_decision_state( + context.session, context.decision_row, decision + ) if not policy_outcome.approved: self._record_decision_rejection( - session=session, - decision=decision, - decision_row=decision_row, - reasons=list(policy_outcome.reasons), - log_template=( - "Decision rejected by execution policy strategy_id=%s symbol=%s reason=%s" - ), + DecisionRejectionRequest( + context.session, + decision, + context.decision_row, + list(policy_outcome.reasons), + "Decision rejected by execution policy strategy_id=%s symbol=%s reason=%s", + ) ) return None return decision, policy_outcome def _passes_risk_verdict( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - strategy: Strategy, - account: dict[str, str], - positions: list[dict[str, Any]], - symbol_allowlist: set[str], - execution_advisor: Mapping[str, Any] | None, + request: RiskVerdictRequest, ) -> bool: + context = request.context verdict = self.risk_engine.evaluate( - session, - decision, - strategy, - account, - positions, - symbol_allowlist, - execution_advisor=execution_advisor, + context.session, + request.decision, + context.strategy, + context.account, + context.positions, + context.symbol_allowlist, + execution_advisor=request.execution_advisor, ) if verdict.approved: return True self._record_decision_rejection( - session=session, - decision=decision, - decision_row=decision_row, - reasons=list(verdict.reasons), - log_template="Decision rejected strategy_id=%s symbol=%s reason=%s", + DecisionRejectionRequest( + context.session, + request.decision, + context.decision_row, + list(verdict.reasons), + "Decision rejected strategy_id=%s symbol=%s reason=%s", + ) ) return False @@ -715,11 +616,13 @@ def _is_trading_submission_allowed( ) -> bool: if not settings.trading_enabled: self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason="trading_disabled", - submission_stage="blocked_trading_disabled", + DecisionBlockRequest( + session=session, + decision=decision, + decision_row=decision_row, + reason="trading_disabled", + submission_stage="blocked_trading_disabled", + ) ) logger.warning( "Decision blocked because trading is disabled strategy_id=%s decision_id=%s symbol=%s", @@ -728,18 +631,22 @@ def _is_trading_submission_allowed( decision.symbol, ) return False - live_submission_gate = self._live_submission_gate(session=session) + live_submission_gate = self._live_submission_gate( + inputs=LiveSubmissionGateInputs(session=session) + ) if settings.trading_mode == "live" and not bool( live_submission_gate.get("allowed", False) ): self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason="capital_stage_shadow", - submission_stage="blocked_capital_stage_shadow", - capital_stage="shadow", - extra_metadata={"live_submission_gate": live_submission_gate}, + DecisionBlockRequest( + session=session, + decision=decision, + decision_row=decision_row, + reason="capital_stage_shadow", + submission_stage="blocked_capital_stage_shadow", + capital_stage="shadow", + extra_metadata={"live_submission_gate": live_submission_gate}, + ) ) logger.info( "Decision held in shadow stage strategy_id=%s decision_id=%s symbol=%s gate_reason=%s", @@ -755,11 +662,13 @@ def _is_trading_submission_allowed( return True reason = self.state.emergency_stop_reason or "emergency_stop_active" self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason=reason, - submission_stage="blocked_emergency_stop", + DecisionBlockRequest( + session=session, + decision=decision, + decision_row=decision_row, + reason=reason, + submission_stage="blocked_emergency_stop", + ) ) logger.error( "Decision blocked by emergency stop strategy_id=%s decision_id=%s symbol=%s reason=%s", @@ -809,23 +718,27 @@ def _submit_decision_execution( ) execution, rejected = self._submit_order_with_handling( - session=session, - execution_client=execution_client, - decision=decision, - decision_row=decision_row, - selected_adapter_name=selected_adapter_name, - retry_delays=policy_outcome.retry_delays, + OrderSubmissionRequest( + session=session, + execution_client=execution_client, + decision=decision, + decision_row=decision_row, + selected_adapter_name=selected_adapter_name, + retry_delays=policy_outcome.retry_delays, + ) ) if rejected: return False self._emit_domain_telemetry( - event_name="torghut.decision.generated", - severity="info", - decision=decision, - decision_row=decision_row, - extra_properties={ - "selected_execution_adapter": selected_adapter_name, - }, + DomainTelemetryEvent( + event_name="torghut.decision.generated", + severity="info", + decision=decision, + decision_row=decision_row, + extra_properties={ + "selected_execution_adapter": selected_adapter_name, + }, + ) ) if execution is None: self._sync_lean_observability(execution_client) @@ -845,14 +758,16 @@ def _submit_decision_execution( self._decision_lifecycle_metadata(submission_stage="submitted"), ) self._emit_domain_telemetry( - event_name="torghut.execution.submitted", - severity="info", - decision=decision, - decision_row=decision_row, - extra_properties={ - "execution_expected_adapter": selected_adapter_name, - "execution_actual_adapter": selected_adapter_name, - }, + DomainTelemetryEvent( + event_name="torghut.execution.submitted", + severity="info", + decision=decision, + decision_row=decision_row, + extra_properties={ + "execution_expected_adapter": selected_adapter_name, + "execution_actual_adapter": selected_adapter_name, + }, + ) ) return True @@ -862,12 +777,14 @@ def _submit_decision_execution( if actual_adapter_name == "alpaca_fallback": actual_adapter_name = "alpaca" self._handle_execution_fallback( - session=session, - decision=decision, - decision_row=decision_row, - execution=execution, - selected_adapter_name=selected_adapter_name, - actual_adapter_name=actual_adapter_name, + ExecutionFallbackRequest( + session=session, + decision=decision, + decision_row=decision_row, + execution=execution, + selected_adapter_name=selected_adapter_name, + actual_adapter_name=actual_adapter_name, + ) ) self._record_lean_shadow_from_execution(execution) self._sync_lean_observability(execution_client) @@ -882,15 +799,17 @@ def _submit_decision_execution( adapter=actual_adapter_name, ) self._emit_domain_telemetry( - event_name="torghut.execution.submitted", - severity="info", - decision=decision, - decision_row=decision_row, - execution=execution, - extra_properties={ - "execution_expected_adapter": selected_adapter_name, - "execution_actual_adapter": actual_adapter_name, - }, + DomainTelemetryEvent( + event_name="torghut.execution.submitted", + severity="info", + decision=decision, + decision_row=decision_row, + execution=execution, + extra_properties={ + "execution_expected_adapter": selected_adapter_name, + "execution_actual_adapter": actual_adapter_name, + }, + ) ) logger.info( "Order submitted strategy_id=%s decision_id=%s symbol=%s adapter=%s alpaca_order_id=%s", @@ -901,6 +820,3 @@ def _submit_decision_execution( execution.alpaca_order_id, ) return True - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/support.py b/services/torghut/app/trading/scheduler/pipeline_modules/support.py new file mode 100644 index 0000000000..6dcebcae20 --- /dev/null +++ b/services/torghut/app/trading/scheduler/pipeline_modules/support.py @@ -0,0 +1,845 @@ +"""Public helper functions for the semantic scheduler pipeline modules.""" + +from __future__ import annotations + +import hashlib +import json +import logging +from collections.abc import Mapping +from dataclasses import dataclass +from datetime import datetime, timezone +from decimal import Decimal +from typing import Any, Literal, Optional, cast + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from ....config import settings +from ....models import TradeDecision +from ...llm.schema import PortfolioSnapshot, RecentDecisionSummary +from ...models import StrategyDecision +from ...prices import MarketSnapshot +from ...regime_hmm import ( + resolve_hmm_context, + resolve_legacy_regime_label, + resolve_regime_context_authority_reason, + resolve_regime_route_label, +) +from ..state import ( + RuntimeUncertaintyGate, + RuntimeUncertaintyGateAction, + normalize_reason_metric, +) + +logger = logging.getLogger(__name__) + +RUNTIME_UNCERTAINTY_GATE_MAX_STALENESS_SECONDS = 15 * 60 +PROJECTED_ORDER_ACTIONS = {"buy", "sell"} +PROJECTED_ORDER_TYPES = {"market", "limit", "stop", "stop_limit"} +PROJECTED_ORDER_TIME_IN_FORCE = {"day", "gtc", "ioc", "fok"} + + +@dataclass(frozen=True) +class OpenOrderProjection: + symbol: str + action: Literal["buy", "sell"] + qty: Decimal + order_type: Literal["market", "limit", "stop", "stop_limit"] + time_in_force: Literal["day", "gtc", "ioc", "fok"] + client_order_id: str + params: dict[str, Any] + + +@dataclass(frozen=True) +class ExistingProjectionContext: + projection_ids: list[str] + projection_only: bool + projection_count: int + had_positions: bool + + +def clone_positions(positions: list[dict[str, Any]]) -> list[dict[str, Any]]: + return [dict(position) for position in positions] + + +def optional_decimal(value: Any) -> Optional[Decimal]: + if value is None: + return None + try: + return Decimal(str(value)) + except (ArithmeticError, ValueError): + return None + + +def optional_int(value: Any) -> Optional[int]: + if value is None: + return None + try: + return int(value) + except (TypeError, ValueError): + return None + + +def coerce_bool(value: Any) -> bool | None: + if isinstance(value, bool): + return value + if isinstance(value, int): + return value != 0 + if isinstance(value, str): + normalized = value.strip().lower() + if normalized in {"true", "t", "1", "yes", "on"}: + return True + if normalized in {"false", "f", "0", "no", "off"}: + return False + return None + + +def coerce_runtime_uncertainty_gate_action( + value: Any, +) -> RuntimeUncertaintyGateAction | None: + if not isinstance(value, str): + return None + normalized = value.strip().lower() + if normalized in {"pass", "degrade", "abstain", "fail"}: + return cast(RuntimeUncertaintyGateAction, normalized) + return None + + +def coerce_strategy_symbols(raw: object) -> set[str]: + if raw is None: + return set() + if isinstance(raw, list): + symbols: set[str] = set() + for symbol in cast(list[Any], raw): + cleaned = str(symbol).strip() + if cleaned: + symbols.add(cleaned) + return symbols + if isinstance(raw, str): + return {symbol.strip() for symbol in raw.split(",") if symbol.strip()} + return set() + + +def coerce_json(value: Any) -> dict[str, Any]: + if isinstance(value, Mapping): + raw = cast(Mapping[str, Any], value) + return {str(key): val for key, val in raw.items()} + return {} + + +def extract_json_error_payload(error: Exception) -> dict[str, Any] | None: + raw = str(error).strip() + if not raw.startswith("{"): + return None + try: + parsed = json.loads(raw) + except json.JSONDecodeError: + return None + if isinstance(parsed, dict): + return cast(dict[str, Any], parsed) + return None + + +def format_order_submit_rejection(error: Exception) -> str: + payload = extract_json_error_payload(error) + if not payload: + return f"alpaca_order_submit_failed {type(error).__name__}: {error}" + source = str(payload.get("source") or "").strip().lower() + code = payload.get("code") + reject_reason = payload.get("reject_reason") + existing_order_id = payload.get("existing_order_id") + if source == "broker_precheck": + parts: list[str] = ["broker_precheck_rejected"] + elif source == "local_pre_submit": + parts = ["local_pre_submit_rejected"] + else: + parts = ["alpaca_order_rejected"] + if code is not None: + parts.append(f"code={code}") + if reject_reason: + parts.append(f"reason={reject_reason}") + if existing_order_id: + parts.append(f"existing_order_id={existing_order_id}") + return " ".join(parts) + + +def price_snapshot_payload(snapshot: MarketSnapshot) -> dict[str, Any]: + return { + "as_of": snapshot.as_of.isoformat(), + "price": str(snapshot.price) if snapshot.price is not None else None, + "spread": str(snapshot.spread) if snapshot.spread is not None else None, + "source": snapshot.source, + } + + +def build_portfolio_snapshot( + account: dict[str, str], + positions: list[dict[str, Any]], +) -> PortfolioSnapshot: + exposure_by_symbol: dict[str, Decimal] = {} + total_exposure = Decimal("0") + for position in positions: + symbol = str(position.get("symbol") or "").strip() + market_value = optional_decimal(position.get("market_value")) + if not symbol or market_value is None: + continue + exposure_by_symbol[symbol] = ( + exposure_by_symbol.get(symbol, Decimal("0")) + market_value + ) + total_exposure += abs(market_value) + return PortfolioSnapshot( + equity=optional_decimal(account.get("equity")), + cash=optional_decimal(account.get("cash")), + buying_power=optional_decimal(account.get("buying_power")), + total_exposure=total_exposure, + exposure_by_symbol=exposure_by_symbol, + positions=positions, + ) + + +def load_recent_decisions( + session: Session, + strategy_id: str, + symbol: str, +) -> list[RecentDecisionSummary]: + if settings.llm_recent_decisions <= 0: + return [] + stmt = ( + select(TradeDecision) + .where(TradeDecision.strategy_id == strategy_id) + .where(TradeDecision.symbol == symbol) + .order_by(TradeDecision.created_at.desc()) + .limit(settings.llm_recent_decisions) + ) + decisions = session.execute(stmt).scalars().all() + summaries: list[RecentDecisionSummary] = [] + for decision in decisions: + decision_json = coerce_json(decision.decision_json) + params_value: object = decision_json.get("params") + params_map: Mapping[str, Any] = {} + if isinstance(params_value, Mapping): + params_map = cast(Mapping[str, Any], params_value) + price = optional_decimal(params_map.get("price")) + if price is None and isinstance(params_map.get("price_snapshot"), Mapping): + snapshot_map = cast(Mapping[str, Any], params_map.get("price_snapshot")) + price = optional_decimal(snapshot_map.get("price")) + summaries.append( + RecentDecisionSummary( + decision_id=str(decision.id), + strategy_id=str(decision.strategy_id), + symbol=decision.symbol, + action=decision_json.get("action", "buy"), + qty=optional_decimal(decision_json.get("qty")) or Decimal("0"), + status=decision.status, + created_at=decision.created_at, + rationale=decision.rationale, + price=price, + ) + ) + return summaries + + +def allocator_rejection_reasons(decision: StrategyDecision) -> list[str]: + allocator = decision.params.get("allocator") + if not isinstance(allocator, Mapping): + return [] + allocator_map = cast(Mapping[str, Any], allocator) + if str(allocator_map.get("status") or "").lower() != "rejected": + return [] + raw_codes = allocator_map.get("reason_codes") + if isinstance(raw_codes, list): + codes = cast(list[Any], raw_codes) + reason_codes = [str(item).strip() for item in codes if str(item).strip()] + if reason_codes: + return reason_codes + return ["allocator_rejected"] + + +def apply_projected_position_decision( + positions: list[dict[str, Any]], + decision: StrategyDecision, +) -> None: + qty = optional_decimal(decision.qty) + if qty is None or qty <= 0 or decision.action not in {"buy", "sell"}: + return + current_qty = position_qty(decision.symbol, positions) + current_market_value = position_market_value(decision.symbol, positions) + delta = qty if decision.action == "buy" else -qty + projected_qty = current_qty + delta + projected_market_value = projected_position_market_value( + current_market_value, + delta, + extract_decision_price(decision), + ) + positions[:] = [ + position for position in positions if position.get("symbol") != decision.symbol + ] + if projected_qty == 0: + return + projected_position = { + "symbol": decision.symbol, + "qty": str(abs(projected_qty)), + "side": "short" if projected_qty < 0 else "long", + } + if projected_market_value is not None: + projected_position["market_value"] = str(projected_market_value) + positions.append(projected_position) + + +def project_open_orders_onto_positions( + positions: list[dict[str, Any]], + open_orders: list[dict[str, Any]], +) -> int: + projected_total = 0 + projection_ts = datetime.now(timezone.utc) + for order in open_orders: + projection = open_order_projection(order, projection_ts) + if projection is None: + continue + context = existing_projection_context(positions, projection.symbol) + projected_total += 1 + apply_projected_position_decision( + positions, + projection_strategy_decision(projection, projection_ts), + ) + record_open_order_projection_metadata(positions, projection, context) + return projected_total + + +def is_runtime_risk_increasing_entry( + decision: StrategyDecision, + positions: list[dict[str, Any]], +) -> bool: + qty = optional_decimal(decision.qty) + if qty is None or qty <= 0: + return False + current_position_qty = position_qty(decision.symbol, positions) + if decision.action == "buy": + if current_position_qty < 0: + return qty > abs(current_position_qty) + return True + if current_position_qty <= 0: + return True + return qty > current_position_qty + + +def resolve_signal_regime(signal: Any) -> Optional[str]: + payload_map = cast(Mapping[str, Any], signal.payload) + macd = optional_decimal(payload_map.get("macd")) + if macd is None and isinstance(payload_map.get("macd"), Mapping): + macd_block = cast(Mapping[str, Any], payload_map.get("macd")) + macd = optional_decimal(macd_block.get("macd")) + macd_signal = optional_decimal(payload_map.get("macd_signal")) + if macd_signal is None and isinstance(payload_map.get("macd"), Mapping): + macd_block = cast(Mapping[str, Any], payload_map.get("macd")) + macd_signal = optional_decimal(macd_block.get("signal")) + resolved = resolve_regime_route_label( + payload_map, + macd=macd, + macd_signal=macd_signal, + ) + if resolved != "unknown": + return resolved + return resolve_legacy_regime_label(payload_map) + + +def resolve_decision_regime_label_with_source( + decision: StrategyDecision, +) -> tuple[Optional[str], str, str | None]: + params = cast(Mapping[str, Any], decision.params) + allocator_label = allocator_regime_label(params) + if allocator_label is not None: + return allocator_label, "allocator", None + hmm_resolution = hmm_regime_label(params) + if hmm_resolution is not None: + return hmm_resolution + direct = params.get("regime_label") + if isinstance(direct, str) and direct.strip(): + return direct.strip().lower(), "legacy", None + legacy_label = resolve_legacy_regime_label(params) + return ( + legacy_label, + "legacy", + None if legacy_label is not None else "missing", + ) + + +def extract_top_regime_posterior_probability( + posterior: Mapping[str, str], +) -> Decimal | None: + top_probability = None + for raw_probability in posterior.values(): + try: + parsed_probability = Decimal(raw_probability) + except (ArithmeticError, ValueError): + continue + if parsed_probability < 0 or parsed_probability > 1: + continue + if top_probability is None or parsed_probability > top_probability: + top_probability = parsed_probability + return top_probability + + +def select_strictest_runtime_uncertainty_gate( + candidates: list[RuntimeUncertaintyGate], +) -> RuntimeUncertaintyGate: + selected = candidates[0] + for candidate in candidates[1:]: + if runtime_uncertainty_gate_rank( + candidate.action + ) > runtime_uncertainty_gate_rank(selected.action): + selected = candidate + return selected + + +def autonomy_gate_report_is_saturated_fail_sentinel( + *, + action: RuntimeUncertaintyGateAction, + coverage_error: Decimal | None, + shift_score: Decimal | None, + conformal_interval_width: Decimal | None, +) -> bool: + if action != "fail" or coverage_error is None or coverage_error < Decimal("1"): + return False + if shift_score is not None and shift_score < Decimal("1"): + return False + if conformal_interval_width is None: + return True + return conformal_interval_width <= 0 + + +def uncertainty_gate_staleness_reason( + source: str, + payload: Mapping[str, Any], +) -> str | None: + if "generated_at" not in payload: + return None + timestamp = coerce_gateway_timestamp(payload.get("generated_at")) + if timestamp is None: + return f"{source}_generated_at_unparseable" + age_seconds = int((datetime.now(timezone.utc) - timestamp).total_seconds()) + if age_seconds > RUNTIME_UNCERTAINTY_GATE_MAX_STALENESS_SECONDS: + return f"{source}_generated_at_stale" + return None + + +def hash_payload(payload: dict[str, Any]) -> str: + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(encoded).hexdigest() + + +def resolve_llm_unavailable_reject_reason(reason: str | None) -> str: + normalized = normalize_reason_metric(reason) + if normalized == "unknown": + return "llm_unavailable_unknown" + if normalized.startswith("llm_unavailable_"): + return normalized + return f"llm_unavailable_{normalized}" + + +def classify_llm_error(error: Exception) -> Optional[str]: + message = str(error) + if message == "llm_response_not_json": + return "llm_response_not_json" + if message == "llm_response_invalid": + return "llm_response_invalid" + return None + + +def resolve_llm_review_error_reject_reason(error: Exception) -> str: + label = classify_llm_error(error) + if label == "llm_response_not_json": + return "llm_review_error_response_not_json" + if label == "llm_response_invalid": + return "llm_review_error_response_invalid" + return f"llm_review_error_{type(error).__name__}" + + +def attach_dspy_lineage( + response_json: dict[str, Any], + *, + artifact_source: str, + error: str | None = None, +) -> None: + payload = response_json.get("dspy") + if isinstance(payload, Mapping): + dspy_payload = { + str(key): value for key, value in cast(Mapping[str, Any], payload).items() + } + if normalize_optional_text(dspy_payload.get("artifact_source")) is None: + dspy_payload["artifact_source"] = artifact_source + if error is not None and error.strip() and "error" not in dspy_payload: + dspy_payload["error"] = error.strip() + response_json["dspy"] = dspy_payload + else: + response_json["dspy"] = runtime_dspy_metadata( + artifact_source=artifact_source, + error=error, + ) + response_json["dspy_lineage"] = build_dspy_lineage(response_json) + + +def committee_trace_has_veto(response_json: Mapping[str, Any]) -> bool: + committee_payload = response_json.get("committee") + if not isinstance(committee_payload, Mapping): + return False + committee_roles = cast(Mapping[str, Any], committee_payload).get("roles") + if not isinstance(committee_roles, Mapping): + return False + for role_payload in cast(Mapping[str, Any], committee_roles).values(): + if not isinstance(role_payload, Mapping): + continue + verdict = normalize_optional_text( + cast(Mapping[str, Any], role_payload).get("verdict") + ) + if verdict == "veto": + return True + return False + + +def build_committee_veto_alignment_payload( + *, + committee_veto: bool, + deterministic_veto: bool, +) -> dict[str, bool]: + return { + "committee_veto": committee_veto, + "deterministic_veto": deterministic_veto, + "aligned": (not committee_veto) or deterministic_veto, + } + + +def normalize_rollout_stage(stage: str) -> str: + if stage.startswith("stage0"): + return "stage0" + if stage.startswith("stage1"): + return "stage1" + if stage.startswith("stage2"): + return "stage2" + if stage.startswith("stage3"): + return "stage3" + return "stage3" + + +def llm_guardrail_controls_snapshot() -> dict[str, Any]: + return { + "min_confidence": settings.llm_min_confidence, + "min_calibrated_probability": settings.llm_min_calibrated_top_probability, + "min_probability_margin": settings.llm_min_probability_margin, + "max_uncertainty_score": settings.llm_max_uncertainty, + "max_uncertainty_band": settings.llm_max_uncertainty_band, + "min_calibration_quality_score": settings.llm_min_calibration_quality_score, + "abstain_fail_mode": settings.llm_abstain_fail_mode, + "escalation_fail_mode": settings.llm_escalate_fail_mode, + "uncertainty_fail_mode": settings.llm_quality_fail_mode, + "effective_fail_mode": settings.llm_effective_fail_mode_for_current_rollout(), + } + + +def position_qty(symbol: str, positions: list[dict[str, Any]]) -> Decimal: + total_qty = Decimal("0") + for position in positions: + if position.get("symbol") != symbol: + continue + qty = optional_decimal(position.get("qty")) + if qty is None: + qty = optional_decimal(position.get("quantity")) + if qty is None: + continue + side = str(position.get("side") or "").strip().lower() + if side == "short": + qty = -abs(qty) + total_qty += qty + return total_qty + + +def position_market_value( + symbol: str, + positions: list[dict[str, Any]], +) -> Decimal | None: + total_market_value = Decimal("0") + has_market_value = False + for position in positions: + if position.get("symbol") != symbol: + continue + market_value = optional_decimal(position.get("market_value")) + if market_value is None: + continue + total_market_value += market_value + has_market_value = True + if not has_market_value: + return None + return total_market_value + + +def projected_position_market_value( + current_market_value: Decimal | None, + delta: Decimal, + decision_price: Decimal | None, +) -> Decimal | None: + if decision_price is None: + return current_market_value + return (current_market_value or Decimal("0")) + (delta * decision_price) + + +def extract_decision_price(decision: StrategyDecision) -> Decimal | None: + for key in ("price", "limit_price", "stop_price"): + value = decision.params.get(key) + if value is None: + value = getattr(decision, key, None) + if value is not None: + return optional_decimal(value) + return None + + +def open_order_projection( + order: Mapping[str, Any], + projection_ts: datetime, +) -> OpenOrderProjection | None: + _ = projection_ts + symbol = str(order.get("symbol") or "").strip().upper() + side = str(order.get("side") or "").strip().lower() + qty = optional_decimal(order.get("qty")) + filled_qty = optional_decimal(order.get("filled_qty")) or Decimal("0") + if not symbol or side not in {"buy", "sell"} or qty is None or qty <= 0: + return None + remaining_qty = qty - max(filled_qty, Decimal("0")) + if remaining_qty <= 0: + return None + action = cast(Literal["buy", "sell"], side) + return OpenOrderProjection( + symbol=symbol, + action=action, + qty=remaining_qty, + order_type=normalized_order_type(order), + time_in_force=normalized_time_in_force(order), + client_order_id=str( + order.get("client_order_id") or order.get("client_orderid") or "" + ).strip(), + params=open_order_price_params(order), + ) + + +def open_order_price_params(order: Mapping[str, Any]) -> dict[str, Any]: + price = ( + optional_decimal(order.get("limit_price")) + or optional_decimal(order.get("stop_price")) + or optional_decimal(order.get("notional_price")) + ) + return {"price": str(price)} if price is not None else {} + + +def normalized_order_type( + order: Mapping[str, Any], +) -> Literal["market", "limit", "stop", "stop_limit"]: + order_type = str(order.get("type") or order.get("order_type") or "market") + normalized = order_type.strip().lower() + if normalized not in PROJECTED_ORDER_TYPES: + normalized = "market" + return cast(Literal["market", "limit", "stop", "stop_limit"], normalized) + + +def normalized_time_in_force( + order: Mapping[str, Any], +) -> Literal["day", "gtc", "ioc", "fok"]: + normalized = str(order.get("time_in_force") or "day").strip().lower() + if normalized not in PROJECTED_ORDER_TIME_IN_FORCE: + normalized = "day" + return cast(Literal["day", "gtc", "ioc", "fok"], normalized) + + +def existing_projection_context( + positions: list[dict[str, Any]], + symbol: str, +) -> ExistingProjectionContext: + existing_positions = [ + position + for position in positions + if str(position.get("symbol") or "").strip().upper() == symbol + ] + projection_ids: list[str] = [] + projection_only = True + projection_count = 0 + for position in existing_positions: + if not bool(position.get("open_order_projection")): + projection_only = False + continue + projection_count += int(position.get("open_order_projection_count") or 1) + projection_ids.extend(projection_ids_from_position(position)) + return ExistingProjectionContext( + projection_ids=projection_ids, + projection_only=projection_only, + projection_count=projection_count, + had_positions=bool(existing_positions), + ) + + +def projection_ids_from_position(position: Mapping[str, Any]) -> list[str]: + projection_ids: list[str] = [] + raw_ids = position.get("open_order_client_order_ids") + if isinstance(raw_ids, list): + projection_ids.extend( + str(item).strip() for item in cast(list[Any], raw_ids) if str(item).strip() + ) + raw_id = str(position.get("open_order_client_order_id") or "").strip() + if raw_id: + projection_ids.append(raw_id) + return projection_ids + + +def projection_strategy_decision( + projection: OpenOrderProjection, + projection_ts: datetime, +) -> StrategyDecision: + return StrategyDecision( + strategy_id="open_order_projection", + symbol=projection.symbol, + event_ts=projection_ts, + timeframe="open_order", + action=projection.action, + qty=projection.qty, + order_type=projection.order_type, + time_in_force=projection.time_in_force, + params=projection.params, + ) + + +def record_open_order_projection_metadata( + positions: list[dict[str, Any]], + projection: OpenOrderProjection, + context: ExistingProjectionContext, +) -> None: + for position in positions: + if str(position.get("symbol") or "").strip().upper() != projection.symbol: + continue + projection_ids = [*context.projection_ids] + if projection.client_order_id: + projection_ids.append(projection.client_order_id) + position["open_order_client_order_id"] = projection.client_order_id + if projection_ids: + position["open_order_client_order_ids"] = list( + dict.fromkeys(projection_ids) + ) + position["open_order_projection"] = True + position["open_order_projection_only"] = context.projection_only and bool( + projection_ids or not context.had_positions + ) + position["open_order_projection_count"] = context.projection_count + 1 + break + + +def allocator_regime_label(params: Mapping[str, Any]) -> str | None: + allocator = params.get("allocator") + if not isinstance(allocator, Mapping): + return None + allocator_regime = cast(Mapping[str, Any], allocator).get("regime_label") + if isinstance(allocator_regime, str) and allocator_regime.strip(): + return allocator_regime.strip().lower() + return None + + +def hmm_regime_label( + params: Mapping[str, Any], +) -> tuple[Optional[str], str, str | None] | None: + raw_regime_hmm = params.get("regime_hmm") + if not isinstance(raw_regime_hmm, Mapping): + return None + regime_context = resolve_hmm_context(cast(Mapping[str, Any], raw_regime_hmm)) + authority_reason = resolve_regime_context_authority_reason(regime_context) + if regime_context.is_authoritative: + return regime_context.regime_id.lower(), "hmm", None + if authority_reason is None: + return None, "hmm", "hmm_non_authoritative" + legacy_label = resolve_legacy_regime_label(params) + if legacy_label is not None: + return legacy_label, "legacy", authority_reason + return None, "none", authority_reason + + +def runtime_uncertainty_gate_rank(action: RuntimeUncertaintyGateAction) -> int: + ranking: dict[RuntimeUncertaintyGateAction, int] = { + "pass": 0, + "degrade": 1, + "abstain": 2, + "fail": 3, + } + return ranking[action] + + +def coerce_gateway_timestamp(value: Any) -> datetime | None: + if isinstance(value, datetime): + return value if value.tzinfo else value.replace(tzinfo=timezone.utc) + if isinstance(value, (int, float)): + return numeric_gateway_timestamp(value) + if not isinstance(value, str): + return None + return text_gateway_timestamp(value) + + +def numeric_gateway_timestamp(value: int | float) -> datetime | None: + try: + return datetime.fromtimestamp(float(value), tz=timezone.utc) + except (OverflowError, OSError, ValueError): + return None + + +def text_gateway_timestamp(value: str) -> datetime | None: + raw_value = value.strip() + if not raw_value: + return None + normalized = raw_value.replace("Z", "+00:00") + try: + parsed = datetime.fromisoformat(normalized) + except ValueError: + return None + return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc) + + +def normalize_optional_text(value: Any) -> str | None: + if value is None: + return None + if isinstance(value, str): + normalized = value.strip() + return normalized or None + normalized = str(value).strip() + return normalized or None + + +def runtime_dspy_metadata( + *, + artifact_source: str, + error: str | None = None, +) -> dict[str, Any]: + payload: dict[str, Any] = { + "mode": settings.llm_dspy_runtime_mode, + "program_name": settings.llm_dspy_program_name, + "signature_version": settings.llm_dspy_signature_version, + "artifact_hash": normalize_optional_text(settings.llm_dspy_artifact_hash), + "artifact_source": artifact_source, + } + if error is not None and error.strip(): + payload["error"] = error.strip() + return payload + + +def build_dspy_lineage(response_json: Mapping[str, Any]) -> dict[str, Any]: + payload = response_json.get("dspy") + dspy_payload: dict[str, Any] = {} + if isinstance(payload, Mapping): + dspy_payload = { + str(key): value for key, value in cast(Mapping[str, Any], payload).items() + } + return { + "mode": normalize_optional_text(dspy_payload.get("mode")) + or settings.llm_dspy_runtime_mode, + "program_name": normalize_optional_text(dspy_payload.get("program_name")) + or settings.llm_dspy_program_name, + "signature_version": normalize_optional_text( + dspy_payload.get("signature_version") + ) + or settings.llm_dspy_signature_version, + "artifact_hash": normalize_optional_text(dspy_payload.get("artifact_hash")) + or normalize_optional_text(settings.llm_dspy_artifact_hash), + "artifact_source": normalize_optional_text(dspy_payload.get("artifact_source")) + or "runtime", + } diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/trading_pipeline.py b/services/torghut/app/trading/scheduler/pipeline_modules/trading_pipeline.py new file mode 100644 index 0000000000..f305aeef47 --- /dev/null +++ b/services/torghut/app/trading/scheduler/pipeline_modules/trading_pipeline.py @@ -0,0 +1,28 @@ +"""Trading pipeline implementation.""" + +from __future__ import annotations + + +from .run_cycle import TradingPipelineRunCycleMixin +from .signal_processing import TradingPipelineSignalProcessingMixin +from .decision_lifecycle import TradingPipelineDecisionLifecycleMixin +from .submission_policy import TradingPipelineSubmissionPolicyMixin +from .runtime_gates import TradingPipelineRuntimeGatesMixin +from .llm_review import TradingPipelineReviewMixin +from .llm_outcomes import TradingPipelineReviewOutcomeMixin + + +class TradingPipeline( + TradingPipelineRunCycleMixin, + TradingPipelineSignalProcessingMixin, + TradingPipelineDecisionLifecycleMixin, + TradingPipelineSubmissionPolicyMixin, + TradingPipelineRuntimeGatesMixin, + TradingPipelineReviewMixin, + TradingPipelineReviewOutcomeMixin, + object, +): + pass + + +__all__ = ["TradingPipeline"] diff --git a/services/torghut/app/trading/scheduler/safety.py b/services/torghut/app/trading/scheduler/safety.py index 4306f37099..c7a808d26e 100644 --- a/services/torghut/app/trading/scheduler/safety.py +++ b/services/torghut/app/trading/scheduler/safety.py @@ -1,5 +1,4 @@ """Scheduler safety and market-session helpers.""" -# pyright: reportUnusedImport=false, reportPrivateUsage=false from __future__ import annotations @@ -145,6 +144,14 @@ def _signal_bootstrap_grace_active( return (reference - started_at).total_seconds() < max(0, int(grace_seconds)) +FRESH_TAIL_NO_SIGNAL_REASONS = _FRESH_TAIL_NO_SIGNAL_REASONS +is_market_session_open = _is_market_session_open +latch_signal_continuity_alert_state = _latch_signal_continuity_alert_state +record_signal_continuity_recovery_cycle = _record_signal_continuity_recovery_cycle +signal_bootstrap_grace_active = _signal_bootstrap_grace_active +signal_tail_is_fresh = _signal_tail_is_fresh + + __all__ = [ "_coerce_recovery_reason_sequence", "_FRESH_TAIL_NO_SIGNAL_REASONS", @@ -156,4 +163,10 @@ def _signal_bootstrap_grace_active( "_signal_bootstrap_grace_active", "_signal_tail_is_fresh", "_split_emergency_stop_reasons", + "FRESH_TAIL_NO_SIGNAL_REASONS", + "is_market_session_open", + "latch_signal_continuity_alert_state", + "record_signal_continuity_recovery_cycle", + "signal_bootstrap_grace_active", + "signal_tail_is_fresh", ] diff --git a/services/torghut/app/trading/scheduler/state.py b/services/torghut/app/trading/scheduler/state.py index ae0190ae4d..c6081e509c 100644 --- a/services/torghut/app/trading/scheduler/state.py +++ b/services/torghut/app/trading/scheduler/state.py @@ -12,10 +12,17 @@ split_reason_codes as _split_reason_codes, ) +normalize_reason_metric = _normalize_reason_metric +optional_decimal = _optional_decimal +split_reason_codes = _split_reason_codes + __all__ = [ "_normalize_reason_metric", "_split_reason_codes", "_optional_decimal", + "normalize_reason_metric", + "split_reason_codes", + "optional_decimal", "RuntimeUncertaintyGateAction", "RuntimeUncertaintyGate", "TradingMetrics", diff --git a/services/torghut/app/trading/scheduler/state.pyi b/services/torghut/app/trading/scheduler/state.pyi index 40fcd2e69e..0eb81de53f 100644 --- a/services/torghut/app/trading/scheduler/state.pyi +++ b/services/torghut/app/trading/scheduler/state.pyi @@ -7,7 +7,7 @@ from collections.abc import Mapping, Sequence from dataclasses import dataclass, field from datetime import datetime from decimal import Decimal -from typing import Any, Literal, Optional, cast +from typing import Any, Literal, Optional, TypeAlias, cast from ..decisions import DecisionRuntimeTelemetry from ..portfolio import AllocationResult from ..route_metadata import coerce_route_text @@ -16,8 +16,11 @@ from ..tca import AdaptiveExecutionPolicyDecision def _normalize_reason_metric(*args: Any, **kwargs: Any) -> Any: ... def _split_reason_codes(*args: Any, **kwargs: Any) -> Any: ... def _optional_decimal(*args: Any, **kwargs: Any) -> Any: ... +def normalize_reason_metric(*args: Any, **kwargs: Any) -> Any: ... +def split_reason_codes(*args: Any, **kwargs: Any) -> Any: ... +def optional_decimal(*args: Any, **kwargs: Any) -> Any: ... -RuntimeUncertaintyGateAction: Any +RuntimeUncertaintyGateAction: TypeAlias = Literal["pass", "degrade", "abstain", "fail"] class RuntimeUncertaintyGate: def __init__(*args: Any, **kwargs: Any) -> None: ... diff --git a/services/torghut/app/trading/scheduler/submission_preparation.py b/services/torghut/app/trading/scheduler/submission_preparation.py index ed56bc3396..a9fd776a7d 100644 --- a/services/torghut/app/trading/scheduler/submission_preparation.py +++ b/services/torghut/app/trading/scheduler/submission_preparation.py @@ -1,14 +1,7 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false +"""Public simple-pipeline submission preparation import surface.""" + from __future__ import annotations -from importlib import import_module as _import_module -import sys as _sys +from .submission_preparation_modules import SimplePipelineSubmissionPreparationMixin -_module_name = __name__ -_parent_name, _, _module_attr = _module_name.rpartition(".") -_impl = _import_module("app.trading.scheduler.submission_preparation_modules") -globals().update(_impl.__dict__) -_sys.modules[_module_name] = _impl -_parent = _sys.modules.get(_parent_name) -if _parent is not None: - setattr(_parent, _module_attr, _impl) +__all__ = ["SimplePipelineSubmissionPreparationMixin"] diff --git a/services/torghut/app/trading/scheduler/submission_preparation_modules/__init__.py b/services/torghut/app/trading/scheduler/submission_preparation_modules/__init__.py index f11583281d..81df355fb1 100644 --- a/services/torghut/app/trading/scheduler/submission_preparation_modules/__init__.py +++ b/services/torghut/app/trading/scheduler/submission_preparation_modules/__init__.py @@ -1,73 +1,17 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false -from __future__ import annotations - -from importlib import import_module as __compat_import_module__ -import logging as __compat_logging__ -import sys as __compat_sys__ -import types as __compat_types__ - -__compat_part_modules__: list[__compat_types__.ModuleType] = [] - - -class __CompatModule__(__compat_types__.ModuleType): - def __setattr__(self, name: str, value: object) -> None: - super().__setattr__(name, value) - for module in __compat_part_modules__: - module.__dict__[name] = value - - -def __compat_export__(module: __compat_types__.ModuleType) -> None: - for name, value in module.__dict__.items(): - if name.startswith("__"): - continue - globals()[name] = value +"""Semantic modules for simple-pipeline submission preparation.""" +from __future__ import annotations -__compat_module__ = __compat_import_module__(f"{__name__}.part_01_statements_74") -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) - -__compat_module__ = __compat_import_module__( - f"{__name__}.part_02_simplepipelinesubmissionpreparationmixinme" -) -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) - -__compat_module__ = __compat_import_module__( - f"{__name__}.part_03_simplepipelinesubmissionpreparationmixinme" -) -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) - -__compat_module__ = __compat_import_module__( - f"{__name__}.part_04_simplepipelinesubmissionpreparationmixinme" +from .direct_submission import ( + SimplePipelineSubmissionPreparationMixin, + SimplePipelineDirectSubmissionMixin, ) -__compat_part_modules__.append(__compat_module__) -__compat_export__(__compat_module__) -for __compat_loaded_module__ in __compat_part_modules__: - __compat_loaded_module__.__dict__.update( - {name: value for name, value in globals().items() if not name.startswith("__")} - ) +from .quote_routeability import SimplePipelineSubmissionQuoteRouteabilityMixin +from .quote_sizing import SimplePipelineSubmissionQuoteSizingMixin -__compat_sys__.modules[__name__].__class__ = __CompatModule__ -logger = __compat_logging__.getLogger(__name__.removesuffix("_modules")) -for __compat_loaded_module__ in globals().get("__compat_part_modules__", ()): - __compat_loaded_module__.__dict__["logger"] = logger __all__ = [ - name - for name in globals() - if not name.startswith("__") and not name.startswith("_CompatModule") + "SimplePipelineSubmissionPreparationMixin", + "SimplePipelineDirectSubmissionMixin", + "SimplePipelineSubmissionQuoteRouteabilityMixin", + "SimplePipelineSubmissionQuoteSizingMixin", ] -del __compat_module__ diff --git a/services/torghut/app/trading/scheduler/submission_preparation_modules/direct_submission.py b/services/torghut/app/trading/scheduler/submission_preparation_modules/direct_submission.py new file mode 100644 index 0000000000..4490194ea4 --- /dev/null +++ b/services/torghut/app/trading/scheduler/submission_preparation_modules/direct_submission.py @@ -0,0 +1,572 @@ +from __future__ import annotations + +from collections.abc import Mapping +from decimal import Decimal +from typing import Any, cast + + +from ....config import settings +from ...firewall import OrderFirewallBlocked +from ...models import StrategyDecision +from ...simple_risk import ( + position_qty_for_symbol, +) +from ..target_plan_helpers import ( + bounded_sim_collection_metadata_from_decision, + optional_decimal, + paper_route_probe_entry_metadata, + simple_buying_power_consumption, + simple_decision_notional, +) + + +from ..pipeline_modules.shared import TradingPipelineBase +from ..pipeline_modules.support import extract_json_error_payload + +from .shared import ( + OrderSubmitRequest, + RiskVerdictRequest, + SubmissionDecisionContext, + SubmitRejectionRequest, + TradingSubmissionRequest, +) +from .quote_sizing import SimplePipelineSubmissionQuoteSizingMixin +from .quote_routeability import SimplePipelineSubmissionQuoteRouteabilityMixin + + +class SimplePipelineDirectSubmissionMixin(TradingPipelineBase): + def _passes_risk_verdict( + self, + request: RiskVerdictRequest | Any | None = None, + **legacy_kwargs: Any, + ) -> bool: + request = self._risk_verdict_request(request, legacy_kwargs) + _ = ( + request.context.strategy, + request.context.account, + request.symbol_allowlist, + request.execution_advisor, + ) + short_reason = self._simple_shortability_reason( + decision=request.decision, + positions=request.context.positions, + ) + if short_reason is None: + return True + self._record_decision_rejection( + session=request.context.session, + decision=request.decision, + decision_row=request.context.decision_row, + reasons=[short_reason], + log_template="Simple-lane decision rejected strategy_id=%s symbol=%s reason=%s", + ) + return False + + def _is_trading_submission_allowed( + self, + request: TradingSubmissionRequest | None = None, + **legacy_kwargs: Any, + ) -> bool: + request = self._trading_submission_request(request, legacy_kwargs) + checks = ( + self._trading_enabled_submission_allowed, + self._firewall_submission_allowed, + self._live_mode_submission_allowed, + self._profitability_floor_submission_allowed, + self._profitability_floor_symbol_submission_allowed, + self._emergency_stop_submission_allowed, + self._paper_route_target_window_submission_allowed, + ) + for check in checks: + if not check(request): + return False + return True + + @staticmethod + def _risk_verdict_request( + request: RiskVerdictRequest | Any | None, + legacy_kwargs: Mapping[str, Any], + ) -> RiskVerdictRequest: + if request is not None: + if hasattr(request, "symbol_allowlist"): + return request + context = request.context + symbol_allowlist = cast( + set[str], + getattr(context, "symbol_allowlist", set[str]()), + ) + return RiskVerdictRequest( + context=SubmissionDecisionContext( + session=context.session, + decision_row=context.decision_row, + strategy=context.strategy, + account=context.account, + positions=context.positions, + ), + decision=request.decision, + symbol_allowlist=symbol_allowlist, + execution_advisor=request.execution_advisor, + ) + return RiskVerdictRequest( + context=SubmissionDecisionContext( + session=legacy_kwargs["session"], + decision_row=legacy_kwargs["decision_row"], + strategy=legacy_kwargs["strategy"], + account=legacy_kwargs["account"], + positions=legacy_kwargs["positions"], + ), + decision=legacy_kwargs["decision"], + symbol_allowlist=legacy_kwargs["symbol_allowlist"], + execution_advisor=legacy_kwargs.get("execution_advisor"), + ) + + @staticmethod + def _trading_submission_request( + request: TradingSubmissionRequest | None, + legacy_kwargs: Mapping[str, Any], + ) -> TradingSubmissionRequest: + if request is not None: + return request + return TradingSubmissionRequest( + session=legacy_kwargs["session"], + decision=legacy_kwargs["decision"], + decision_row=legacy_kwargs["decision_row"], + ) + + def _trading_enabled_submission_allowed( + self, + request: TradingSubmissionRequest, + ) -> bool: + if not settings.trading_enabled: + self._block_decision_submission( + session=request.session, + decision=request.decision, + decision_row=request.decision_row, + reason="trading_disabled", + submission_stage="blocked_trading_disabled", + ) + return False + return True + + def _firewall_submission_allowed( + self, + request: TradingSubmissionRequest, + ) -> bool: + firewall_status = self.order_firewall.status() + if firewall_status.kill_switch_enabled: + self._record_decision_rejection( + session=request.session, + decision=request.decision, + decision_row=request.decision_row, + reasons=["kill_switch_enabled"], + log_template="Simple-lane decision rejected strategy_id=%s symbol=%s reason=%s", + ) + return False + return True + + def _live_mode_submission_allowed( + self, + request: TradingSubmissionRequest, + ) -> bool: + if settings.trading_mode == "live": + live_submission_gate = self._live_submission_gate(session=request.session) + if not bool(live_submission_gate.get("allowed", False)): + self._block_decision_submission( + session=request.session, + decision=request.decision, + decision_row=request.decision_row, + reason=str( + live_submission_gate.get("reason") + or "live_submission_gate_blocked" + ), + submission_stage="blocked_live_submission_gate", + extra_metadata={"live_submission_gate": live_submission_gate}, + ) + return False + return True + + def _profitability_floor_submission_allowed( + self, + request: TradingSubmissionRequest, + ) -> bool: + decision = request.decision + session = request.session + decision_row = request.decision_row + proof_floor = self._profitability_proof_floor(session=session) + proof_floor_block_reason = self._proof_floor_submission_block_reason( + proof_floor + ) + if proof_floor_block_reason is None: + return True + collection_metadata = self._bounded_sim_collection_metadata(decision) + if not settings.trading_simple_submit_enabled and collection_metadata is None: + self._block_simple_submit_disabled(request, proof_floor_block_reason) + return False + if self._paper_route_probe_applies(decision, collection_metadata): + return True + self._block_decision_submission( + session=session, + decision=decision, + decision_row=decision_row, + reason=proof_floor_block_reason, + submission_stage="blocked_profitability_proof_floor", + capital_stage=str(proof_floor.get("capital_state") or "zero_notional"), + extra_metadata={"profitability_proof_floor": dict(proof_floor)}, + ) + return False + + def _profitability_floor_symbol_submission_allowed( + self, + request: TradingSubmissionRequest, + ) -> bool: + decision = request.decision + collection_metadata = self._bounded_sim_collection_metadata(decision) + if self._paper_route_probe_applies(decision, collection_metadata): + return True + proof_floor = self._profitability_proof_floor(session=request.session) + proof_floor_symbol_block_reason = self._proof_floor_symbol_block_reason( + proof_floor, + decision.symbol, + ) + if proof_floor_symbol_block_reason is not None: + self._block_decision_submission( + session=request.session, + decision=decision, + decision_row=request.decision_row, + reason=proof_floor_symbol_block_reason, + submission_stage="blocked_profitability_route_symbol", + capital_stage=str(proof_floor.get("capital_state") or "zero_notional"), + extra_metadata={"profitability_proof_floor": dict(proof_floor)}, + ) + return False + return True + + def _emergency_stop_submission_allowed( + self, + request: TradingSubmissionRequest, + ) -> bool: + if settings.trading_emergency_stop_enabled and self.state.emergency_stop_active: + self._block_decision_submission( + session=request.session, + decision=request.decision, + decision_row=request.decision_row, + reason=self.state.emergency_stop_reason or "emergency_stop_active", + submission_stage="blocked_emergency_stop", + ) + return False + return True + + def _paper_route_target_window_submission_allowed( + self, + request: TradingSubmissionRequest, + ) -> bool: + decision = request.decision + active_target_window = self._active_bounded_paper_route_target_window( + request.decision + ) + if active_target_window is not None: + collection_metadata = self._bounded_sim_collection_metadata(decision) + exit_metadata = self._paper_route_probe_exit_metadata(decision) + if collection_metadata is None and exit_metadata is None: + self._block_decision_submission( + session=request.session, + decision=decision, + decision_row=request.decision_row, + reason="paper_route_target_window_requires_scoped_source_decision", + submission_stage="blocked_paper_route_target_window_unscoped", + capital_stage="shadow", + extra_metadata={ + "paper_route_target_window": active_target_window, + "simple_lane": { + "submit_enabled": settings.trading_simple_submit_enabled, + "bounded_sim_collection_required": True, + "bounded_sim_collection_bypass": False, + }, + }, + ) + return False + return True + + def _block_simple_submit_disabled( + self, + request: TradingSubmissionRequest, + proof_floor_block_reason: str, + ) -> None: + self._block_decision_submission( + session=request.session, + decision=request.decision, + decision_row=request.decision_row, + reason="simple_submit_disabled", + submission_stage="blocked_simple_submit_disabled", + capital_stage="shadow", + extra_metadata={ + "simple_lane": { + "submit_enabled": False, + "bounded_sim_collection_bypass": False, + "bounded_sim_collection_required": True, + "proof_floor_block_reason": proof_floor_block_reason, + } + }, + ) + + def _bounded_sim_collection_metadata( + self, + decision: StrategyDecision, + ) -> Mapping[str, Any] | None: + return bounded_sim_collection_metadata_from_decision( + decision, + account_label=self.account_label, + trading_mode=settings.trading_mode, + ) + + def _paper_route_probe_applies( + self, + decision: StrategyDecision, + collection_metadata: Mapping[str, Any] | None, + ) -> bool: + return settings.trading_mode == "paper" and ( + self._paper_route_probe_exit_metadata(decision) is not None + or paper_route_probe_entry_metadata(decision.params) is not None + or collection_metadata is not None + ) + + def _execution_client_for_symbol( + self, symbol: str, *, symbol_allowlist: set[str] | None = None + ) -> Any: + _ = (symbol, symbol_allowlist) + return self.execution_adapter + + def _submit_order_with_handling( + self, + request: OrderSubmitRequest | None = None, + **legacy_kwargs: Any, + ) -> tuple[Any | None, bool]: + request = self._order_submit_request(request, legacy_kwargs) + try: + retry_delays_seconds = [float(delay) for delay in request.retry_delays] + execution = self.executor.submit_order( + request.session, + request.execution_client, + request.decision, + request.decision_row, + self.account_label, + execution_expected_adapter=request.selected_adapter_name, + retry_delays=retry_delays_seconds, + ) + return execution, False + except OrderFirewallBlocked: + return self._reject_submit( + SubmitRejectionRequest( + session=request.session, + decision=request.decision, + decision_row=request.decision_row, + selected_adapter_name=request.selected_adapter_name, + reason="kill_switch_enabled", + rejection_type="firewall_blocked", + ) + ) + except Exception as exc: + payload = extract_json_error_payload(exc) or {} + reason = self._map_submit_exception(payload) + metadata = {"broker_precheck": payload} if payload else None + return self._reject_submit( + SubmitRejectionRequest( + session=request.session, + decision=request.decision, + decision_row=request.decision_row, + selected_adapter_name=request.selected_adapter_name, + reason=reason, + rejection_type="submit_failed", + metadata=metadata, + ) + ) + + @staticmethod + def _order_submit_request( + request: OrderSubmitRequest | None, + legacy_kwargs: Mapping[str, Any], + ) -> OrderSubmitRequest: + if request is not None: + return request + return OrderSubmitRequest( + session=legacy_kwargs["session"], + execution_client=legacy_kwargs["execution_client"], + decision=legacy_kwargs["decision"], + decision_row=legacy_kwargs["decision_row"], + selected_adapter_name=legacy_kwargs["selected_adapter_name"], + retry_delays=legacy_kwargs["retry_delays"], + ) + + def _reject_submit( + self, + request: SubmitRejectionRequest | None = None, + **legacy_kwargs: Any, + ) -> tuple[None, bool]: + request = self._submit_rejection_request(request, legacy_kwargs) + self.state.metrics.orders_rejected_total += 1 + self.state.metrics.record_decision_state("rejected") + self.state.metrics.record_decision_rejection_reasons([request.reason]) + self.state.metrics.record_execution_submit_result( + status="rejected", + adapter=request.selected_adapter_name, + ) + self.executor.mark_rejected( + request.session, + request.decision_row, + request.reason, + metadata_update=self._decision_lifecycle_metadata( + submission_stage="rejected_submit", + extra=request.metadata, + ), + ) + self._emit_domain_telemetry( + event_name="torghut.execution.rejected", + severity="warning", + decision=request.decision, + decision_row=request.decision_row, + reason_codes=[request.reason], + extra_properties={"rejection_type": request.rejection_type}, + ) + return None, True + + @staticmethod + def _submit_rejection_request( + request: SubmitRejectionRequest | None, + legacy_kwargs: Mapping[str, Any], + ) -> SubmitRejectionRequest: + if request is not None: + return request + return SubmitRejectionRequest( + session=legacy_kwargs["session"], + decision=legacy_kwargs["decision"], + decision_row=legacy_kwargs["decision_row"], + selected_adapter_name=legacy_kwargs["selected_adapter_name"], + reason=legacy_kwargs["reason"], + rejection_type=legacy_kwargs["rejection_type"], + metadata=legacy_kwargs.get("metadata"), + ) + + @staticmethod + def _map_submit_exception(payload: Mapping[str, Any]) -> str: + source = str(payload.get("source") or "").strip().lower() + code = str(payload.get("code") or "").strip().lower() + if source == "local_pre_submit": + if code in {"local_qty_invalid_increment"}: + return "invalid_qty_increment" + if code in {"local_qty_below_min", "local_qty_non_positive"}: + return "qty_below_min_after_clamp" + if code in { + "local_account_shorting_disabled", + "local_symbol_not_shortable", + "local_symbol_not_tradable", + "local_shorts_not_allowed", + "shorting_metadata_unavailable", + }: + return "shorting_not_allowed_for_asset" + return "broker_precheck_failed" + return "broker_submit_failed" + + def _simple_shortability_reason( + self, + *, + decision: StrategyDecision, + positions: list[dict[str, Any]], + ) -> str | None: + if not self._sell_order_needs_shortability(decision, positions): + return None + if not settings.trading_allow_shorts: + return "shorting_not_allowed_for_asset" + return self._account_or_asset_shortability_reason(decision.symbol) + + @staticmethod + def _sell_order_needs_shortability( + decision: StrategyDecision, + positions: list[dict[str, Any]], + ) -> bool: + if decision.action != "sell": + return False + current_qty = position_qty_for_symbol(positions, decision.symbol) + return not (current_qty > 0 and decision.qty <= current_qty) + + def _account_or_asset_shortability_reason(self, symbol: str) -> str | None: + account = self.order_firewall.get_account() + if account is not None: + shorting_enabled = account.get("shorting_enabled") + if isinstance(shorting_enabled, bool) and not shorting_enabled: + return "shorting_not_allowed_for_asset" + elif settings.trading_mode == "live": + return "shorting_not_allowed_for_asset" + + asset = self.order_firewall.get_asset(symbol) + if asset is not None: + tradable = asset.get("tradable") + shortable = asset.get("shortable") + if isinstance(tradable, bool) and not tradable: + return "shorting_not_allowed_for_asset" + if isinstance(shortable, bool) and not shortable: + return "shorting_not_allowed_for_asset" + elif settings.trading_mode == "live": + return "shorting_not_allowed_for_asset" + return None + + @staticmethod + def _apply_simple_projected_position( + positions: list[dict[str, Any]], + decision: StrategyDecision, + ) -> None: + normalized_symbol = decision.symbol.strip().upper() + updated = False + for position in positions: + if str(position.get("symbol") or "").strip().upper() != normalized_symbol: + continue + raw_qty = position.get("qty") or position.get("quantity") or "0" + try: + qty = Decimal(str(raw_qty)) + except (ArithmeticError, ValueError): + qty = Decimal("0") + side = str(position.get("side") or "").strip().lower() + signed_qty = -abs(qty) if side == "short" else qty + delta = decision.qty if decision.action == "buy" else -decision.qty + next_qty = signed_qty + delta + position["qty"] = str(abs(next_qty)) + position["side"] = "short" if next_qty < 0 else "long" + updated = True + break + if not updated: + positions.append( + { + "symbol": normalized_symbol, + "qty": str(decision.qty), + "side": "long" if decision.action == "buy" else "short", + } + ) + + @staticmethod + def _apply_simple_projected_buying_power( + account: dict[str, str], + positions: list[dict[str, Any]], + decision: StrategyDecision, + ) -> None: + buying_power = optional_decimal(account.get("buying_power")) + if buying_power is None: + return + notional = simple_decision_notional(decision) + if notional is None or notional <= 0: + return + consumed = simple_buying_power_consumption( + positions=positions, + decision=decision, + notional=notional, + ) + if consumed <= 0: + return + account["buying_power"] = str(max(buying_power - consumed, Decimal("0"))) + + +class SimplePipelineSubmissionPreparationMixin( + SimplePipelineSubmissionQuoteSizingMixin, + SimplePipelineSubmissionQuoteRouteabilityMixin, + SimplePipelineDirectSubmissionMixin, + object, +): + pass diff --git a/services/torghut/app/trading/scheduler/submission_preparation_modules/part_01_statements_74.py b/services/torghut/app/trading/scheduler/submission_preparation_modules/part_01_statements_74.py deleted file mode 100644 index 0c2b323f6b..0000000000 --- a/services/torghut/app/trading/scheduler/submission_preparation_modules/part_01_statements_74.py +++ /dev/null @@ -1,80 +0,0 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false - -from __future__ import annotations - -import logging -from collections.abc import Mapping, Sequence -from datetime import datetime, timezone -from decimal import Decimal, ROUND_DOWN -from typing import Any, Literal, Optional, cast - -from sqlalchemy.orm import Session - -from ....config import settings -from ....models import ( - Strategy, - TradeDecision, -) -from ...firewall import OrderFirewallBlocked -from ...models import SignalEnvelope, StrategyDecision -from ...prices import MarketSnapshot -from ...quote_quality import ( - QuoteQualityPolicy, - QuoteQualityStatus, - _status, - assess_signal_quote_quality, -) -from ...quantity_rules import quantize_qty_for_symbol, resolve_quantity_resolution -from ...runtime_decision_authority import ( - BOUNDED_PAPER_ROUTE_COLLECTION_SOURCE_DECISION_MODE, - ROUTE_ACQUISITION_SOURCE_DECISION_MODE, - STRATEGY_SIGNAL_PAPER_SOURCE_DECISION_MODE, - normalize_source_decision_mode, -) -from ...simple_risk import ( - position_qty_for_symbol, - prepare_simple_decision, -) -from ..pipeline_helpers import ( - _extract_json_error_payload, - _price_snapshot_payload, -) -from ..target_plan_helpers import ( - _PAPER_ROUTE_PROBE_QTY_STEP, - _TargetProbeQuantityResolution, - _bounded_collection_decision_requires_target_notional_sizing, - _bounded_paper_route_collection_entry_metadata, - _bounded_sim_collection_metadata_from_decision, - _decimal_from_mapping, - _executable_bid_ask_present, - _first_decimal, - _mapping_value, - _min_optional_decimal, - _optional_decimal, - _paper_route_probe_entry_metadata, - _parse_target_datetime, - _pct_cap_to_notional, - _quote_snapshot_from_mapping, - _quote_snapshot_reference_price, - _safe_int, - _safe_text, - _simple_buying_power_consumption, - _simple_decision_notional, - _snapshot_has_executable_quote, - _target_metadata_quote_snapshot, - _target_notional_sizing_audit_from_params, - _target_probe_cap, - _target_probe_symbol_actions, - _target_probe_symbol_notional_budget, - _target_probe_symbol_quantities, - _target_symbols, - _text_from_mapping, -) - -# ruff: noqa: F401,F403,F405,F811,F821 - - -logger = logging.getLogger(__name__) - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/submission_preparation_modules/part_04_simplepipelinesubmissionpreparationmixinme.py b/services/torghut/app/trading/scheduler/submission_preparation_modules/part_04_simplepipelinesubmissionpreparationmixinme.py deleted file mode 100644 index 043b8f7104..0000000000 --- a/services/torghut/app/trading/scheduler/submission_preparation_modules/part_04_simplepipelinesubmissionpreparationmixinme.py +++ /dev/null @@ -1,461 +0,0 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false - -from __future__ import annotations - -import logging -from collections.abc import Mapping, Sequence -from datetime import datetime, timezone -from decimal import Decimal, ROUND_DOWN -from typing import Any, Literal, Optional, cast - -from sqlalchemy.orm import Session - -from ....config import settings -from ....models import ( - Strategy, - TradeDecision, -) -from ...firewall import OrderFirewallBlocked -from ...models import SignalEnvelope, StrategyDecision -from ...prices import MarketSnapshot -from ...quote_quality import ( - QuoteQualityPolicy, - QuoteQualityStatus, - _status, - assess_signal_quote_quality, -) -from ...quantity_rules import quantize_qty_for_symbol, resolve_quantity_resolution -from ...runtime_decision_authority import ( - BOUNDED_PAPER_ROUTE_COLLECTION_SOURCE_DECISION_MODE, - ROUTE_ACQUISITION_SOURCE_DECISION_MODE, - STRATEGY_SIGNAL_PAPER_SOURCE_DECISION_MODE, - normalize_source_decision_mode, -) -from ...simple_risk import ( - position_qty_for_symbol, - prepare_simple_decision, -) -from ..pipeline_helpers import ( - _extract_json_error_payload, - _price_snapshot_payload, -) -from ..target_plan_helpers import ( - _PAPER_ROUTE_PROBE_QTY_STEP, - _TargetProbeQuantityResolution, - _bounded_collection_decision_requires_target_notional_sizing, - _bounded_paper_route_collection_entry_metadata, - _bounded_sim_collection_metadata_from_decision, - _decimal_from_mapping, - _executable_bid_ask_present, - _first_decimal, - _mapping_value, - _min_optional_decimal, - _optional_decimal, - _paper_route_probe_entry_metadata, - _parse_target_datetime, - _pct_cap_to_notional, - _quote_snapshot_from_mapping, - _quote_snapshot_reference_price, - _safe_int, - _safe_text, - _simple_buying_power_consumption, - _simple_decision_notional, - _snapshot_has_executable_quote, - _target_metadata_quote_snapshot, - _target_notional_sizing_audit_from_params, - _target_probe_cap, - _target_probe_symbol_actions, - _target_probe_symbol_notional_budget, - _target_probe_symbol_quantities, - _target_symbols, - _text_from_mapping, -) - -# ruff: noqa: F401,F403,F405,F811,F821 - -from .part_01_statements_74 import * -from .part_02_simplepipelinesubmissionpreparationmixinme import * -from .part_03_simplepipelinesubmissionpreparationmixinme import * - - -class _SimplePipelineSubmissionPreparationMixinMethodsPart3: - def _passes_risk_verdict( - self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - strategy: Strategy, - account: dict[str, str], - positions: list[dict[str, Any]], - symbol_allowlist: set[str], - execution_advisor: Mapping[str, Any] | None, - ) -> bool: - _ = (strategy, account, symbol_allowlist, execution_advisor) - short_reason = self._simple_shortability_reason( - decision=decision, - positions=positions, - ) - if short_reason is None: - return True - self._record_decision_rejection( - session=session, - decision=decision, - decision_row=decision_row, - reasons=[short_reason], - log_template="Simple-lane decision rejected strategy_id=%s symbol=%s reason=%s", - ) - return False - - def _is_trading_submission_allowed( - self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - ) -> bool: - if not settings.trading_enabled: - self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason="trading_disabled", - submission_stage="blocked_trading_disabled", - ) - return False - firewall_status = self.order_firewall.status() - if firewall_status.kill_switch_enabled: - self._record_decision_rejection( - session=session, - decision=decision, - decision_row=decision_row, - reasons=["kill_switch_enabled"], - log_template="Simple-lane decision rejected strategy_id=%s symbol=%s reason=%s", - ) - return False - if settings.trading_mode == "live": - live_submission_gate = self._live_submission_gate(session=session) - if not bool(live_submission_gate.get("allowed", False)): - self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason=str( - live_submission_gate.get("reason") - or "live_submission_gate_blocked" - ), - submission_stage="blocked_live_submission_gate", - extra_metadata={"live_submission_gate": live_submission_gate}, - ) - return False - proof_floor = self._profitability_proof_floor(session=session) - proof_floor_block_reason = self._proof_floor_submission_block_reason( - proof_floor - ) - paper_route_probe_applied = False - if proof_floor_block_reason is not None: - collection_metadata = _bounded_sim_collection_metadata_from_decision( - decision, - account_label=self.account_label, - trading_mode=settings.trading_mode, - ) - if not settings.trading_simple_submit_enabled: - if collection_metadata is None: - self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason="simple_submit_disabled", - submission_stage="blocked_simple_submit_disabled", - capital_stage="shadow", - extra_metadata={ - "simple_lane": { - "submit_enabled": False, - "bounded_sim_collection_bypass": False, - "bounded_sim_collection_required": True, - "proof_floor_block_reason": proof_floor_block_reason, - } - }, - ) - return False - if settings.trading_mode == "paper" and ( - self._paper_route_probe_exit_metadata(decision) is not None - or _paper_route_probe_entry_metadata(decision.params) is not None - or collection_metadata is not None - ): - paper_route_probe_applied = True - if not paper_route_probe_applied: - self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason=proof_floor_block_reason, - submission_stage="blocked_profitability_proof_floor", - capital_stage=str( - proof_floor.get("capital_state") or "zero_notional" - ), - extra_metadata={"profitability_proof_floor": dict(proof_floor)}, - ) - return False - proof_floor_symbol_block_reason = ( - self._proof_floor_symbol_block_reason( - proof_floor, - decision.symbol, - ) - if not paper_route_probe_applied - else None - ) - if proof_floor_symbol_block_reason is not None: - self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason=proof_floor_symbol_block_reason, - submission_stage="blocked_profitability_route_symbol", - capital_stage=str(proof_floor.get("capital_state") or "zero_notional"), - extra_metadata={"profitability_proof_floor": dict(proof_floor)}, - ) - return False - if settings.trading_emergency_stop_enabled and self.state.emergency_stop_active: - self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason=self.state.emergency_stop_reason or "emergency_stop_active", - submission_stage="blocked_emergency_stop", - ) - return False - active_target_window = self._active_bounded_paper_route_target_window(decision) - if active_target_window is not None: - collection_metadata = _bounded_sim_collection_metadata_from_decision( - decision, - account_label=self.account_label, - trading_mode=settings.trading_mode, - ) - exit_metadata = self._paper_route_probe_exit_metadata(decision) - if collection_metadata is None and exit_metadata is None: - self._block_decision_submission( - session=session, - decision=decision, - decision_row=decision_row, - reason="paper_route_target_window_requires_scoped_source_decision", - submission_stage="blocked_paper_route_target_window_unscoped", - capital_stage="shadow", - extra_metadata={ - "paper_route_target_window": active_target_window, - "simple_lane": { - "submit_enabled": settings.trading_simple_submit_enabled, - "bounded_sim_collection_required": True, - "bounded_sim_collection_bypass": False, - }, - }, - ) - return False - return True - - def _execution_client_for_symbol( - self, symbol: str, *, symbol_allowlist: set[str] | None = None - ) -> Any: - _ = (symbol, symbol_allowlist) - return self.execution_adapter - - def _submit_order_with_handling( - self, - *, - session: Session, - execution_client: Any, - decision: StrategyDecision, - decision_row: TradeDecision, - selected_adapter_name: str, - retry_delays: list[int], - ) -> tuple[Any | None, bool]: - try: - retry_delays_seconds = [float(delay) for delay in retry_delays] - execution = self.executor.submit_order( - session, - execution_client, - decision, - decision_row, - self.account_label, - execution_expected_adapter=selected_adapter_name, - retry_delays=retry_delays_seconds, - ) - return execution, False - except OrderFirewallBlocked: - return self._reject_submit( - session=session, - decision=decision, - decision_row=decision_row, - selected_adapter_name=selected_adapter_name, - reason="kill_switch_enabled", - rejection_type="firewall_blocked", - ) - except Exception as exc: - payload = _extract_json_error_payload(exc) or {} - reason = self._map_submit_exception(payload) - metadata = {"broker_precheck": payload} if payload else None - return self._reject_submit( - session=session, - decision=decision, - decision_row=decision_row, - selected_adapter_name=selected_adapter_name, - reason=reason, - rejection_type="submit_failed", - metadata=metadata, - ) - - def _reject_submit( - self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - selected_adapter_name: str, - reason: str, - rejection_type: str, - metadata: Mapping[str, Any] | None = None, - ) -> tuple[None, bool]: - self.state.metrics.orders_rejected_total += 1 - self.state.metrics.record_decision_state("rejected") - self.state.metrics.record_decision_rejection_reasons([reason]) - self.state.metrics.record_execution_submit_result( - status="rejected", - adapter=selected_adapter_name, - ) - self.executor.mark_rejected( - session, - decision_row, - reason, - metadata_update=self._decision_lifecycle_metadata( - submission_stage="rejected_submit", - extra=metadata, - ), - ) - self._emit_domain_telemetry( - event_name="torghut.execution.rejected", - severity="warning", - decision=decision, - decision_row=decision_row, - reason_codes=[reason], - extra_properties={"rejection_type": rejection_type}, - ) - return None, True - - @staticmethod - def _map_submit_exception(payload: Mapping[str, Any]) -> str: - source = str(payload.get("source") or "").strip().lower() - code = str(payload.get("code") or "").strip().lower() - if source == "local_pre_submit": - if code in {"local_qty_invalid_increment"}: - return "invalid_qty_increment" - if code in {"local_qty_below_min", "local_qty_non_positive"}: - return "qty_below_min_after_clamp" - if code in { - "local_account_shorting_disabled", - "local_symbol_not_shortable", - "local_symbol_not_tradable", - "local_shorts_not_allowed", - "shorting_metadata_unavailable", - }: - return "shorting_not_allowed_for_asset" - return "broker_precheck_failed" - return "broker_submit_failed" - - def _simple_shortability_reason( - self, - *, - decision: StrategyDecision, - positions: list[dict[str, Any]], - ) -> str | None: - if decision.action != "sell": - return None - current_qty = position_qty_for_symbol(positions, decision.symbol) - if current_qty > 0 and decision.qty <= current_qty: - return None - if not settings.trading_allow_shorts: - return "shorting_not_allowed_for_asset" - - account = self.order_firewall.get_account() - if account is not None: - shorting_enabled = account.get("shorting_enabled") - if isinstance(shorting_enabled, bool) and not shorting_enabled: - return "shorting_not_allowed_for_asset" - elif settings.trading_mode == "live": - return "shorting_not_allowed_for_asset" - - asset = self.order_firewall.get_asset(decision.symbol) - if asset is not None: - tradable = asset.get("tradable") - shortable = asset.get("shortable") - if isinstance(tradable, bool) and not tradable: - return "shorting_not_allowed_for_asset" - if isinstance(shortable, bool) and not shortable: - return "shorting_not_allowed_for_asset" - elif settings.trading_mode == "live": - return "shorting_not_allowed_for_asset" - return None - - @staticmethod - def _apply_simple_projected_position( - positions: list[dict[str, Any]], - decision: StrategyDecision, - ) -> None: - normalized_symbol = decision.symbol.strip().upper() - updated = False - for position in positions: - if str(position.get("symbol") or "").strip().upper() != normalized_symbol: - continue - raw_qty = position.get("qty") or position.get("quantity") or "0" - try: - qty = Decimal(str(raw_qty)) - except (ArithmeticError, ValueError): - qty = Decimal("0") - side = str(position.get("side") or "").strip().lower() - signed_qty = -abs(qty) if side == "short" else qty - delta = decision.qty if decision.action == "buy" else -decision.qty - next_qty = signed_qty + delta - position["qty"] = str(abs(next_qty)) - position["side"] = "short" if next_qty < 0 else "long" - updated = True - break - if not updated: - positions.append( - { - "symbol": normalized_symbol, - "qty": str(decision.qty), - "side": "long" if decision.action == "buy" else "short", - } - ) - - @staticmethod - def _apply_simple_projected_buying_power( - account: dict[str, str], - positions: list[dict[str, Any]], - decision: StrategyDecision, - ) -> None: - buying_power = _optional_decimal(account.get("buying_power")) - if buying_power is None: - return - notional = _simple_decision_notional(decision) - if notional is None or notional <= 0: - return - consumed = _simple_buying_power_consumption( - positions=positions, - decision=decision, - notional=notional, - ) - if consumed <= 0: - return - account["buying_power"] = str(max(buying_power - consumed, Decimal("0"))) - - -class SimplePipelineSubmissionPreparationMixin( - _SimplePipelineSubmissionPreparationMixinMethodsPart1, - _SimplePipelineSubmissionPreparationMixinMethodsPart2, - _SimplePipelineSubmissionPreparationMixinMethodsPart3, - object, -): - pass - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/submission_preparation_modules/part_03_simplepipelinesubmissionpreparationmixinme.py b/services/torghut/app/trading/scheduler/submission_preparation_modules/quote_routeability.py similarity index 54% rename from services/torghut/app/trading/scheduler/submission_preparation_modules/part_03_simplepipelinesubmissionpreparationmixinme.py rename to services/torghut/app/trading/scheduler/submission_preparation_modules/quote_routeability.py index 96b86615ee..5bf7c9390a 100644 --- a/services/torghut/app/trading/scheduler/submission_preparation_modules/part_03_simplepipelinesubmissionpreparationmixinme.py +++ b/services/torghut/app/trading/scheduler/submission_preparation_modules/quote_routeability.py @@ -1,258 +1,62 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false - from __future__ import annotations -import logging -from collections.abc import Mapping, Sequence -from datetime import datetime, timezone -from decimal import Decimal, ROUND_DOWN +from collections.abc import Mapping +from decimal import Decimal from typing import Any, Literal, Optional, cast from sqlalchemy.orm import Session from ....config import settings from ....models import ( - Strategy, TradeDecision, ) -from ...firewall import OrderFirewallBlocked -from ...models import SignalEnvelope, StrategyDecision +from ...models import StrategyDecision from ...prices import MarketSnapshot from ...quote_quality import ( - QuoteQualityPolicy, QuoteQualityStatus, - _status, - assess_signal_quote_quality, -) -from ...quantity_rules import quantize_qty_for_symbol, resolve_quantity_resolution -from ...runtime_decision_authority import ( - BOUNDED_PAPER_ROUTE_COLLECTION_SOURCE_DECISION_MODE, - ROUTE_ACQUISITION_SOURCE_DECISION_MODE, - STRATEGY_SIGNAL_PAPER_SOURCE_DECISION_MODE, - normalize_source_decision_mode, ) from ...simple_risk import ( - position_qty_for_symbol, prepare_simple_decision, ) -from ..pipeline_helpers import ( - _extract_json_error_payload, - _price_snapshot_payload, -) from ..target_plan_helpers import ( - _PAPER_ROUTE_PROBE_QTY_STEP, - _TargetProbeQuantityResolution, - _bounded_collection_decision_requires_target_notional_sizing, - _bounded_paper_route_collection_entry_metadata, - _bounded_sim_collection_metadata_from_decision, - _decimal_from_mapping, - _executable_bid_ask_present, - _first_decimal, - _mapping_value, - _min_optional_decimal, - _optional_decimal, - _paper_route_probe_entry_metadata, - _parse_target_datetime, - _pct_cap_to_notional, - _quote_snapshot_from_mapping, - _quote_snapshot_reference_price, - _safe_int, - _safe_text, - _simple_buying_power_consumption, - _simple_decision_notional, - _snapshot_has_executable_quote, - _target_metadata_quote_snapshot, - _target_notional_sizing_audit_from_params, - _target_probe_cap, - _target_probe_symbol_actions, - _target_probe_symbol_notional_budget, - _target_probe_symbol_quantities, - _target_symbols, - _text_from_mapping, + mapping_value, + min_optional_decimal, + optional_decimal, + paper_route_probe_entry_metadata, + pct_cap_to_notional, + safe_int, + safe_text, + target_notional_sizing_audit_from_params, + target_plan_symbols, ) -# ruff: noqa: F401,F403,F405,F811,F821 -from .part_01_statements_74 import * -from .part_02_simplepipelinesubmissionpreparationmixinme import * +from ..pipeline_modules.shared import TradingPipelineBase + +from .quote_routeability_values import ( + assess_paper_route_quote_status, + quote_routeability_inputs, + quote_routeability_values, +) +from .shared import ( + QuoteRouteabilityPayloadRequest, + SubmissionPreparationRequest, + logger, +) -class _SimplePipelineSubmissionPreparationMixinMethodsPart2: +class SimplePipelineSubmissionQuoteRouteabilityMixin(TradingPipelineBase): def _paper_route_quote_routeability( self, decision: StrategyDecision, snapshot: MarketSnapshot | None, ) -> tuple[QuoteQualityStatus, dict[str, object]]: - params = decision.params - price_snapshot_raw = params.get("price_snapshot") - price_snapshot: Mapping[str, Any] - if isinstance(price_snapshot_raw, Mapping): - price_snapshot = cast(Mapping[str, Any], price_snapshot_raw) - else: - price_snapshot = {} - target_quote_snapshot = _target_metadata_quote_snapshot( - params, - symbol=decision.symbol, - ) - target_price_snapshot: Mapping[str, Any] = ( - target_quote_snapshot - if target_quote_snapshot is not None - else cast(Mapping[str, Any], {}) - ) - if not price_snapshot and target_quote_snapshot is not None: - price_snapshot = target_quote_snapshot - - params_price = _optional_decimal(params.get("price")) - snapshot_price = snapshot.price if snapshot is not None else None - payload_price = _decimal_from_mapping( - price_snapshot, - ("price", "mid", "mid_price", "midpoint"), - ) - target_payload_price = _decimal_from_mapping( - target_price_snapshot, - ("price", "mid", "mid_price", "midpoint"), - ) - price = _first_decimal( - snapshot_price, - payload_price, - target_payload_price, - params_price, - ) - - snapshot_bid = snapshot.bid if snapshot is not None else None - payload_bid = _decimal_from_mapping( - price_snapshot, - ("bid", "bid_px", "bid_price", "bp"), - ) - target_payload_bid = _decimal_from_mapping( - target_price_snapshot, - ("bid", "bid_px", "bid_price", "bp"), - ) - params_bid = _optional_decimal(params.get("imbalance_bid_px")) - bid = _first_decimal(snapshot_bid, payload_bid, target_payload_bid, params_bid) - - snapshot_ask = snapshot.ask if snapshot is not None else None - payload_ask = _decimal_from_mapping( - price_snapshot, - ("ask", "ask_px", "ask_price", "ap"), - ) - target_payload_ask = _decimal_from_mapping( - target_price_snapshot, - ("ask", "ask_px", "ask_price", "ap"), - ) - params_ask = _optional_decimal(params.get("imbalance_ask_px")) - ask = _first_decimal(snapshot_ask, payload_ask, target_payload_ask, params_ask) - - snapshot_spread = snapshot.spread if snapshot is not None else None - payload_spread = _decimal_from_mapping( - price_snapshot, - ("spread", "imbalance_spread"), - ) - target_payload_spread = _decimal_from_mapping( - target_price_snapshot, - ("spread", "imbalance_spread"), - ) - params_spread = _optional_decimal(params.get("spread")) - computed_spread = ask - bid if bid is not None and ask is not None else None - spread = _first_decimal( - snapshot_spread, - payload_spread, - target_payload_spread, - params_spread, - computed_spread, - ) - using_target_executable_quote = ( - target_quote_snapshot is not None - and snapshot_bid is None - and snapshot_ask is None - and payload_bid is None - and payload_ask is None - and target_payload_bid is not None - and target_payload_ask is not None - ) - target_quote_as_of = ( - _parse_target_datetime(target_price_snapshot.get("quote_as_of")) - or _parse_target_datetime(target_price_snapshot.get("as_of")) - or _parse_target_datetime(target_price_snapshot.get("timestamp")) - ) - price_snapshot_quote_as_of = ( - _parse_target_datetime(price_snapshot.get("quote_as_of")) - or _parse_target_datetime(price_snapshot.get("as_of")) - or _parse_target_datetime(price_snapshot.get("timestamp")) - ) - quote_as_of = ( - target_quote_as_of - if using_target_executable_quote and target_quote_as_of is not None - else ( - snapshot.quote_as_of - if snapshot is not None and snapshot.quote_as_of is not None - else price_snapshot_quote_as_of or target_quote_as_of - ) - ) - target_source = _text_from_mapping( - target_price_snapshot, - ("quote_source", "source", "feed"), - ) - price_snapshot_source = _text_from_mapping( - price_snapshot, - ("quote_source", "source", "feed"), - ) - source = ( - target_source - if using_target_executable_quote and target_source is not None - else ( - str( - ( - snapshot.quote_source - if snapshot is not None and snapshot.quote_source is not None - else price_snapshot_source - or target_source - or (snapshot.source if snapshot is not None else "") - ) - or "" - ).strip() - or None - ) - ) - quality_payload: dict[str, object] = { - "price": price, - "imbalance_bid_px": bid, - "imbalance_ask_px": ask, - "spread": spread, - "price_snapshot": { - "source": source, - "quote_source": source, - "as_of": quote_as_of.isoformat() if quote_as_of is not None else None, - "quote_as_of": quote_as_of.isoformat() - if quote_as_of is not None - else None, - "price": str(price) if price is not None else None, - "bid": str(bid) if bid is not None else None, - "ask": str(ask) if ask is not None else None, - "spread": str(spread) if spread is not None else None, - }, - } - status = assess_signal_quote_quality( - signal=SignalEnvelope( - event_ts=decision.event_ts, - symbol=decision.symbol, - payload=quality_payload, - timeframe=decision.timeframe, - ), - previous_price=None, - policy=QuoteQualityPolicy( - max_executable_spread_bps=settings.trading_signal_max_executable_spread_bps, - max_quote_mid_jump_bps=settings.trading_signal_max_quote_mid_jump_bps, - max_jump_with_wide_spread_bps=settings.trading_signal_max_jump_with_wide_spread_bps, - max_executable_quote_age_seconds=settings.trading_executable_quote_lookback_seconds, - ), - ) - quote_lookup_diagnostics = ( - snapshot.quote_lookup_diagnostics if snapshot is not None else None - ) + inputs = quote_routeability_inputs(decision, snapshot) + values = quote_routeability_values(decision, inputs) + status = assess_paper_route_quote_status(decision, values) status = self._apply_quote_lookup_diagnostic_reason( status, - quote_lookup_diagnostics=quote_lookup_diagnostics, + quote_lookup_diagnostics=values.quote_lookup_diagnostics, ) target_mismatch = self._paper_route_target_plan_source_mismatch(decision) if target_mismatch is not None: @@ -262,7 +66,7 @@ def _paper_route_quote_routeability( spread_bps=status.spread_bps, jump_bps=status.jump_bps, quote_age_seconds=status.quote_age_seconds, - source=source, + source=values.source, price=status.price, bid=status.bid, ask=status.ask, @@ -275,12 +79,14 @@ def _paper_route_quote_routeability( ), ) routeability = self._paper_route_quote_routeability_payload( - decision=decision, - status=status, - source=source, - quote_as_of=quote_as_of, - quote_lookup_diagnostics=quote_lookup_diagnostics, - target_mismatch=target_mismatch, + QuoteRouteabilityPayloadRequest( + decision=decision, + status=status, + source=values.source, + quote_as_of=values.quote_as_of, + quote_lookup_diagnostics=values.quote_lookup_diagnostics, + target_mismatch=target_mismatch, + ) ) return status, routeability @@ -294,7 +100,7 @@ def _apply_quote_lookup_diagnostic_reason( return status if not isinstance(quote_lookup_diagnostics, Mapping): return status - diagnostic_reason = _safe_text( + diagnostic_reason = safe_text( quote_lookup_diagnostics.get("latest_quote_rejected_reason") ) if diagnostic_reason not in { @@ -304,10 +110,10 @@ def _apply_quote_lookup_diagnostic_reason( "spread_bps_exceeded", }: return status - return _status( + return QuoteQualityStatus( valid=False, reason=diagnostic_reason, - spread_bps=_optional_decimal( + spread_bps=optional_decimal( quote_lookup_diagnostics.get("latest_quote_spread_bps") ) or status.spread_bps, @@ -323,29 +129,25 @@ def _apply_quote_lookup_diagnostic_reason( def _paper_route_target_plan_source_mismatch( decision: StrategyDecision, ) -> dict[str, object] | None: - metadata = _mapping_value( + metadata = mapping_value( decision.params.get("paper_route_target_plan_source_decision") - ) or _mapping_value(decision.params.get("paper_route_target_plan")) + ) or mapping_value(decision.params.get("paper_route_target_plan")) if metadata is None: return None symbol = decision.symbol.strip().upper() - metadata_symbol = _safe_text(metadata.get("symbol")) - target_symbols = _target_symbols(metadata) + metadata_symbol = safe_text(metadata.get("symbol")) + target_symbols = target_plan_symbols(metadata) mismatches: list[str] = [] if metadata_symbol is not None and metadata_symbol.upper() != symbol: mismatches.append("target_plan_symbol_mismatch") if target_symbols and symbol not in target_symbols: mismatches.append("target_plan_symbol_scope_mismatch") - expected_action = ( - SimplePipelineSubmissionPreparationMixin._target_plan_action_for_symbol( - metadata, - symbol=symbol, - ) + expected_action = SimplePipelineSubmissionQuoteRouteabilityMixin._target_plan_action_for_symbol( + metadata, + symbol=symbol, ) - decision_action = ( - SimplePipelineSubmissionPreparationMixin._normalize_target_plan_action( - decision.action - ) + decision_action = SimplePipelineSubmissionQuoteRouteabilityMixin._normalize_target_plan_action( + decision.action ) if expected_action is not None and decision_action != expected_action: mismatches.append("target_plan_side_mismatch") @@ -375,10 +177,10 @@ def _target_plan_action_for_symbol( ).items(): if str(raw_symbol).strip().upper() != normalized_symbol: continue - return SimplePipelineSubmissionPreparationMixin._normalize_target_plan_action( + return SimplePipelineSubmissionQuoteRouteabilityMixin._normalize_target_plan_action( raw_action ) - metadata_symbol = _safe_text(metadata.get("symbol")) + metadata_symbol = safe_text(metadata.get("symbol")) direct_symbol_matches = ( metadata_symbol is None or metadata_symbol.upper() == normalized_symbol ) @@ -391,10 +193,8 @@ def _target_plan_action_for_symbol( "action", "side", ): - action = ( - SimplePipelineSubmissionPreparationMixin._normalize_target_plan_action( - metadata.get(key) - ) + action = SimplePipelineSubmissionQuoteRouteabilityMixin._normalize_target_plan_action( + metadata.get(key) ) if action is not None: return action @@ -411,22 +211,20 @@ def _normalize_target_plan_action(value: object) -> Literal["buy", "sell"] | Non @staticmethod def _paper_route_quote_routeability_payload( - *, - decision: StrategyDecision, - status: QuoteQualityStatus, - source: str | None, - quote_as_of: datetime | None, - quote_lookup_diagnostics: Mapping[str, object] | None = None, - target_mismatch: Mapping[str, object] | None = None, + request: QuoteRouteabilityPayloadRequest, ) -> dict[str, object]: + decision = request.decision + status = cast(QuoteQualityStatus, request.status) blockers = [] if status.valid else [status.reason or "missing_executable_quote"] return { "schema_version": "torghut.paper-route-quote-routeability.v1", "status": "accepted" if status.valid else "blocked", "reason": status.reason if not status.valid else "executable_quote_ready", "symbol": decision.symbol.strip().upper(), - "source": source, - "quote_as_of": quote_as_of.isoformat() if quote_as_of is not None else None, + "source": request.source, + "quote_as_of": request.quote_as_of.isoformat() + if request.quote_as_of is not None + else None, "quote_age_seconds": str(status.quote_age_seconds) if status.quote_age_seconds is not None else None, @@ -461,11 +259,11 @@ def _paper_route_quote_routeability_payload( "promotion_allowed": False, "final_authority_ok": False, }, - "target_plan_source_mismatch": dict(target_mismatch) - if target_mismatch is not None + "target_plan_source_mismatch": dict(request.target_mismatch) + if request.target_mismatch is not None else None, - "quote_lookup_diagnostics": dict(quote_lookup_diagnostics) - if isinstance(quote_lookup_diagnostics, Mapping) + "quote_lookup_diagnostics": dict(request.quote_lookup_diagnostics) + if isinstance(request.quote_lookup_diagnostics, Mapping) else None, } @@ -473,27 +271,65 @@ def _paper_route_quote_routeability_payload( def _paper_route_quote_routeability_retry_metadata( decision_row: TradeDecision, ) -> dict[str, object] | None: - if decision_row.status != "rejected": - return None - decision_json_raw = decision_row.decision_json - if not isinstance(decision_json_raw, Mapping): + retry_state = SimplePipelineSubmissionQuoteRouteabilityMixin._quote_routeability_retry_state( + decision_row + ) + if retry_state is None: return None - decision_json = cast(Mapping[str, Any], decision_json_raw) - params = _mapping_value(decision_json.get("params")) - if params is None: + decision_json, routeability, reason = retry_state + retry_attempts = safe_int( + decision_json.get("paper_route_quote_routeability_retry_attempts") + ) + if not SimplePipelineSubmissionQuoteRouteabilityMixin._quote_retry_allowed( + retry_attempts + ): return None - if not ( + return { + "previous_decision_status": "rejected", + "previous_submission_stage": "rejected_quote_routeability", + "previous_quote_routeability_reason": reason, + "previous_quote_routeability": dict(routeability), + "previous_retry_attempts": retry_attempts, + } + + @staticmethod + def _quote_routeability_retry_state( + decision_row: TradeDecision, + ) -> tuple[Mapping[str, Any], Mapping[str, Any], str] | None: + retry_state: tuple[Mapping[str, Any], Mapping[str, Any], str] | None = None + decision_json_raw = decision_row.decision_json + if decision_row.status == "rejected" and isinstance( + decision_json_raw, + Mapping, + ): + decision_json = cast(Mapping[str, Any], decision_json_raw) + params = mapping_value(decision_json.get("params")) + if ( + params is not None + and SimplePipelineSubmissionQuoteRouteabilityMixin._has_retryable_quote_scope( + params + ) + ): + retry_state = SimplePipelineSubmissionQuoteRouteabilityMixin._routeability_retry_reason( + decision_json, + params, + ) + return retry_state + + @staticmethod + def _has_retryable_quote_scope(params: Mapping[str, Any]) -> bool: + return ( isinstance(params.get("paper_route_target_plan_source_decision"), Mapping) or isinstance(params.get("paper_route_target_plan"), Mapping) or isinstance(params.get("paper_route_probe"), Mapping) - ): - return None - routeability = _mapping_value(params.get("quote_routeability")) - if routeability is None: - return None - if _safe_text(routeability.get("status")) != "blocked": - return None - reason = _safe_text(routeability.get("reason")) + ) + + @staticmethod + def _routeability_retry_reason( + decision_json: Mapping[str, Any], + params: Mapping[str, Any], + ) -> tuple[Mapping[str, Any], Mapping[str, Any], str] | None: + routeability = mapping_value(params.get("quote_routeability")) retryable_reasons = { "absent_snapshot_fallback", "missing_executable_quote", @@ -507,24 +343,20 @@ def _paper_route_quote_routeability_retry_metadata( "spread_bps_exceeded", "wide_spread_midpoint_jump", } + if routeability is None or safe_text(routeability.get("status")) != "blocked": + return None + reason = safe_text(routeability.get("reason")) if reason not in retryable_reasons: return None - retry_attempts = _safe_int( - decision_json.get("paper_route_quote_routeability_retry_attempts") - ) + return decision_json, routeability, reason + + @staticmethod + def _quote_retry_allowed(retry_attempts: int) -> bool: retry_limit = max( - _safe_int(settings.trading_simple_paper_route_probe_retry_attempt_limit), + safe_int(settings.trading_simple_paper_route_probe_retry_attempt_limit), 0, ) - if retry_limit <= 0 or retry_attempts >= retry_limit: - return None - return { - "previous_decision_status": "rejected", - "previous_submission_stage": "rejected_quote_routeability", - "previous_quote_routeability_reason": reason, - "previous_quote_routeability": dict(routeability), - "previous_retry_attempts": retry_attempts, - } + return retry_limit > 0 and retry_attempts < retry_limit def _reopen_rejected_paper_route_quote_routeability_decision( self, @@ -550,10 +382,10 @@ def _reopen_rejected_paper_route_quote_routeability_decision( if isinstance(raw_decision_json, Mapping) else {} ) - retry_attempts = _safe_int( + retry_attempts = safe_int( decision_json.get("paper_route_quote_routeability_retry_attempts") ) - params_mapping = _mapping_value(decision_json.get("params")) + params_mapping = mapping_value(decision_json.get("params")) params = dict(params_mapping) if params_mapping is not None else {} params.pop("quote_routeability", None) decision_json["params"] = params @@ -594,30 +426,107 @@ def _reopen_rejected_paper_route_quote_routeability_decision( def _prepare_decision_for_submission( self, - *, - session: Session, - decision: StrategyDecision, - decision_row: TradeDecision, - strategy: Strategy, - account: dict[str, str], - positions: list[dict[str, Any]], + request: SubmissionPreparationRequest | None = None, + **legacy_kwargs: Any, ) -> tuple[StrategyDecision, Optional[MarketSnapshot]] | None: - decision, bounded_exit_window_reject_reason = ( - self._bounded_collection_exit_window_guarded_decision(decision) + request = self._submission_preparation_request(request, legacy_kwargs) + decision = self._exit_window_guarded_submission_decision(request) + if decision is None: + return None + request = self._submission_request_with_decision(request, decision) + priced = self._quote_routeable_submission_decision(request) + if priced is None: + return None + decision, snapshot = priced + request = self._submission_request_with_decision(request, decision) + decision = self._target_sized_submission_decision(request) + if decision is None: + return None + request = self._submission_request_with_decision(request, decision) + decision = self._paper_route_probe_capped_submission_decision(request) + request = self._submission_request_with_decision(request, decision) + preparation = self._simple_lane_preparation(request) + prepared_decision = self._prechecked_target_aligned_decision(preparation) + return self._finalize_prepared_submission( + request=request, + preparation=preparation, + prepared_decision=prepared_decision, + snapshot=snapshot, ) - if bounded_exit_window_reject_reason is not None: - self.executor.update_decision_params(session, decision_row, decision.params) - self.executor.sync_decision_state(session, decision_row, decision) - self._record_decision_rejection( - session=session, - decision=decision, - decision_row=decision_row, - reasons=[bounded_exit_window_reject_reason], - log_template="Simple-lane decision rejected strategy_id=%s symbol=%s reason=%s", + + @staticmethod + def _submission_preparation_request( + request: SubmissionPreparationRequest | None, + legacy_kwargs: Mapping[str, Any], + ) -> SubmissionPreparationRequest: + if request is not None: + return request + if "context" in legacy_kwargs: + context = legacy_kwargs["context"] + return SubmissionPreparationRequest( + session=context.session, + decision=legacy_kwargs["decision"], + decision_row=context.decision_row, + strategy=context.strategy, + account=context.account, + positions=context.positions, ) - return None + return SubmissionPreparationRequest( + session=legacy_kwargs["session"], + decision=legacy_kwargs["decision"], + decision_row=legacy_kwargs["decision_row"], + strategy=legacy_kwargs["strategy"], + account=legacy_kwargs["account"], + positions=legacy_kwargs["positions"], + ) + + @staticmethod + def _submission_request_with_decision( + request: SubmissionPreparationRequest, + decision: StrategyDecision, + ) -> SubmissionPreparationRequest: + return SubmissionPreparationRequest( + session=request.session, + decision=decision, + decision_row=request.decision_row, + strategy=request.strategy, + account=request.account, + positions=request.positions, + ) + + def _exit_window_guarded_submission_decision( + self, + request: SubmissionPreparationRequest, + ) -> StrategyDecision | None: + decision, bounded_exit_window_reject_reason = ( + self._bounded_collection_exit_window_guarded_decision(request.decision) + ) + if bounded_exit_window_reject_reason is None: + return decision + self.executor.update_decision_params( + request.session, + request.decision_row, + decision.params, + ) + self.executor.sync_decision_state( + request.session, request.decision_row, decision + ) + self._record_decision_rejection( + session=request.session, + decision=decision, + decision_row=request.decision_row, + reasons=[bounded_exit_window_reject_reason], + log_template="Simple-lane decision rejected strategy_id=%s symbol=%s reason=%s", + ) + return None + + def _quote_routeable_submission_decision( + self, + request: SubmissionPreparationRequest, + ) -> tuple[StrategyDecision, Optional[MarketSnapshot]] | None: decision, snapshot = self._ensure_decision_price( - decision, signal_price=decision.params.get("price") + request.decision, + signal_price=request.decision.params.get("price"), ) if self._paper_route_decision_requires_executable_quote(decision): quote_status, routeability = self._paper_route_quote_routeability( @@ -627,36 +536,59 @@ def _prepare_decision_for_submission( params_update = dict(decision.params) params_update["quote_routeability"] = routeability decision = decision.model_copy(update={"params": params_update}) - self.executor.update_decision_params(session, decision_row, params_update) + self.executor.update_decision_params( + request.session, + request.decision_row, + params_update, + ) if not quote_status.valid: reason = quote_status.reason or "missing_executable_quote" self._record_decision_rejection( - session=session, + session=request.session, decision=decision, - decision_row=decision_row, + decision_row=request.decision_row, reasons=[reason], log_template="Simple-lane decision rejected strategy_id=%s symbol=%s reason=%s", ) return None + return decision, snapshot + + def _target_sized_submission_decision( + self, + request: SubmissionPreparationRequest, + ) -> StrategyDecision | None: decision, bounded_target_sizing_reject_reason = ( self._bounded_collection_target_notional_sized_decision( - decision=decision, - strategy=strategy, - positions=positions, + decision=request.decision, + strategy=request.strategy, + positions=request.positions, ) ) - self.executor.update_decision_params(session, decision_row, decision.params) - self.executor.sync_decision_state(session, decision_row, decision) + self.executor.update_decision_params( + request.session, + request.decision_row, + decision.params, + ) + self.executor.sync_decision_state( + request.session, request.decision_row, decision + ) if bounded_target_sizing_reject_reason is not None: self._record_decision_rejection( - session=session, + session=request.session, decision=decision, - decision_row=decision_row, + decision_row=request.decision_row, reasons=[bounded_target_sizing_reject_reason], log_template="Simple-lane decision rejected strategy_id=%s symbol=%s reason=%s", ) return None - proof_floor = self._profitability_proof_floor(session=session) + return decision + + def _paper_route_probe_capped_submission_decision( + self, + request: SubmissionPreparationRequest, + ) -> StrategyDecision: + decision = request.decision + proof_floor = self._profitability_proof_floor(session=request.session) proof_floor_block_reason = self._proof_floor_submission_block_reason( proof_floor ) @@ -664,14 +596,14 @@ def _prepare_decision_for_submission( proof_floor_block_reason is not None and settings.trading_mode == "paper" and self._paper_route_probe_exit_metadata(decision) is None - and _paper_route_probe_entry_metadata(decision.params) is None + and paper_route_probe_entry_metadata(decision.params) is None ): probe_context = self._paper_route_probe_context( proof_floor=proof_floor, decision=decision, - strategy=strategy, - session=session, - strategies=[strategy], + strategy=request.strategy, + session=request.session, + strategies=[request.strategy], ) capped_decision = self._paper_route_probe_capped_decision( decision=decision, @@ -679,41 +611,47 @@ def _prepare_decision_for_submission( context=probe_context or {}, ) if capped_decision is not None: - decision = capped_decision - max_notional_per_order = _min_optional_decimal( - _optional_decimal(settings.trading_simple_max_notional_per_order), - _optional_decimal(settings.trading_max_notional_per_trade), - _optional_decimal(strategy.max_notional_per_trade), - ) - equity = _optional_decimal(account.get("equity")) - max_notional_per_symbol = _min_optional_decimal( - _optional_decimal(settings.trading_simple_max_notional_per_symbol), - _optional_decimal(settings.trading_allocator_max_symbol_notional), - _pct_cap_to_notional( + return capped_decision + return decision + + def _simple_lane_preparation( + self, + request: SubmissionPreparationRequest, + ) -> Any: + max_notional_per_order = min_optional_decimal( + optional_decimal(settings.trading_simple_max_notional_per_order), + optional_decimal(settings.trading_max_notional_per_trade), + optional_decimal(request.strategy.max_notional_per_trade), + ) + equity = optional_decimal(request.account.get("equity")) + max_notional_per_symbol = min_optional_decimal( + optional_decimal(settings.trading_simple_max_notional_per_symbol), + optional_decimal(settings.trading_allocator_max_symbol_notional), + pct_cap_to_notional( equity=equity, - pct=_optional_decimal(settings.trading_max_position_pct_equity), + pct=optional_decimal(settings.trading_max_position_pct_equity), ), - _pct_cap_to_notional( + pct_cap_to_notional( equity=equity, - pct=_optional_decimal(strategy.max_position_pct_equity), + pct=optional_decimal(request.strategy.max_position_pct_equity), ), ) - preparation = prepare_simple_decision( - decision=decision, - account=account, - positions=positions, + return prepare_simple_decision( + decision=request.decision, + account=request.account, + positions=request.positions, fractional_equities_enabled=settings.trading_fractional_equities_enabled, allow_shorts=settings.trading_allow_shorts, max_notional_per_order=max_notional_per_order, max_notional_per_symbol=max_notional_per_symbol, - buying_power_reserve_bps=_optional_decimal( + buying_power_reserve_bps=optional_decimal( settings.trading_simple_buying_power_reserve_bps ) or Decimal("0"), - max_order_pct_equity=_optional_decimal( + max_order_pct_equity=optional_decimal( settings.trading_simple_max_order_pct_equity ), - max_gross_exposure_pct_equity=_optional_decimal( + max_gross_exposure_pct_equity=optional_decimal( settings.trading_simple_max_gross_exposure_pct_equity ), require_equity_for_exposure_increase=( @@ -721,11 +659,35 @@ def _prepare_decision_for_submission( or settings.trading_simple_paper_route_probe_enabled ), ) - target_notional_sizing = _target_notional_sizing_audit_from_params( - decision.params + + def _prechecked_target_aligned_decision(self, preparation: Any) -> StrategyDecision: + target_notional_sizing = self._precheck_adjusted_target_notional_sizing( + preparation + ) + prepared_for_alignment = preparation.decision + if target_notional_sizing is not None: + prepared_action: Literal["buy", "sell"] = ( + "sell" + if str(preparation.decision.action).strip().lower() == "sell" + else "buy" + ) + prepared_for_alignment = self._apply_bounded_collection_target_sizing_audit( + preparation.decision, + audit=target_notional_sizing, + qty=preparation.decision.qty, + action=prepared_action, + ) + return self._align_prechecked_paper_route_probe_cap(prepared_for_alignment) + + @staticmethod + def _precheck_adjusted_target_notional_sizing( + preparation: Any, + ) -> dict[str, Any] | None: + target_notional_sizing = target_notional_sizing_audit_from_params( + preparation.decision.params ) expected_target_qty = ( - _optional_decimal(target_notional_sizing.get("resolved_qty")) + optional_decimal(target_notional_sizing.get("resolved_qty")) if target_notional_sizing is not None else None ) @@ -752,7 +714,7 @@ def _prepare_decision_for_submission( ) adjusted_audit["precheck_resolved_qty"] = str(preparation.decision.qty) adjusted_audit["precheck_expected_target_qty"] = str(expected_target_qty) - reference_price = _optional_decimal(adjusted_audit.get("reference_price")) + reference_price = optional_decimal(adjusted_audit.get("reference_price")) if reference_price is not None: adjusted_audit["precheck_resolved_notional"] = str( preparation.decision.qty * reference_price @@ -764,7 +726,7 @@ def _prepare_decision_for_submission( preparation.decision.qty * reference_price ) adjusted_audit["resolved_qty"] = str(preparation.decision.qty) - simple_lane_precheck = _mapping_value( + simple_lane_precheck = mapping_value( preparation.decision.params.get("simple_lane") ) if simple_lane_precheck is not None: @@ -777,39 +739,38 @@ def _prepare_decision_for_submission( adjusted_audit[f"precheck_{key}"] = bool( simple_lane_precheck.get(key) ) - target_notional_sizing = adjusted_audit - prepared_for_alignment = preparation.decision - if target_notional_sizing is not None: - prepared_action: Literal["buy", "sell"] = ( - "sell" - if str(preparation.decision.action).strip().lower() == "sell" - else "buy" - ) - prepared_for_alignment = self._apply_bounded_collection_target_sizing_audit( - preparation.decision, - audit=target_notional_sizing, - qty=preparation.decision.qty, - action=prepared_action, - ) - prepared_decision = self._align_prechecked_paper_route_probe_cap( - prepared_for_alignment + return adjusted_audit + return target_notional_sizing + + def _finalize_prepared_submission( + self, + *, + request: SubmissionPreparationRequest, + preparation: Any, + prepared_decision: StrategyDecision, + snapshot: MarketSnapshot | None, + ) -> tuple[StrategyDecision, Optional[MarketSnapshot]] | None: + self.executor.sync_decision_state( + request.session, + request.decision_row, + prepared_decision, ) - self.executor.sync_decision_state(session, decision_row, prepared_decision) if preparation.diagnostics: params_update = dict(prepared_decision.params) params_update["simple_lane_precheck"] = preparation.diagnostics - self.executor.update_decision_params(session, decision_row, params_update) + self.executor.update_decision_params( + request.session, + request.decision_row, + params_update, + ) if not preparation.approved or preparation.reject_reason is not None: reason = preparation.reject_reason or "broker_precheck_failed" self._record_decision_rejection( - session=session, + session=request.session, decision=prepared_decision, - decision_row=decision_row, + decision_row=request.decision_row, reasons=[reason], log_template="Simple-lane decision rejected strategy_id=%s symbol=%s reason=%s", ) return None return prepared_decision, snapshot - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/submission_preparation_modules/quote_routeability_values.py b/services/torghut/app/trading/scheduler/submission_preparation_modules/quote_routeability_values.py new file mode 100644 index 0000000000..eaf8e90adb --- /dev/null +++ b/services/torghut/app/trading/scheduler/submission_preparation_modules/quote_routeability_values.py @@ -0,0 +1,273 @@ +from __future__ import annotations + +from collections.abc import Mapping +from datetime import datetime +from decimal import Decimal +from typing import Any, cast + +from ....config import settings +from ...models import SignalEnvelope, StrategyDecision +from ...prices import MarketSnapshot +from ...quote_quality import ( + QuoteQualityPolicy, + QuoteQualityStatus, + assess_signal_quote_quality, +) +from ..target_plan_helpers import ( + decimal_from_mapping, + first_decimal, + optional_decimal, + parse_target_datetime, + target_metadata_quote_snapshot, + text_from_mapping, +) + +from .shared import QuoteRouteabilityInputs, QuoteRouteabilityValues + + +def quote_routeability_inputs( + decision: StrategyDecision, + snapshot: MarketSnapshot | None, +) -> QuoteRouteabilityInputs: + price_snapshot_raw = decision.params.get("price_snapshot") + price_snapshot = ( + cast(Mapping[str, Any], price_snapshot_raw) + if isinstance(price_snapshot_raw, Mapping) + else cast(Mapping[str, Any], {}) + ) + target_quote_snapshot = target_metadata_quote_snapshot( + decision.params, + symbol=decision.symbol, + ) + target_price_snapshot = target_quote_snapshot or cast(Mapping[str, Any], {}) + if not price_snapshot and target_quote_snapshot is not None: + price_snapshot = target_quote_snapshot + return QuoteRouteabilityInputs( + price_snapshot=price_snapshot, + target_price_snapshot=target_price_snapshot, + target_quote_snapshot=target_quote_snapshot, + snapshot=snapshot, + ) + + +def quote_routeability_values( + decision: StrategyDecision, + inputs: QuoteRouteabilityInputs, +) -> QuoteRouteabilityValues: + bid = _routeability_bid(decision, inputs) + ask = _routeability_ask(decision, inputs) + spread = _routeability_spread(decision, inputs, bid=bid, ask=ask) + using_target_quote = _uses_target_executable_quote(inputs) + return QuoteRouteabilityValues( + price=_routeability_price(decision, inputs), + bid=bid, + ask=ask, + spread=spread, + source=_routeability_source(inputs, using_target_quote=using_target_quote), + quote_as_of=_routeability_quote_as_of( + inputs, + using_target_quote=using_target_quote, + ), + quote_lookup_diagnostics=( + inputs.snapshot.quote_lookup_diagnostics + if inputs.snapshot is not None + else None + ), + ) + + +def assess_paper_route_quote_status( + decision: StrategyDecision, + values: QuoteRouteabilityValues, +) -> QuoteQualityStatus: + return assess_signal_quote_quality( + signal=SignalEnvelope( + event_ts=decision.event_ts, + symbol=decision.symbol, + payload={ + "price": values.price, + "imbalance_bid_px": values.bid, + "imbalance_ask_px": values.ask, + "spread": values.spread, + "price_snapshot": { + "source": values.source, + "quote_source": values.source, + "as_of": values.quote_as_of.isoformat() + if values.quote_as_of is not None + else None, + "quote_as_of": values.quote_as_of.isoformat() + if values.quote_as_of is not None + else None, + "price": str(values.price) if values.price is not None else None, + "bid": str(values.bid) if values.bid is not None else None, + "ask": str(values.ask) if values.ask is not None else None, + "spread": str(values.spread) if values.spread is not None else None, + }, + }, + timeframe=decision.timeframe, + ), + previous_price=None, + policy=QuoteQualityPolicy( + max_executable_spread_bps=settings.trading_signal_max_executable_spread_bps, + max_quote_mid_jump_bps=settings.trading_signal_max_quote_mid_jump_bps, + max_jump_with_wide_spread_bps=settings.trading_signal_max_jump_with_wide_spread_bps, + max_executable_quote_age_seconds=settings.trading_executable_quote_lookback_seconds, + ), + ) + + +def _routeability_price( + decision: StrategyDecision, + inputs: QuoteRouteabilityInputs, +) -> Decimal | None: + return first_decimal( + inputs.snapshot.price if inputs.snapshot is not None else None, + decimal_from_mapping( + inputs.price_snapshot, + ("price", "mid", "mid_price", "midpoint"), + ), + decimal_from_mapping( + inputs.target_price_snapshot, + ("price", "mid", "mid_price", "midpoint"), + ), + optional_decimal(decision.params.get("price")), + ) + + +def _routeability_bid( + decision: StrategyDecision, + inputs: QuoteRouteabilityInputs, +) -> Decimal | None: + return first_decimal( + inputs.snapshot.bid if inputs.snapshot is not None else None, + decimal_from_mapping( + inputs.price_snapshot, + ("bid", "bid_px", "bid_price", "bp"), + ), + decimal_from_mapping( + inputs.target_price_snapshot, + ("bid", "bid_px", "bid_price", "bp"), + ), + optional_decimal(decision.params.get("imbalance_bid_px")), + ) + + +def _routeability_ask( + decision: StrategyDecision, + inputs: QuoteRouteabilityInputs, +) -> Decimal | None: + return first_decimal( + inputs.snapshot.ask if inputs.snapshot is not None else None, + decimal_from_mapping( + inputs.price_snapshot, + ("ask", "ask_px", "ask_price", "ap"), + ), + decimal_from_mapping( + inputs.target_price_snapshot, + ("ask", "ask_px", "ask_price", "ap"), + ), + optional_decimal(decision.params.get("imbalance_ask_px")), + ) + + +def _routeability_spread( + decision: StrategyDecision, + inputs: QuoteRouteabilityInputs, + *, + bid: Decimal | None, + ask: Decimal | None, +) -> Decimal | None: + computed_spread = ask - bid if bid is not None and ask is not None else None + return first_decimal( + inputs.snapshot.spread if inputs.snapshot is not None else None, + decimal_from_mapping(inputs.price_snapshot, ("spread", "imbalance_spread")), + decimal_from_mapping( + inputs.target_price_snapshot, + ("spread", "imbalance_spread"), + ), + optional_decimal(decision.params.get("spread")), + computed_spread, + ) + + +def _uses_target_executable_quote(inputs: QuoteRouteabilityInputs) -> bool: + return ( + inputs.target_quote_snapshot is not None + and (inputs.snapshot is None or inputs.snapshot.bid is None) + and (inputs.snapshot is None or inputs.snapshot.ask is None) + and decimal_from_mapping( + inputs.price_snapshot, + ("bid", "bid_px", "bid_price", "bp"), + ) + is None + and decimal_from_mapping( + inputs.price_snapshot, + ("ask", "ask_px", "ask_price", "ap"), + ) + is None + and decimal_from_mapping( + inputs.target_price_snapshot, + ("bid", "bid_px", "bid_price", "bp"), + ) + is not None + and decimal_from_mapping( + inputs.target_price_snapshot, + ("ask", "ask_px", "ask_price", "ap"), + ) + is not None + ) + + +def _routeability_quote_as_of( + inputs: QuoteRouteabilityInputs, + *, + using_target_quote: bool, +) -> datetime | None: + target_quote_as_of = ( + parse_target_datetime(inputs.target_price_snapshot.get("quote_as_of")) + or parse_target_datetime(inputs.target_price_snapshot.get("as_of")) + or parse_target_datetime(inputs.target_price_snapshot.get("timestamp")) + ) + price_snapshot_quote_as_of = ( + parse_target_datetime(inputs.price_snapshot.get("quote_as_of")) + or parse_target_datetime(inputs.price_snapshot.get("as_of")) + or parse_target_datetime(inputs.price_snapshot.get("timestamp")) + ) + if using_target_quote and target_quote_as_of is not None: + return target_quote_as_of + if inputs.snapshot is not None and inputs.snapshot.quote_as_of is not None: + return inputs.snapshot.quote_as_of + return price_snapshot_quote_as_of or target_quote_as_of + + +def _routeability_source( + inputs: QuoteRouteabilityInputs, + *, + using_target_quote: bool, +) -> str | None: + target_source = text_from_mapping( + inputs.target_price_snapshot, + ("quote_source", "source", "feed"), + ) + price_snapshot_source = text_from_mapping( + inputs.price_snapshot, + ("quote_source", "source", "feed"), + ) + if using_target_quote and target_source is not None: + return target_source + snapshot_source = inputs.snapshot.source if inputs.snapshot is not None else "" + snapshot_quote_source = ( + inputs.snapshot.quote_source + if inputs.snapshot is not None and inputs.snapshot.quote_source is not None + else None + ) + return ( + str( + snapshot_quote_source + or price_snapshot_source + or target_source + or snapshot_source + or "" + ).strip() + or None + ) diff --git a/services/torghut/app/trading/scheduler/submission_preparation_modules/part_02_simplepipelinesubmissionpreparationmixinme.py b/services/torghut/app/trading/scheduler/submission_preparation_modules/quote_sizing.py similarity index 65% rename from services/torghut/app/trading/scheduler/submission_preparation_modules/part_02_simplepipelinesubmissionpreparationmixinme.py rename to services/torghut/app/trading/scheduler/submission_preparation_modules/quote_sizing.py index a5d1e1d9ec..776943d796 100644 --- a/services/torghut/app/trading/scheduler/submission_preparation_modules/part_02_simplepipelinesubmissionpreparationmixinme.py +++ b/services/torghut/app/trading/scheduler/submission_preparation_modules/quote_sizing.py @@ -1,29 +1,17 @@ -# pyright: reportMissingImports=false, reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownParameterType=false, reportUnknownLambdaType=false, reportUnusedImport=false, reportUnusedClass=false, reportUnusedFunction=false, reportUnusedVariable=false, reportUndefinedVariable=false, reportUnsupportedDunderAll=false, reportAttributeAccessIssue=false, reportUntypedBaseClass=false, reportGeneralTypeIssues=false, reportInvalidTypeForm=false, reportReturnType=false, reportOptionalMemberAccess=false, reportArgumentType=false, reportCallIssue=false, reportPrivateUsage=false, reportUnnecessaryComparison=false, reportMissingTypeStubs=false, reportUnnecessaryCast=false - from __future__ import annotations -import logging -from collections.abc import Mapping, Sequence +from collections.abc import Mapping from datetime import datetime, timezone from decimal import Decimal, ROUND_DOWN from typing import Any, Literal, Optional, cast -from sqlalchemy.orm import Session from ....config import settings from ....models import ( Strategy, - TradeDecision, ) -from ...firewall import OrderFirewallBlocked from ...models import SignalEnvelope, StrategyDecision from ...prices import MarketSnapshot -from ...quote_quality import ( - QuoteQualityPolicy, - QuoteQualityStatus, - _status, - assess_signal_quote_quality, -) from ...quantity_rules import quantize_qty_for_symbol, resolve_quantity_resolution from ...runtime_decision_authority import ( BOUNDED_PAPER_ROUTE_COLLECTION_SOURCE_DECISION_MODE, @@ -33,50 +21,44 @@ ) from ...simple_risk import ( position_qty_for_symbol, - prepare_simple_decision, -) -from ..pipeline_helpers import ( - _extract_json_error_payload, - _price_snapshot_payload, ) +from ..pipeline_modules.support import price_snapshot_payload from ..target_plan_helpers import ( - _PAPER_ROUTE_PROBE_QTY_STEP, - _TargetProbeQuantityResolution, - _bounded_collection_decision_requires_target_notional_sizing, - _bounded_paper_route_collection_entry_metadata, - _bounded_sim_collection_metadata_from_decision, - _decimal_from_mapping, - _executable_bid_ask_present, - _first_decimal, - _mapping_value, - _min_optional_decimal, - _optional_decimal, - _paper_route_probe_entry_metadata, - _parse_target_datetime, - _pct_cap_to_notional, - _quote_snapshot_from_mapping, - _quote_snapshot_reference_price, - _safe_int, - _safe_text, - _simple_buying_power_consumption, - _simple_decision_notional, - _snapshot_has_executable_quote, - _target_metadata_quote_snapshot, - _target_notional_sizing_audit_from_params, - _target_probe_cap, - _target_probe_symbol_actions, - _target_probe_symbol_notional_budget, - _target_probe_symbol_quantities, - _target_symbols, - _text_from_mapping, + PAPER_ROUTE_PROBE_QTY_STEP, + TargetProbeQuantityResolution, + bounded_collection_decision_requires_target_notional_sizing, + bounded_paper_route_collection_entry_metadata, + decimal_from_mapping, + executable_bid_ask_present, + mapping_value, + optional_decimal, + parse_target_datetime, + quote_snapshot_from_mapping, + quote_snapshot_reference_price, + safe_text, + snapshot_has_executable_quote as target_snapshot_has_executable_quote, + target_metadata_quote_snapshot, + target_probe_cap, + target_probe_symbol_actions, + target_probe_symbol_notional_budget, + target_probe_symbol_quantities, + target_plan_symbols, + text_from_mapping, ) -# ruff: noqa: F401,F403,F405,F811,F821 -from .part_01_statements_74 import * +from ..pipeline_modules.shared import TradingPipelineBase + +from .shared import ( + TargetQuantityDecisionRequest, + TargetQuantityResolutionRequest, + TargetSizingContext, + TargetSizingPriceRequest, + logger, +) -class _SimplePipelineSubmissionPreparationMixinMethodsPart1: +class SimplePipelineSubmissionQuoteSizingMixin(TradingPipelineBase): def _submission_control_plane_snapshot( self, *, @@ -116,11 +98,11 @@ def _ensure_decision_price( timeframe=decision.timeframe, ) ) - target_snapshot = _target_metadata_quote_snapshot( + target_snapshot = target_metadata_quote_snapshot( decision.params, symbol=decision.symbol, ) - snapshot_has_executable_quote = ( + snapshot_quote_executable = ( snapshot is not None and snapshot.bid is not None and snapshot.ask is not None @@ -131,10 +113,10 @@ def _ensure_decision_price( ( snapshot is None or snapshot.price is None - or (requires_executable_quote and not snapshot_has_executable_quote) + or (requires_executable_quote and not snapshot_quote_executable) ) and target_snapshot is not None - and _snapshot_has_executable_quote(target_snapshot) + and target_snapshot_has_executable_quote(target_snapshot) ): updated_params = self._paper_route_params_with_quote_snapshot( decision.params, @@ -168,19 +150,19 @@ def _paper_route_params_with_quote_snapshot( signal_price: Any, ) -> dict[str, Any]: updated_params = dict(params) - price = _decimal_from_mapping( + price = decimal_from_mapping( snapshot, ("price", "mid", "mid_price", "midpoint") ) - bid = _decimal_from_mapping(snapshot, ("bid", "bid_px", "bid_price", "bp")) - ask = _decimal_from_mapping(snapshot, ("ask", "ask_px", "ask_price", "ap")) - spread = _decimal_from_mapping(snapshot, ("spread", "imbalance_spread")) + bid = decimal_from_mapping(snapshot, ("bid", "bid_px", "bid_price", "bp")) + ask = decimal_from_mapping(snapshot, ("ask", "ask_px", "ask_price", "ap")) + spread = decimal_from_mapping(snapshot, ("spread", "imbalance_spread")) computed_spread = ask - bid if bid is not None and ask is not None else None quote_as_of = ( - _parse_target_datetime(snapshot.get("quote_as_of")) - or _parse_target_datetime(snapshot.get("as_of")) - or _parse_target_datetime(snapshot.get("timestamp")) + parse_target_datetime(snapshot.get("quote_as_of")) + or parse_target_datetime(snapshot.get("as_of")) + or parse_target_datetime(snapshot.get("timestamp")) ) - source = _text_from_mapping(snapshot, ("quote_source", "source", "feed")) + source = text_from_mapping(snapshot, ("quote_source", "source", "feed")) if price is None and bid is not None and ask is not None: price = (bid + ask) / Decimal("2") if signal_price is None and price is not None: @@ -210,18 +192,18 @@ def _paper_route_params_with_quote_snapshot( @staticmethod def _decision_has_executable_quote_payload(decision: StrategyDecision) -> bool: params = decision.params - if _executable_bid_ask_present(params): + if executable_bid_ask_present(params): return True price_snapshot = params.get("price_snapshot") if isinstance(price_snapshot, Mapping): - return _executable_bid_ask_present(cast(Mapping[str, Any], price_snapshot)) + return executable_bid_ask_present(cast(Mapping[str, Any], price_snapshot)) return False @staticmethod def _paper_route_price_snapshot_payload( snapshot: MarketSnapshot, ) -> dict[str, Any]: - payload = _price_snapshot_payload(snapshot) + payload = price_snapshot_payload(snapshot) if snapshot.bid is not None: payload["bid"] = str(snapshot.bid) if snapshot.ask is not None: @@ -234,21 +216,18 @@ def _paper_route_price_snapshot_payload( def _paper_route_target_sizing_price( self, - *, - target: Mapping[str, Any], - symbol: str, - action: Literal["buy", "sell"], - event_ts: datetime, - timeframe: str, + request: TargetSizingPriceRequest | None = None, + **legacy_kwargs: Any, ) -> tuple[Decimal | None, dict[str, Any], str | None]: - target_snapshot = _target_metadata_quote_snapshot( - target, - symbol=symbol, - ) or _quote_snapshot_from_mapping(target, symbol=symbol) + request = self._target_sizing_price_request(request, legacy_kwargs) + target_snapshot = target_metadata_quote_snapshot( + request.target, + symbol=request.symbol, + ) or quote_snapshot_from_mapping(request.target, symbol=request.symbol) if target_snapshot is not None: - reference_price = _quote_snapshot_reference_price( + reference_price = quote_snapshot_reference_price( target_snapshot, - action=action, + action=request.action, ) if reference_price is not None and reference_price > 0: price_params = self._paper_route_params_with_quote_snapshot( @@ -266,25 +245,25 @@ def _paper_route_target_sizing_price( try: snapshot = price_fetcher.fetch_market_snapshot( SignalEnvelope( - event_ts=event_ts, - symbol=symbol, + event_ts=request.event_ts, + symbol=request.symbol, payload={}, - timeframe=timeframe, + timeframe=request.timeframe, ) ) except Exception: logger.exception( "Failed to fetch paper-route target sizing quote symbol=%s timeframe=%s", - symbol, - timeframe, + request.symbol, + request.timeframe, ) return None, {}, None if snapshot is None: return None, {}, None snapshot_payload = self._paper_route_price_snapshot_payload(snapshot) - reference_price = _quote_snapshot_reference_price( + reference_price = quote_snapshot_reference_price( snapshot_payload, - action=action, + action=request.action, ) if reference_price is None or reference_price <= 0: return None, {"price_snapshot": snapshot_payload}, "price_fetcher_snapshot" @@ -298,80 +277,78 @@ def _paper_route_target_sizing_price( "price_fetcher_snapshot", ) + @staticmethod + def _target_sizing_price_request( + request: TargetSizingPriceRequest | None, + legacy_kwargs: Mapping[str, Any], + ) -> TargetSizingPriceRequest: + if request is not None: + return request + return TargetSizingPriceRequest( + target=legacy_kwargs["target"], + symbol=legacy_kwargs["symbol"], + action=legacy_kwargs["action"], + event_ts=legacy_kwargs["event_ts"], + timeframe=legacy_kwargs["timeframe"], + ) + def _paper_route_target_quantity_resolution( self, - *, - target: Mapping[str, Any], - symbol: str, - symbols: Sequence[str], - action: Literal["buy", "sell"], - requested_qty: Decimal, - symbol_quantities: Mapping[str, Decimal], - max_notional: Decimal, - event_ts: datetime, - timeframe: str, - ) -> _TargetProbeQuantityResolution | None: - if requested_qty <= 0: + request: TargetQuantityResolutionRequest | None = None, + **legacy_kwargs: Any, + ) -> TargetProbeQuantityResolution | None: + request = self._target_quantity_resolution_request(request, legacy_kwargs) + if request.requested_qty <= 0: return None - normalized_symbol = symbol.strip().upper() - symbol_budget = _target_probe_symbol_notional_budget( - target=target, + normalized_symbol = request.symbol.strip().upper() + symbol_budget = target_probe_symbol_notional_budget( + target=request.target, symbol=normalized_symbol, - symbols=symbols, - symbol_quantities=symbol_quantities, - max_notional=max_notional, + symbols=request.symbols, + symbol_quantities=request.symbol_quantities, + max_notional=request.max_notional, ) reference_price, price_params, price_source = ( self._paper_route_target_sizing_price( - target=target, - symbol=normalized_symbol, - action=action, - event_ts=event_ts, - timeframe=timeframe, + TargetSizingPriceRequest( + target=request.target, + symbol=normalized_symbol, + action=request.action, + event_ts=request.event_ts, + timeframe=request.timeframe, + ) ) ) - audit: dict[str, Any] = { - "schema_version": "torghut.paper-route-target-notional-sizing.v1", - "symbol": normalized_symbol, - "action": action, - "sizing_source": "quantity_fallback", - "requested_qty": str(requested_qty), - "resolved_qty": str(requested_qty), - "target_notional": str(_target_probe_cap(target) or max_notional), - "paper_route_probe_max_notional": str(max_notional), - "symbol_notional_budget": ( - str(symbol_budget) if symbol_budget is not None else None - ), - "reference_price": ( - str(reference_price) if reference_price is not None else None - ), - "reference_price_source": price_source, - "symbols": [item.strip().upper() for item in symbols if item.strip()], - "blockers": [], - } + audit = self._target_quantity_resolution_audit( + request, + normalized_symbol=normalized_symbol, + symbol_budget=symbol_budget, + reference_price=reference_price, + price_source=price_source, + ) if reference_price is not None and reference_price > 0: - requested_notional = requested_qty * reference_price + requested_notional = request.requested_qty * reference_price audit["requested_notional"] = str(requested_notional) if symbol_budget is not None and requested_notional > 0: audit["notional_scale_gap"] = str(symbol_budget / requested_notional) if symbol_budget is None or symbol_budget <= 0: audit["blockers"] = ["paper_route_target_symbol_notional_budget_missing"] - return _TargetProbeQuantityResolution( - qty=requested_qty, + return TargetProbeQuantityResolution( + qty=request.requested_qty, audit=audit, price_params=price_params, ) if reference_price is None or reference_price <= 0: audit["blockers"] = ["paper_route_target_notional_price_missing"] - return _TargetProbeQuantityResolution( - qty=requested_qty, + return TargetProbeQuantityResolution( + qty=request.requested_qty, audit=audit, price_params=price_params, ) resolved_qty = (symbol_budget / reference_price).quantize( - _PAPER_ROUTE_PROBE_QTY_STEP, + PAPER_ROUTE_PROBE_QTY_STEP, rounding=ROUND_DOWN, ) if resolved_qty <= 0: @@ -380,37 +357,89 @@ def _paper_route_target_quantity_resolution( audit["sizing_source"] = "target_notional" audit["resolved_qty"] = str(resolved_qty) audit["resolved_notional"] = str(resolved_qty * reference_price) - audit["overrode_requested_qty"] = resolved_qty != requested_qty - return _TargetProbeQuantityResolution( + audit["overrode_requested_qty"] = resolved_qty != request.requested_qty + return TargetProbeQuantityResolution( qty=resolved_qty, audit=audit, price_params=price_params, ) + @staticmethod + def _target_quantity_resolution_request( + request: TargetQuantityResolutionRequest | None, + legacy_kwargs: Mapping[str, Any], + ) -> TargetQuantityResolutionRequest: + if request is not None: + return request + return TargetQuantityResolutionRequest( + target=legacy_kwargs["target"], + symbol=legacy_kwargs["symbol"], + symbols=legacy_kwargs["symbols"], + action=legacy_kwargs["action"], + requested_qty=legacy_kwargs["requested_qty"], + symbol_quantities=legacy_kwargs["symbol_quantities"], + max_notional=legacy_kwargs["max_notional"], + event_ts=legacy_kwargs["event_ts"], + timeframe=legacy_kwargs["timeframe"], + ) + + @staticmethod + def _target_quantity_resolution_audit( + request: TargetQuantityResolutionRequest, + *, + normalized_symbol: str, + symbol_budget: Decimal | None, + reference_price: Decimal | None, + price_source: str | None, + ) -> dict[str, Any]: + return { + "schema_version": "torghut.paper-route-target-notional-sizing.v1", + "symbol": normalized_symbol, + "action": request.action, + "sizing_source": "quantity_fallback", + "requested_qty": str(request.requested_qty), + "resolved_qty": str(request.requested_qty), + "target_notional": str( + target_probe_cap(request.target) or request.max_notional + ), + "paper_route_probe_max_notional": str(request.max_notional), + "symbol_notional_budget": ( + str(symbol_budget) if symbol_budget is not None else None + ), + "reference_price": ( + str(reference_price) if reference_price is not None else None + ), + "reference_price_source": price_source, + "symbols": [ + item.strip().upper() for item in request.symbols if item.strip() + ], + "blockers": [], + } + @staticmethod def _decision_quote_snapshot_for_target_sizing( decision: StrategyDecision, ) -> dict[str, Any] | None: normalized_symbol = decision.symbol.strip().upper() - snapshot = _quote_snapshot_from_mapping( + snapshot = quote_snapshot_from_mapping( decision.params, symbol=normalized_symbol, ) if snapshot is not None: return dict(snapshot) - price = _decimal_from_mapping( + price = decimal_from_mapping( decision.params, ("price", "reference_price", "mid", "mid_price"), ) - bid = _decimal_from_mapping( + bid = decimal_from_mapping( decision.params, ("imbalance_bid_px", "bid", "bid_px", "bid_price"), ) - ask = _decimal_from_mapping( + ask = decimal_from_mapping( decision.params, ("imbalance_ask_px", "ask", "ask_px", "ask_price"), ) - spread = _decimal_from_mapping( + spread = decimal_from_mapping( decision.params, ("imbalance_spread", "spread"), ) @@ -436,7 +465,7 @@ def _bounded_collection_target_sizing_payload( ) -> dict[str, Any]: normalized_symbol = decision.symbol.strip().upper() target = dict(metadata) - if _quote_snapshot_from_mapping(target, symbol=normalized_symbol) is None: + if quote_snapshot_from_mapping(target, symbol=normalized_symbol) is None: snapshot = self._decision_quote_snapshot_for_target_sizing(decision) if snapshot is not None: target["price_snapshot"] = snapshot @@ -448,7 +477,7 @@ def _bounded_collection_exit_window_elapsed( decision: StrategyDecision, metadata: Mapping[str, Any], ) -> tuple[datetime, datetime] | None: - exit_due_at = _parse_target_datetime(metadata.get("exit_due_at")) + exit_due_at = parse_target_datetime(metadata.get("exit_due_at")) if exit_due_at is None: return None event_ts = decision.event_ts @@ -490,7 +519,7 @@ def _apply_bounded_collection_exit_window_audit( "paper_route_target_plan", "paper_route_probe", ): - metadata = _mapping_value(params.get(key)) + metadata = mapping_value(params.get(key)) if metadata is None: continue updated_metadata = dict(metadata) @@ -505,7 +534,7 @@ def _bounded_collection_exit_window_guarded_decision( ) -> tuple[StrategyDecision, str | None]: if self._paper_route_probe_exit_metadata(decision) is not None: return decision, None - metadata = _bounded_paper_route_collection_entry_metadata(decision.params) + metadata = bounded_paper_route_collection_entry_metadata(decision.params) if metadata is None: return decision, None elapsed = self._bounded_collection_exit_window_elapsed( @@ -537,13 +566,13 @@ def _apply_bounded_collection_target_sizing_audit( audit_payload = dict(audit) params["paper_route_target_notional_sizing"] = audit_payload - reference_price = _optional_decimal(audit_payload.get("reference_price")) + reference_price = optional_decimal(audit_payload.get("reference_price")) notional = qty * reference_price if reference_price is not None else None simple_lane = dict(cast(Mapping[str, Any], params.get("simple_lane") or {})) simple_lane["final_qty"] = str(qty) simple_lane["paper_route_target_notional_sizing"] = audit_payload simple_lane["target_source_notional_sized"] = ( - _safe_text(audit_payload.get("sizing_source")) == "target_notional" + safe_text(audit_payload.get("sizing_source")) == "target_notional" ) if notional is not None: simple_lane["notional"] = str(notional) @@ -554,13 +583,13 @@ def _apply_bounded_collection_target_sizing_audit( "paper_route_target_plan", "paper_route_probe", ): - metadata = _mapping_value(params.get(key)) + metadata = mapping_value(params.get(key)) if metadata is None: continue updated_metadata = dict(metadata) updated_metadata["paper_route_target_notional_sizing"] = audit_payload updated_metadata["target_source_notional_sized"] = ( - _safe_text(audit_payload.get("sizing_source")) == "target_notional" + safe_text(audit_payload.get("sizing_source")) == "target_notional" ) params[key] = updated_metadata @@ -579,27 +608,9 @@ def _bounded_collection_target_notional_sized_decision( if self._paper_route_probe_exit_metadata(decision) is not None: return decision, None - metadata = _bounded_paper_route_collection_entry_metadata(decision.params) + metadata = bounded_paper_route_collection_entry_metadata(decision.params) if metadata is None: - if not _bounded_collection_decision_requires_target_notional_sizing( - decision.params - ): - return decision, None - audit = { - "schema_version": "torghut.paper-route-target-notional-sizing.v1", - "symbol": decision.symbol.strip().upper(), - "action": decision.action, - "sizing_source": "missing", - "requested_qty": str(decision.qty), - "resolved_qty": str(decision.qty), - "blockers": ["bounded_paper_route_target_metadata_missing"], - } - updated = self._apply_bounded_collection_target_sizing_audit( - decision, - audit=audit, - qty=decision.qty, - ) - return updated, "bounded_paper_route_target_notional_sizing_missing" + return self._missing_target_sizing_metadata_decision(decision) exit_guarded_decision, exit_window_reason = ( self._bounded_collection_exit_window_guarded_decision(decision) @@ -607,96 +618,160 @@ def _bounded_collection_target_notional_sized_decision( if exit_window_reason is not None: return exit_guarded_decision, exit_window_reason + _ = strategy + sizing_context = self._target_sizing_context(decision, metadata) + if sizing_context.max_notional is None or sizing_context.max_notional <= 0: + return self._missing_target_notional_cap_decision(decision, sizing_context) + + quantity_resolution = self._paper_route_target_quantity_resolution( + TargetQuantityResolutionRequest( + target=sizing_context.target, + symbol=sizing_context.symbol, + symbols=sizing_context.symbols, + action=sizing_context.action, + requested_qty=sizing_context.requested_qty, + symbol_quantities=sizing_context.symbol_quantities, + max_notional=sizing_context.max_notional, + event_ts=decision.event_ts, + timeframe=decision.timeframe, + ) + ) + return self._target_quantity_sized_decision( + TargetQuantityDecisionRequest( + decision=decision, + sizing_context=sizing_context, + quantity_resolution=quantity_resolution, + positions=positions, + ) + ) + + def _missing_target_sizing_metadata_decision( + self, + decision: StrategyDecision, + ) -> tuple[StrategyDecision, str | None]: + if not bounded_collection_decision_requires_target_notional_sizing( + decision.params + ): + return decision, None + audit = { + "schema_version": "torghut.paper-route-target-notional-sizing.v1", + "symbol": decision.symbol.strip().upper(), + "action": decision.action, + "sizing_source": "missing", + "requested_qty": str(decision.qty), + "resolved_qty": str(decision.qty), + "blockers": ["bounded_paper_route_target_metadata_missing"], + } + updated = self._apply_bounded_collection_target_sizing_audit( + decision, + audit=audit, + qty=decision.qty, + ) + return updated, "bounded_paper_route_target_notional_sizing_missing" + + def _target_sizing_context( + self, + decision: StrategyDecision, + metadata: Mapping[str, Any], + ) -> TargetSizingContext: target = self._bounded_collection_target_sizing_payload( decision=decision, metadata=metadata, ) normalized_symbol = decision.symbol.strip().upper() - target_symbols = sorted(_target_symbols(target)) + target_symbols = sorted(target_plan_symbols(target)) if normalized_symbol not in target_symbols: target_symbols.append(normalized_symbol) - symbol_quantities = _target_probe_symbol_quantities(target, target_symbols) + symbol_quantities = target_probe_symbol_quantities(target, target_symbols) requested_qty = symbol_quantities.get(normalized_symbol) or decision.qty action: Literal["buy", "sell"] = ( "sell" if str(decision.action).strip().lower() == "sell" else "buy" ) - action = _target_probe_symbol_actions(target, target_symbols).get( - normalized_symbol, - action, - ) - max_notional = _target_probe_cap(target) - if max_notional is None or max_notional <= 0: - audit = { - "schema_version": "torghut.paper-route-target-notional-sizing.v1", - "symbol": normalized_symbol, - "action": action, - "sizing_source": "missing", - "requested_qty": str(requested_qty), - "resolved_qty": str(decision.qty), - "symbols": target_symbols, - "blockers": ["bounded_paper_route_target_notional_cap_missing"], - } - updated = self._apply_bounded_collection_target_sizing_audit( - decision, - audit=audit, - qty=decision.qty, - action=action, - ) - return updated, "bounded_paper_route_target_notional_sizing_missing" - - quantity_resolution = self._paper_route_target_quantity_resolution( + return TargetSizingContext( target=target, symbol=normalized_symbol, symbols=target_symbols, - action=action, - requested_qty=requested_qty, symbol_quantities=symbol_quantities, - max_notional=max_notional, - event_ts=decision.event_ts, - timeframe=decision.timeframe, + requested_qty=requested_qty, + action=target_probe_symbol_actions(target, target_symbols).get( + normalized_symbol, + action, + ), + max_notional=target_probe_cap(target), + ) + + def _missing_target_notional_cap_decision( + self, + decision: StrategyDecision, + sizing_context: TargetSizingContext, + ) -> tuple[StrategyDecision, str | None]: + audit = { + "schema_version": "torghut.paper-route-target-notional-sizing.v1", + "symbol": sizing_context.symbol, + "action": sizing_context.action, + "sizing_source": "missing", + "requested_qty": str(sizing_context.requested_qty), + "resolved_qty": str(decision.qty), + "symbols": sizing_context.symbols, + "blockers": ["bounded_paper_route_target_notional_cap_missing"], + } + updated = self._apply_bounded_collection_target_sizing_audit( + decision, + audit=audit, + qty=decision.qty, + action=sizing_context.action, ) + return updated, "bounded_paper_route_target_notional_sizing_missing" + + def _target_quantity_sized_decision( + self, + request: TargetQuantityDecisionRequest, + ) -> tuple[StrategyDecision, str | None]: + decision = request.decision + sizing_context = request.sizing_context + quantity_resolution = request.quantity_resolution if quantity_resolution is None: audit = { "schema_version": "torghut.paper-route-target-notional-sizing.v1", - "symbol": normalized_symbol, - "action": action, + "symbol": sizing_context.symbol, + "action": sizing_context.action, "sizing_source": "missing", - "requested_qty": str(requested_qty), + "requested_qty": str(sizing_context.requested_qty), "resolved_qty": str(decision.qty), - "target_notional": str(max_notional), - "symbols": target_symbols, + "target_notional": str(sizing_context.max_notional), + "symbols": sizing_context.symbols, "blockers": ["paper_route_target_notional_qty_below_min_step"], } updated = self._apply_bounded_collection_target_sizing_audit( decision, audit=audit, qty=decision.qty, - action=action, + action=sizing_context.action, ) return updated, "bounded_paper_route_target_notional_sizing_missing" audit = dict(quantity_resolution.audit) - if _safe_text(audit.get("sizing_source")) != "target_notional": + if safe_text(audit.get("sizing_source")) != "target_notional": updated = self._apply_bounded_collection_target_sizing_audit( decision, audit=audit, qty=quantity_resolution.qty, - action=action, + action=sizing_context.action, price_params=quantity_resolution.price_params, ) return updated, "bounded_paper_route_target_notional_sizing_missing" - position_qty = position_qty_for_symbol(positions, normalized_symbol) + position_qty = position_qty_for_symbol(request.positions, sizing_context.symbol) broker_resolution = resolve_quantity_resolution( - normalized_symbol, - action=action, + sizing_context.symbol, + action=sizing_context.action, global_enabled=settings.trading_fractional_equities_enabled, allow_shorts=settings.trading_allow_shorts, position_qty=position_qty, requested_qty=quantity_resolution.qty, ) broker_qty = quantize_qty_for_symbol( - normalized_symbol, + sizing_context.symbol, quantity_resolution.qty, fractional_equities_enabled=broker_resolution.fractional_allowed, ) @@ -709,12 +784,12 @@ def _bounded_collection_target_notional_sized_decision( decision, audit=audit, qty=decision.qty, - action=action, + action=sizing_context.action, price_params=quantity_resolution.price_params, ) return updated, "bounded_paper_route_target_notional_sizing_missing" - reference_price = _optional_decimal(audit.get("reference_price")) + reference_price = optional_decimal(audit.get("reference_price")) audit["target_notional_resolved_qty"] = audit.get("resolved_qty") audit["target_notional_resolved_notional"] = audit.get("resolved_notional") audit["broker_quantity_resolution"] = broker_resolution.to_payload() @@ -729,7 +804,7 @@ def _bounded_collection_target_notional_sized_decision( decision, audit=audit, qty=broker_qty, - action=action, + action=sizing_context.action, price_params=quantity_resolution.price_params, ) return updated, None @@ -757,6 +832,3 @@ def _paper_route_decision_requires_executable_quote( ROUTE_ACQUISITION_SOURCE_DECISION_MODE, STRATEGY_SIGNAL_PAPER_SOURCE_DECISION_MODE, } - - -__all__ = [name for name in globals() if not name.startswith("__")] diff --git a/services/torghut/app/trading/scheduler/submission_preparation_modules/shared.py b/services/torghut/app/trading/scheduler/submission_preparation_modules/shared.py new file mode 100644 index 0000000000..eaaa230b48 --- /dev/null +++ b/services/torghut/app/trading/scheduler/submission_preparation_modules/shared.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from decimal import Decimal +import logging +from typing import TYPE_CHECKING, Any, Literal + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + + from sqlalchemy.orm import Session + + from ....models import Strategy, TradeDecision + from ...models import StrategyDecision + from ...prices import MarketSnapshot + from .quote_sizing import TargetProbeQuantityResolution + + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class SubmissionDecisionContext: + session: Session + decision_row: TradeDecision + strategy: Strategy + account: dict[str, str] + positions: list[dict[str, Any]] + + +@dataclass(frozen=True) +class RiskVerdictRequest: + context: SubmissionDecisionContext + decision: StrategyDecision + symbol_allowlist: set[str] + execution_advisor: Mapping[str, Any] | None + + +@dataclass(frozen=True) +class TradingSubmissionRequest: + session: Session + decision: StrategyDecision + decision_row: TradeDecision + + +@dataclass(frozen=True) +class OrderSubmitRequest: + session: Session + execution_client: Any + decision: StrategyDecision + decision_row: TradeDecision + selected_adapter_name: str + retry_delays: list[int] + + +@dataclass(frozen=True) +class SubmitRejectionRequest: + session: Session + decision: StrategyDecision + decision_row: TradeDecision + selected_adapter_name: str + reason: str + rejection_type: str + metadata: Mapping[str, Any] | None = None + + +@dataclass(frozen=True) +class QuoteRouteabilityInputs: + price_snapshot: Mapping[str, Any] + target_price_snapshot: Mapping[str, Any] + target_quote_snapshot: Mapping[str, Any] | None + snapshot: MarketSnapshot | None + + +@dataclass(frozen=True) +class QuoteRouteabilityValues: + price: Decimal | None + bid: Decimal | None + ask: Decimal | None + spread: Decimal | None + source: str | None + quote_as_of: datetime | None + quote_lookup_diagnostics: Mapping[str, object] | None + + +@dataclass(frozen=True) +class QuoteRouteabilityPayloadRequest: + decision: StrategyDecision + status: Any + source: str | None + quote_as_of: datetime | None + quote_lookup_diagnostics: Mapping[str, object] | None = None + target_mismatch: Mapping[str, object] | None = None + + +@dataclass(frozen=True) +class SubmissionPreparationRequest: + session: Session + decision: StrategyDecision + decision_row: TradeDecision + strategy: Strategy + account: dict[str, str] + positions: list[dict[str, Any]] + + +@dataclass(frozen=True) +class TargetSizingPriceRequest: + target: Mapping[str, Any] + symbol: str + action: Literal["buy", "sell"] + event_ts: datetime + timeframe: str + + +@dataclass(frozen=True) +class TargetQuantityResolutionRequest: + target: Mapping[str, Any] + symbol: str + symbols: Sequence[str] + action: Literal["buy", "sell"] + requested_qty: Decimal + symbol_quantities: Mapping[str, Decimal] + max_notional: Decimal + event_ts: datetime + timeframe: str + + +@dataclass(frozen=True) +class TargetSizingContext: + target: Mapping[str, Any] + symbol: str + symbols: list[str] + symbol_quantities: Mapping[str, Decimal] + requested_qty: Decimal + action: Literal["buy", "sell"] + max_notional: Decimal | None + + +@dataclass(frozen=True) +class TargetQuantityDecisionRequest: + decision: StrategyDecision + sizing_context: TargetSizingContext + quantity_resolution: TargetProbeQuantityResolution | None + positions: list[dict[str, Any]] diff --git a/services/torghut/app/trading/scheduler/target_plan_helpers.py b/services/torghut/app/trading/scheduler/target_plan_helpers.py index 1cf6aa8a09..f09b446d50 100644 --- a/services/torghut/app/trading/scheduler/target_plan_helpers.py +++ b/services/torghut/app/trading/scheduler/target_plan_helpers.py @@ -83,6 +83,44 @@ target_notional_sizing_audit_from_params as _target_notional_sizing_audit_from_params, ) +PAPER_ROUTE_PROBE_QTY_STEP = _PAPER_ROUTE_PROBE_QTY_STEP +TargetProbeQuantityResolution = _TargetProbeQuantityResolution +bounded_collection_decision_requires_target_notional_sizing = ( + _bounded_collection_decision_requires_target_notional_sizing +) +bounded_paper_route_collection_entry_metadata = ( + _bounded_paper_route_collection_entry_metadata +) +bounded_sim_collection_metadata_from_decision = ( + _bounded_sim_collection_metadata_from_decision +) +decimal_from_mapping = _decimal_from_mapping +executable_bid_ask_present = _executable_bid_ask_present +first_decimal = _first_decimal +mapping_value = _mapping_value +min_optional_decimal = _min_optional_decimal +optional_decimal = _optional_decimal +paper_route_probe_entry_metadata = _paper_route_probe_entry_metadata +parse_target_datetime = _parse_target_datetime +pct_cap_to_notional = _pct_cap_to_notional +quote_snapshot_from_mapping = _quote_snapshot_from_mapping +quote_snapshot_reference_price = _quote_snapshot_reference_price +safe_int = _safe_int +safe_text = _safe_text +simple_buying_power_consumption = _simple_buying_power_consumption +simple_decision_notional = _simple_decision_notional +snapshot_has_executable_quote = _snapshot_has_executable_quote +target_metadata_quote_snapshot = _target_metadata_quote_snapshot +target_notional_sizing_audit_from_params = _target_notional_sizing_audit_from_params +target_probe_cap = _target_probe_cap +target_probe_symbol_actions = _target_probe_symbol_actions +target_probe_symbol_notional_budget = _target_probe_symbol_notional_budget +target_probe_symbol_quantities = _target_probe_symbol_quantities +target_symbols = _target_symbols +target_plan_symbols = _target_symbols +text_from_mapping = _text_from_mapping + + __all__ = [ "_BOUNDED_SIM_COLLECTION_ACCOUNT_LABEL", "_BOUNDED_SIM_COLLECTION_BLOCKER_FIELDS", @@ -162,4 +200,34 @@ "_simple_drift_thresholds", "_strategy_signal_paper_entry_metadata", "_target_notional_sizing_audit_from_params", + "PAPER_ROUTE_PROBE_QTY_STEP", + "TargetProbeQuantityResolution", + "bounded_collection_decision_requires_target_notional_sizing", + "bounded_paper_route_collection_entry_metadata", + "bounded_sim_collection_metadata_from_decision", + "decimal_from_mapping", + "executable_bid_ask_present", + "first_decimal", + "mapping_value", + "min_optional_decimal", + "optional_decimal", + "paper_route_probe_entry_metadata", + "parse_target_datetime", + "pct_cap_to_notional", + "quote_snapshot_from_mapping", + "quote_snapshot_reference_price", + "safe_int", + "safe_text", + "simple_buying_power_consumption", + "simple_decision_notional", + "snapshot_has_executable_quote", + "target_metadata_quote_snapshot", + "target_notional_sizing_audit_from_params", + "target_probe_cap", + "target_probe_symbol_actions", + "target_probe_symbol_notional_budget", + "target_probe_symbol_quantities", + "target_symbols", + "target_plan_symbols", + "text_from_mapping", ] diff --git a/services/torghut/app/trading/scheduler/target_plan_helpers.pyi b/services/torghut/app/trading/scheduler/target_plan_helpers.pyi index 2fb5bc9c6d..a294694e5f 100644 --- a/services/torghut/app/trading/scheduler/target_plan_helpers.pyi +++ b/services/torghut/app/trading/scheduler/target_plan_helpers.pyi @@ -154,3 +154,42 @@ def _simple_drift_thresholds(*args: Any, **kwargs: Any) -> Any: ... def _pct_cap_to_notional(*args: Any, **kwargs: Any) -> Any: ... def _simple_decision_notional(*args: Any, **kwargs: Any) -> Any: ... def _simple_buying_power_consumption(*args: Any, **kwargs: Any) -> Any: ... + +PAPER_ROUTE_PROBE_QTY_STEP: Any + +class TargetProbeQuantityResolution: + def __init__(*args: Any, **kwargs: Any) -> None: ... + qty: Decimal + audit: dict[str, Any] + price_params: dict[str, Any] + +def bounded_collection_decision_requires_target_notional_sizing( + *args: Any, **kwargs: Any +) -> Any: ... +def bounded_paper_route_collection_entry_metadata(*args: Any, **kwargs: Any) -> Any: ... +def bounded_sim_collection_metadata_from_decision(*args: Any, **kwargs: Any) -> Any: ... +def decimal_from_mapping(*args: Any, **kwargs: Any) -> Any: ... +def executable_bid_ask_present(*args: Any, **kwargs: Any) -> Any: ... +def first_decimal(*args: Any, **kwargs: Any) -> Any: ... +def mapping_value(*args: Any, **kwargs: Any) -> Any: ... +def min_optional_decimal(*args: Any, **kwargs: Any) -> Any: ... +def optional_decimal(*args: Any, **kwargs: Any) -> Any: ... +def paper_route_probe_entry_metadata(*args: Any, **kwargs: Any) -> Any: ... +def parse_target_datetime(*args: Any, **kwargs: Any) -> Any: ... +def pct_cap_to_notional(*args: Any, **kwargs: Any) -> Any: ... +def quote_snapshot_from_mapping(*args: Any, **kwargs: Any) -> Any: ... +def quote_snapshot_reference_price(*args: Any, **kwargs: Any) -> Any: ... +def safe_int(*args: Any, **kwargs: Any) -> Any: ... +def safe_text(*args: Any, **kwargs: Any) -> Any: ... +def simple_buying_power_consumption(*args: Any, **kwargs: Any) -> Any: ... +def simple_decision_notional(*args: Any, **kwargs: Any) -> Any: ... +def snapshot_has_executable_quote(*args: Any, **kwargs: Any) -> Any: ... +def target_metadata_quote_snapshot(*args: Any, **kwargs: Any) -> Any: ... +def target_notional_sizing_audit_from_params(*args: Any, **kwargs: Any) -> Any: ... +def target_plan_symbols(*args: Any, **kwargs: Any) -> Any: ... +def target_probe_cap(*args: Any, **kwargs: Any) -> Any: ... +def target_probe_symbol_actions(*args: Any, **kwargs: Any) -> Any: ... +def target_probe_symbol_notional_budget(*args: Any, **kwargs: Any) -> Any: ... +def target_probe_symbol_quantities(*args: Any, **kwargs: Any) -> Any: ... +def target_symbols(*args: Any, **kwargs: Any) -> Any: ... +def text_from_mapping(*args: Any, **kwargs: Any) -> Any: ... diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_a.py b/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_a.py index b2a6d70f33..ea2fed448e 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_a.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_a.py @@ -1,7 +1,32 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + CountingLLMReviewEngine, + DSPyRuntimeUnsupportedStateError, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + FakeLLMReviewEngine, + LLMDecisionReview, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + SignalEnvelope, + Strategy, + TradeDecision, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _set_llm_guardrails, + datetime, + patch, + select, + timezone, +) class TestTradingPipelineDspyGateA(TradingPipelineTestCaseBase): @@ -207,7 +232,7 @@ def evaluate_live_readiness(self) -> tuple[bool, tuple[str, ...]]: error=DSPyRuntimeUnsupportedStateError("dspy_runtime_disabled") ) with patch( - "app.trading.scheduler.pipeline.DSPyReviewRuntime.from_settings", + "app.trading.scheduler.pipeline_modules.llm_review.DSPyReviewRuntime.from_settings", return_value=_UnavailableLiveRuntime(), ): pipeline = TradingPipeline( @@ -237,15 +262,15 @@ def evaluate_live_readiness(self) -> tuple[bool, tuple[str, ...]]: self._seed_promotion_certificate_evidence() with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=eligible_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), patch( - "app.trading.scheduler.pipeline.load_quant_evidence_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.load_quant_evidence_status", return_value=self._healthy_live_quant_status(), ), ): @@ -389,7 +414,7 @@ def evaluate_live_readiness(self) -> tuple[bool, tuple[str, ...]]: engine = CountingLLMReviewEngine() with patch( - "app.trading.scheduler.pipeline.DSPyReviewRuntime.from_settings", + "app.trading.scheduler.pipeline_modules.llm_review.DSPyReviewRuntime.from_settings", return_value=_AvailableLiveRuntime(), ): pipeline = TradingPipeline( @@ -419,15 +444,15 @@ def evaluate_live_readiness(self) -> tuple[bool, tuple[str, ...]]: self._seed_promotion_certificate_evidence() with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=eligible_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), patch( - "app.trading.scheduler.pipeline.load_quant_evidence_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.load_quant_evidence_status", return_value=self._healthy_live_quant_status(), ), ): @@ -564,7 +589,7 @@ def evaluate_live_readiness(self) -> tuple[bool, tuple[str, ...]]: engine = CountingLLMReviewEngine() with patch( - "app.trading.scheduler.pipeline.DSPyReviewRuntime.from_settings", + "app.trading.scheduler.pipeline_modules.llm_review.DSPyReviewRuntime.from_settings", return_value=_UnavailableLiveRuntime(), ): pipeline = TradingPipeline( diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_b.py b/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_b.py index bab3e6c7d8..cc787fae78 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_b.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_b.py @@ -1,7 +1,30 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + CountingLLMReviewEngine, + DSPyReviewRuntime, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + LLMDecisionReview, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + SignalEnvelope, + Strategy, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _set_llm_guardrails, + datetime, + patch, + select, + timezone, +) class TestTradingPipelineDspyGateB(TradingPipelineTestCaseBase): @@ -91,7 +114,7 @@ def test_pipeline_llm_dspy_live_runtime_gate_blocks_malformed_artifact_hash( ) with patch( - "app.trading.scheduler.pipeline.DSPyReviewRuntime.from_settings" + "app.trading.scheduler.pipeline_modules.llm_review.DSPyReviewRuntime.from_settings" ) as from_settings: pipeline = TradingPipeline( alpaca_client=FakeAlpacaClient(), @@ -227,7 +250,7 @@ def test_pipeline_llm_dspy_live_runtime_gate_blocks_before_readiness_probe( engine = CountingLLMReviewEngine() with patch( - "app.trading.scheduler.pipeline.DSPyReviewRuntime.from_settings" + "app.trading.scheduler.pipeline_modules.llm_review.DSPyReviewRuntime.from_settings" ) as from_settings: pipeline = TradingPipeline( alpaca_client=FakeAlpacaClient(), diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_c.py b/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_c.py index 24f92534bb..ee16121f5f 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_c.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_c.py @@ -1,7 +1,33 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + CountingLLMReviewEngine, + DSPyReviewRuntime, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + LLMDecisionReview, + OrderExecutor, + OrderFirewall, + PositionedAlpacaClient, + Reconciler, + RiskEngine, + SignalEnvelope, + SimulationExecutionAdapter, + Strategy, + TradeDecision, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _set_llm_guardrails, + datetime, + patch, + select, + timezone, +) class TestTradingPipelineDspyGateC(TradingPipelineTestCaseBase): @@ -102,7 +128,7 @@ def test_pipeline_llm_dspy_live_runtime_gate_can_pass_through_with_degraded_qty( session_factory=self.session_local, llm_review_engine=CountingLLMReviewEngine(), ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True eligible_summary = { "promotion_eligible_total": 1, @@ -116,15 +142,15 @@ def test_pipeline_llm_dspy_live_runtime_gate_can_pass_through_with_degraded_qty( self._seed_promotion_certificate_evidence() with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=eligible_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), patch( - "app.trading.scheduler.pipeline.load_quant_evidence_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.load_quant_evidence_status", return_value=self._healthy_live_quant_status(), ), ): @@ -297,15 +323,15 @@ def test_pipeline_llm_shadow_bootstrap_artifact_keeps_live_submission_operationa self._seed_promotion_certificate_evidence() with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=eligible_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), patch( - "app.trading.scheduler.pipeline.load_quant_evidence_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.load_quant_evidence_status", return_value=self._healthy_live_quant_status(), ), ): @@ -485,7 +511,7 @@ def test_pipeline_llm_dspy_runtime_reduced_sell_uses_seeded_simulation_inventory session_factory=self.session_local, llm_review_engine=CountingLLMReviewEngine(), ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True eligible_summary = { "promotion_eligible_total": 1, @@ -499,23 +525,27 @@ def test_pipeline_llm_dspy_runtime_reduced_sell_uses_seeded_simulation_inventory self._seed_promotion_certificate_evidence() with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=eligible_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), patch( - "app.trading.scheduler.pipeline.load_quant_evidence_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.load_quant_evidence_status", return_value=self._healthy_live_quant_status(), ), patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=signal.event_ts, ), patch( - "app.trading.execution_adapters.active_simulation_runtime_context", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.trading_now", + return_value=signal.event_ts, + ), + patch( + "app.trading.execution_adapters_modules.adapter_types.active_simulation_runtime_context", return_value={"run_id": "sim-test", "dataset_id": "dataset-a"}, ), ): @@ -829,15 +859,15 @@ def test_pipeline_llm_disabled_keeps_live_submission_operational(self) -> None: self._seed_promotion_certificate_evidence() with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=eligible_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), patch( - "app.trading.scheduler.pipeline.load_quant_evidence_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.load_quant_evidence_status", return_value=self._healthy_live_quant_status(), ), ): diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_d.py b/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_d.py index 651b8e11d7..9edfd8c240 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_d.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_dspy_gate_d.py @@ -1,7 +1,32 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + CountingLLMReviewEngine, + DSPyRuntimeUnsupportedStateError, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + FakeLLMReviewEngine, + LLMDecisionReview, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + SignalEnvelope, + Strategy, + TradeDecision, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _set_llm_guardrails, + datetime, + patch, + select, + timezone, +) class TestTradingPipelineDspyGateD(TradingPipelineTestCaseBase): @@ -83,7 +108,7 @@ def evaluate_live_readiness(self) -> tuple[bool, tuple[str, ...]]: error=DSPyRuntimeUnsupportedStateError("dspy_runtime_disabled") ) with patch( - "app.trading.scheduler.pipeline.DSPyReviewRuntime.from_settings", + "app.trading.scheduler.pipeline_modules.llm_review.DSPyReviewRuntime.from_settings", return_value=_UnavailableLiveRuntime(), ): pipeline = TradingPipeline( diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_execution_llm_a.py b/services/torghut/tests/pipeline/test_trading_pipeline_execution_llm_a.py index 3bc17da088..479ce060a8 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_execution_llm_a.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_execution_llm_a.py @@ -1,7 +1,36 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + AdaptiveExecutionPolicyDecision, + CountingAlpacaClient, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + FakeLLMReviewEngine, + FakePriceFetcher, + LLMDecisionReview, + OrderExecutor, + OrderFirewall, + Reconciler, + RejectingAlpacaClient, + RiskEngine, + SellInventoryConflictAlpacaClient, + SignalEnvelope, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _set_llm_guardrails, + datetime, + patch, + select, + timezone, +) class TestTradingPipelineExecutionLlmA(TradingPipelineTestCaseBase): @@ -92,7 +121,7 @@ def test_pipeline_persists_adaptive_policy_and_records_fallback_metric( ) with patch( - "app.trading.scheduler.pipeline.derive_adaptive_execution_policy", + "app.trading.scheduler.pipeline_modules.submission_policy.derive_adaptive_execution_policy", return_value=fallback_policy, ): pipeline.run_once() diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_execution_llm_b.py b/services/torghut/tests/pipeline/test_trading_pipeline_execution_llm_b.py index 6f185f36c6..5940f2edaa 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_execution_llm_b.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_execution_llm_b.py @@ -1,7 +1,34 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + FakeLLMReviewEngine, + LLMDecisionReview, + OrderExecutor, + OrderFirewall, + Path, + Reconciler, + RiskEngine, + SignalEnvelope, + Strategy, + TradeDecision, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _build_dspy_lineage, + _committee_trace_has_veto, + _set_llm_guardrails, + datetime, + patch, + select, + tempfile, + timezone, +) class TestTradingPipelineExecutionLlmB(TradingPipelineTestCaseBase): @@ -305,15 +332,15 @@ def test_pipeline_llm_failure_fallbacks(self) -> None: self._seed_promotion_certificate_evidence() with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=eligible_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), patch( - "app.trading.scheduler.pipeline.load_quant_evidence_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.load_quant_evidence_status", return_value=self._healthy_live_quant_status(), ), ): diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_a.py b/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_a.py index 28948ef95d..d4b1ea5677 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_a.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_a.py @@ -1,7 +1,28 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + Decimal, + DecisionEngine, + FakeAlpacaClient, + FakeIngestor, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + SimpleTradingPipeline, + Strategy, + StrategyDecision, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _bounded_sim_collection_metadata_from_decision, + cast, + datetime, + paper_route_target_plan_from_payload, + patch, + timezone, +) class TestTradingPipelineExternalTargetsA(TradingPipelineTestCaseBase): @@ -38,7 +59,7 @@ def test_bounded_signal_scope_records_target_plan_fetch_failure( account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True now = datetime(2026, 6, 1, 14, 30, tzinfo=timezone.utc) try: with ( @@ -120,7 +141,7 @@ def test_bounded_signal_scope_records_configured_target_plan_missing_symbols( account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True now = datetime(2026, 6, 1, 14, 30, tzinfo=timezone.utc) try: with ( @@ -184,7 +205,7 @@ def test_paper_route_target_source_decisions_record_target_plan_unavailable( account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True now = datetime(2026, 6, 1, 14, 30, tzinfo=timezone.utc) try: with patch( diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_c.py b/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_c.py index a2cf9847ba..e791589844 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_c.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_c.py @@ -1,7 +1,34 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + MarketSnapshot, + Mock, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + SignalEnvelope, + SimpleTradingPipeline, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _target_price_snapshots, + cast, + datetime, + patch, + select, + timedelta, + timezone, +) class TestTradingPipelineExternalTargetsC(TradingPipelineTestCaseBase): @@ -38,7 +65,7 @@ def test_bounded_hpairs_target_source_records_decisions_and_executions( account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with self.session_local() as session: strategy = Strategy( @@ -139,7 +166,11 @@ def priced_decision( return_value=now, ), patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", + return_value=now, + ), + patch( + "app.trading.scheduler.pipeline_modules.decision_lifecycle.trading_now", return_value=now, ), ): diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_d.py b/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_d.py index 7338c87193..d26e1fbc68 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_d.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_external_targets_d.py @@ -1,7 +1,37 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + BOUNDED_PAPER_ROUTE_COLLECTION_SOURCE_DECISION_MODE, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + FakePriceFetcher, + Mock, + NoSignalReasonIngestor, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + Session, + SignalBatch, + SignalEnvelope, + SimpleTradingPipeline, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + cast, + datetime, + patch, + select, + timedelta, + timezone, +) class TestTradingPipelineExternalTargetsD(TradingPipelineTestCaseBase): @@ -86,7 +116,7 @@ def test_run_once_blocks_bounded_target_decisions_when_signal_ingest_times_out( session_factory=self.session_local, price_fetcher=FakePriceFetcher(Decimal("100")), ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( @@ -102,7 +132,14 @@ def test_run_once_blocks_bounded_target_decisions_when_signal_ingest_times_out( patch( "app.trading.scheduler.simple_pipeline.trading_now", return_value=now ), - patch("app.trading.scheduler.pipeline.trading_now", return_value=now), + patch( + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", + return_value=now, + ), + patch( + "app.trading.scheduler.pipeline_modules.decision_lifecycle.trading_now", + return_value=now, + ), ): pipeline.run_once() @@ -224,7 +261,7 @@ def test_run_once_scopes_hpairs_signal_ingest_and_emits_candidate_decisions( session_factory=self.session_local, price_fetcher=FakePriceFetcher(Decimal("100")), ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( @@ -246,7 +283,14 @@ def test_run_once_scopes_hpairs_signal_ingest_and_emits_candidate_decisions( patch( "app.trading.scheduler.simple_pipeline.trading_now", return_value=now ), - patch("app.trading.scheduler.pipeline.trading_now", return_value=now), + patch( + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", + return_value=now, + ), + patch( + "app.trading.scheduler.pipeline_modules.decision_lifecycle.trading_now", + return_value=now, + ), patch("app.trading.simulation.trading_now", return_value=now), ): pipeline.run_once() @@ -375,7 +419,7 @@ def target(**overrides: object) -> dict[str, object]: account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True targets = [ target(paper_route_account_pre_session_blockers=["not_flat"]), target( diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_live_regime_a.py b/services/torghut/tests/pipeline/test_trading_pipeline_live_regime_a.py index bc2ddc1986..6b27d34cbf 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_live_regime_a.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_live_regime_a.py @@ -1,7 +1,32 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Decimal, + DecisionEngine, + FakeAlpacaClient, + FakeIngestor, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + SignalEnvelope, + Strategy, + StrategyDecision, + StrategyHypothesis, + StrategyHypothesisMetricWindow, + StrategyPromotionDecision, + TradeDecision, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + VNextDatasetSnapshot, + datetime, + patch, + select, + timedelta, + timezone, +) class TestTradingPipelineLiveRegimeA(TradingPipelineTestCaseBase): @@ -398,15 +423,15 @@ def test_live_shadow_stage_blocks_policy_approved_decision(self) -> None: with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=eligible_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), patch( - "app.trading.scheduler.pipeline.load_quant_evidence_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.load_quant_evidence_status", return_value=self._healthy_quant_status(account_label="paper"), ), ): @@ -605,15 +630,15 @@ def test_live_submission_allows_autonomy_eligible_canary_without_static_flag( with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=eligible_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), patch( - "app.trading.scheduler.pipeline.load_quant_evidence_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.load_quant_evidence_status", return_value=self._healthy_live_quant_status(), ), ): @@ -742,11 +767,11 @@ def test_live_submission_blocks_autonomy_eligible_canary_without_promotion_evide with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=blocked_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), ): diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_live_regime_b.py b/services/torghut/tests/pipeline/test_trading_pipeline_live_regime_b.py index 2d01e0ccc4..834be93ea5 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_live_regime_b.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_live_regime_b.py @@ -1,7 +1,31 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + FakePriceFetcher, + OrderExecutor, + OrderFirewall, + Path, + Reconciler, + RiskEngine, + SignalEnvelope, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + datetime, + patch, + select, + tempfile, + timezone, +) class TestTradingPipelineLiveRegimeB(TradingPipelineTestCaseBase): @@ -85,15 +109,15 @@ def test_run_once_blocks_live_submission_when_quant_latest_store_is_empty( with ( patch( - "app.trading.scheduler.pipeline.build_hypothesis_runtime_summary", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_hypothesis_runtime_summary", return_value=allowed_summary, ), patch( - "app.trading.scheduler.pipeline.build_empirical_jobs_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.build_empirical_jobs_status", return_value={"ready": True, "status": "healthy"}, ), patch( - "app.trading.scheduler.pipeline.load_quant_evidence_status", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.load_quant_evidence_status", return_value={ "required": True, "ok": False, diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_a.py b/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_a.py index 5e22078725..0308224794 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_a.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_a.py @@ -1,7 +1,35 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + BOUNDED_PAPER_ROUTE_COLLECTION_SOURCE_DECISION_MODE, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + FakePriceFetcher, + Mapping, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + Sequence, + SimpleTradingPipeline, + Strategy, + TradeDecision, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _target_price_snapshots, + cast, + datetime, + materialize_bounded_paper_route_target_plan, + patch, + select, + timedelta, + timezone, +) class TestTradingPipelineMaterializedTargetPlanA(TradingPipelineTestCaseBase): @@ -112,7 +140,7 @@ def test_simple_pipeline_no_signal_cycle_generates_target_plan_source_decision( ask=Decimal("100.01"), ), ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( @@ -134,7 +162,14 @@ def test_simple_pipeline_no_signal_cycle_generates_target_plan_source_decision( "app.trading.scheduler.simple_pipeline.trading_now", return_value=now, ), - patch("app.trading.scheduler.pipeline.trading_now", return_value=now), + patch( + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", + return_value=now, + ), + patch( + "app.trading.scheduler.pipeline_modules.decision_lifecycle.trading_now", + return_value=now, + ), patch("app.trading.simulation.trading_now", return_value=now), ): pipeline.run_once() @@ -406,7 +441,7 @@ def test_simple_pipeline_submits_materialized_target_plan_source_decisions( ask=Decimal("100.01"), ), ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( @@ -424,7 +459,14 @@ def test_simple_pipeline_submits_materialized_target_plan_source_decisions( "app.trading.scheduler.simple_pipeline.trading_now", return_value=now, ), - patch("app.trading.scheduler.pipeline.trading_now", return_value=now), + patch( + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", + return_value=now, + ), + patch( + "app.trading.scheduler.pipeline_modules.decision_lifecycle.trading_now", + return_value=now, + ), patch("app.trading.simulation.trading_now", return_value=now), ): pipeline.run_once() @@ -624,7 +666,7 @@ def test_materialized_target_plan_expired_rows_fail_closed(self) -> None: account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: False # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: False with patch( "app.trading.scheduler.simple_pipeline.trading_now", return_value=now, diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_b.py b/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_b.py index 40f1fe4f1a..b80d4be8bf 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_b.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_b.py @@ -1,7 +1,37 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + BOUNDED_PAPER_ROUTE_COLLECTION_SOURCE_DECISION_MODE, + Decimal, + DecisionEngine, + FakeAlpacaClient, + FakeIngestor, + FakePriceFetcher, + MarketSnapshot, + NoSignalReasonIngestor, + OrderExecutor, + OrderFirewall, + PriceFetcher, + ROUTE_ACQUISITION_SOURCE_DECISION_MODE, + Reconciler, + RiskEngine, + SimpleNamespace, + SimpleTradingPipeline, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + cast, + datetime, + materialize_bounded_paper_route_target_plan, + patch, + select, + timezone, + uuid4, +) class TestTradingPipelineMaterializedTargetPlanB(TradingPipelineTestCaseBase): @@ -158,7 +188,7 @@ def test_simple_pipeline_processes_materialized_target_plan_when_signal_ingest_u ask=Decimal("100.01"), ), ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( @@ -176,7 +206,10 @@ def test_simple_pipeline_processes_materialized_target_plan_when_signal_ingest_u "app.trading.scheduler.simple_pipeline.trading_now", return_value=now, ), - patch("app.trading.scheduler.pipeline.trading_now", return_value=now), + patch( + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", + return_value=now, + ), patch("app.trading.simulation.trading_now", return_value=now), ): pipeline.run_once() diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_c.py b/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_c.py index b44cd9860f..2703d67c74 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_c.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_materialized_target_plan_c.py @@ -1,7 +1,35 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + BOUNDED_PAPER_ROUTE_COLLECTION_SOURCE_DECISION_MODE, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + Mapping, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + SimpleTradingPipeline, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _target_price_snapshots, + cast, + datetime, + materialize_bounded_paper_route_target_plan, + patch, + select, + timedelta, + timezone, + uuid4, +) class TestTradingPipelineMaterializedTargetPlanC(TradingPipelineTestCaseBase): @@ -208,7 +236,7 @@ def add_row( account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: False # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: False with patch( "app.trading.scheduler.simple_pipeline.trading_now", return_value=now, @@ -221,7 +249,7 @@ def add_row( positions=[], ) ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True decisions = pipeline._paper_route_materialized_planned_decisions( session=session, strategies=[strategy], @@ -360,7 +388,7 @@ def test_materialized_target_plan_reconciles_matching_open_order_projections( account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True matching_projection_positions = [ { @@ -591,7 +619,7 @@ def test_materialized_target_plan_ignores_transient_batch_symbol_filter( account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with patch( "app.trading.scheduler.simple_pipeline.trading_now", diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_position_projection_a.py b/services/torghut/tests/pipeline/test_trading_pipeline_position_projection_a.py index 1369528153..57f6e4e33d 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_position_projection_a.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_position_projection_a.py @@ -1,7 +1,23 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + Decimal, + Execution, + Mock, + SimpleNamespace, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipeline, + TradingPipelineTestCaseBase, + _apply_projected_position_decision, + _project_open_orders_onto_positions, + datetime, + patch, + timedelta, + timezone, +) class TestTradingPipelinePositionProjectionA(TradingPipelineTestCaseBase): @@ -261,7 +277,7 @@ def list_orders(self, status: str = "all") -> list[dict[str, str]]: pipeline.execution_adapter = AdapterWithoutOpenOrders() with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=session_open + timedelta(minutes=150), ): positions = pipeline._resolve_execution_context_positions( @@ -349,7 +365,7 @@ def list_orders(self, status: str = "all") -> list[dict[str, str]]: pipeline.execution_adapter = AdapterWithoutOpenOrders() with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=session_open + timedelta(minutes=150), ): positions = pipeline._resolve_execution_context_positions( @@ -444,7 +460,7 @@ def list_orders(self, status: str = "all") -> list[dict[str, str]]: pipeline.execution_adapter = AdapterWithoutOpenOrders() with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=session_open + timedelta(minutes=150), ): positions = pipeline._resolve_execution_context_positions( @@ -516,7 +532,7 @@ def list_orders(self, status: str = "all") -> list[dict[str, str]]: pipeline.execution_adapter = AdapterWithoutOpenOrders() with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=session_open + timedelta(minutes=150), ): positions = pipeline._resolve_execution_context_positions( @@ -607,7 +623,7 @@ def list_orders(self, status: str = "all") -> list[dict[str, str]]: pipeline.execution_adapter = AdapterWithoutOpenOrders() with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=session_open + timedelta(minutes=150), ): positions = pipeline._resolve_execution_context_positions( @@ -642,7 +658,7 @@ def test_attach_current_session_strategy_position_tags_fails_closed_on_query_err positions = [{"symbol": "AAPL", "qty": "1", "side": "long"}] with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=session_open + timedelta(minutes=150), ): tagged = pipeline._attach_current_session_strategy_position_tags( @@ -695,7 +711,7 @@ def test_attach_current_session_strategy_position_tags_ignores_invalid_fill_rows positions = [{"symbol": "AAPL", "qty": "1", "side": "long"}] with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=session_open + timedelta(minutes=150), ): tagged = pipeline._attach_current_session_strategy_position_tags( diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_position_projection_b.py b/services/torghut/tests/pipeline/test_trading_pipeline_position_projection_b.py index cf398acc03..4b69a04835 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_position_projection_b.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_position_projection_b.py @@ -1,7 +1,40 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +import json +import tempfile +from datetime import datetime, timedelta, timezone +from decimal import Decimal +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock + +from sqlalchemy import select + +from app import config +from app.models import Strategy, TradeDecision +from app.trading.decisions import ( + _is_entry_action_for_strategies, + _is_exit_action_for_strategies, + _strategy_uses_position_isolation, + DecisionEngine, +) +from app.trading.execution import OrderExecutor +from app.trading.firewall import OrderFirewall +from app.trading.models import SignalEnvelope, StrategyDecision +from app.trading.reconcile import Reconciler +from app.trading.risk import RiskEngine +from app.trading.scheduler.pipeline import TradingPipeline +from app.trading.scheduler.state import TradingState +from app.trading.universe import UniverseResolver +from tests.pipeline.trading_pipeline_base import TradingPipelineTestCaseBase +from tests.pipeline.trading_pipeline_support import ( + FakeAlpacaClient, + FakeIngestor, + FakeLLMReviewEngine, + PositionedAlpacaClient, + SellInventoryConflictRetryClient, + _set_llm_guardrails, +) class TestTradingPipelinePositionProjectionB(TradingPipelineTestCaseBase): @@ -15,7 +48,7 @@ def test_attach_strategy_position_tag_fail_closed_edges(self) -> None: } self.assertFalse( - TradingPipeline._same_side_position_exposure( + TradingPipeline.same_side_position_exposure( Decimal("0"), Decimal("2"), ) diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_probe_exits_a.py b/services/torghut/tests/pipeline/test_trading_pipeline_probe_exits_a.py index c226643828..a3d11bedcf 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_probe_exits_a.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_probe_exits_a.py @@ -1,7 +1,38 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + BOUNDED_PAPER_ROUTE_COLLECTION_SOURCE_DECISION_MODE, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + Mapping, + OrderExecutor, + OrderFirewall, + PositionedAlpacaClient, + ROUTE_ACQUISITION_SOURCE_DECISION_MODE, + Reconciler, + RiskEngine, + STRATEGY_SIGNAL_PAPER_SOURCE_DECISION_MODE, + SignalEnvelope, + SimpleTradingPipeline, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _bounded_paper_route_collection_entry_metadata, + _paper_route_probe_entry_metadata, + _strategy_signal_paper_entry_metadata, + cast, + datetime, + patch, + select, + timezone, +) class TestTradingPipelineProbeExitsA(TradingPipelineTestCaseBase): @@ -102,7 +133,7 @@ def test_simple_pipeline_retries_blocked_paper_route_decision_without_new_signal account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_probe_exits_b.py b/services/torghut/tests/pipeline/test_trading_pipeline_probe_exits_b.py index 36afa7866c..2e323a6dae 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_probe_exits_b.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_probe_exits_b.py @@ -1,7 +1,37 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + Mapping, + OrderExecutor, + OrderFirewall, + PositionedAlpacaClient, + Reconciler, + RiskEngine, + SignalEnvelope, + SimpleTradingPipeline, + SimulationExecutionAdapter, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _bounded_sim_collection_target_with_runtime_account_audit, + _paper_route_probe_lineage_from_params, + _target_probe_symbol_notional_budget, + cast, + datetime, + patch, + select, + timezone, + uuid4, +) class TestTradingPipelineProbeExitsB(TradingPipelineTestCaseBase): @@ -115,7 +145,7 @@ def test_simple_pipeline_reopens_rejected_paper_route_probe_exit( account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch( "app.trading.scheduler.simple_pipeline.trading_now", @@ -591,7 +621,7 @@ def test_simple_pipeline_paper_route_exit_helpers_cover_filter_edges( account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: False # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: False with ( patch( "app.trading.scheduler.simple_pipeline.trading_now", @@ -767,7 +797,7 @@ def test_simple_pipeline_allows_bounded_paper_route_probe_for_tca_repair( account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True proof_floor = { "route_state": "repair_only", diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_quote_outcome.py b/services/torghut/tests/pipeline/test_trading_pipeline_quote_outcome.py index b38cb4f1f8..acb9a7d807 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_quote_outcome.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_quote_outcome.py @@ -1,7 +1,33 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Decimal, + DecisionEngine, + FakeAlpacaClient, + FakeIngestor, + FakePriceFetcher, + Mapping, + MarketSnapshot, + OrderExecutor, + OrderFirewall, + Reconciler, + RejectedSignalOutcomeEvent, + RiskEngine, + SQLAlchemyError, + Session, + SignalEnvelope, + TimelinePriceFetcher, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + build_hypothesis_runtime_summary, + cast, + datetime, + select, + timedelta, + timezone, +) class TestTradingPipelineQuoteOutcome(TradingPipelineTestCaseBase): @@ -536,7 +562,9 @@ def failing_session_factory() -> Session: session_factory=failing_session_factory ) - with self.assertLogs("app.trading.scheduler.pipeline", level="ERROR"): + with self.assertLogs( + "app.trading.scheduler.pipeline_modules.signal_processing", level="ERROR" + ): pipeline._persist_rejected_signal_outcome_event( { "event_id": "reject-event-1", diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_route_execution_a.py b/services/torghut/tests/pipeline/test_trading_pipeline_route_execution_a.py index 4d7bb43359..b1a6312965 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_route_execution_a.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_route_execution_a.py @@ -1,7 +1,34 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + Mock, + OrderExecutor, + OrderFirewall, + Reconciler, + RecordingDecisionEngine, + RiskEngine, + SignalEnvelope, + SimpleTradingPipeline, + SimulationExecutionAdapter, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + cast, + datetime, + patch, + select, + timezone, +) class TestTradingPipelineRouteExecutionA(TradingPipelineTestCaseBase): @@ -313,7 +340,7 @@ def test_simple_pipeline_blocks_symbol_excluded_from_route_candidates( account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True proof_floor = { "route_state": "live_micro_candidate", @@ -441,7 +468,7 @@ def test_simple_pipeline_skips_out_of_strategy_universe_signals_before_quote_qua account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True pipeline.run_once() @@ -499,7 +526,7 @@ def test_simple_pipeline_reconcile_updates_execution_status(self) -> None: account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with patch.object( SimpleTradingPipeline, @@ -571,7 +598,7 @@ def test_simple_pipeline_uses_client_order_id_for_idempotency(self) -> None: account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with patch.object( SimpleTradingPipeline, diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_route_execution_b.py b/services/torghut/tests/pipeline/test_trading_pipeline_route_execution_b.py index da39333528..5e5f62f4c0 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_route_execution_b.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_route_execution_b.py @@ -1,7 +1,30 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + CursorAdvancingFakeIngestor, + Decimal, + DecisionEngine, + FakeAlpacaClient, + FakeIngestor, + OrderExecutor, + OrderFirewall, + Reconciler, + RecordingDecisionEngine, + RiskEngine, + SignalBatch, + SignalEnvelope, + Strategy, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + WarmupIngestor, + _with_default_executable_quote, + datetime, + patch, + timedelta, + timezone, +) class TestTradingPipelineRouteExecutionB(TradingPipelineTestCaseBase): @@ -152,7 +175,7 @@ def test_signal_batch_preserves_lag_metric_for_alpha_readiness(self) -> None: account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with self.session_local() as session: prepared = pipeline._prepare_batch_for_decisions( @@ -210,7 +233,7 @@ def test_signal_batch_counts_feature_rows_when_quality_enforcement_disabled( account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with self.session_local() as session: prepared = pipeline._prepare_batch_for_decisions( @@ -404,7 +427,7 @@ def test_pipeline_accepts_replayed_batch_in_simulation_mode(self) -> None: account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with patch( "app.trading.features.simulation_context_enabled", @@ -468,10 +491,10 @@ def test_pipeline_suppresses_no_signal_alert_during_bootstrap_grace(self) -> Non account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with patch( - "app.trading.scheduler.pipeline._signal_bootstrap_grace_active", + "app.trading.scheduler.pipeline_modules.decision_lifecycle.signal_bootstrap_grace_active", return_value=True, ): pipeline.record_no_signal_batch( @@ -529,7 +552,7 @@ def test_pipeline_treats_fresh_tail_state_as_expected_staleness(self) -> None: account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True pipeline.record_no_signal_batch( SignalBatch( @@ -593,7 +616,7 @@ def test_pipeline_alerts_when_tail_lag_is_stale(self) -> None: account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True pipeline.record_no_signal_batch( SignalBatch( diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_target_plan_source_a.py b/services/torghut/tests/pipeline/test_trading_pipeline_target_plan_source_a.py index 7cbe490772..dfd096dd0a 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_target_plan_source_a.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_target_plan_source_a.py @@ -1,7 +1,41 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + FakePriceFetcher, + Mock, + OrderExecutor, + OrderFirewall, + ROUTE_ACQUISITION_SOURCE_DECISION_MODE, + Reconciler, + RiskEngine, + STRATEGY_SIGNAL_PAPER_SOURCE_DECISION_MODE, + Session, + SignalEnvelope, + SimpleTradingPipeline, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _parse_target_datetime, + _target_price_snapshots, + _target_probe_action, + _target_probe_window, + cast, + datetime, + patch, + select, + timedelta, + timezone, + uuid4, +) class TestTradingPipelineTargetPlanSourceA(TradingPipelineTestCaseBase): @@ -105,7 +139,7 @@ def test_simple_pipeline_signal_cycle_still_generates_target_plan_source_decisio ask=Decimal("100.01"), ), ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( @@ -132,7 +166,14 @@ def test_simple_pipeline_signal_cycle_still_generates_target_plan_source_decisio "app.trading.scheduler.simple_pipeline.trading_now", return_value=now, ), - patch("app.trading.scheduler.pipeline.trading_now", return_value=now), + patch( + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", + return_value=now, + ), + patch( + "app.trading.scheduler.pipeline_modules.decision_lifecycle.trading_now", + return_value=now, + ), patch("app.trading.simulation.trading_now", return_value=now), ): pipeline.run_once() @@ -226,7 +267,7 @@ def test_simple_pipeline_target_plan_source_decision_requires_open_window( session_factory=self.session_local, price_fetcher=FakePriceFetcher(Decimal("100")), ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( @@ -383,7 +424,7 @@ def test_paper_route_target_source_decisions_guard_paths_and_dedupes( account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with patch( "app.trading.scheduler.simple_pipeline.trading_now", @@ -409,7 +450,7 @@ def test_paper_route_target_source_decisions_guard_paths_and_dedupes( ) config.settings.trading_simple_paper_route_probe_enabled = True - pipeline._is_market_session_open = lambda _now=None: False # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: False self.assertEqual( pipeline._paper_route_target_source_decisions( strategies=[strategy], @@ -418,7 +459,7 @@ def test_paper_route_target_source_decisions_guard_paths_and_dedupes( [], ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True for skipped_target in ( { **target, @@ -498,7 +539,7 @@ def test_paper_route_target_source_decisions_skip_symbols_with_open_exposure( account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( @@ -615,7 +656,7 @@ def test_paper_route_target_source_decisions_skip_db_strategy_exposure( account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_target_plan_source_b.py b/services/torghut/tests/pipeline/test_trading_pipeline_target_plan_source_b.py index faf5d2c6e8..00b4580dbb 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_target_plan_source_b.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_target_plan_source_b.py @@ -1,7 +1,30 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + Decimal, + DecisionEngine, + FakeAlpacaClient, + FakeIngestor, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + SignalEnvelope, + SimpleTradingPipeline, + Strategy, + StrategyDecision, + TradeDecision, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + cast, + datetime, + json, + patch, + select, + timezone, +) class TestTradingPipelineTargetPlanSourceB(TradingPipelineTestCaseBase): @@ -138,7 +161,7 @@ def test_simple_pipeline_paper_route_probe_can_repair_symbol_outside_candidates( account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True proof_floor = { "route_state": "repair_only", diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_warmup_submission_a.py b/services/torghut/tests/pipeline/test_trading_pipeline_warmup_submission_a.py index 0adb931b01..34a7f4a29f 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_warmup_submission_a.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_warmup_submission_a.py @@ -1,7 +1,38 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + CountingAlpacaClient, + CursorErrorWarmupIngestor, + Decimal, + DecisionEngine, + FakeAlpacaClient, + FakeIngestor, + FetchErrorWarmupIngestor, + Mock, + OrderExecutor, + OrderFirewall, + PositionSnapshot, + RaisingObserveDecisionEngine, + Reconciler, + RecordingDecisionEngine, + RiskEngine, + Session, + SignalEnvelope, + SimpleTradingPipeline, + Strategy, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + TransactionAwareWarmupIngestor, + UniverseResolver, + WarmupIngestor, + cast, + date, + datetime, + patch, + select, + timezone, +) class TestTradingPipelineWarmupSubmissionA(TradingPipelineTestCaseBase): @@ -105,10 +136,10 @@ def test_simple_pipeline_snapshots_account_for_paper_route_window_without_signal account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: False # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: False with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=datetime(2026, 3, 26, 13, 20, tzinfo=timezone.utc), ): pipeline.run_once() @@ -159,7 +190,7 @@ def test_runtime_window_account_snapshot_skips_existing_snapshot(self) -> None: session.commit() with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=datetime(2026, 3, 26, 13, 20, tzinfo=timezone.utc), ): pipeline._capture_runtime_window_account_snapshot_if_due(session) @@ -190,17 +221,17 @@ def test_runtime_window_account_snapshot_rolls_back_capture_failure(self) -> Non account_label="TORGHUT_SIM", session_factory=self.session_local, ) - pipeline._get_account_snapshot = Mock( # type: ignore[method-assign] + pipeline._get_account_snapshot = Mock( side_effect=RuntimeError("broker unavailable") ) with self.session_local() as session: with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=datetime(2026, 3, 26, 13, 20, tzinfo=timezone.utc), ): with self.assertLogs( - "app.trading.scheduler.pipeline", + "app.trading.scheduler.pipeline_modules.run_cycle", level="ERROR", ) as logs: pipeline._capture_runtime_window_account_snapshot_if_due(session) @@ -294,12 +325,10 @@ def test_pipeline_warms_session_context_with_bounded_replay_window(self) -> None account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = ( # type: ignore[method-assign] - lambda _now=None: True - ) + pipeline._is_market_session_open = lambda _now=None: True with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=datetime(2026, 3, 26, 15, 0, tzinfo=timezone.utc), ): pipeline.run_once() @@ -341,7 +370,7 @@ def test_session_context_warmup_closes_transaction_before_replay_fetch( pipeline = self._build_warmup_pipeline(ingestor=ingestor) with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=datetime(2026, 3, 26, 15, 0, tzinfo=timezone.utc), ): pipeline._warm_session_context_from_open(session) @@ -361,7 +390,7 @@ def test_session_context_warmup_rolls_back_when_transaction_close_fails( session.commit.side_effect = RuntimeError("commit failed") with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=datetime(2026, 3, 26, 15, 0, tzinfo=timezone.utc), ): pipeline._warm_session_context_from_open(cast(Session, session)) @@ -382,14 +411,14 @@ def test_session_context_warmup_ignores_preopen_and_empty_cursor_windows( with self.session_local() as session: with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=datetime(2026, 3, 26, 12, 0, tzinfo=timezone.utc), ): pipeline._warm_session_context_from_open(session) ingestor.cursor_at = datetime(2026, 3, 26, 13, 30, tzinfo=timezone.utc) with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=datetime(2026, 3, 26, 14, 0, tzinfo=timezone.utc), ): pipeline._warm_session_context_from_open(session) @@ -412,7 +441,7 @@ def test_session_context_warmup_handles_cursor_and_fetch_errors(self) -> None: pipeline = self._build_warmup_pipeline(ingestor=ingestor) with self.session_local() as session: with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=datetime(2026, 3, 26, 15, 0, tzinfo=timezone.utc), ): pipeline._warm_session_context_from_open(session) @@ -442,7 +471,7 @@ def test_session_context_warmup_normalizes_naive_cursor_and_skips_bad_signals( with self.session_local() as session: with patch( - "app.trading.scheduler.pipeline.trading_now", + "app.trading.scheduler.pipeline_modules.run_cycle.trading_now", return_value=datetime(2026, 3, 26, 15, 0, tzinfo=timezone.utc), ): pipeline._warm_session_context_from_open(session) diff --git a/services/torghut/tests/pipeline/test_trading_pipeline_warmup_submission_b.py b/services/torghut/tests/pipeline/test_trading_pipeline_warmup_submission_b.py index 3f6f3c5f5c..ce50b5b819 100644 --- a/services/torghut/tests/pipeline/test_trading_pipeline_warmup_submission_b.py +++ b/services/torghut/tests/pipeline/test_trading_pipeline_warmup_submission_b.py @@ -1,7 +1,35 @@ from __future__ import annotations -# ruff: noqa: F403,F405 -from tests.pipeline.trading_pipeline_base import * +from tests.pipeline.trading_pipeline_base import ( + Any, + Decimal, + DecisionEngine, + Execution, + FakeAlpacaClient, + FakeIngestor, + MarketContextBundle, + OrderExecutor, + OrderFirewall, + Reconciler, + RiskEngine, + Session, + SignalEnvelope, + SimpleNamespace, + SimpleTradingPipeline, + Strategy, + TradeDecision, + TradingPipeline, + TradingPipelineTestCaseBase, + TradingState, + UniverseResolver, + _market_context_bundle, + cast, + datetime, + patch, + select, + timedelta, + timezone, +) class TestTradingPipelineWarmupSubmissionB(TradingPipelineTestCaseBase): @@ -62,7 +90,7 @@ def test_simple_pipeline_submits_live_order_when_shared_gate_allows_and_persists account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with ( patch.object( @@ -177,7 +205,7 @@ def test_simple_pipeline_paper_order_updates_proof_counters_without_live_promoti account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with patch.object( SimpleTradingPipeline, @@ -454,7 +482,7 @@ def _fetch(symbol: str) -> tuple[MarketContextBundle | None, str | None]: fetch_calls.append(symbol) return _market_context_bundle(symbol=symbol), None - pipeline._fetch_market_context = _fetch # type: ignore[method-assign] + pipeline._fetch_market_context = _fetch now = datetime.now(timezone.utc) pipeline.state.last_market_context_checked_at = now - timedelta(seconds=15) @@ -695,7 +723,7 @@ def test_simple_pipeline_blocks_live_order_when_shared_gate_blocks( account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with patch.object( SimpleTradingPipeline, @@ -784,7 +812,7 @@ def test_simple_pipeline_blocks_live_order_when_simple_submit_disabled_with_shar account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with patch.object( TradingPipeline, @@ -882,7 +910,7 @@ def test_simple_pipeline_blocks_paper_order_when_profitability_floor_zero_notion account_label="paper", session_factory=self.session_local, ) - pipeline._is_market_session_open = lambda _now=None: True # type: ignore[method-assign] + pipeline._is_market_session_open = lambda _now=None: True with patch.object( SimpleTradingPipeline, From e3e0b5bed1ed120dd9e2f8dc380ad421eca48c25 Mon Sep 17 00:00:00 2001 From: Greg Konush <12027037+gregkonush@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:43:28 -0700 Subject: [PATCH 2/2] fix(torghut): preserve llm review persistence call shape --- .../pipeline_modules/llm_outcomes.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/services/torghut/app/trading/scheduler/pipeline_modules/llm_outcomes.py b/services/torghut/app/trading/scheduler/pipeline_modules/llm_outcomes.py index 087bde4c85..e0d7635ae2 100644 --- a/services/torghut/app/trading/scheduler/pipeline_modules/llm_outcomes.py +++ b/services/torghut/app/trading/scheduler/pipeline_modules/llm_outcomes.py @@ -653,8 +653,26 @@ def _get_account_snapshot(self, session: Session): @staticmethod def _persist_llm_review( - record: LLMReviewRecord, + record: LLMReviewRecord | None = None, + **legacy_kwargs: Any, ) -> None: + if record is None: + record = LLMReviewRecord( + session=legacy_kwargs["session"], + decision_row=legacy_kwargs["decision_row"], + model=legacy_kwargs["model"], + prompt_version=legacy_kwargs["prompt_version"], + request_json=legacy_kwargs["request_json"], + response_json=legacy_kwargs["response_json"], + verdict=legacy_kwargs["verdict"], + confidence=legacy_kwargs["confidence"], + adjusted_qty=legacy_kwargs["adjusted_qty"], + adjusted_order_type=legacy_kwargs["adjusted_order_type"], + rationale=legacy_kwargs["rationale"], + risk_flags=legacy_kwargs["risk_flags"], + tokens_prompt=legacy_kwargs["tokens_prompt"], + tokens_completion=legacy_kwargs["tokens_completion"], + ) request_payload = coerce_json_payload(record.request_json) response_payload_json = dict(record.response_json) attach_dspy_lineage(