Finalize trading doc alignment
This commit is contained in:
@@ -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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user