Finalize trading doc alignment

This commit is contained in:
Codex
2026-05-06 21:11:46 +08:00
parent 241990af84
commit ad5f4802ba
18 changed files with 641 additions and 108 deletions
@@ -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;
+2
View File
@@ -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",
+6
View File
@@ -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")
+7 -2
View File
@@ -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"],
+6
View File
@@ -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
+4
View File
@@ -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,
+13 -1
View File
@@ -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)
+14 -1
View File
@@ -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)
+8
View File
@@ -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)
+107 -4
View File
@@ -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"),
}
+40 -5
View File
@@ -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),
},
)
@@ -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
@@ -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"))
+79
View File
@@ -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')}"
)
@@ -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 = []
+27 -77
View File
@@ -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);
+8 -8
View File
@@ -3,13 +3,13 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI ICT Dashboard</title>
<title>AI ICT Trading Workbench</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<header class="topbar">
<div>
<h1>AI ICT Dashboard</h1>
<h1>AI ICT Trading Workbench</h1>
<p>最小可视化看板</p>
</div>
<div id="healthStatus" class="health-card">Loading...</div>
@@ -93,7 +93,7 @@
<div id="postTradeReviewPanel" class="detail-panel"></div>
</section>
<section id="trainingSection" class="panel panel-wide training-panel">
<section id="trainingSection" class="panel panel-wide training-panel" hidden>
<h2>Training / 训练</h2>
<nav class="training-nav" aria-label="Training dashboard sections">
<a href="#trainingTransferPanel">Import / Export</a>
@@ -164,27 +164,27 @@
<canvas id="candlesChart" width="960" height="260"></canvas>
</section>
<section class="panel">
<section class="panel" hidden>
<h2>Signals</h2>
<div class="table-wrap"><table id="signalsTable"></table></div>
</section>
<section class="panel">
<section class="panel" hidden>
<h2>Sessions</h2>
<div class="table-wrap"><table id="sessionsTable"></table></div>
</section>
<section class="panel panel-wide">
<section class="panel panel-wide" hidden>
<h2>Executions</h2>
<div class="table-wrap"><table id="executionsTable"></table></div>
</section>
<section class="panel panel-wide">
<section class="panel panel-wide" hidden>
<h2>Backtests</h2>
<div id="backtestsCards" class="cards"></div>
</section>
<section class="panel panel-wide">
<section class="panel panel-wide" hidden>
<h2>Details</h2>
<div id="detailMeta" class="metric">点击任一表格行或回测卡片查看详情</div>
<pre id="detailPanel" class="detail-panel">暂无数据</pre>
+10
View File
@@ -174,6 +174,16 @@ class ApiServerRoutingTests(unittest.TestCase):
self.assertEqual(status_weekly, 200)
self.assertEqual(payload_weekly["entry_count"], 2)
def test_routes_trading_executions_query(self) -> None:
status, payload = route_request(
"/trading/executions",
{"instrument_id": ["1"], "limit": ["2"]},
dependencies={"trading_executions": lambda **kwargs: [{"execution_id": 1, "execution_status": "open"}]},
)
self.assertEqual(status, 200)
self.assertEqual(payload["items"], [{"execution_id": 1, "execution_status": "open"}])
def test_routes_trading_journal_generate(self) -> None:
status, payload = route_request(
"/trading/journal/generate",