diff --git a/migrations/028_promote_trading_runtime_columns.sql b/migrations/028_promote_trading_runtime_columns.sql new file mode 100644 index 0000000..63984cd --- /dev/null +++ b/migrations/028_promote_trading_runtime_columns.sql @@ -0,0 +1,117 @@ +alter table if exists trade_plan_candidates + add column if not exists session_code text, + add column if not exists candidate_state text, + add column if not exists quality_score numeric(10,4), + add column if not exists entry_low numeric(20,10), + add column if not exists entry_high numeric(20,10), + add column if not exists stop_loss numeric(20,10), + add column if not exists tp1 numeric(20,10), + add column if not exists tp2 numeric(20,10), + add column if not exists rr_tp1 numeric(8,4), + add column if not exists rr_tp2 numeric(8,4), + add column if not exists expiry_ts timestamptz, + add column if not exists management_rules jsonb not null default '[]'::jsonb, + add column if not exists ai_explanation text; + +update trade_plan_candidates +set + session_code = coalesce(session_code, meta->>'session_code'), + candidate_state = coalesce(candidate_state, status), + quality_score = coalesce(quality_score, nullif(meta->>'quality_score', '')::numeric), + entry_low = coalesce(entry_low, nullif(entry->>'entry_low', '')::numeric), + entry_high = coalesce(entry_high, nullif(entry->>'entry_high', '')::numeric), + stop_loss = coalesce(stop_loss, nullif(entry->>'stop_loss', '')::numeric), + tp1 = coalesce(tp1, nullif(entry->>'tp1', '')::numeric), + tp2 = coalesce(tp2, nullif(entry->>'tp2', '')::numeric), + rr_tp1 = coalesce(rr_tp1, nullif(entry->>'rr_tp1', '')::numeric), + rr_tp2 = coalesce(rr_tp2, nullif(entry->>'rr_tp2', '')::numeric), + expiry_ts = coalesce(expiry_ts, expires_ts), + management_rules = case + when management_rules <> '[]'::jsonb then management_rules + when entry ? 'management_rules' then entry->'management_rules' + when meta ? 'management_rules' then meta->'management_rules' + else '[]'::jsonb + end, + ai_explanation = coalesce(ai_explanation, meta->>'ai_explanation', narrative) +where true; + +alter table if exists trade_signals + add column if not exists trading_plan_id bigint references trading_plans(id), + add column if not exists candidate_id bigint references trade_plan_candidates(id), + add column if not exists primary_draw jsonb not null default '{}'::jsonb, + add column if not exists target_plan jsonb not null default '{}'::jsonb, + add column if not exists risk_governor_decision jsonb not null default '{}'::jsonb, + add column if not exists confirmation_quality jsonb not null default '{}'::jsonb, + add column if not exists quality_score numeric(10,4), + add column if not exists ai_explanation text; + +update trade_signals +set + trading_plan_id = coalesce(trading_plan_id, nullif(meta->>'trading_plan_id', '')::bigint), + candidate_id = coalesce(candidate_id, nullif(meta->>'candidate_id', '')::bigint), + primary_draw = case + when primary_draw <> '{}'::jsonb then primary_draw + when meta ? 'primary_draw' then meta->'primary_draw' + else '{}'::jsonb + end, + target_plan = case + when target_plan <> '{}'::jsonb then target_plan + when meta ? 'target_plan' then meta->'target_plan' + else '{}'::jsonb + end, + risk_governor_decision = case + when risk_governor_decision <> '{}'::jsonb then risk_governor_decision + when meta ? 'risk_governor_decision' then meta->'risk_governor_decision' + else '{}'::jsonb + end, + confirmation_quality = case + when confirmation_quality <> '{}'::jsonb then confirmation_quality + when meta ? 'confirmation_quality' then meta->'confirmation_quality' + when meta ? 'evidence_quality' then meta->'evidence_quality' + else '{}'::jsonb + end, + quality_score = coalesce(quality_score, nullif(meta->>'quality_score', '')::numeric), + ai_explanation = coalesce(ai_explanation, meta->>'ai_explanation', meta->>'structure_quality') +where true; + +alter table if exists trade_executions + add column if not exists execution_ticket_id bigint references execution_tickets(id), + add column if not exists confirmation_required boolean not null default false, + add column if not exists confirmed_by text, + add column if not exists confirmed_at timestamptz, + add column if not exists broker_order_ids jsonb not null default '[]'::jsonb, + add column if not exists bracket_state jsonb not null default '{}'::jsonb, + add column if not exists fill_state text, + add column if not exists position_state text, + add column if not exists close_reason text, + add column if not exists reconciliation_state jsonb not null default '{}'::jsonb, + add column if not exists quality_score numeric(10,4), + add column if not exists ai_explanation text; + +update trade_executions +set + execution_ticket_id = coalesce(execution_ticket_id, nullif(meta->>'execution_ticket_id', '')::bigint), + confirmation_required = coalesce(confirmation_required, false) or coalesce((meta->>'confirmation_required')::boolean, (meta->>'requires_confirmation')::boolean, false), + confirmed_by = coalesce(confirmed_by, meta->>'confirmed_by'), + confirmed_at = coalesce(confirmed_at, nullif(meta->>'confirmed_at', '')::timestamptz), + broker_order_ids = case + when broker_order_ids <> '[]'::jsonb then broker_order_ids + when meta ? 'broker_order_ids' then meta->'broker_order_ids' + else '[]'::jsonb + end, + bracket_state = case + when bracket_state <> '{}'::jsonb then bracket_state + when meta ? 'bracket_state' then meta->'bracket_state' + else '{}'::jsonb + end, + fill_state = coalesce(fill_state, meta->>'fill_state'), + position_state = coalesce(position_state, meta->>'position_state'), + close_reason = coalesce(close_reason, meta->>'close_reason'), + reconciliation_state = case + when reconciliation_state <> '{}'::jsonb then reconciliation_state + when meta ? 'reconciliation_state' then meta->'reconciliation_state' + else '{}'::jsonb + end, + quality_score = coalesce(quality_score, nullif(meta->>'quality_score', '')::numeric), + ai_explanation = coalesce(ai_explanation, meta->>'ai_explanation') +where true; diff --git a/src/api/__init__.py b/src/api/__init__.py index efa3691..d7f28d0 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -29,6 +29,7 @@ from src.api.trading_runtime import ( run_candidate_confirm_check, run_trading_watch, sync_execution, + get_trading_executions, ) from src.api.trading_journal import get_recent_trading_journal, get_trading_journal_summary from src.api.trading_plans import get_recent_trading_plans @@ -46,6 +47,7 @@ __all__ = [ "get_trading_market_state", "get_trading_plan_by_id", "get_trading_ticket_by_id", + "get_trading_executions", "get_trading_journal_summary", "get_recent_trading_plans", "get_weekly_review", diff --git a/src/api/server.py b/src/api/server.py index fa9d5fe..e41bf11 100644 --- a/src/api/server.py +++ b/src/api/server.py @@ -28,6 +28,7 @@ from src.api.trading_runtime import ( get_trading_market_state, get_trading_plan_by_id, get_trading_ticket_by_id, + get_trading_executions, get_weekly_review, rerun_ticket_risk_check, run_candidate_confirm_check, @@ -73,6 +74,7 @@ def build_api_dependencies() -> dict: "trading_candidate_confirm_check": run_candidate_confirm_check, "trading_candidate_build_ticket": build_ticket_from_candidate, "trading_ticket_by_id": get_trading_ticket_by_id, + "trading_executions": get_trading_executions, "trading_ticket_risk_check": rerun_ticket_risk_check, "trading_ticket_confirm": confirm_ticket_only, "trading_ticket_execute": execute_ticket, @@ -238,6 +240,10 @@ def route_request( instrument_id = _require_int(query, "instrument_id") limit = _get_int(query, "limit", 20) return 200, {"items": deps["executions"](instrument_id=instrument_id, limit=limit)} + if method == "GET" and path == "/trading/executions": + instrument_id = _require_int(query, "instrument_id") + limit = _get_int(query, "limit", 20) + return 200, {"items": deps["trading_executions"](instrument_id=instrument_id, limit=limit)} if method == "GET" and path == "/trading/tickets": instrument_id = _get_optional_int(query, "instrument_id") signal_id = _get_optional_int(query, "signal_id") diff --git a/src/api/signals.py b/src/api/signals.py index f81f555..4f09ab0 100644 --- a/src/api/signals.py +++ b/src/api/signals.py @@ -7,8 +7,11 @@ from src.services.signals import normalize_signal_invalidations def get_recent_signals(instrument_id: int, limit: int = 20) -> list[dict]: sql = text( """ - select id, instrument_id, model_code, side, status, entry_type, entry_low, entry_high, stop_loss, tp1, tp2, - rr_tp1, rr_tp2, session_code, created_ts, expires_ts, invalidations, evidence, meta + select id, instrument_id, model_code, side, status, trading_plan_id, candidate_id, + entry_type, entry_low, entry_high, stop_loss, tp1, tp2, + rr_tp1, rr_tp2, session_code, created_ts, expires_ts, invalidations, + evidence, primary_draw, target_plan, risk_governor_decision, + confirmation_quality, quality_score, ai_explanation, meta from trade_signals where instrument_id = :instrument_id order by created_ts desc @@ -33,6 +36,8 @@ def normalize_signal_row(row: dict) -> dict: payload["fvg_fill_pct"] = float(meta.get("fvg_fill_pct") or 0.0) payload["fvg_touch_count"] = int(meta.get("fvg_touch_count") or 0) payload["fvg_data_quality_warnings"] = list(meta.get("fvg_data_quality_warnings") or []) + payload["quality_score"] = float(payload.get("quality_score") or meta.get("quality_score") or 0.0) + payload["ai_explanation"] = payload.get("ai_explanation") or meta.get("ai_explanation") payload["invalidations_normalized"] = normalize_signal_invalidations(invalidations) payload["signal_context"] = { "structure_state": payload["structure_state"], diff --git a/src/api/trade_plan_candidates.py b/src/api/trade_plan_candidates.py index 786e95b..9e38307 100644 --- a/src/api/trade_plan_candidates.py +++ b/src/api/trade_plan_candidates.py @@ -28,4 +28,10 @@ def normalize_trade_plan_candidate_row(row: dict) -> dict: payload["present_conditions"] = list(confirmation_state.get("present_conditions") or []) payload["rejected_conditions"] = list(confirmation_state.get("rejected_conditions") or []) payload["reason_codes"] = list(confirmation_state.get("reason_codes") or []) + payload["quality_score"] = float(payload.get("quality_score") or 0.0) + payload["ai_explanation"] = payload.get("ai_explanation") + payload["entry_zone"] = { + "entry_low": payload.get("entry_low"), + "entry_high": payload.get("entry_high"), + } return payload diff --git a/src/api/trading_runtime.py b/src/api/trading_runtime.py index b8a0acb..c1c2081 100644 --- a/src/api/trading_runtime.py +++ b/src/api/trading_runtime.py @@ -149,6 +149,10 @@ def get_trading_ticket_by_id(ticket_id: int) -> Optional[dict]: return dict(row) if row else None +def get_trading_executions(instrument_id: int, limit: int = 20) -> list[dict]: + return ExecutionService().build_execution_dashboard_items(instrument_id=instrument_id, limit=limit) + + def rerun_ticket_risk_check( *, ticket_id: int, diff --git a/src/models/trade_execution.py b/src/models/trade_execution.py index 458034d..ae890c0 100644 --- a/src/models/trade_execution.py +++ b/src/models/trade_execution.py @@ -1,5 +1,5 @@ from typing import Any, Optional, Union -from sqlalchemy import ForeignKey, Numeric, Text +from sqlalchemy import Boolean, ForeignKey, Numeric, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column @@ -11,6 +11,7 @@ class TradeExecution(Base): id: Mapped[int] = mapped_column(primary_key=True) signal_id: Mapped[int] = mapped_column(ForeignKey("trade_signals.id"), nullable=False) + execution_ticket_id: Mapped[Optional[int]] = mapped_column(ForeignKey("execution_tickets.id")) venue_order_id: Mapped[Optional[str]] = mapped_column(Text) execution_mode: Mapped[str] = mapped_column(Text, nullable=False) side: Mapped[str] = mapped_column(Text, nullable=False) @@ -21,7 +22,18 @@ class TradeExecution(Base): size: Mapped[Optional[float]] = mapped_column(Numeric(28, 10)) risk_pct: Mapped[Optional[float]] = mapped_column(Numeric(8, 4)) status: Mapped[str] = mapped_column(Text, nullable=False) + confirmation_required: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + confirmed_by: Mapped[Optional[str]] = mapped_column(Text) + confirmed_at: Mapped[Optional[object]] = mapped_column() opened_ts: Mapped[Optional[object]] = mapped_column() closed_ts: Mapped[Optional[object]] = mapped_column() pnl: Mapped[Optional[float]] = mapped_column(Numeric(28, 10)) + broker_order_ids: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) + bracket_state: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) + fill_state: Mapped[Optional[str]] = mapped_column(Text) + position_state: Mapped[Optional[str]] = mapped_column(Text) + close_reason: Mapped[Optional[str]] = mapped_column(Text) + reconciliation_state: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) + quality_score: Mapped[Optional[float]] = mapped_column(Numeric(10, 4)) + ai_explanation: Mapped[Optional[str]] = mapped_column(Text) meta: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) diff --git a/src/models/trade_plan_candidate.py b/src/models/trade_plan_candidate.py index 9d65790..7b4f0e8 100644 --- a/src/models/trade_plan_candidate.py +++ b/src/models/trade_plan_candidate.py @@ -1,6 +1,6 @@ from typing import Optional -from sqlalchemy import ForeignKey, Text +from sqlalchemy import ForeignKey, Numeric, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column @@ -16,14 +16,27 @@ class TradePlanCandidate(Base): model_code: Mapped[str] = mapped_column(Text, nullable=False) status: Mapped[str] = mapped_column(Text, nullable=False) side: Mapped[Optional[str]] = mapped_column(Text) + session_code: Mapped[Optional[str]] = mapped_column(Text) + candidate_state: Mapped[Optional[str]] = mapped_column(Text) setup_timeframe: Mapped[str] = mapped_column(Text, nullable=False) trigger_timeframe: Mapped[str] = mapped_column(Text, nullable=False) created_ts: Mapped[object] = mapped_column(nullable=False) expires_ts: Mapped[Optional[object]] = mapped_column() + expiry_ts: Mapped[Optional[object]] = mapped_column() + quality_score: Mapped[Optional[float]] = mapped_column(Numeric(10, 4)) confirmation_state: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) missing_confirmations: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) evidence: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) entry: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) + entry_low: Mapped[Optional[float]] = mapped_column(Numeric(20, 10)) + entry_high: Mapped[Optional[float]] = mapped_column(Numeric(20, 10)) + stop_loss: Mapped[Optional[float]] = mapped_column(Numeric(20, 10)) + tp1: Mapped[Optional[float]] = mapped_column(Numeric(20, 10)) + tp2: Mapped[Optional[float]] = mapped_column(Numeric(20, 10)) + rr_tp1: Mapped[Optional[float]] = mapped_column(Numeric(8, 4)) + rr_tp2: Mapped[Optional[float]] = mapped_column(Numeric(8, 4)) invalidations: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) + management_rules: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) + ai_explanation: Mapped[Optional[str]] = mapped_column(Text) narrative: Mapped[str] = mapped_column(Text, nullable=False) meta: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) diff --git a/src/models/trade_signal.py b/src/models/trade_signal.py index 01146b8..278c404 100644 --- a/src/models/trade_signal.py +++ b/src/models/trade_signal.py @@ -14,6 +14,8 @@ class TradeSignal(Base): model_code: Mapped[str] = mapped_column(Text, nullable=False) side: Mapped[str] = mapped_column(Text, nullable=False) status: Mapped[str] = mapped_column(Text, nullable=False) + trading_plan_id: Mapped[Optional[int]] = mapped_column(ForeignKey("trading_plans.id")) + candidate_id: Mapped[Optional[int]] = mapped_column(ForeignKey("trade_plan_candidates.id")) bias_snapshot_id: Mapped[Optional[int]] = mapped_column(ForeignKey("bias_snapshots.id")) entry_type: Mapped[str] = mapped_column(Text, nullable=False) entry_low: Mapped[Optional[float]] = mapped_column(Numeric(20, 10)) @@ -28,4 +30,10 @@ class TradeSignal(Base): expires_ts: Mapped[Optional[object]] = mapped_column() invalidations: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) evidence: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) + primary_draw: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) + target_plan: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) + risk_governor_decision: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) + confirmation_quality: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) + quality_score: Mapped[Optional[float]] = mapped_column(Numeric(10, 4)) + ai_explanation: Mapped[Optional[str]] = mapped_column(Text) meta: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) diff --git a/src/repositories/execution_repository.py b/src/repositories/execution_repository.py index e06b185..f5fa264 100644 --- a/src/repositories/execution_repository.py +++ b/src/repositories/execution_repository.py @@ -17,6 +17,8 @@ class ExecutionRepository: ts.model_code, ts.side, ts.status, + ts.trading_plan_id, + ts.candidate_id, ts.entry_type, ts.entry_low, ts.entry_high, @@ -30,6 +32,12 @@ class ExecutionRepository: ts.expires_ts, ts.invalidations, ts.evidence, + ts.primary_draw, + ts.target_plan, + ts.risk_governor_decision, + ts.confirmation_quality, + ts.quality_score, + ts.ai_explanation, ts.meta, bs.execution_intensity, bs.daily_bias, @@ -47,8 +55,10 @@ class ExecutionRepository: def fetch_latest_execution(self, signal_id: int, execution_mode: str) -> Optional[dict]: sql = text( """ - select id, signal_id, execution_mode, side, entry_price, stop_loss, take_profit_1, take_profit_2, - size, risk_pct, status, opened_ts, closed_ts, meta + select id, signal_id, execution_ticket_id, execution_mode, side, entry_price, stop_loss, take_profit_1, take_profit_2, + size, risk_pct, status, confirmation_required, confirmed_by, confirmed_at, + opened_ts, closed_ts, pnl, broker_order_ids, bracket_state, fill_state, + position_state, close_reason, reconciliation_state, quality_score, ai_explanation, meta from trade_executions where signal_id = :signal_id and execution_mode = :execution_mode @@ -66,8 +76,10 @@ class ExecutionRepository: def fetch_execution_by_id(self, execution_id: int) -> Optional[dict]: sql = text( """ - select id, signal_id, execution_mode, side, entry_price, stop_loss, take_profit_1, take_profit_2, - size, risk_pct, status, opened_ts, closed_ts, meta + select id, signal_id, execution_ticket_id, execution_mode, side, entry_price, stop_loss, take_profit_1, take_profit_2, + size, risk_pct, status, confirmation_required, confirmed_by, confirmed_at, + opened_ts, closed_ts, pnl, broker_order_ids, bracket_state, fill_state, + position_state, close_reason, reconciliation_state, quality_score, ai_explanation, meta from trade_executions where id = :execution_id limit 1 @@ -130,6 +142,7 @@ class ExecutionRepository: select te.id as execution_id, te.signal_id, + te.execution_ticket_id, ts.instrument_id, te.execution_mode, te.status as execution_status, @@ -140,6 +153,17 @@ class ExecutionRepository: te.take_profit_2, te.opened_ts, te.closed_ts, + te.confirmation_required, + te.confirmed_by, + te.confirmed_at, + te.broker_order_ids, + te.bracket_state, + te.fill_state, + te.position_state, + te.close_reason, + te.reconciliation_state, + te.quality_score, + te.ai_explanation, te.meta as execution_meta, ts.status as signal_status, ts.session_code, @@ -193,6 +217,30 @@ class ExecutionRepository: "meta": json.dumps(meta, default=str), }, ) + session.execute( + text( + """ + update trade_executions + set fill_state = :fill_state, + position_state = :position_state, + close_reason = :close_reason, + reconciliation_state = cast(:reconciliation_state as jsonb), + bracket_state = cast(:bracket_state as jsonb), + broker_order_ids = cast(:broker_order_ids as jsonb), + confirmed_by = :confirmed_by, + confirmed_at = :confirmed_at, + execution_ticket_id = :execution_ticket_id, + confirmation_required = :confirmation_required, + quality_score = :quality_score, + ai_explanation = :ai_explanation + where id = :execution_id + """ + ), + { + "execution_id": execution_id, + **_derive_execution_schema_fields(meta=meta), + }, + ) def insert_execution_ticket( self, @@ -443,10 +491,12 @@ class ExecutionRepository: opened_ts, meta: dict, ) -> int: + schema_fields = _derive_execution_schema_fields(meta=meta) sql = text( """ insert into trade_executions ( signal_id, + execution_ticket_id, venue_order_id, execution_mode, side, @@ -457,10 +507,22 @@ class ExecutionRepository: size, risk_pct, status, + confirmation_required, + confirmed_by, + confirmed_at, opened_ts, + broker_order_ids, + bracket_state, + fill_state, + position_state, + close_reason, + reconciliation_state, + quality_score, + ai_explanation, meta ) values ( :signal_id, + :execution_ticket_id, :venue_order_id, :execution_mode, :side, @@ -471,7 +533,18 @@ class ExecutionRepository: :size, :risk_pct, :status, + :confirmation_required, + :confirmed_by, + :confirmed_at, :opened_ts, + cast(:broker_order_ids as jsonb), + cast(:bracket_state as jsonb), + :fill_state, + :position_state, + :close_reason, + cast(:reconciliation_state as jsonb), + :quality_score, + :ai_explanation, cast(:meta as jsonb) ) returning id @@ -482,6 +555,7 @@ class ExecutionRepository: sql, { "signal_id": signal_id, + "execution_ticket_id": schema_fields["execution_ticket_id"], "venue_order_id": venue_order_id, "execution_mode": execution_mode, "side": side, @@ -492,8 +566,37 @@ class ExecutionRepository: "size": size, "risk_pct": risk_pct, "status": status, + "confirmation_required": schema_fields["confirmation_required"], + "confirmed_by": schema_fields["confirmed_by"], + "confirmed_at": schema_fields["confirmed_at"], "opened_ts": opened_ts, + "broker_order_ids": json.dumps(schema_fields["broker_order_ids"], default=str), + "bracket_state": json.dumps(schema_fields["bracket_state"], default=str), + "fill_state": schema_fields["fill_state"], + "position_state": schema_fields["position_state"], + "close_reason": schema_fields["close_reason"], + "reconciliation_state": json.dumps(schema_fields["reconciliation_state"], default=str), + "quality_score": schema_fields["quality_score"], + "ai_explanation": schema_fields["ai_explanation"], "meta": json.dumps(meta), }, ) return int(result.scalar_one()) + + +def _derive_execution_schema_fields(*, meta: dict) -> dict: + meta = dict(meta or {}) + return { + "execution_ticket_id": meta.get("execution_ticket_id"), + "confirmation_required": bool(meta.get("confirmation_required", meta.get("requires_confirmation", False))), + "confirmed_by": meta.get("confirmed_by"), + "confirmed_at": meta.get("confirmed_at"), + "broker_order_ids": list(meta.get("broker_order_ids") or []), + "bracket_state": dict(meta.get("bracket_state") or {}), + "fill_state": meta.get("fill_state"), + "position_state": meta.get("position_state"), + "close_reason": meta.get("close_reason"), + "reconciliation_state": dict(meta.get("reconciliation_state") or {}), + "quality_score": meta.get("quality_score"), + "ai_explanation": meta.get("ai_explanation"), + } diff --git a/src/repositories/signal_repository.py b/src/repositories/signal_repository.py index 63e0499..ebd5991 100644 --- a/src/repositories/signal_repository.py +++ b/src/repositories/signal_repository.py @@ -79,8 +79,9 @@ class SignalRepository: def fetch_latest_trade_signal(self, instrument_id: int, model_code: str) -> Optional[dict]: sql = text( """ - select id, side, status, bias_snapshot_id, entry_type, entry_low, entry_high, stop_loss, tp1, tp2, - rr_tp1, rr_tp2, session_code, invalidations, evidence, meta + select id, side, status, trading_plan_id, candidate_id, bias_snapshot_id, entry_type, entry_low, entry_high, stop_loss, tp1, tp2, + rr_tp1, rr_tp2, session_code, invalidations, evidence, primary_draw, target_plan, + risk_governor_decision, confirmation_quality, quality_score, ai_explanation, meta from trade_signals where instrument_id = :instrument_id and model_code = :model_code @@ -98,11 +99,13 @@ class SignalRepository: def fetch_latest_trade_signal_for_candidate(self, candidate_id: int) -> Optional[dict]: sql = text( """ - select id, instrument_id, model_code, side, status, bias_snapshot_id, entry_type, entry_low, entry_high, + select id, instrument_id, model_code, side, status, trading_plan_id, candidate_id, bias_snapshot_id, entry_type, entry_low, entry_high, stop_loss, tp1, tp2, rr_tp1, rr_tp2, session_code, created_ts, expires_ts, - invalidations, evidence, meta + invalidations, evidence, primary_draw, target_plan, risk_governor_decision, + confirmation_quality, quality_score, ai_explanation, meta from trade_signals - where cast(coalesce(meta, '{}'::jsonb)->>'candidate_id' as bigint) = :candidate_id + where candidate_id = :candidate_id + or cast(coalesce(meta, '{}'::jsonb)->>'candidate_id' as bigint) = :candidate_id order by created_ts desc, id desc limit 1 """ @@ -227,6 +230,8 @@ class SignalRepository: model_code: str, side: str, status: str, + trading_plan_id: Optional[int], + candidate_id: Optional[int], bias_snapshot_id: int, entry_type: str, entry_low: float, @@ -241,6 +246,12 @@ class SignalRepository: expires_ts, invalidations: list, evidence: dict, + primary_draw: dict, + target_plan: dict, + risk_governor_decision: dict, + confirmation_quality: dict, + quality_score: Optional[float], + ai_explanation: Optional[str], meta: dict, ) -> None: sql = text( @@ -250,6 +261,8 @@ class SignalRepository: model_code, side, status, + trading_plan_id, + candidate_id, bias_snapshot_id, entry_type, entry_low, @@ -264,12 +277,20 @@ class SignalRepository: expires_ts, invalidations, evidence, + primary_draw, + target_plan, + risk_governor_decision, + confirmation_quality, + quality_score, + ai_explanation, meta ) values ( :instrument_id, :model_code, :side, :status, + :trading_plan_id, + :candidate_id, :bias_snapshot_id, :entry_type, :entry_low, @@ -284,6 +305,12 @@ class SignalRepository: :expires_ts, cast(:invalidations as jsonb), cast(:evidence as jsonb), + cast(:primary_draw as jsonb), + cast(:target_plan as jsonb), + cast(:risk_governor_decision as jsonb), + cast(:confirmation_quality as jsonb), + :quality_score, + :ai_explanation, cast(:meta as jsonb) ) """ @@ -296,6 +323,8 @@ class SignalRepository: "model_code": model_code, "side": side, "status": status, + "trading_plan_id": trading_plan_id, + "candidate_id": candidate_id, "bias_snapshot_id": bias_snapshot_id, "entry_type": entry_type, "entry_low": entry_low, @@ -310,6 +339,12 @@ class SignalRepository: "expires_ts": expires_ts, "invalidations": json.dumps(invalidations), "evidence": json.dumps(evidence), + "primary_draw": json.dumps(primary_draw, default=str), + "target_plan": json.dumps(target_plan, default=str), + "risk_governor_decision": json.dumps(risk_governor_decision, default=str), + "confirmation_quality": json.dumps(confirmation_quality, default=str), + "quality_score": quality_score, + "ai_explanation": ai_explanation, "meta": json.dumps(meta), }, ) diff --git a/src/repositories/trade_plan_candidate_repository.py b/src/repositories/trade_plan_candidate_repository.py index 3b36bdc..9d7b160 100644 --- a/src/repositories/trade_plan_candidate_repository.py +++ b/src/repositories/trade_plan_candidate_repository.py @@ -182,9 +182,11 @@ class TradePlanCandidateRepository: def fetch_latest_candidate(self, trading_plan_id: int, model_code: str) -> Optional[dict]: sql = text( """ - select id, instrument_id, trading_plan_id, model_code, status, side, setup_timeframe, - trigger_timeframe, created_ts, expires_ts, confirmation_state, - missing_confirmations, evidence, entry, invalidations, narrative, meta + select id, instrument_id, trading_plan_id, model_code, status, side, session_code, + candidate_state, setup_timeframe, trigger_timeframe, created_ts, expires_ts, expiry_ts, + quality_score, confirmation_state, missing_confirmations, evidence, entry, + entry_low, entry_high, stop_loss, tp1, tp2, rr_tp1, rr_tp2, invalidations, + management_rules, ai_explanation, narrative, meta from trade_plan_candidates where trading_plan_id = :trading_plan_id and model_code = :model_code @@ -207,15 +209,28 @@ class TradePlanCandidateRepository: model_code: str, status: str, side: Optional[str], + session_code: Optional[str], + candidate_state: str, setup_timeframe: str, trigger_timeframe: str, created_ts, expires_ts, + expiry_ts, + quality_score: float, confirmation_state: dict, missing_confirmations: list, evidence: dict, entry: dict, + entry_low, + entry_high, + stop_loss, + tp1, + tp2, + rr_tp1, + rr_tp2, invalidations: list, + management_rules: list, + ai_explanation: str, narrative: str, meta: dict, ) -> int: @@ -227,15 +242,28 @@ class TradePlanCandidateRepository: model_code, status, side, + session_code, + candidate_state, setup_timeframe, trigger_timeframe, created_ts, expires_ts, + expiry_ts, + quality_score, confirmation_state, missing_confirmations, evidence, entry, + entry_low, + entry_high, + stop_loss, + tp1, + tp2, + rr_tp1, + rr_tp2, invalidations, + management_rules, + ai_explanation, narrative, meta ) values ( @@ -244,15 +272,28 @@ class TradePlanCandidateRepository: :model_code, :status, :side, + :session_code, + :candidate_state, :setup_timeframe, :trigger_timeframe, :created_ts, :expires_ts, + :expiry_ts, + :quality_score, cast(:confirmation_state as jsonb), cast(:missing_confirmations as jsonb), cast(:evidence as jsonb), cast(:entry as jsonb), + :entry_low, + :entry_high, + :stop_loss, + :tp1, + :tp2, + :rr_tp1, + :rr_tp2, cast(:invalidations as jsonb), + cast(:management_rules as jsonb), + :ai_explanation, :narrative, cast(:meta as jsonb) ) @@ -268,15 +309,28 @@ class TradePlanCandidateRepository: "model_code": model_code, "status": status, "side": side, + "session_code": session_code, + "candidate_state": candidate_state, "setup_timeframe": setup_timeframe, "trigger_timeframe": trigger_timeframe, "created_ts": created_ts, "expires_ts": expires_ts, + "expiry_ts": expiry_ts, + "quality_score": quality_score, "confirmation_state": json.dumps(confirmation_state, default=str), "missing_confirmations": json.dumps(missing_confirmations, default=str), "evidence": json.dumps(evidence, default=str), "entry": json.dumps(entry, default=str), + "entry_low": entry_low, + "entry_high": entry_high, + "stop_loss": stop_loss, + "tp1": tp1, + "tp2": tp2, + "rr_tp1": rr_tp1, + "rr_tp2": rr_tp2, "invalidations": json.dumps(invalidations, default=str), + "management_rules": json.dumps(management_rules, default=str), + "ai_explanation": ai_explanation, "narrative": narrative, "meta": json.dumps(meta, default=str), }, @@ -286,9 +340,11 @@ class TradePlanCandidateRepository: def fetch_recent_candidates(self, instrument_id: int, limit: int = 20) -> list[dict]: sql = text( """ - select id, instrument_id, trading_plan_id, model_code, status, side, setup_timeframe, - trigger_timeframe, created_ts, expires_ts, confirmation_state, - missing_confirmations, evidence, entry, invalidations, narrative, meta + select id, instrument_id, trading_plan_id, model_code, status, side, session_code, + candidate_state, setup_timeframe, trigger_timeframe, created_ts, expires_ts, expiry_ts, + quality_score, confirmation_state, missing_confirmations, evidence, entry, + entry_low, entry_high, stop_loss, tp1, tp2, rr_tp1, rr_tp2, invalidations, + management_rules, ai_explanation, narrative, meta from trade_plan_candidates where instrument_id = :instrument_id order by created_ts desc, id desc @@ -302,9 +358,11 @@ class TradePlanCandidateRepository: def fetch_candidate_by_id(self, candidate_id: int) -> Optional[dict]: sql = text( """ - select id, instrument_id, trading_plan_id, model_code, status, side, setup_timeframe, - trigger_timeframe, created_ts, expires_ts, confirmation_state, - missing_confirmations, evidence, entry, invalidations, narrative, meta + select id, instrument_id, trading_plan_id, model_code, status, side, session_code, + candidate_state, setup_timeframe, trigger_timeframe, created_ts, expires_ts, expiry_ts, + quality_score, confirmation_state, missing_confirmations, evidence, entry, + entry_low, entry_high, stop_loss, tp1, tp2, rr_tp1, rr_tp2, invalidations, + management_rules, ai_explanation, narrative, meta from trade_plan_candidates where id = :candidate_id limit 1 diff --git a/src/services/execution/execution_service.py b/src/services/execution/execution_service.py index 4ea9939..bf2f4cc 100644 --- a/src/services/execution/execution_service.py +++ b/src/services/execution/execution_service.py @@ -791,6 +791,10 @@ def evaluate_execution_decision( status = "pending" if allowed else "rejected" meta["requires_confirmation"] = requires_confirmation + quality_score = calculate_execution_quality_score(allowed=allowed, reasons=reasons) + ai_explanation = build_execution_ai_explanation(execution_mode=execution_mode, reasons=reasons, allowed=allowed) + meta["quality_score"] = quality_score + meta["ai_explanation"] = ai_explanation return ExecutionDecision( allowed=allowed, @@ -805,6 +809,18 @@ def evaluate_execution_decision( ) +def calculate_execution_quality_score(*, allowed: bool, reasons: list[str]) -> float: + if allowed: + return 100.0 + return max(0.0, 100.0 - (len(reasons) * 12.5)) + + +def build_execution_ai_explanation(*, execution_mode: str, reasons: list[str], allowed: bool) -> str: + if allowed: + return f"{execution_mode}:allowed" + return f"{execution_mode}:blocked:{','.join(reasons)}" + + def signal_requires_structural_targets(signal: dict) -> bool: meta = dict(signal.get("meta") or {}) return bool(signal.get("model_code")) or bool(meta.get("target_plan_required")) diff --git a/src/services/signals/signal_service.py b/src/services/signals/signal_service.py index e9161c0..7192878 100644 --- a/src/services/signals/signal_service.py +++ b/src/services/signals/signal_service.py @@ -42,6 +42,14 @@ class SignalCandidate: invalidations: list[str] evidence: dict meta: dict + trading_plan_id: Optional[int] = None + candidate_id: Optional[int] = None + quality_score: float = 0.0 + ai_explanation: str = "" + primary_draw: dict = None + target_plan: dict = None + confirmation_quality: dict = None + risk_governor_decision: dict = None class SignalService: @@ -124,9 +132,25 @@ class SignalService: evidence_quality = validate_signal_evidence(evidence, required_keys=self.required_evidence_keys) if not evidence_quality['is_complete']: return None + quality_score = calculate_signal_quality_score( + structure_alignment=structure_alignment, + evidence_quality=evidence_quality, + rr_decision=rr_decision, + session_decision=session_decision, + target_plan=target_plan_payload, + ) + ai_explanation = build_signal_ai_explanation( + structure_alignment=structure_alignment, + evidence_quality=evidence_quality, + rr_decision=rr_decision, + session_decision=session_decision, + target_plan=target_plan_payload, + ) return SignalCandidate( side=side, status=session_decision['status'], + trading_plan_id=None, + candidate_id=None, entry_low=entry_low, entry_high=entry_high, stop_loss=stop_loss, @@ -139,6 +163,12 @@ class SignalService: expires_ts=expires_ts, invalidations=invalidations, evidence=evidence, + quality_score=quality_score, + ai_explanation=ai_explanation, + primary_draw={}, + target_plan=target_plan_payload, + confirmation_quality=evidence_quality, + risk_governor_decision={}, meta={ 'session_filter': session_decision['reason'], 'execution_permission': session_decision['execution_permission'], @@ -165,6 +195,8 @@ class SignalService: 'target_selection_status': target_plan_payload['status'], 'risk_governor_decision': None, 'confirmation_quality': evidence_quality, + 'quality_score': quality_score, + 'ai_explanation': ai_explanation, }, ) @@ -182,6 +214,8 @@ class SignalService: model_code='SWEEP_MSS_FVG', side=candidate.side, status=candidate.status, + trading_plan_id=candidate.trading_plan_id, + candidate_id=candidate.candidate_id, bias_snapshot_id=bias['id'], entry_type='limit', entry_low=candidate.entry_low, @@ -196,6 +230,12 @@ class SignalService: expires_ts=candidate.expires_ts, invalidations=candidate.invalidations, evidence=candidate.evidence, + primary_draw=candidate.primary_draw, + target_plan=candidate.target_plan, + risk_governor_decision=candidate.risk_governor_decision, + confirmation_quality=candidate.confirmation_quality, + quality_score=candidate.quality_score, + ai_explanation=candidate.ai_explanation, meta=candidate.meta, ) return 1 @@ -533,3 +573,42 @@ def classify_session_for_signal(session_code: Optional[str]) -> dict[str, object 'execution_permission': False, 'reason': 'off_hours', } + + +def calculate_signal_quality_score( + *, + structure_alignment: dict[str, object], + evidence_quality: dict[str, object], + rr_decision: dict[str, object], + session_decision: dict[str, object], + target_plan: dict, +) -> float: + score = 0.0 + if structure_alignment.get('is_actionable'): + score += 35.0 + if evidence_quality.get('is_complete'): + score += 20.0 + if rr_decision.get('is_actionable'): + score += 20.0 + if session_decision.get('execution_permission'): + score += 10.0 + if target_plan.get('status') == 'ready': + score += 15.0 + return max(0.0, min(100.0, round(score, 4))) + + +def build_signal_ai_explanation( + *, + structure_alignment: dict[str, object], + evidence_quality: dict[str, object], + rr_decision: dict[str, object], + session_decision: dict[str, object], + target_plan: dict, +) -> str: + return ( + f"{structure_alignment.get('reason')}|" + f"evidence={'complete' if evidence_quality.get('is_complete') else 'missing'}|" + f"rr={rr_decision.get('reason')}|" + f"session={session_decision.get('reason')}|" + f"targets={target_plan.get('status')}" + ) diff --git a/src/services/trade_plan_candidates/setup_watcher_service.py b/src/services/trade_plan_candidates/setup_watcher_service.py index e2e1765..ac37a78 100644 --- a/src/services/trade_plan_candidates/setup_watcher_service.py +++ b/src/services/trade_plan_candidates/setup_watcher_service.py @@ -22,15 +22,28 @@ class TradePlanCandidateDraft: model_code: str status: str side: Optional[str] + session_code: Optional[str] + candidate_state: str setup_timeframe: str trigger_timeframe: str created_ts: object expires_ts: Optional[object] + expiry_ts: Optional[object] + quality_score: float confirmation_state: dict missing_confirmations: list[str] evidence: dict entry: dict + entry_low: Optional[float] + entry_high: Optional[float] + stop_loss: Optional[float] + tp1: Optional[float] + tp2: Optional[float] + rr_tp1: Optional[float] + rr_tp2: Optional[float] invalidations: list + management_rules: list[dict] + ai_explanation: str narrative: str meta: dict @@ -41,15 +54,28 @@ class TradePlanCandidateDraft: "model_code": self.model_code, "status": self.status, "side": self.side, + "session_code": self.session_code, + "candidate_state": self.candidate_state, "setup_timeframe": self.setup_timeframe, "trigger_timeframe": self.trigger_timeframe, "created_ts": self.created_ts, "expires_ts": self.expires_ts, + "expiry_ts": self.expiry_ts, + "quality_score": self.quality_score, "confirmation_state": self.confirmation_state, "missing_confirmations": self.missing_confirmations, "evidence": self.evidence, "entry": self.entry, + "entry_low": self.entry_low, + "entry_high": self.entry_high, + "stop_loss": self.stop_loss, + "tp1": self.tp1, + "tp2": self.tp2, + "rr_tp1": self.rr_tp1, + "rr_tp2": self.rr_tp2, "invalidations": self.invalidations, + "management_rules": self.management_rules, + "ai_explanation": self.ai_explanation, "narrative": self.narrative, "meta": self.meta, } @@ -184,21 +210,54 @@ def build_trade_plan_candidate_from_inputs( confirmation_state["executable"] = confirmation.executable and has_complete_structural_target_plan(target_plan) confirmation_state["missing_conditions"] = missing_confirmations confirmation_state["reason_codes"] = reason_codes + quality_score = calculate_candidate_quality_score( + confirmation_state=confirmation_state, + target_plan=target_plan or {}, + status=status, + ) + ai_explanation = build_candidate_ai_explanation( + status=status, + reason_codes=reason_codes, + missing_confirmations=missing_confirmations, + target_plan=target_plan or {}, + ) + entry_low = _float_or_none(entry.get("entry_low")) + entry_high = _float_or_none(entry.get("entry_high")) + stop_loss = _float_or_none(entry.get("stop_loss")) + tp1 = _float_or_none(entry.get("tp1")) + tp2 = _float_or_none(entry.get("tp2")) + rr_tp1 = calculate_rr(entry_low=entry_low, entry_high=entry_high, stop_loss=stop_loss, target_price=tp1) + rr_tp2 = calculate_rr(entry_low=entry_low, entry_high=entry_high, stop_loss=stop_loss, target_price=tp2) + session_code = next(iter(trading_plan.get("allowed_sessions") or []), None) + management_rules = list((target_plan or {}).get("management_rules") or []) return TradePlanCandidateDraft( instrument_id=int(trading_plan["instrument_id"]), trading_plan_id=int(trading_plan["id"]), model_code=MODEL_OSOK_DAILY_RANGE_SWEEP_MSS_FVG_RETRACE, status=status, side=side, + session_code=session_code, + candidate_state=status, setup_timeframe=setup_tf, trigger_timeframe=trigger_tf, created_ts=created_ts, expires_ts=expires_ts, + expiry_ts=expires_ts, + quality_score=quality_score, confirmation_state=confirmation_state, missing_confirmations=missing_confirmations, evidence=evidence, entry=entry, + entry_low=entry_low, + entry_high=entry_high, + stop_loss=stop_loss, + tp1=tp1, + tp2=tp2, + rr_tp1=rr_tp1, + rr_tp2=rr_tp2, invalidations=invalidations, + management_rules=management_rules, + ai_explanation=ai_explanation, narrative=build_candidate_narrative( trading_plan_id=trading_plan.get("id"), side=side, @@ -210,8 +269,10 @@ def build_trade_plan_candidate_from_inputs( "executable": confirmation.executable and has_complete_structural_target_plan(target_plan), "reason_codes": reason_codes, "rejected_conditions": rejected_conditions, + "quality_score": quality_score, + "ai_explanation": ai_explanation, "target_plan": target_plan or {}, - "management_rules": list((target_plan or {}).get("management_rules") or []), + "management_rules": management_rules, }, ) @@ -268,6 +329,18 @@ def build_candidate_entry( if target_plan: entry["target_plan"] = target_plan entry["management_rules"] = list(target_plan.get("management_rules") or []) + entry["rr_tp1"] = calculate_rr( + entry_low=entry_low, + entry_high=entry_high, + stop_loss=entry.get("stop_loss"), + target_price=entry.get("tp1"), + ) + entry["rr_tp2"] = calculate_rr( + entry_low=entry_low, + entry_high=entry_high, + stop_loss=entry.get("stop_loss"), + target_price=entry.get("tp2"), + ) return entry @@ -328,6 +401,42 @@ def _float_or_none(value) -> Optional[float]: return parsed +def calculate_rr(*, entry_low, entry_high, stop_loss, target_price) -> Optional[float]: + entry_low = _float_or_none(entry_low) + entry_high = _float_or_none(entry_high) + stop_loss = _float_or_none(stop_loss) + target_price = _float_or_none(target_price) + if None in {entry_low, entry_high, stop_loss, target_price}: + return None + entry_price = (entry_low + entry_high) / 2 + risk_distance = abs(entry_price - stop_loss) + if risk_distance <= 0: + return None + return abs(target_price - entry_price) / risk_distance + + +def calculate_candidate_quality_score(*, confirmation_state: dict, target_plan: dict, status: str) -> float: + required = list(confirmation_state.get("required_conditions") or []) + present = list(confirmation_state.get("present_conditions") or []) + missing = list(confirmation_state.get("missing_conditions") or []) + rejected = list(confirmation_state.get("rejected_conditions") or []) + base = (len(present) / len(required) * 100.0) if required else 0.0 + if target_plan.get("status") != "ready": + base -= 20.0 + base -= len(missing) * 10.0 + base -= len(rejected) * 15.0 + if status == "executable": + base += 10.0 + return max(0.0, min(100.0, round(base, 4))) + + +def build_candidate_ai_explanation(*, status: str, reason_codes: list[str], missing_confirmations: list[str], target_plan: dict) -> str: + missing = ",".join(missing_confirmations) if missing_confirmations else "none" + reasons = ",".join(reason_codes) if reason_codes else "aligned" + target_state = target_plan.get("status") or "unknown" + return f"{status}:{target_state}:missing={missing}:reasons={reasons}" + + def _dedupe(values: list[str]) -> list[str]: seen = set() result = [] diff --git a/src/web/app.js b/src/web/app.js index 2c25b02..a06cf55 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -134,6 +134,7 @@ function clearNode(node) { let selectedDetailKey = null; function renderTable(table, columns, rows, onRowClick, rowKey) { + if (!table) return; clearNode(table); const thead = document.createElement('thead'); @@ -323,7 +324,7 @@ async function runTradingAction(url, body = {}) { } } -function renderMarketStatePanel(payload) { +function renderMarketStatePanel(payload, executionItems = []) { const state = payload?.market_state || {}; const freshness = state?.freshness || {}; const session = state?.session || {}; @@ -334,7 +335,7 @@ function renderMarketStatePanel(payload) { ['data freshness', freshness.code || 'unknown'], ['news state', news.code || 'unknown'], ['account risk state', payload?.backtest_quality_gate?.passed === false ? 'gate_blocked' : 'nominal'], - ['open position', executionsTable?.querySelectorAll('tbody tr')?.length || 0], + ['open position', executionItems.filter((item) => item.execution_status === 'open').length], ['kill switch', payload?.risk_decision?.reject_codes?.includes('execution_kill_switch_enabled') ? 'active' : 'off'], ], 'No market state'); } @@ -551,73 +552,24 @@ function cardDetailHandler(titleBuilder, keyValue, payload) { }; } -function renderDashboardData(health, candles, signals, sessions, executions, backtests, tradingPlan, marketState, tradeCandidates, executionTickets, journalSummary, journalItems, liveAudits) { - const signalItems = applySignalFilters(signals.items || []); - const sessionItems = applyTextFilters(sessions.items || [], ['id', 'session_code', 'status', 'session_date']); - const executionItems = applyExecutionFilters(executions.items || []); - const backtestItems = applyTextFilters(backtests.items || [], ['id']); +function renderDashboardData(health, candles, tradingPlan, marketState, tradeCandidates, executionTickets, tradingExecutions, journalSummary, journalItems, weeklyReview) { + const candidateItems = tradeCandidates.items || []; + const executionItems = applyExecutionFilters(tradingExecutions.items || []); renderHealth(health); updateActiveFilters(); - renderOverview(signalItems, sessionItems, executionItems, backtestItems); - renderMarketStatePanel(marketState); + renderOverview(candidateItems, executionItems, journalSummary, weeklyReview); + renderMarketStatePanel(marketState, executionItems); renderPreMarketPlanPanel(tradingPlan); - renderTradeCandidatesPanel(tradeCandidates.items || []); + renderTradeCandidatesPanel(candidateItems); renderExecutionTicketsPanel(executionTickets.items || []); - renderPositionManagementPanel(executionItems, liveAudits.items || []); + renderPositionManagementPanel(executionItems, []); renderPostTradeReviewPanel(journalSummary, journalItems.items || []); - renderTable(signalsTable, [ - tableColumn('id', 'ID'), - tableColumn('side', 'Side'), - statusColumn('status', 'Status'), - statusColumn('structure_state', 'Structure'), - tableColumn('mss_status_reason', 'MSS Reason'), - tableColumn('fvg_status_reason', 'FVG Reason'), - { - key: 'warnings', - label: 'Warnings', - render: (row) => formatListForCell(getSignalDataQualityWarnings(row)), - }, - { - key: 'invalidations', - label: 'Invalidations', - render: (row) => formatListForCell(getNormalizedSignalInvalidations(row)), - }, - tableColumn('entry_low', 'Entry Low'), - tableColumn('entry_high', 'Entry High'), - tableColumn('session_code', 'Session'), - tableColumn('created_ts', 'Created'), - ], signalItems, (row) => { - const structureState = getSignalStructureState(row); - setSelectedDetail(`Signal #${row.id} [${structureState}]`, buildSignalDetailPayload(row), row.id); - refreshDashboard(); - }, 'id'); - renderTable(sessionsTable, [ - tableColumn('id', 'ID'), - tableColumn('session_code', 'Session'), - tableColumn('session_date', 'Date'), - statusColumn('status', 'Status'), - tableColumn('high', 'High'), - tableColumn('low', 'Low'), - tableColumn('midpoint', 'Mid'), - ], sessionItems, rowDetailHandler((row) => `Session #${row.id}`, 'id'), 'id'); - renderTable(executionsTable, [ - tableColumn('execution_id', 'Execution ID'), - tableColumn('signal_id', 'Signal ID'), - statusColumn('execution_status', 'Status'), - statusColumn('lifecycle_state', 'Lifecycle'), - tableColumn('side', 'Side'), - tableColumn('session_code', 'Session'), - statusColumn('backtest_outcome', 'Outcome'), - statusColumn('audit_status', 'Audit'), - tableColumn('opened_ts', 'Opened'), - tableColumn('closed_ts', 'Closed'), - ], executionItems, rowDetailHandler((row) => `Execution #${row.execution_id}`, 'execution_id'), 'execution_id'); - renderBacktests(backtestItems); drawCandles(candles.items || []); } function renderBacktests(items) { + if (!backtestsCards) return; clearNode(backtestsCards); items.forEach((item) => { @@ -700,24 +652,23 @@ function renderHealth(health) { healthStatus.append(app, db, tz); } -function renderOverview(signals, sessions, executions, backtests) { +function renderOverview(candidates, executions, journalSummary, weeklyReview) { + if (!overviewCards) return; clearNode(overviewCards); - const signalCount = signals.length; - const openSignals = signals.filter((item) => item.status === 'awaiting_confirmation').length; - const sessionCount = sessions.length; + const candidateCount = candidates.length; + const executableCandidates = candidates.filter((item) => item.executable).length; const openExecutions = executions.filter((item) => item.execution_status === 'open').length; const closedExecutions = executions.filter((item) => item.execution_status === 'closed').length; - const latestBacktest = backtests[0]?.summary || {}; const cards = [ - { label: 'Signals', value: signalCount }, - { label: 'Actionable Signals', value: openSignals }, - { label: 'Sessions', value: sessionCount }, + { label: 'Candidates', value: candidateCount }, + { label: 'Executable Candidates', value: executableCandidates }, { label: 'Open Executions', value: openExecutions }, { label: 'Closed Executions', value: closedExecutions }, - { label: 'Latest Win Rate', value: `${((latestBacktest.win_rate ?? 0) * 100).toFixed(1)}%` }, - { label: 'Latest Executed', value: latestBacktest.executed_signals ?? 0 }, - { label: 'Quality Gate', value: formatQualityGate(latestBacktest) }, + { label: 'Framework Rate', value: `${(((journalSummary?.framework_execution_rate ?? weeklyReview?.framework_execution_rate) ?? 0) * 100).toFixed(1)}%` }, + { label: 'Risk Violations', value: journalSummary?.risk_violation_count ?? weeklyReview?.risk_violation_count ?? 0 }, + { label: 'High Session', value: weeklyReview?.high_quality_session?.session_code || 'n/a' }, + { label: 'Low Session', value: weeklyReview?.low_quality_session?.session_code || 'n/a' }, ]; cards.forEach((item) => { @@ -738,11 +689,13 @@ function renderOverview(signals, sessions, executions, backtests) { } function showDetail(title, payload) { + if (!detailMeta || !detailPanel) return; setText(detailMeta, title); setText(detailPanel, JSON.stringify(payload, null, 2)); } function drawCandles(items) { + if (!canvas || !ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); if (!items.length) { ctx.fillStyle = '#9db0d3'; @@ -1297,23 +1250,20 @@ async function refreshDashboard() { const limit = limitInput.value || '20'; try { - const [health, candles, signals, sessions, executions, backtests, tradingPlan, marketState, tradeCandidates, executionTickets, journalSummary, journalItems, liveAudits] = await Promise.all([ + const [health, candles, tradingPlan, marketState, tradeCandidates, executionTickets, tradingExecutions, journalSummary, journalItems, weeklyReview] = await Promise.all([ fetchJson('/health'), fetchJson(`/candles?instrument_id=${instrumentId}&timeframe=${encodeURIComponent(timeframe)}&limit=${limit}`), - fetchJson(`/signals?instrument_id=${instrumentId}&limit=${limit}`), - fetchJson(`/sessions?instrument_id=${instrumentId}&limit=${limit}`), - fetchJson(`/executions?instrument_id=${instrumentId}&limit=${limit}`), - fetchJson(`/backtests?instrument_id=${instrumentId}&limit=${Math.min(Number(limit), 10)}`), fetchJson(`/trading/plans/latest?instrument_id=${instrumentId}`).catch(() => null), fetchJson(`/trading/market-state?instrument_id=${instrumentId}&timeframe=${encodeURIComponent(timeframe)}`).catch(() => ({ market_state: {} })), fetchJson(`/trading/candidates?instrument_id=${instrumentId}&limit=${limit}`).catch(() => ({ items: [] })), fetchJson(`/trading/tickets?instrument_id=${instrumentId}&limit=${limit}`).catch(() => ({ items: [] })), + fetchJson(`/trading/executions?instrument_id=${instrumentId}&limit=${limit}`).catch(() => ({ items: [] })), fetchJson(`/trading/journal/summary?instrument_id=${instrumentId}`).catch(() => ({ entry_count: 0, next_week_actions: [] })), fetchJson(`/trading/journal?instrument_id=${instrumentId}&limit=${limit}`).catch(() => ({ items: [] })), - fetchJson(`/live-operation-audits?instrument_id=${instrumentId}&limit=${limit}`).catch(() => ({ items: [] })), + fetchJson(`/trading/research/weekly-review?instrument_id=${instrumentId}`).catch(() => ({ entry_count: 0, next_week_actions: [] })), ]); - renderDashboardData(health, candles, signals, sessions, executions, backtests, tradingPlan, marketState, tradeCandidates, executionTickets, journalSummary, journalItems, liveAudits); + renderDashboardData(health, candles, tradingPlan, marketState, tradeCandidates, executionTickets, tradingExecutions, journalSummary, journalItems, weeklyReview); updateLastUpdated(); } catch (error) { renderLoadError(error); diff --git a/src/web/index.html b/src/web/index.html index ceae3a8..3ddbc17 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -3,13 +3,13 @@ - AI ICT Dashboard + AI ICT Trading Workbench
-

AI ICT Dashboard

+

AI ICT Trading Workbench

最小可视化看板

Loading...
@@ -93,7 +93,7 @@
-
+ -
+ -
+ -
+ -
+ -
+