Audit doc alignment trading workbench
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
create table if not exists market_states (
|
||||||
|
id bigserial primary key,
|
||||||
|
instrument_id bigint not null references instruments(id),
|
||||||
|
snapshot_ts timestamptz not null,
|
||||||
|
timeframe text not null,
|
||||||
|
session_code text,
|
||||||
|
data_freshness text,
|
||||||
|
candle_age_seconds numeric(18,4),
|
||||||
|
news_window_state jsonb not null default '{}'::jsonb,
|
||||||
|
opening_volatility_state jsonb not null default '{}'::jsonb,
|
||||||
|
account_state jsonb not null default '{}'::jsonb,
|
||||||
|
position_state jsonb not null default '{}'::jsonb,
|
||||||
|
meta jsonb not null default '{}'::jsonb,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create index if not exists idx_market_states_instr_snapshot_desc
|
||||||
|
on market_states (instrument_id, snapshot_ts desc);
|
||||||
@@ -9,6 +9,26 @@ from src.api.server import route_request, serve_api
|
|||||||
from src.api.sessions import get_recent_sessions
|
from src.api.sessions import get_recent_sessions
|
||||||
from src.api.signals import get_recent_signals
|
from src.api.signals import get_recent_signals
|
||||||
from src.api.trade_plan_candidates import get_recent_trade_plan_candidates
|
from src.api.trade_plan_candidates import get_recent_trade_plan_candidates
|
||||||
|
from src.api.trading_runtime import (
|
||||||
|
activate_trading_plan,
|
||||||
|
build_ticket_from_candidate,
|
||||||
|
cancel_execution,
|
||||||
|
cancel_trading_plan,
|
||||||
|
confirm_ticket_only,
|
||||||
|
execute_ticket,
|
||||||
|
generate_trading_journal,
|
||||||
|
generate_trading_plan,
|
||||||
|
get_latest_trading_plan,
|
||||||
|
get_trading_candidate_by_id,
|
||||||
|
get_trading_market_state,
|
||||||
|
get_trading_plan_by_id,
|
||||||
|
get_trading_ticket_by_id,
|
||||||
|
get_weekly_review,
|
||||||
|
rerun_ticket_risk_check,
|
||||||
|
run_candidate_confirm_check,
|
||||||
|
run_trading_watch,
|
||||||
|
sync_execution,
|
||||||
|
)
|
||||||
from src.api.trading_journal import get_recent_trading_journal, get_trading_journal_summary
|
from src.api.trading_journal import get_recent_trading_journal, get_trading_journal_summary
|
||||||
from src.api.trading_plans import get_recent_trading_plans
|
from src.api.trading_plans import get_recent_trading_plans
|
||||||
|
|
||||||
@@ -21,8 +41,26 @@ __all__ = [
|
|||||||
"get_recent_live_operation_audits",
|
"get_recent_live_operation_audits",
|
||||||
"get_recent_trade_plan_candidates",
|
"get_recent_trade_plan_candidates",
|
||||||
"get_recent_trading_journal",
|
"get_recent_trading_journal",
|
||||||
|
"get_latest_trading_plan",
|
||||||
|
"get_trading_market_state",
|
||||||
|
"get_trading_plan_by_id",
|
||||||
|
"get_trading_ticket_by_id",
|
||||||
"get_trading_journal_summary",
|
"get_trading_journal_summary",
|
||||||
"get_recent_trading_plans",
|
"get_recent_trading_plans",
|
||||||
|
"get_weekly_review",
|
||||||
|
"generate_trading_journal",
|
||||||
|
"generate_trading_plan",
|
||||||
|
"activate_trading_plan",
|
||||||
|
"cancel_trading_plan",
|
||||||
|
"run_trading_watch",
|
||||||
|
"get_trading_candidate_by_id",
|
||||||
|
"run_candidate_confirm_check",
|
||||||
|
"build_ticket_from_candidate",
|
||||||
|
"rerun_ticket_risk_check",
|
||||||
|
"confirm_ticket_only",
|
||||||
|
"execute_ticket",
|
||||||
|
"cancel_execution",
|
||||||
|
"sync_execution",
|
||||||
"get_recent_backtest_summaries",
|
"get_recent_backtest_summaries",
|
||||||
"route_request",
|
"route_request",
|
||||||
"serve_api",
|
"serve_api",
|
||||||
|
|||||||
@@ -13,6 +13,26 @@ from src.api.live_operation_audits import get_recent_live_operation_audits
|
|||||||
from src.api.sessions import get_recent_sessions
|
from src.api.sessions import get_recent_sessions
|
||||||
from src.api.signals import get_recent_signals
|
from src.api.signals import get_recent_signals
|
||||||
from src.api.trade_plan_candidates import get_recent_trade_plan_candidates
|
from src.api.trade_plan_candidates import get_recent_trade_plan_candidates
|
||||||
|
from src.api.trading_runtime import (
|
||||||
|
activate_trading_plan,
|
||||||
|
build_ticket_from_candidate,
|
||||||
|
cancel_execution,
|
||||||
|
cancel_trading_plan,
|
||||||
|
confirm_ticket_only,
|
||||||
|
execute_ticket,
|
||||||
|
generate_trading_journal,
|
||||||
|
generate_trading_plan,
|
||||||
|
get_latest_trading_plan,
|
||||||
|
get_trading_candidate_by_id,
|
||||||
|
get_trading_market_state,
|
||||||
|
get_trading_plan_by_id,
|
||||||
|
get_trading_ticket_by_id,
|
||||||
|
get_weekly_review,
|
||||||
|
rerun_ticket_risk_check,
|
||||||
|
run_candidate_confirm_check,
|
||||||
|
run_trading_watch,
|
||||||
|
sync_execution,
|
||||||
|
)
|
||||||
from src.api.trading_journal import get_recent_trading_journal, get_trading_journal_summary
|
from src.api.trading_journal import get_recent_trading_journal, get_trading_journal_summary
|
||||||
from src.api.trading_plans import get_recent_trading_plans
|
from src.api.trading_plans import get_recent_trading_plans
|
||||||
from src.api.training import (
|
from src.api.training import (
|
||||||
@@ -41,8 +61,26 @@ def build_api_dependencies() -> dict:
|
|||||||
"sessions": get_recent_sessions,
|
"sessions": get_recent_sessions,
|
||||||
"signals": get_recent_signals,
|
"signals": get_recent_signals,
|
||||||
"trade_plan_candidates": get_recent_trade_plan_candidates,
|
"trade_plan_candidates": get_recent_trade_plan_candidates,
|
||||||
|
"trading_plan_generate": generate_trading_plan,
|
||||||
|
"trading_plan_latest": get_latest_trading_plan,
|
||||||
|
"trading_plan_by_id": get_trading_plan_by_id,
|
||||||
|
"trading_plan_activate": activate_trading_plan,
|
||||||
|
"trading_plan_cancel": cancel_trading_plan,
|
||||||
|
"trading_market_state": get_trading_market_state,
|
||||||
|
"trading_watch_run": run_trading_watch,
|
||||||
|
"trading_candidate_by_id": get_trading_candidate_by_id,
|
||||||
|
"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_ticket_risk_check": rerun_ticket_risk_check,
|
||||||
|
"trading_ticket_confirm": confirm_ticket_only,
|
||||||
|
"trading_ticket_execute": execute_ticket,
|
||||||
|
"trading_execution_cancel": cancel_execution,
|
||||||
|
"trading_execution_sync": sync_execution,
|
||||||
"trading_journal": get_recent_trading_journal,
|
"trading_journal": get_recent_trading_journal,
|
||||||
"trading_journal_summary": get_trading_journal_summary,
|
"trading_journal_summary": get_trading_journal_summary,
|
||||||
|
"trading_journal_generate": generate_trading_journal,
|
||||||
|
"trading_weekly_review": get_weekly_review,
|
||||||
"trading_plans": get_recent_trading_plans,
|
"trading_plans": get_recent_trading_plans,
|
||||||
"backtests": get_recent_backtest_summaries,
|
"backtests": get_recent_backtest_summaries,
|
||||||
"executions": execution_service.build_execution_dashboard_items,
|
"executions": execution_service.build_execution_dashboard_items,
|
||||||
@@ -104,13 +142,92 @@ def route_request(
|
|||||||
instrument_id = _require_int(query, "instrument_id")
|
instrument_id = _require_int(query, "instrument_id")
|
||||||
limit = _get_int(query, "limit", 20)
|
limit = _get_int(query, "limit", 20)
|
||||||
return 200, {"items": deps["trading_plans"](instrument_id=instrument_id, limit=limit)}
|
return 200, {"items": deps["trading_plans"](instrument_id=instrument_id, limit=limit)}
|
||||||
|
if method == "POST" and path == "/trading/plans/generate":
|
||||||
|
instrument_id = _require_json_int(body or {}, "instrument_id")
|
||||||
|
return 200, deps["trading_plan_generate"](
|
||||||
|
instrument_id=instrument_id,
|
||||||
|
bias_tf=_get_body_value(body, "bias_tf", "1m"),
|
||||||
|
setup_tf=_get_body_value(body, "setup_tf", "1m"),
|
||||||
|
trigger_tf=_get_body_value(body, "trigger_tf", "1m"),
|
||||||
|
activate=_get_body_bool(body, "activate", False),
|
||||||
|
)
|
||||||
|
if method == "GET" and path == "/trading/plans/latest":
|
||||||
|
instrument_id = _require_int(query, "instrument_id")
|
||||||
|
plan = deps["trading_plan_latest"](instrument_id=instrument_id)
|
||||||
|
if plan is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, plan
|
||||||
|
if method == "GET" and len(path_parts) == 3 and path_parts[:2] == ["trading", "plans"]:
|
||||||
|
plan = deps["trading_plan_by_id"](_parse_path_int(path_parts[2], "plan_id"))
|
||||||
|
if plan is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, plan
|
||||||
|
if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "plans"] and path_parts[3] == "activate":
|
||||||
|
plan = deps["trading_plan_activate"](_parse_path_int(path_parts[2], "plan_id"))
|
||||||
|
if plan is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, plan
|
||||||
|
if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "plans"] and path_parts[3] == "cancel":
|
||||||
|
plan = deps["trading_plan_cancel"](_parse_path_int(path_parts[2], "plan_id"))
|
||||||
|
if plan is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, plan
|
||||||
|
if method == "GET" and path == "/trading/market-state":
|
||||||
|
instrument_id = _require_int(query, "instrument_id")
|
||||||
|
timeframe = _get_single(query, "timeframe", "1m")
|
||||||
|
return 200, deps["trading_market_state"](instrument_id=instrument_id, timeframe=timeframe)
|
||||||
|
if method == "POST" and path == "/trading/watch/run":
|
||||||
|
instrument_id = _require_json_int(body or {}, "instrument_id")
|
||||||
|
candidate = deps["trading_watch_run"](
|
||||||
|
instrument_id=instrument_id,
|
||||||
|
setup_tf=_get_body_value(body, "setup_tf", "1m"),
|
||||||
|
trigger_tf=_get_body_value(body, "trigger_tf", "1m"),
|
||||||
|
)
|
||||||
|
return 200, candidate or {"candidate_generated": False}
|
||||||
|
if method == "GET" and path == "/trading/candidates":
|
||||||
|
instrument_id = _require_int(query, "instrument_id")
|
||||||
|
limit = _get_int(query, "limit", 20)
|
||||||
|
return 200, {"items": deps["trade_plan_candidates"](instrument_id=instrument_id, limit=limit)}
|
||||||
|
if method == "GET" and len(path_parts) == 3 and path_parts[:2] == ["trading", "candidates"]:
|
||||||
|
candidate = deps["trading_candidate_by_id"](_parse_path_int(path_parts[2], "candidate_id"))
|
||||||
|
if candidate is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, candidate
|
||||||
|
if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "candidates"] and path_parts[3] == "confirm-check":
|
||||||
|
payload = deps["trading_candidate_confirm_check"](_parse_path_int(path_parts[2], "candidate_id"))
|
||||||
|
if payload is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, payload
|
||||||
|
if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "candidates"] and path_parts[3] == "build-ticket":
|
||||||
|
payload = deps["trading_candidate_build_ticket"](
|
||||||
|
candidate_id=_parse_path_int(path_parts[2], "candidate_id"),
|
||||||
|
execution_mode=_get_body_value(body, "execution_mode", "semi_auto"),
|
||||||
|
account_equity=_get_body_float(body, "account_equity", 10000.0),
|
||||||
|
risk_pct=_get_body_float(body, "risk_pct", 0.01),
|
||||||
|
)
|
||||||
|
if payload is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, payload
|
||||||
if method == "GET" and path == "/trading-journal":
|
if method == "GET" and path == "/trading-journal":
|
||||||
instrument_id = _get_optional_int(query, "instrument_id")
|
instrument_id = _get_optional_int(query, "instrument_id")
|
||||||
limit = _get_int(query, "limit", 20)
|
limit = _get_int(query, "limit", 20)
|
||||||
return 200, {"items": deps["trading_journal"](instrument_id=instrument_id, limit=limit)}
|
return 200, {"items": deps["trading_journal"](instrument_id=instrument_id, limit=limit)}
|
||||||
|
if method == "GET" and path == "/trading/journal":
|
||||||
|
instrument_id = _get_optional_int(query, "instrument_id")
|
||||||
|
limit = _get_int(query, "limit", 20)
|
||||||
|
return 200, {"items": deps["trading_journal"](instrument_id=instrument_id, limit=limit)}
|
||||||
if method == "GET" and path == "/trading-journal-summary":
|
if method == "GET" and path == "/trading-journal-summary":
|
||||||
instrument_id = _get_optional_int(query, "instrument_id")
|
instrument_id = _get_optional_int(query, "instrument_id")
|
||||||
return 200, deps["trading_journal_summary"](instrument_id=instrument_id)
|
return 200, deps["trading_journal_summary"](instrument_id=instrument_id)
|
||||||
|
if method == "GET" and path == "/trading/journal/summary":
|
||||||
|
instrument_id = _get_optional_int(query, "instrument_id")
|
||||||
|
return 200, deps["trading_journal_summary"](instrument_id=instrument_id)
|
||||||
|
if method == "GET" and path == "/trading/research/weekly-review":
|
||||||
|
instrument_id = _get_optional_int(query, "instrument_id")
|
||||||
|
return 200, deps["trading_weekly_review"](instrument_id=instrument_id)
|
||||||
|
if method == "POST" and path == "/trading/journal/generate":
|
||||||
|
execution_id = _get_body_optional_int(body, "execution_id")
|
||||||
|
return 200, deps["trading_journal_generate"](execution_id=execution_id)
|
||||||
if method == "GET" and path == "/backtests":
|
if method == "GET" and path == "/backtests":
|
||||||
instrument_id = _get_optional_int(query, "instrument_id")
|
instrument_id = _get_optional_int(query, "instrument_id")
|
||||||
limit = _get_int(query, "limit", 10)
|
limit = _get_int(query, "limit", 10)
|
||||||
@@ -119,6 +236,49 @@ def route_request(
|
|||||||
instrument_id = _require_int(query, "instrument_id")
|
instrument_id = _require_int(query, "instrument_id")
|
||||||
limit = _get_int(query, "limit", 20)
|
limit = _get_int(query, "limit", 20)
|
||||||
return 200, {"items": deps["executions"](instrument_id=instrument_id, limit=limit)}
|
return 200, {"items": deps["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")
|
||||||
|
if instrument_id is None and signal_id is None:
|
||||||
|
raise ValueError("missing_required_query_param:instrument_id_or_signal_id")
|
||||||
|
limit = _get_int(query, "limit", 20)
|
||||||
|
return 200, {"items": deps["execution_tickets"](instrument_id=instrument_id, signal_id=signal_id, limit=limit)}
|
||||||
|
if method == "GET" and len(path_parts) == 3 and path_parts[:2] == ["trading", "tickets"]:
|
||||||
|
ticket = deps["trading_ticket_by_id"](_parse_path_int(path_parts[2], "ticket_id"))
|
||||||
|
if ticket is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, ticket
|
||||||
|
if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "tickets"] and path_parts[3] == "risk-check":
|
||||||
|
payload = deps["trading_ticket_risk_check"](
|
||||||
|
_parse_path_int(path_parts[2], "ticket_id"),
|
||||||
|
account_equity=_get_body_float(body, "account_equity", 10000.0),
|
||||||
|
risk_pct=_get_body_float(body, "risk_pct", 0.01),
|
||||||
|
)
|
||||||
|
if payload is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, payload
|
||||||
|
if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "tickets"] and path_parts[3] == "confirm":
|
||||||
|
payload = deps["trading_ticket_confirm"](
|
||||||
|
_parse_path_int(path_parts[2], "ticket_id"),
|
||||||
|
confirmed_by=_get_body_value(body, "confirmed_by", "local_operator"),
|
||||||
|
)
|
||||||
|
if payload is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, payload
|
||||||
|
if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "tickets"] and path_parts[3] == "paper-execute":
|
||||||
|
payload = deps["trading_ticket_execute"](
|
||||||
|
_parse_path_int(path_parts[2], "ticket_id"),
|
||||||
|
confirmed_by=_get_body_value(body, "confirmed_by", "local_operator"),
|
||||||
|
execution_mode_expected="paper",
|
||||||
|
)
|
||||||
|
return 200, payload
|
||||||
|
if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "tickets"] and path_parts[3] == "semi-auto-submit":
|
||||||
|
payload = deps["trading_ticket_execute"](
|
||||||
|
_parse_path_int(path_parts[2], "ticket_id"),
|
||||||
|
confirmed_by=_get_body_value(body, "confirmed_by", "local_operator"),
|
||||||
|
execution_mode_expected="semi_auto",
|
||||||
|
)
|
||||||
|
return 200, payload
|
||||||
if method == "GET" and path == "/execution-tickets":
|
if method == "GET" and path == "/execution-tickets":
|
||||||
instrument_id = _get_optional_int(query, "instrument_id")
|
instrument_id = _get_optional_int(query, "instrument_id")
|
||||||
signal_id = _get_optional_int(query, "signal_id")
|
signal_id = _get_optional_int(query, "signal_id")
|
||||||
@@ -141,6 +301,16 @@ def route_request(
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "executions"] and path_parts[3] == "cancel":
|
||||||
|
payload = deps["trading_execution_cancel"](_parse_path_int(path_parts[2], "execution_id"))
|
||||||
|
if payload is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, payload
|
||||||
|
if method == "POST" and len(path_parts) == 4 and path_parts[:2] == ["trading", "executions"] and path_parts[3] == "sync":
|
||||||
|
payload = deps["trading_execution_sync"](_parse_path_int(path_parts[2], "execution_id"))
|
||||||
|
if payload is None:
|
||||||
|
return 404, {"error": "not_found"}
|
||||||
|
return 200, payload
|
||||||
if method == "GET" and path == "/training/cases":
|
if method == "GET" and path == "/training/cases":
|
||||||
status = _get_single(query, "status")
|
status = _get_single(query, "status")
|
||||||
skill_tag = _get_single(query, "skill_tag")
|
skill_tag = _get_single(query, "skill_tag")
|
||||||
@@ -318,6 +488,36 @@ def _get_limit(query: dict, key: str, default: int, max_value: int) -> int:
|
|||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def _get_body_value(body: Optional[dict], key: str, default=None):
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
return default
|
||||||
|
value = body.get(key)
|
||||||
|
if value is None or value == "":
|
||||||
|
return default
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _get_body_bool(body: Optional[dict], key: str, default: bool) -> bool:
|
||||||
|
value = _get_body_value(body, key, default)
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_body_float(body: Optional[dict], key: str, default: float) -> float:
|
||||||
|
value = _get_body_value(body, key, default)
|
||||||
|
return float(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_body_optional_int(body: Optional[dict], key: str):
|
||||||
|
if not isinstance(body, dict):
|
||||||
|
return None
|
||||||
|
value = body.get(key)
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
|
||||||
def _get_single(query: dict, key: str, default: Optional[str] = None) -> Optional[str]:
|
def _get_single(query: dict, key: str, default: Optional[str] = None) -> Optional[str]:
|
||||||
values = query.get(key)
|
values = query.get(key)
|
||||||
if not values:
|
if not values:
|
||||||
@@ -384,6 +584,10 @@ def _parse_required_json_identifier(body: dict, key: str) -> int:
|
|||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def _require_json_int(body: dict, key: str) -> int:
|
||||||
|
return _parse_required_json_identifier(body, key)
|
||||||
|
|
||||||
|
|
||||||
def _parse_body_identifier(value, key: str) -> int:
|
def _parse_body_identifier(value, key: str) -> int:
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
raise ValueError(f"invalid_body_field:{key}")
|
raise ValueError(f"invalid_body_field:{key}")
|
||||||
|
|||||||
@@ -0,0 +1,332 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.api.execution_tickets import get_recent_execution_tickets
|
||||||
|
from src.api.trade_plan_candidates import get_recent_trade_plan_candidates, normalize_trade_plan_candidate_row
|
||||||
|
from src.api.trading_plans import get_recent_trading_plans, normalize_trading_plan_row
|
||||||
|
from src.repositories.signal_repository import SignalRepository
|
||||||
|
from src.repositories.trade_plan_candidate_repository import TradePlanCandidateRepository
|
||||||
|
from src.repositories.trading_plan_repository import TradingPlanRepository
|
||||||
|
from src.services.execution import ExecutionService
|
||||||
|
from src.services.journal import TradingJournalService, WeeklyReviewService
|
||||||
|
from src.services.market_state import MarketStateService
|
||||||
|
from src.services.trade_plan_candidates import SetupWatcherService
|
||||||
|
from src.services.trading_plans import ACTIVE_PLAN_STATUS, TradingPlanService
|
||||||
|
|
||||||
|
|
||||||
|
def generate_trading_plan(
|
||||||
|
*,
|
||||||
|
instrument_id: int,
|
||||||
|
bias_tf: str = "1m",
|
||||||
|
setup_tf: str = "1m",
|
||||||
|
trigger_tf: str = "1m",
|
||||||
|
activate: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
service = TradingPlanService()
|
||||||
|
candidate = service.build_plan(
|
||||||
|
instrument_id=instrument_id,
|
||||||
|
bias_tf=bias_tf,
|
||||||
|
setup_tf=setup_tf,
|
||||||
|
trigger_tf=trigger_tf,
|
||||||
|
)
|
||||||
|
plan_id = service.persist_plan(candidate)
|
||||||
|
row = TradingPlanRepository().fetch_trading_plan_by_id(plan_id) or candidate.to_dict()
|
||||||
|
payload = normalize_trading_plan_row(dict(row))
|
||||||
|
if activate and payload.get("status") == ACTIVE_PLAN_STATUS:
|
||||||
|
updated = activate_trading_plan(plan_id)
|
||||||
|
if updated is not None:
|
||||||
|
payload = updated
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_trading_plan(instrument_id: int) -> Optional[dict]:
|
||||||
|
row = TradingPlanRepository().fetch_latest_trading_plan(instrument_id)
|
||||||
|
return normalize_trading_plan_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_trading_plan_by_id(plan_id: int) -> Optional[dict]:
|
||||||
|
row = TradingPlanRepository().fetch_trading_plan_by_id(plan_id)
|
||||||
|
return normalize_trading_plan_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def activate_trading_plan(plan_id: int) -> Optional[dict]:
|
||||||
|
row = TradingPlanRepository().update_trading_plan_status(
|
||||||
|
plan_id,
|
||||||
|
status="active",
|
||||||
|
activated_ts=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
return normalize_trading_plan_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_trading_plan(plan_id: int) -> Optional[dict]:
|
||||||
|
row = TradingPlanRepository().update_trading_plan_status(
|
||||||
|
plan_id,
|
||||||
|
status="cancelled",
|
||||||
|
activated_ts=None,
|
||||||
|
)
|
||||||
|
return normalize_trading_plan_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_trading_market_state(instrument_id: int, timeframe: str = "1m") -> dict:
|
||||||
|
service = MarketStateService()
|
||||||
|
market_state = service.build_market_state(instrument_id=instrument_id, timeframe=timeframe)
|
||||||
|
snapshot_id = service.persist_market_state_snapshot(market_state=market_state)
|
||||||
|
quality_gate = service.repository.fetch_latest_backtest_quality_gate(instrument_id=instrument_id)
|
||||||
|
return {
|
||||||
|
"snapshot_id": snapshot_id,
|
||||||
|
"market_state": market_state.to_dict(),
|
||||||
|
"backtest_quality_gate": quality_gate,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_trading_watch(
|
||||||
|
*,
|
||||||
|
instrument_id: int,
|
||||||
|
setup_tf: str = "1m",
|
||||||
|
trigger_tf: str = "1m",
|
||||||
|
) -> Optional[dict]:
|
||||||
|
service = SetupWatcherService()
|
||||||
|
candidate = service.build_candidate(
|
||||||
|
instrument_id=instrument_id,
|
||||||
|
setup_tf=setup_tf,
|
||||||
|
trigger_tf=trigger_tf,
|
||||||
|
)
|
||||||
|
candidate_id = service.persist_candidate(candidate)
|
||||||
|
if candidate is None:
|
||||||
|
return None
|
||||||
|
if candidate_id:
|
||||||
|
row = TradePlanCandidateRepository().fetch_candidate_by_id(candidate_id)
|
||||||
|
if row:
|
||||||
|
return normalize_trade_plan_candidate_row(row)
|
||||||
|
return normalize_trade_plan_candidate_row(candidate.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
def get_trading_candidate_by_id(candidate_id: int) -> Optional[dict]:
|
||||||
|
row = TradePlanCandidateRepository().fetch_candidate_by_id(candidate_id)
|
||||||
|
return normalize_trade_plan_candidate_row(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def run_candidate_confirm_check(candidate_id: int) -> Optional[dict]:
|
||||||
|
candidate = get_trading_candidate_by_id(candidate_id)
|
||||||
|
if not candidate:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"candidate_id": candidate["id"],
|
||||||
|
"status": candidate["status"],
|
||||||
|
"executable": candidate.get("executable", False),
|
||||||
|
"confirmation_state": candidate.get("confirmation_state") or {},
|
||||||
|
"missing_conditions": candidate.get("missing_conditions") or [],
|
||||||
|
"rejected_conditions": candidate.get("rejected_conditions") or [],
|
||||||
|
"reason_codes": candidate.get("reason_codes") or [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_ticket_from_candidate(
|
||||||
|
*,
|
||||||
|
candidate_id: int,
|
||||||
|
execution_mode: str = "semi_auto",
|
||||||
|
account_equity: float = 10000.0,
|
||||||
|
risk_pct: float = 0.01,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
candidate_row = TradePlanCandidateRepository().fetch_candidate_by_id(candidate_id)
|
||||||
|
if not candidate_row:
|
||||||
|
return None
|
||||||
|
candidate = normalize_trade_plan_candidate_row(candidate_row)
|
||||||
|
if not candidate.get("executable"):
|
||||||
|
raise ValueError("candidate_not_executable")
|
||||||
|
signal_id = ensure_trade_signal_for_candidate(candidate_row)
|
||||||
|
ticket = ExecutionService().create_execution_ticket(
|
||||||
|
signal_id=signal_id,
|
||||||
|
execution_mode=execution_mode,
|
||||||
|
account_equity=account_equity,
|
||||||
|
risk_pct=risk_pct,
|
||||||
|
)
|
||||||
|
return ticket.to_dict() if ticket else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_trading_ticket_by_id(ticket_id: int) -> Optional[dict]:
|
||||||
|
row = ExecutionService().repository.fetch_execution_ticket_by_id(ticket_id)
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def rerun_ticket_risk_check(
|
||||||
|
*,
|
||||||
|
ticket_id: int,
|
||||||
|
account_equity: float = 10000.0,
|
||||||
|
risk_pct: float = 0.01,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
service = ExecutionService()
|
||||||
|
ticket = service.repository.fetch_execution_ticket_by_id(ticket_id)
|
||||||
|
if not ticket:
|
||||||
|
return None
|
||||||
|
signal = service.repository.fetch_signal_for_execution(int(ticket["signal_id"]))
|
||||||
|
if not signal:
|
||||||
|
return None
|
||||||
|
risk_context = service.build_risk_context_for_signal(signal)
|
||||||
|
decision = service.prepare_execution(
|
||||||
|
signal_id=int(ticket["signal_id"]),
|
||||||
|
execution_mode=str(ticket["execution_mode"]),
|
||||||
|
account_equity=account_equity,
|
||||||
|
risk_pct=risk_pct,
|
||||||
|
risk_context=risk_context,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"ticket_id": ticket_id,
|
||||||
|
"decision": decision.meta if decision else None,
|
||||||
|
"allowed": decision.allowed if decision else False,
|
||||||
|
"reasons": decision.reasons if decision else ["signal_not_found"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_ticket_only(ticket_id: int, confirmed_by: str) -> Optional[dict]:
|
||||||
|
service = ExecutionService()
|
||||||
|
ticket = service.repository.fetch_execution_ticket_by_id(ticket_id)
|
||||||
|
if not ticket:
|
||||||
|
return None
|
||||||
|
signal = service.repository.fetch_signal_for_execution(int(ticket["signal_id"]))
|
||||||
|
if not signal:
|
||||||
|
return None
|
||||||
|
risk_context = service.build_risk_context_for_signal(signal)
|
||||||
|
decision = service.prepare_execution(
|
||||||
|
signal_id=int(ticket["signal_id"]),
|
||||||
|
execution_mode=str(ticket["execution_mode"]),
|
||||||
|
risk_context=risk_context,
|
||||||
|
)
|
||||||
|
confirmed_at = datetime.now(timezone.utc)
|
||||||
|
status = "confirmed" if decision and decision.allowed else "rejected"
|
||||||
|
snapshot = service.repository.fetch_execution_ticket_by_id(ticket_id).get("ticket_snapshot") or {}
|
||||||
|
new_snapshot = service.repository.fetch_execution_ticket_by_id(ticket_id).get("ticket_snapshot") or {}
|
||||||
|
if decision:
|
||||||
|
new_snapshot = {
|
||||||
|
**dict(snapshot),
|
||||||
|
"confirmation": {
|
||||||
|
"confirmed_by": confirmed_by if decision.allowed else None,
|
||||||
|
"confirmed_at": confirmed_at.isoformat() if decision.allowed else None,
|
||||||
|
},
|
||||||
|
"decision": {
|
||||||
|
"allowed": decision.allowed,
|
||||||
|
"status": decision.status,
|
||||||
|
"reasons": list(decision.reasons),
|
||||||
|
"requires_confirmation": decision.requires_confirmation,
|
||||||
|
"risk_pct": decision.risk_pct,
|
||||||
|
"size": decision.size,
|
||||||
|
"entry_price": decision.entry_price,
|
||||||
|
"meta": dict(decision.meta),
|
||||||
|
},
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
service.repository.update_execution_ticket(
|
||||||
|
ticket_id=ticket_id,
|
||||||
|
status=status,
|
||||||
|
ticket_snapshot=new_snapshot,
|
||||||
|
confirmed_by=confirmed_by if status == "confirmed" else None,
|
||||||
|
confirmed_at=confirmed_at if status == "confirmed" else None,
|
||||||
|
execution_id=ticket.get("execution_id"),
|
||||||
|
)
|
||||||
|
updated = service.repository.fetch_execution_ticket_by_id(ticket_id)
|
||||||
|
return dict(updated) if updated else None
|
||||||
|
|
||||||
|
|
||||||
|
def execute_ticket(ticket_id: int, *, confirmed_by: str, execution_mode_expected: str) -> Optional[dict]:
|
||||||
|
ticket = get_trading_ticket_by_id(ticket_id)
|
||||||
|
if not ticket:
|
||||||
|
return None
|
||||||
|
if ticket.get("execution_mode") != execution_mode_expected:
|
||||||
|
raise ValueError("ticket_execution_mode_mismatch")
|
||||||
|
service = ExecutionService()
|
||||||
|
if ticket.get("status") == "awaiting_confirmation":
|
||||||
|
service.confirm_execution_ticket(ticket_id, confirmed_by=confirmed_by)
|
||||||
|
full = service.submit_execution_ticket(ticket_id)
|
||||||
|
return full.to_dict() if full else None
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_execution(execution_id: int) -> Optional[dict]:
|
||||||
|
service = ExecutionService()
|
||||||
|
return service.persist_execution_observation(
|
||||||
|
trade_execution=service.repository.fetch_execution_by_id(execution_id),
|
||||||
|
observation={"source": "manual", "status": "cancelled", "close_reason": "cancelled"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_execution(execution_id: int) -> Optional[dict]:
|
||||||
|
return ExecutionService().settle_execution_by_id_from_latest_backtest(execution_id)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_trading_journal(execution_id: Optional[int] = None) -> dict:
|
||||||
|
service = TradingJournalService()
|
||||||
|
if execution_id is not None:
|
||||||
|
journal_id = service.upsert_entry_for_execution(execution_id)
|
||||||
|
return {"created": 1 if journal_id else 0, "execution_id": execution_id, "journal_id": journal_id}
|
||||||
|
return service.backfill_missing_entries()
|
||||||
|
|
||||||
|
|
||||||
|
def get_weekly_review(instrument_id: Optional[int] = None) -> dict:
|
||||||
|
return WeeklyReviewService().build_summary(instrument_id=instrument_id)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_trade_signal_for_candidate(candidate_row: dict) -> int:
|
||||||
|
repository = SignalRepository()
|
||||||
|
existing = repository.fetch_latest_trade_signal_for_candidate(int(candidate_row["id"]))
|
||||||
|
if existing:
|
||||||
|
return int(existing["id"])
|
||||||
|
|
||||||
|
entry = dict(candidate_row.get("entry") or {})
|
||||||
|
if not entry:
|
||||||
|
raise ValueError("candidate_missing_entry")
|
||||||
|
trading_plan = TradingPlanRepository().fetch_trading_plan_by_id(int(candidate_row["trading_plan_id"])) or {}
|
||||||
|
allowed_sessions = list(trading_plan.get("allowed_sessions") or [])
|
||||||
|
entry_low = float(entry.get("entry_low") or 0.0)
|
||||||
|
entry_high = float(entry.get("entry_high") or 0.0)
|
||||||
|
stop_loss = float(entry.get("stop_loss") or 0.0)
|
||||||
|
tp1 = float(entry.get("tp1") or 0.0)
|
||||||
|
tp2 = float(entry.get("tp2") or 0.0)
|
||||||
|
entry_price = (entry_low + entry_high) / 2 if entry_low and entry_high else (entry_low or entry_high)
|
||||||
|
risk_distance = abs(entry_price - stop_loss)
|
||||||
|
rr_tp1 = abs(tp1 - entry_price) / risk_distance if risk_distance else 0.0
|
||||||
|
rr_tp2 = abs(tp2 - entry_price) / risk_distance if risk_distance else 0.0
|
||||||
|
invalidation_codes = [
|
||||||
|
str(item.get("code"))
|
||||||
|
for item in list(candidate_row.get("invalidations") or [])
|
||||||
|
if item.get("code")
|
||||||
|
]
|
||||||
|
repository.insert_trade_signal(
|
||||||
|
instrument_id=int(candidate_row["instrument_id"]),
|
||||||
|
model_code=str(candidate_row.get("model_code") or "UNKNOWN"),
|
||||||
|
side=str(candidate_row.get("side") or "buy"),
|
||||||
|
status="awaiting_confirmation",
|
||||||
|
bias_snapshot_id=None,
|
||||||
|
entry_type=str(entry.get("entry_type") or "limit"),
|
||||||
|
entry_low=entry_low,
|
||||||
|
entry_high=entry_high,
|
||||||
|
stop_loss=stop_loss,
|
||||||
|
tp1=tp1,
|
||||||
|
tp2=tp2,
|
||||||
|
rr_tp1=rr_tp1,
|
||||||
|
rr_tp2=rr_tp2,
|
||||||
|
session_code=allowed_sessions[0] if allowed_sessions else None,
|
||||||
|
created_ts=candidate_row.get("created_ts"),
|
||||||
|
expires_ts=candidate_row.get("expires_ts"),
|
||||||
|
invalidations=invalidation_codes,
|
||||||
|
evidence={
|
||||||
|
**dict(candidate_row.get("evidence") or {}),
|
||||||
|
"candidate_id": candidate_row.get("id"),
|
||||||
|
"trading_plan_id": candidate_row.get("trading_plan_id"),
|
||||||
|
},
|
||||||
|
meta={
|
||||||
|
**dict(candidate_row.get("meta") or {}),
|
||||||
|
"candidate_id": candidate_row.get("id"),
|
||||||
|
"trading_plan_id": candidate_row.get("trading_plan_id"),
|
||||||
|
"target_plan_required": True,
|
||||||
|
"execution_permission": True,
|
||||||
|
"target_plan": dict(entry.get("target_plan") or {}),
|
||||||
|
"management_rules": list(entry.get("management_rules") or []),
|
||||||
|
"structure_state": "aligned",
|
||||||
|
"primary_draw": dict((trading_plan or {}).get("primary_draw") or {}),
|
||||||
|
"risk_governor_decision": None,
|
||||||
|
"confirmation_quality": dict(candidate_row.get("confirmation_state") or {}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
created = repository.fetch_latest_trade_signal_for_candidate(int(candidate_row["id"]))
|
||||||
|
if not created:
|
||||||
|
raise ValueError("signal_not_created_from_candidate")
|
||||||
|
return int(created["id"])
|
||||||
@@ -12,6 +12,7 @@ from src.models.instrument import Instrument
|
|||||||
from src.models.live_operation_audit import LiveOperationAudit
|
from src.models.live_operation_audit import LiveOperationAudit
|
||||||
from src.models.liquidity_pool import LiquidityPool
|
from src.models.liquidity_pool import LiquidityPool
|
||||||
from src.models.market_session import MarketSession
|
from src.models.market_session import MarketSession
|
||||||
|
from src.models.market_state_snapshot import MarketStateSnapshot
|
||||||
from src.models.structure_event import StructureEvent
|
from src.models.structure_event import StructureEvent
|
||||||
from src.models.sweep_event import SweepEvent
|
from src.models.sweep_event import SweepEvent
|
||||||
from src.models.swing import Swing
|
from src.models.swing import Swing
|
||||||
@@ -43,6 +44,7 @@ __all__ = [
|
|||||||
"LiveOperationAudit",
|
"LiveOperationAudit",
|
||||||
"LiquidityPool",
|
"LiquidityPool",
|
||||||
"MarketSession",
|
"MarketSession",
|
||||||
|
"MarketStateSnapshot",
|
||||||
"StructureEvent",
|
"StructureEvent",
|
||||||
"SweepEvent",
|
"SweepEvent",
|
||||||
"Swing",
|
"Swing",
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from sqlalchemy import ForeignKey, Numeric, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from src.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class MarketStateSnapshot(Base):
|
||||||
|
__tablename__ = "market_states"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
instrument_id: Mapped[int] = mapped_column(ForeignKey("instruments.id"), nullable=False)
|
||||||
|
snapshot_ts: Mapped[object] = mapped_column(nullable=False)
|
||||||
|
timeframe: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
|
session_code: Mapped[str] = mapped_column(Text)
|
||||||
|
data_freshness: Mapped[str] = mapped_column(Text)
|
||||||
|
candle_age_seconds: Mapped[float] = mapped_column(Numeric(18, 4))
|
||||||
|
news_window_state: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
||||||
|
opening_volatility_state: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
||||||
|
account_state: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
||||||
|
position_state: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
||||||
|
meta: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
@@ -60,3 +61,83 @@ class MarketStateRepository:
|
|||||||
quality_gate["backtest_run_id"] = payload.get("id")
|
quality_gate["backtest_run_id"] = payload.get("id")
|
||||||
quality_gate["observed_ts"] = payload.get("ended_ts") or payload.get("started_ts")
|
quality_gate["observed_ts"] = payload.get("ended_ts") or payload.get("started_ts")
|
||||||
return quality_gate
|
return quality_gate
|
||||||
|
|
||||||
|
def insert_market_state_snapshot(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
instrument_id: int,
|
||||||
|
snapshot_ts,
|
||||||
|
timeframe: str,
|
||||||
|
session_code: str,
|
||||||
|
data_freshness: str,
|
||||||
|
candle_age_seconds,
|
||||||
|
news_window_state: dict,
|
||||||
|
opening_volatility_state: dict,
|
||||||
|
account_state: dict,
|
||||||
|
position_state: dict,
|
||||||
|
meta: dict,
|
||||||
|
) -> int:
|
||||||
|
sql = text(
|
||||||
|
"""
|
||||||
|
insert into market_states (
|
||||||
|
instrument_id,
|
||||||
|
snapshot_ts,
|
||||||
|
timeframe,
|
||||||
|
session_code,
|
||||||
|
data_freshness,
|
||||||
|
candle_age_seconds,
|
||||||
|
news_window_state,
|
||||||
|
opening_volatility_state,
|
||||||
|
account_state,
|
||||||
|
position_state,
|
||||||
|
meta
|
||||||
|
) values (
|
||||||
|
:instrument_id,
|
||||||
|
:snapshot_ts,
|
||||||
|
:timeframe,
|
||||||
|
:session_code,
|
||||||
|
:data_freshness,
|
||||||
|
:candle_age_seconds,
|
||||||
|
cast(:news_window_state as jsonb),
|
||||||
|
cast(:opening_volatility_state as jsonb),
|
||||||
|
cast(:account_state as jsonb),
|
||||||
|
cast(:position_state as jsonb),
|
||||||
|
cast(:meta as jsonb)
|
||||||
|
)
|
||||||
|
returning id
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with SessionLocal.begin() as session:
|
||||||
|
result = session.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
"instrument_id": instrument_id,
|
||||||
|
"snapshot_ts": snapshot_ts,
|
||||||
|
"timeframe": timeframe,
|
||||||
|
"session_code": session_code,
|
||||||
|
"data_freshness": data_freshness,
|
||||||
|
"candle_age_seconds": candle_age_seconds,
|
||||||
|
"news_window_state": json.dumps(news_window_state, default=str),
|
||||||
|
"opening_volatility_state": json.dumps(opening_volatility_state, default=str),
|
||||||
|
"account_state": json.dumps(account_state, default=str),
|
||||||
|
"position_state": json.dumps(position_state, default=str),
|
||||||
|
"meta": json.dumps(meta, default=str),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return int(result.scalar_one())
|
||||||
|
|
||||||
|
def fetch_recent_market_state_snapshots(self, instrument_id: int, limit: int = 20) -> list[dict]:
|
||||||
|
sql = text(
|
||||||
|
"""
|
||||||
|
select id, instrument_id, snapshot_ts, timeframe, session_code, data_freshness,
|
||||||
|
candle_age_seconds, news_window_state, opening_volatility_state,
|
||||||
|
account_state, position_state, meta
|
||||||
|
from market_states
|
||||||
|
where instrument_id = :instrument_id
|
||||||
|
order by snapshot_ts desc, id desc
|
||||||
|
limit :limit
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with SessionLocal() as session:
|
||||||
|
rows = session.execute(sql, {"instrument_id": instrument_id, "limit": limit}).mappings().all()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|||||||
@@ -95,6 +95,22 @@ class SignalRepository:
|
|||||||
).mappings().first()
|
).mappings().first()
|
||||||
return dict(row) if row else None
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
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,
|
||||||
|
stop_loss, tp1, tp2, rr_tp1, rr_tp2, session_code, created_ts, expires_ts,
|
||||||
|
invalidations, evidence, meta
|
||||||
|
from trade_signals
|
||||||
|
where cast(coalesce(meta, '{}'::jsonb)->>'candidate_id' as bigint) = :candidate_id
|
||||||
|
order by created_ts desc, id desc
|
||||||
|
limit 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with SessionLocal() as session:
|
||||||
|
row = session.execute(sql, {"candidate_id": candidate_id}).mappings().first()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
def fetch_session_at(self, instrument_id: int, ts) -> Optional[dict]:
|
def fetch_session_at(self, instrument_id: int, ts) -> Optional[dict]:
|
||||||
sql = text(
|
sql = text(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -298,3 +298,18 @@ class TradePlanCandidateRepository:
|
|||||||
with SessionLocal() as session:
|
with SessionLocal() as session:
|
||||||
rows = session.execute(sql, {"instrument_id": instrument_id, "limit": limit}).mappings().all()
|
rows = session.execute(sql, {"instrument_id": instrument_id, "limit": limit}).mappings().all()
|
||||||
return [dict(row) for row in rows]
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
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
|
||||||
|
from trade_plan_candidates
|
||||||
|
where id = :candidate_id
|
||||||
|
limit 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with SessionLocal() as session:
|
||||||
|
row = session.execute(sql, {"candidate_id": candidate_id}).mappings().first()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|||||||
@@ -220,3 +220,62 @@ class TradingPlanRepository:
|
|||||||
with SessionLocal() as session:
|
with SessionLocal() as session:
|
||||||
rows = session.execute(sql, {"instrument_id": instrument_id, "limit": limit}).mappings().all()
|
rows = session.execute(sql, {"instrument_id": instrument_id, "limit": limit}).mappings().all()
|
||||||
return [dict(row) for row in rows]
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
def fetch_trading_plan_by_id(self, plan_id: int) -> Optional[dict]:
|
||||||
|
sql = text(
|
||||||
|
"""
|
||||||
|
select id, instrument_id, plan_date, status, narrative, primary_draw, secondary_draw,
|
||||||
|
bias, allowed_sessions, invalidations, no_trade_conditions, inputs, quality,
|
||||||
|
timeframe_set, created_ts, activated_ts
|
||||||
|
from trading_plans
|
||||||
|
where id = :plan_id
|
||||||
|
limit 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with SessionLocal() as session:
|
||||||
|
row = session.execute(sql, {"plan_id": plan_id}).mappings().first()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
def fetch_latest_trading_plan(self, instrument_id: int) -> Optional[dict]:
|
||||||
|
sql = text(
|
||||||
|
"""
|
||||||
|
select id, instrument_id, plan_date, status, narrative, primary_draw, secondary_draw,
|
||||||
|
bias, allowed_sessions, invalidations, no_trade_conditions, inputs, quality,
|
||||||
|
timeframe_set, created_ts, activated_ts
|
||||||
|
from trading_plans
|
||||||
|
where instrument_id = :instrument_id
|
||||||
|
order by
|
||||||
|
case when status = 'active' then 0 else 1 end,
|
||||||
|
plan_date desc,
|
||||||
|
created_ts desc,
|
||||||
|
id desc
|
||||||
|
limit 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with SessionLocal() as session:
|
||||||
|
row = session.execute(sql, {"instrument_id": instrument_id}).mappings().first()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
def update_trading_plan_status(self, plan_id: int, status: str, activated_ts=None) -> Optional[dict]:
|
||||||
|
sql = text(
|
||||||
|
"""
|
||||||
|
update trading_plans
|
||||||
|
set status = :status,
|
||||||
|
activated_ts = :activated_ts,
|
||||||
|
updated_at = now()
|
||||||
|
where id = :plan_id
|
||||||
|
returning id, instrument_id, plan_date, status, narrative, primary_draw, secondary_draw,
|
||||||
|
bias, allowed_sessions, invalidations, no_trade_conditions, inputs, quality,
|
||||||
|
timeframe_set, created_ts, activated_ts
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
with SessionLocal.begin() as session:
|
||||||
|
row = session.execute(
|
||||||
|
sql,
|
||||||
|
{
|
||||||
|
"plan_id": plan_id,
|
||||||
|
"status": status,
|
||||||
|
"activated_ts": activated_ts,
|
||||||
|
},
|
||||||
|
).mappings().first()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|||||||
@@ -519,6 +519,83 @@ class ExecutionService:
|
|||||||
ticket_snapshot=dict(updated.get("ticket_snapshot") or {}),
|
ticket_snapshot=dict(updated.get("ticket_snapshot") or {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def submit_execution_ticket(
|
||||||
|
self,
|
||||||
|
ticket_id: int,
|
||||||
|
*,
|
||||||
|
account_equity: float = 10000.0,
|
||||||
|
risk_pct: float = 0.01,
|
||||||
|
account_state: Optional[AccountRiskState] = None,
|
||||||
|
risk_context: Optional[RiskContext] = None,
|
||||||
|
) -> Optional[ExecutionTicketDecision]:
|
||||||
|
ticket = self.repository.fetch_execution_ticket_by_id(ticket_id)
|
||||||
|
if not ticket:
|
||||||
|
return None
|
||||||
|
if ticket.get("status") != EXECUTION_TICKET_STATUS_CONFIRMED:
|
||||||
|
return ExecutionTicketDecision(
|
||||||
|
ticket_id=int(ticket["id"]),
|
||||||
|
status=str(ticket["status"]),
|
||||||
|
execution_mode=str(ticket["execution_mode"]),
|
||||||
|
signal_id=int(ticket["signal_id"]),
|
||||||
|
execution_id=ticket.get("execution_id"),
|
||||||
|
reasons=["execution_ticket_not_confirmed"],
|
||||||
|
ticket_snapshot=dict(ticket.get("ticket_snapshot") or {}),
|
||||||
|
)
|
||||||
|
signal = self.repository.fetch_signal_for_execution(int(ticket["signal_id"]))
|
||||||
|
if not signal:
|
||||||
|
return None
|
||||||
|
full_risk_context = self.build_risk_context_for_signal(
|
||||||
|
signal,
|
||||||
|
account_state=account_state,
|
||||||
|
risk_context=risk_context,
|
||||||
|
)
|
||||||
|
execution_id = self.create_execution(
|
||||||
|
signal_id=int(ticket["signal_id"]),
|
||||||
|
execution_mode=str(ticket["execution_mode"]),
|
||||||
|
account_equity=account_equity,
|
||||||
|
risk_pct=risk_pct,
|
||||||
|
confirmed=True,
|
||||||
|
account_state=account_state,
|
||||||
|
risk_context=full_risk_context,
|
||||||
|
ticket_id=int(ticket["id"]),
|
||||||
|
ticket_snapshot=dict(ticket.get("ticket_snapshot") or {}),
|
||||||
|
confirmed_by=ticket.get("confirmed_by"),
|
||||||
|
confirmed_at=ticket.get("confirmed_at"),
|
||||||
|
)
|
||||||
|
if execution_id is None:
|
||||||
|
return ExecutionTicketDecision(
|
||||||
|
ticket_id=int(ticket["id"]),
|
||||||
|
status=str(ticket["status"]),
|
||||||
|
execution_mode=str(ticket["execution_mode"]),
|
||||||
|
signal_id=int(ticket["signal_id"]),
|
||||||
|
execution_id=None,
|
||||||
|
reasons=["execution_creation_failed"],
|
||||||
|
ticket_snapshot=dict(ticket.get("ticket_snapshot") or {}),
|
||||||
|
)
|
||||||
|
snapshot = dict(ticket.get("ticket_snapshot") or {})
|
||||||
|
snapshot["execution"] = {
|
||||||
|
"execution_id": execution_id,
|
||||||
|
"submitted_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
}
|
||||||
|
self.repository.update_execution_ticket(
|
||||||
|
ticket_id=int(ticket["id"]),
|
||||||
|
status="submitted",
|
||||||
|
ticket_snapshot=snapshot,
|
||||||
|
confirmed_by=ticket.get("confirmed_by"),
|
||||||
|
confirmed_at=ticket.get("confirmed_at"),
|
||||||
|
execution_id=execution_id,
|
||||||
|
)
|
||||||
|
updated = self.repository.fetch_execution_ticket_by_id(int(ticket["id"])) or ticket
|
||||||
|
return ExecutionTicketDecision(
|
||||||
|
ticket_id=int(updated["id"]),
|
||||||
|
status=str(updated["status"]),
|
||||||
|
execution_mode=str(updated["execution_mode"]),
|
||||||
|
signal_id=int(updated["signal_id"]),
|
||||||
|
execution_id=updated.get("execution_id"),
|
||||||
|
reasons=[],
|
||||||
|
ticket_snapshot=dict(updated.get("ticket_snapshot") or {}),
|
||||||
|
)
|
||||||
|
|
||||||
def create_execution(
|
def create_execution(
|
||||||
self,
|
self,
|
||||||
signal_id: int,
|
signal_id: int,
|
||||||
@@ -679,6 +756,13 @@ def evaluate_execution_decision(
|
|||||||
meta["risk_distance"] = risk_distance
|
meta["risk_distance"] = risk_distance
|
||||||
meta["account_equity"] = account_equity
|
meta["account_equity"] = account_equity
|
||||||
meta["account_risk"] = account_state.to_dict() if hasattr(account_state, "to_dict") else (account_state.__dict__ if account_state else None)
|
meta["account_risk"] = account_state.to_dict() if hasattr(account_state, "to_dict") else (account_state.__dict__ if account_state else None)
|
||||||
|
meta.setdefault("execution_ticket_id", None)
|
||||||
|
meta.setdefault("confirmation_required", execution_mode == "semi_auto")
|
||||||
|
meta.setdefault("confirmed_by", None)
|
||||||
|
meta.setdefault("confirmed_at", None)
|
||||||
|
meta.setdefault("broker_order_ids", [])
|
||||||
|
meta.setdefault("bracket_state", {"status": "not_submitted", "oco_group_id": None})
|
||||||
|
meta.setdefault("reconciliation_state", {"status": "not_reconciled"})
|
||||||
|
|
||||||
allowed = len(reasons) == 0
|
allowed = len(reasons) == 0
|
||||||
requires_confirmation = execution_mode == "semi_auto"
|
requires_confirmation = execution_mode == "semi_auto"
|
||||||
|
|||||||
@@ -126,6 +126,31 @@ class MarketStateService:
|
|||||||
opening_volatility=opening_volatility,
|
opening_volatility=opening_volatility,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def persist_market_state_snapshot(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
market_state: MarketState,
|
||||||
|
account_state: Optional[dict] = None,
|
||||||
|
position_state: Optional[dict] = None,
|
||||||
|
meta: Optional[dict] = None,
|
||||||
|
) -> int:
|
||||||
|
return self.repository.insert_market_state_snapshot(
|
||||||
|
instrument_id=market_state.instrument_id,
|
||||||
|
snapshot_ts=market_state.observed_ts,
|
||||||
|
timeframe=market_state.timeframe,
|
||||||
|
session_code=market_state.session.session_code,
|
||||||
|
data_freshness=market_state.freshness.code,
|
||||||
|
candle_age_seconds=market_state.freshness.age_seconds,
|
||||||
|
news_window_state=market_state.news_window.to_dict(),
|
||||||
|
opening_volatility_state=market_state.opening_volatility.to_dict(),
|
||||||
|
account_state=dict(account_state or {}),
|
||||||
|
position_state=dict(position_state or {}),
|
||||||
|
meta={
|
||||||
|
"latest_candle": dict(market_state.latest_candle or {}),
|
||||||
|
**dict(meta or {}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def evaluate_freshness(*, latest_ts, now: datetime, max_age_seconds: int) -> FreshnessState:
|
def evaluate_freshness(*, latest_ts, now: datetime, max_age_seconds: int) -> FreshnessState:
|
||||||
if latest_ts is None:
|
if latest_ts is None:
|
||||||
|
|||||||
@@ -157,9 +157,14 @@ class SignalService:
|
|||||||
'signal_sweep_id': evidence.get('sweep_id'),
|
'signal_sweep_id': evidence.get('sweep_id'),
|
||||||
'liquidity_target_id': evidence.get('liquidity_target_id'),
|
'liquidity_target_id': evidence.get('liquidity_target_id'),
|
||||||
'rr_filter': rr_decision,
|
'rr_filter': rr_decision,
|
||||||
|
'trading_plan_id': None,
|
||||||
|
'candidate_id': None,
|
||||||
|
'primary_draw': {},
|
||||||
'target_plan': target_plan_payload,
|
'target_plan': target_plan_payload,
|
||||||
'management_rules': target_plan_payload['management_rules'],
|
'management_rules': target_plan_payload['management_rules'],
|
||||||
'target_selection_status': target_plan_payload['status'],
|
'target_selection_status': target_plan_payload['status'],
|
||||||
|
'risk_governor_decision': None,
|
||||||
|
'confirmation_quality': evidence_quality,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+259
-3
@@ -14,6 +14,12 @@ const sessionsTable = document.getElementById('sessionsTable');
|
|||||||
const executionsTable = document.getElementById('executionsTable');
|
const executionsTable = document.getElementById('executionsTable');
|
||||||
const backtestsCards = document.getElementById('backtestsCards');
|
const backtestsCards = document.getElementById('backtestsCards');
|
||||||
const overviewCards = document.getElementById('overviewCards');
|
const overviewCards = document.getElementById('overviewCards');
|
||||||
|
const marketStatePanel = document.getElementById('marketStatePanel');
|
||||||
|
const preMarketPlanPanel = document.getElementById('preMarketPlanPanel');
|
||||||
|
const tradeCandidatesPanel = document.getElementById('tradeCandidatesPanel');
|
||||||
|
const executionTicketsPanel = document.getElementById('executionTicketsPanel');
|
||||||
|
const positionManagementPanel = document.getElementById('positionManagementPanel');
|
||||||
|
const postTradeReviewPanel = document.getElementById('postTradeReviewPanel');
|
||||||
const detailMeta = document.getElementById('detailMeta');
|
const detailMeta = document.getElementById('detailMeta');
|
||||||
const detailPanel = document.getElementById('detailPanel');
|
const detailPanel = document.getElementById('detailPanel');
|
||||||
const trainingCasesList = document.getElementById('trainingCasesList');
|
const trainingCasesList = document.getElementById('trainingCasesList');
|
||||||
@@ -285,6 +291,243 @@ function updateActiveFilters() {
|
|||||||
setText(activeFilters, `Filters: ${active.length ? active.join(', ') : 'none'}`);
|
setText(activeFilters, `Filters: ${active.length ? active.join(', ') : 'none'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderKeyValuePanel(container, entries, emptyText = 'No data', preserveExisting = false) {
|
||||||
|
if (!container) return;
|
||||||
|
if (!preserveExisting) {
|
||||||
|
clearNode(container);
|
||||||
|
}
|
||||||
|
if (!entries.length) {
|
||||||
|
const empty = document.createElement('div');
|
||||||
|
empty.className = 'metric';
|
||||||
|
setText(empty, emptyText);
|
||||||
|
container.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entries.forEach(([label, value]) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'metric';
|
||||||
|
setText(row, `${label}: ${value == null || value === '' ? 'n/a' : value}`);
|
||||||
|
container.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runTradingAction(url, body = {}) {
|
||||||
|
try {
|
||||||
|
await fetchJson(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
await refreshDashboard();
|
||||||
|
} catch (error) {
|
||||||
|
showDetail('Trading Action Error', { error: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarketStatePanel(payload) {
|
||||||
|
const state = payload?.market_state || {};
|
||||||
|
const freshness = state?.freshness || {};
|
||||||
|
const session = state?.session || {};
|
||||||
|
const news = state?.news_window || {};
|
||||||
|
renderKeyValuePanel(marketStatePanel, [
|
||||||
|
['symbol', instrumentIdInput.value || '1'],
|
||||||
|
['session', session.session_code || 'OFF_HOURS'],
|
||||||
|
['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],
|
||||||
|
['kill switch', payload?.risk_decision?.reject_codes?.includes('execution_kill_switch_enabled') ? 'active' : 'off'],
|
||||||
|
], 'No market state');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPreMarketPlanPanel(plan) {
|
||||||
|
if (!preMarketPlanPanel) return;
|
||||||
|
clearNode(preMarketPlanPanel);
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'training-actions';
|
||||||
|
const generateBtn = document.createElement('button');
|
||||||
|
generateBtn.type = 'button';
|
||||||
|
setText(generateBtn, 'Generate Plan');
|
||||||
|
generateBtn.addEventListener('click', () => runTradingAction('/trading/plans/generate', {
|
||||||
|
instrument_id: Number(instrumentIdInput.value || 1),
|
||||||
|
bias_tf: timeframeInput.value || '1m',
|
||||||
|
setup_tf: timeframeInput.value || '1m',
|
||||||
|
trigger_tf: timeframeInput.value || '1m',
|
||||||
|
activate: true,
|
||||||
|
}));
|
||||||
|
actions.appendChild(generateBtn);
|
||||||
|
preMarketPlanPanel.appendChild(actions);
|
||||||
|
if (!plan) {
|
||||||
|
renderKeyValuePanel(preMarketPlanPanel, [], 'No latest trading plan', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const actionRow = document.createElement('div');
|
||||||
|
actionRow.className = 'training-actions';
|
||||||
|
const activateBtn = document.createElement('button');
|
||||||
|
activateBtn.type = 'button';
|
||||||
|
setText(activateBtn, 'Activate');
|
||||||
|
activateBtn.addEventListener('click', () => runTradingAction(`/trading/plans/${plan.id}/activate`, {}));
|
||||||
|
const cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.type = 'button';
|
||||||
|
setText(cancelBtn, 'Cancel');
|
||||||
|
cancelBtn.addEventListener('click', () => runTradingAction(`/trading/plans/${plan.id}/cancel`, {}));
|
||||||
|
actionRow.append(activateBtn, cancelBtn);
|
||||||
|
preMarketPlanPanel.appendChild(actionRow);
|
||||||
|
renderKeyValuePanel(preMarketPlanPanel, [
|
||||||
|
['Narrative', plan.narrative],
|
||||||
|
['primary Draw', JSON.stringify(plan.primary_draw || {})],
|
||||||
|
['secondary Draw', JSON.stringify(plan.secondary_draw || {})],
|
||||||
|
['Bias', plan.bias],
|
||||||
|
['allowed sessions', (plan.allowed_sessions || []).join(', ')],
|
||||||
|
['no-trade conditions', (plan.no_trade_conditions || []).join(', ') || 'none'],
|
||||||
|
['invalidation', JSON.stringify((plan.invalidations || [])[0] || {})],
|
||||||
|
], 'No pre-market plan', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTradeCandidatesPanel(items) {
|
||||||
|
if (!tradeCandidatesPanel) return;
|
||||||
|
clearNode(tradeCandidatesPanel);
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'training-actions';
|
||||||
|
const watchBtn = document.createElement('button');
|
||||||
|
watchBtn.type = 'button';
|
||||||
|
setText(watchBtn, 'Run Watch');
|
||||||
|
watchBtn.addEventListener('click', () => runTradingAction('/trading/watch/run', {
|
||||||
|
instrument_id: Number(instrumentIdInput.value || 1),
|
||||||
|
setup_tf: timeframeInput.value || '1m',
|
||||||
|
trigger_tf: timeframeInput.value || '1m',
|
||||||
|
}));
|
||||||
|
actions.appendChild(watchBtn);
|
||||||
|
tradeCandidatesPanel.appendChild(actions);
|
||||||
|
const candidate = (items || [])[0];
|
||||||
|
if (!candidate) {
|
||||||
|
renderKeyValuePanel(tradeCandidatesPanel, [], 'No setup candidate', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const actionRow = document.createElement('div');
|
||||||
|
actionRow.className = 'training-actions';
|
||||||
|
const confirmCheckBtn = document.createElement('button');
|
||||||
|
confirmCheckBtn.type = 'button';
|
||||||
|
setText(confirmCheckBtn, 'Confirm Check');
|
||||||
|
confirmCheckBtn.addEventListener('click', () => runTradingAction(`/trading/candidates/${candidate.id}/confirm-check`, {}));
|
||||||
|
const buildTicketBtn = document.createElement('button');
|
||||||
|
buildTicketBtn.type = 'button';
|
||||||
|
setText(buildTicketBtn, 'Build Ticket');
|
||||||
|
buildTicketBtn.addEventListener('click', () => runTradingAction(`/trading/candidates/${candidate.id}/build-ticket`, { execution_mode: 'semi_auto' }));
|
||||||
|
actionRow.append(confirmCheckBtn, buildTicketBtn);
|
||||||
|
tradeCandidatesPanel.appendChild(actionRow);
|
||||||
|
renderKeyValuePanel(tradeCandidatesPanel, [
|
||||||
|
['setup candidate', `${candidate.model_code} / ${candidate.status}`],
|
||||||
|
['confirmation chain', JSON.stringify(candidate.confirmation_state || {})],
|
||||||
|
['quality score', candidate.executable ? 'executable' : 'needs_confirmation'],
|
||||||
|
['missing requirements', (candidate.missing_conditions || []).join(', ') || 'none'],
|
||||||
|
['evidence links', JSON.stringify(candidate.evidence || {})],
|
||||||
|
], 'No setup candidate', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExecutionTicketsPanel(items) {
|
||||||
|
if (!executionTicketsPanel) return;
|
||||||
|
clearNode(executionTicketsPanel);
|
||||||
|
const ticket = (items || [])[0];
|
||||||
|
if (!ticket) {
|
||||||
|
renderKeyValuePanel(executionTicketsPanel, [], 'No execution ticket', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const actionRow = document.createElement('div');
|
||||||
|
actionRow.className = 'training-actions';
|
||||||
|
const confirmBtn = document.createElement('button');
|
||||||
|
confirmBtn.type = 'button';
|
||||||
|
setText(confirmBtn, 'Confirm');
|
||||||
|
confirmBtn.addEventListener('click', () => runTradingAction(`/trading/tickets/${ticket.id}/confirm`, { confirmed_by: 'dashboard_user' }));
|
||||||
|
const riskBtn = document.createElement('button');
|
||||||
|
riskBtn.type = 'button';
|
||||||
|
setText(riskBtn, 'Risk Check');
|
||||||
|
riskBtn.addEventListener('click', () => runTradingAction(`/trading/tickets/${ticket.id}/risk-check`, {}));
|
||||||
|
const paperBtn = document.createElement('button');
|
||||||
|
paperBtn.type = 'button';
|
||||||
|
setText(paperBtn, 'Paper Execute');
|
||||||
|
paperBtn.addEventListener('click', () => runTradingAction(`/trading/tickets/${ticket.id}/paper-execute`, { confirmed_by: 'dashboard_user' }));
|
||||||
|
const semiBtn = document.createElement('button');
|
||||||
|
semiBtn.type = 'button';
|
||||||
|
setText(semiBtn, 'Semi-auto Submit');
|
||||||
|
semiBtn.addEventListener('click', () => runTradingAction(`/trading/tickets/${ticket.id}/semi-auto-submit`, { confirmed_by: 'dashboard_user' }));
|
||||||
|
actionRow.append(confirmBtn, riskBtn, paperBtn, semiBtn);
|
||||||
|
executionTicketsPanel.appendChild(actionRow);
|
||||||
|
const snapshot = ticket.ticket_snapshot || {};
|
||||||
|
const decision = snapshot.decision || {};
|
||||||
|
renderKeyValuePanel(executionTicketsPanel, [
|
||||||
|
['side', snapshot.signal?.side || ticket.side],
|
||||||
|
['entry zone', `${decision.meta?.entry_low ?? 'n/a'} - ${decision.meta?.entry_high ?? 'n/a'}`],
|
||||||
|
['stop', decision.meta?.stop_loss],
|
||||||
|
['TP1 / TP2', `${decision.meta?.tp1 ?? 'n/a'} / ${decision.meta?.tp2 ?? 'n/a'}`],
|
||||||
|
['RR', `${decision.meta?.rr_tp1 ?? 'n/a'} / ${decision.meta?.rr_tp2 ?? 'n/a'}`],
|
||||||
|
['risk amount', decision.risk_pct],
|
||||||
|
['expiry', snapshot.signal?.expires_ts || ticket.expires_ts],
|
||||||
|
['management rules', JSON.stringify(decision.meta?.management_rules || [])],
|
||||||
|
['risk governor result', JSON.stringify(decision.meta?.risk_governor || {})],
|
||||||
|
], 'No execution ticket', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPositionManagementPanel(executions, liveAudits) {
|
||||||
|
if (!positionManagementPanel) return;
|
||||||
|
clearNode(positionManagementPanel);
|
||||||
|
const execution = (executions || [])[0];
|
||||||
|
if (!execution) {
|
||||||
|
renderKeyValuePanel(positionManagementPanel, [], 'No current position', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const actionRow = document.createElement('div');
|
||||||
|
actionRow.className = 'training-actions';
|
||||||
|
const cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.type = 'button';
|
||||||
|
setText(cancelBtn, 'Cancel');
|
||||||
|
cancelBtn.addEventListener('click', () => runTradingAction(`/trading/executions/${execution.execution_id}/cancel`, {}));
|
||||||
|
const syncBtn = document.createElement('button');
|
||||||
|
syncBtn.type = 'button';
|
||||||
|
setText(syncBtn, 'Sync');
|
||||||
|
syncBtn.addEventListener('click', () => runTradingAction(`/trading/executions/${execution.execution_id}/sync`, {}));
|
||||||
|
actionRow.append(cancelBtn, syncBtn);
|
||||||
|
positionManagementPanel.appendChild(actionRow);
|
||||||
|
renderKeyValuePanel(positionManagementPanel, [
|
||||||
|
['current position', execution.side || 'n/a'],
|
||||||
|
['fill state', execution.fill_state],
|
||||||
|
['TP progress', `${execution.tp1_hit ? 'tp1' : '-'} ${execution.tp2_hit ? 'tp2' : ''}`.trim() || 'none'],
|
||||||
|
['stop state', execution.stop_loss_hit ? 'hit' : 'safe'],
|
||||||
|
['invalidation alerts', (execution.error_tags || []).join(', ') || 'none'],
|
||||||
|
['cancel / close / sync actions', (liveAudits || []).map((item) => item.operation_type).join(', ') || 'sync_only'],
|
||||||
|
], 'No current position', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPostTradeReviewPanel(summary, journalItems) {
|
||||||
|
if (!postTradeReviewPanel) return;
|
||||||
|
clearNode(postTradeReviewPanel);
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'training-actions';
|
||||||
|
const generateBtn = document.createElement('button');
|
||||||
|
generateBtn.type = 'button';
|
||||||
|
setText(generateBtn, 'Generate Journal');
|
||||||
|
generateBtn.addEventListener('click', () => runTradingAction('/trading/journal/generate', {}));
|
||||||
|
actions.appendChild(generateBtn);
|
||||||
|
postTradeReviewPanel.appendChild(actions);
|
||||||
|
const latest = (journalItems || [])[0];
|
||||||
|
if (!latest) {
|
||||||
|
renderKeyValuePanel(postTradeReviewPanel, [
|
||||||
|
['entry count', summary?.entry_count ?? 0],
|
||||||
|
['next actions', (summary?.next_week_actions || []).join(', ') || 'none'],
|
||||||
|
], 'No journal entries', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderKeyValuePanel(postTradeReviewPanel, [
|
||||||
|
['model compliance', latest.model_compliance],
|
||||||
|
['risk compliance', latest.risk_compliance],
|
||||||
|
['execution violations', latest.execution_violation],
|
||||||
|
['model failure vs execution failure', `${latest.model_failure ? 'model_failure' : 'no_model_failure'} / ${latest.execution_violation ? 'execution_violation' : 'clean_execution'}`],
|
||||||
|
['next actions', (latest.next_actions || []).join(', ') || 'none'],
|
||||||
|
['high quality session', summary?.high_quality_session?.session_code || 'n/a'],
|
||||||
|
['low quality session', summary?.low_quality_session?.session_code || 'n/a'],
|
||||||
|
['weekly next actions', (summary?.next_week_actions || []).join(', ') || 'none'],
|
||||||
|
], 'No post-trade review', true);
|
||||||
|
}
|
||||||
|
|
||||||
function setSelectedDetail(title, payload, key) {
|
function setSelectedDetail(title, payload, key) {
|
||||||
selectedDetailKey = key;
|
selectedDetailKey = key;
|
||||||
showDetail(title, payload);
|
showDetail(title, payload);
|
||||||
@@ -304,7 +547,7 @@ function cardDetailHandler(titleBuilder, keyValue, payload) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDashboardData(health, candles, signals, sessions, executions, backtests) {
|
function renderDashboardData(health, candles, signals, sessions, executions, backtests, tradingPlan, marketState, tradeCandidates, executionTickets, journalSummary, journalItems, liveAudits) {
|
||||||
const signalItems = applySignalFilters(signals.items || []);
|
const signalItems = applySignalFilters(signals.items || []);
|
||||||
const sessionItems = applyTextFilters(sessions.items || [], ['id', 'session_code', 'status', 'session_date']);
|
const sessionItems = applyTextFilters(sessions.items || [], ['id', 'session_code', 'status', 'session_date']);
|
||||||
const executionItems = applyExecutionFilters(executions.items || []);
|
const executionItems = applyExecutionFilters(executions.items || []);
|
||||||
@@ -313,6 +556,12 @@ function renderDashboardData(health, candles, signals, sessions, executions, bac
|
|||||||
renderHealth(health);
|
renderHealth(health);
|
||||||
updateActiveFilters();
|
updateActiveFilters();
|
||||||
renderOverview(signalItems, sessionItems, executionItems, backtestItems);
|
renderOverview(signalItems, sessionItems, executionItems, backtestItems);
|
||||||
|
renderMarketStatePanel(marketState);
|
||||||
|
renderPreMarketPlanPanel(tradingPlan);
|
||||||
|
renderTradeCandidatesPanel(tradeCandidates.items || []);
|
||||||
|
renderExecutionTicketsPanel(executionTickets.items || []);
|
||||||
|
renderPositionManagementPanel(executionItems, liveAudits.items || []);
|
||||||
|
renderPostTradeReviewPanel(journalSummary, journalItems.items || []);
|
||||||
renderTable(signalsTable, [
|
renderTable(signalsTable, [
|
||||||
tableColumn('id', 'ID'),
|
tableColumn('id', 'ID'),
|
||||||
tableColumn('side', 'Side'),
|
tableColumn('side', 'Side'),
|
||||||
@@ -1044,16 +1293,23 @@ async function refreshDashboard() {
|
|||||||
const limit = limitInput.value || '20';
|
const limit = limitInput.value || '20';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [health, candles, signals, sessions, executions, backtests] = await Promise.all([
|
const [health, candles, signals, sessions, executions, backtests, tradingPlan, marketState, tradeCandidates, executionTickets, journalSummary, journalItems, liveAudits] = await Promise.all([
|
||||||
fetchJson('/health'),
|
fetchJson('/health'),
|
||||||
fetchJson(`/candles?instrument_id=${instrumentId}&timeframe=${encodeURIComponent(timeframe)}&limit=${limit}`),
|
fetchJson(`/candles?instrument_id=${instrumentId}&timeframe=${encodeURIComponent(timeframe)}&limit=${limit}`),
|
||||||
fetchJson(`/signals?instrument_id=${instrumentId}&limit=${limit}`),
|
fetchJson(`/signals?instrument_id=${instrumentId}&limit=${limit}`),
|
||||||
fetchJson(`/sessions?instrument_id=${instrumentId}&limit=${limit}`),
|
fetchJson(`/sessions?instrument_id=${instrumentId}&limit=${limit}`),
|
||||||
fetchJson(`/executions?instrument_id=${instrumentId}&limit=${limit}`),
|
fetchJson(`/executions?instrument_id=${instrumentId}&limit=${limit}`),
|
||||||
fetchJson(`/backtests?instrument_id=${instrumentId}&limit=${Math.min(Number(limit), 10)}`),
|
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/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: [] })),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
renderDashboardData(health, candles, signals, sessions, executions, backtests);
|
renderDashboardData(health, candles, signals, sessions, executions, backtests, tradingPlan, marketState, tradeCandidates, executionTickets, journalSummary, journalItems, liveAudits);
|
||||||
updateLastUpdated();
|
updateLastUpdated();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
renderLoadError(error);
|
renderLoadError(error);
|
||||||
|
|||||||
@@ -63,6 +63,36 @@
|
|||||||
<div id="overviewCards" class="cards overview-cards"></div>
|
<div id="overviewCards" class="cards overview-cards"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel panel-wide">
|
||||||
|
<h2>Trading Status</h2>
|
||||||
|
<div id="marketStatePanel" class="cards"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel panel-wide">
|
||||||
|
<h2>Pre-Market Plan</h2>
|
||||||
|
<div id="preMarketPlanPanel" class="detail-panel"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel panel-wide">
|
||||||
|
<h2>Intraday Candidates</h2>
|
||||||
|
<div id="tradeCandidatesPanel" class="detail-panel"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel panel-wide">
|
||||||
|
<h2>Execution Tickets</h2>
|
||||||
|
<div id="executionTicketsPanel" class="detail-panel"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel panel-wide">
|
||||||
|
<h2>Position Management</h2>
|
||||||
|
<div id="positionManagementPanel" class="detail-panel"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel panel-wide">
|
||||||
|
<h2>Post-Trade Review</h2>
|
||||||
|
<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">
|
||||||
<h2>Training / 训练</h2>
|
<h2>Training / 训练</h2>
|
||||||
<nav class="training-nav" aria-label="Training dashboard sections">
|
<nav class="training-nav" aria-label="Training dashboard sections">
|
||||||
|
|||||||
@@ -15,6 +15,168 @@ from scripts import run_execution, run_execution_settlement, run_offline_checks,
|
|||||||
|
|
||||||
|
|
||||||
class ApiServerRoutingTests(unittest.TestCase):
|
class ApiServerRoutingTests(unittest.TestCase):
|
||||||
|
def test_routes_trading_plan_generate(self) -> None:
|
||||||
|
status, payload = route_request(
|
||||||
|
"/trading/plans/generate",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={"instrument_id": 1, "activate": True},
|
||||||
|
dependencies={
|
||||||
|
"trading_plan_generate": lambda **kwargs: {"id": 1, "status": "active", **kwargs},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status, 200)
|
||||||
|
self.assertEqual(payload["instrument_id"], 1)
|
||||||
|
self.assertTrue(payload["activate"])
|
||||||
|
|
||||||
|
def test_routes_trading_plan_latest(self) -> None:
|
||||||
|
status, payload = route_request(
|
||||||
|
"/trading/plans/latest",
|
||||||
|
{"instrument_id": ["1"]},
|
||||||
|
dependencies={"trading_plan_latest": lambda **kwargs: {"id": 9, "status": "active"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status, 200)
|
||||||
|
self.assertEqual(payload["id"], 9)
|
||||||
|
|
||||||
|
def test_routes_trading_market_state(self) -> None:
|
||||||
|
status, payload = route_request(
|
||||||
|
"/trading/market-state",
|
||||||
|
{"instrument_id": ["1"], "timeframe": ["5m"]},
|
||||||
|
dependencies={"trading_market_state": lambda **kwargs: {"snapshot_id": 1, "market_state": kwargs}},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status, 200)
|
||||||
|
self.assertEqual(payload["snapshot_id"], 1)
|
||||||
|
self.assertEqual(payload["market_state"]["timeframe"], "5m")
|
||||||
|
|
||||||
|
def test_routes_trading_watch_run(self) -> None:
|
||||||
|
status, payload = route_request(
|
||||||
|
"/trading/watch/run",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={"instrument_id": 1, "setup_tf": "15m", "trigger_tf": "5m"},
|
||||||
|
dependencies={"trading_watch_run": lambda **kwargs: {"id": 4, "status": "executable", **kwargs}},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status, 200)
|
||||||
|
self.assertEqual(payload["setup_tf"], "15m")
|
||||||
|
|
||||||
|
def test_routes_trading_candidate_detail_and_confirm_check(self) -> None:
|
||||||
|
status_detail, payload_detail = route_request(
|
||||||
|
"/trading/candidates/4",
|
||||||
|
{},
|
||||||
|
dependencies={"trading_candidate_by_id": lambda candidate_id: {"id": candidate_id, "status": "executable"}},
|
||||||
|
)
|
||||||
|
status_check, payload_check = route_request(
|
||||||
|
"/trading/candidates/4/confirm-check",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={},
|
||||||
|
dependencies={"trading_candidate_confirm_check": lambda candidate_id: {"candidate_id": candidate_id, "executable": True}},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status_detail, 200)
|
||||||
|
self.assertEqual(payload_detail["id"], 4)
|
||||||
|
self.assertEqual(status_check, 200)
|
||||||
|
self.assertTrue(payload_check["executable"])
|
||||||
|
|
||||||
|
def test_routes_trading_ticket_flow(self) -> None:
|
||||||
|
status_build, payload_build = route_request(
|
||||||
|
"/trading/candidates/4/build-ticket",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={"execution_mode": "semi_auto"},
|
||||||
|
dependencies={"trading_candidate_build_ticket": lambda **kwargs: {"ticket_id": 11, **kwargs}},
|
||||||
|
)
|
||||||
|
status_get, payload_get = route_request(
|
||||||
|
"/trading/tickets/11",
|
||||||
|
{},
|
||||||
|
dependencies={"trading_ticket_by_id": lambda ticket_id: {"id": ticket_id, "status": "awaiting_confirmation"}},
|
||||||
|
)
|
||||||
|
status_risk, payload_risk = route_request(
|
||||||
|
"/trading/tickets/11/risk-check",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={},
|
||||||
|
dependencies={"trading_ticket_risk_check": lambda *args, **kwargs: {"ticket_id": 11, "allowed": False}},
|
||||||
|
)
|
||||||
|
status_confirm, payload_confirm = route_request(
|
||||||
|
"/trading/tickets/11/confirm",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={"confirmed_by": "alice"},
|
||||||
|
dependencies={"trading_ticket_confirm": lambda *args, **kwargs: {"id": 11, "status": "confirmed"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status_build, 200)
|
||||||
|
self.assertEqual(payload_build["ticket_id"], 11)
|
||||||
|
self.assertEqual(status_get, 200)
|
||||||
|
self.assertEqual(payload_get["status"], "awaiting_confirmation")
|
||||||
|
self.assertEqual(status_risk, 200)
|
||||||
|
self.assertFalse(payload_risk["allowed"])
|
||||||
|
self.assertEqual(status_confirm, 200)
|
||||||
|
self.assertEqual(payload_confirm["status"], "confirmed")
|
||||||
|
|
||||||
|
def test_routes_trading_execute_cancel_sync_and_weekly_review(self) -> None:
|
||||||
|
status_paper, payload_paper = route_request(
|
||||||
|
"/trading/tickets/11/paper-execute",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={"confirmed_by": "alice"},
|
||||||
|
dependencies={"trading_ticket_execute": lambda *args, **kwargs: {"status": "open", "mode": kwargs["execution_mode_expected"]}},
|
||||||
|
)
|
||||||
|
status_semi, payload_semi = route_request(
|
||||||
|
"/trading/tickets/11/semi-auto-submit",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={"confirmed_by": "alice"},
|
||||||
|
dependencies={"trading_ticket_execute": lambda *args, **kwargs: {"status": "pending", "mode": kwargs["execution_mode_expected"]}},
|
||||||
|
)
|
||||||
|
status_cancel, payload_cancel = route_request(
|
||||||
|
"/trading/executions/7/cancel",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={},
|
||||||
|
dependencies={"trading_execution_cancel": lambda execution_id: {"id": execution_id, "status": "cancelled"}},
|
||||||
|
)
|
||||||
|
status_sync, payload_sync = route_request(
|
||||||
|
"/trading/executions/7/sync",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={},
|
||||||
|
dependencies={"trading_execution_sync": lambda execution_id: {"id": execution_id, "status": "closed"}},
|
||||||
|
)
|
||||||
|
status_weekly, payload_weekly = route_request(
|
||||||
|
"/trading/research/weekly-review",
|
||||||
|
{"instrument_id": ["1"]},
|
||||||
|
dependencies={"trading_weekly_review": lambda **kwargs: {"entry_count": 2, "next_week_actions": ["review_risk_process"]}},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status_paper, 200)
|
||||||
|
self.assertEqual(payload_paper["mode"], "paper")
|
||||||
|
self.assertEqual(status_semi, 200)
|
||||||
|
self.assertEqual(payload_semi["mode"], "semi_auto")
|
||||||
|
self.assertEqual(status_cancel, 200)
|
||||||
|
self.assertEqual(payload_cancel["status"], "cancelled")
|
||||||
|
self.assertEqual(status_sync, 200)
|
||||||
|
self.assertEqual(payload_sync["status"], "closed")
|
||||||
|
self.assertEqual(status_weekly, 200)
|
||||||
|
self.assertEqual(payload_weekly["entry_count"], 2)
|
||||||
|
|
||||||
|
def test_routes_trading_journal_generate(self) -> None:
|
||||||
|
status, payload = route_request(
|
||||||
|
"/trading/journal/generate",
|
||||||
|
{},
|
||||||
|
method="POST",
|
||||||
|
body={"execution_id": 7},
|
||||||
|
dependencies={"trading_journal_generate": lambda **kwargs: {"created": 1, **kwargs}},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status, 200)
|
||||||
|
self.assertEqual(payload["execution_id"], 7)
|
||||||
|
|
||||||
def test_routes_health(self) -> None:
|
def test_routes_health(self) -> None:
|
||||||
status, payload = route_request(
|
status, payload = route_request(
|
||||||
"/health",
|
"/health",
|
||||||
@@ -117,6 +279,17 @@ class ApiServerRoutingTests(unittest.TestCase):
|
|||||||
self.assertEqual(status, 200)
|
self.assertEqual(status, 200)
|
||||||
self.assertEqual(payload["items"], [{"id": 1, "framework_execution": False}])
|
self.assertEqual(payload["items"], [{"id": 1, "framework_execution": False}])
|
||||||
|
|
||||||
|
status_doc, payload_doc = route_request(
|
||||||
|
"/trading/journal",
|
||||||
|
{"instrument_id": ["1"], "limit": ["2"]},
|
||||||
|
dependencies={
|
||||||
|
"trading_journal": lambda **kwargs: [{"id": 2, "framework_execution": True}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status_doc, 200)
|
||||||
|
self.assertEqual(payload_doc["items"], [{"id": 2, "framework_execution": True}])
|
||||||
|
|
||||||
def test_routes_trading_journal_summary_query(self) -> None:
|
def test_routes_trading_journal_summary_query(self) -> None:
|
||||||
status, payload = route_request(
|
status, payload = route_request(
|
||||||
"/trading-journal-summary",
|
"/trading-journal-summary",
|
||||||
@@ -129,6 +302,17 @@ class ApiServerRoutingTests(unittest.TestCase):
|
|||||||
self.assertEqual(status, 200)
|
self.assertEqual(status, 200)
|
||||||
self.assertEqual(payload["entry_count"], 3)
|
self.assertEqual(payload["entry_count"], 3)
|
||||||
|
|
||||||
|
status_doc, payload_doc = route_request(
|
||||||
|
"/trading/journal/summary",
|
||||||
|
{"instrument_id": ["1"]},
|
||||||
|
dependencies={
|
||||||
|
"trading_journal_summary": lambda **kwargs: {"entry_count": 4, "next_week_actions": ["review_model_context"]},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(status_doc, 200)
|
||||||
|
self.assertEqual(payload_doc["entry_count"], 4)
|
||||||
|
|
||||||
def test_routes_health(self) -> None:
|
def test_routes_health(self) -> None:
|
||||||
status, payload = route_request(
|
status, payload = route_request(
|
||||||
"/health",
|
"/health",
|
||||||
|
|||||||
@@ -440,6 +440,43 @@ class ExecutionServiceTests(unittest.TestCase):
|
|||||||
self.assertEqual(kwargs["meta"]["execution_ticket_id"], 81)
|
self.assertEqual(kwargs["meta"]["execution_ticket_id"], 81)
|
||||||
self.assertEqual(kwargs["meta"]["confirmed_by"], "paper_user")
|
self.assertEqual(kwargs["meta"]["confirmed_by"], "paper_user")
|
||||||
|
|
||||||
|
def test_submit_confirmed_ticket_creates_execution_and_updates_ticket(self) -> None:
|
||||||
|
repository = Mock()
|
||||||
|
ticket_row = {
|
||||||
|
"id": 91,
|
||||||
|
"signal_id": 1,
|
||||||
|
"execution_mode": "semi_auto",
|
||||||
|
"status": "confirmed",
|
||||||
|
"created_ts": "created",
|
||||||
|
"expires_ts": "later",
|
||||||
|
"confirmed_by": "alice",
|
||||||
|
"confirmed_at": "2026-05-06T00:00:00+00:00",
|
||||||
|
"execution_id": None,
|
||||||
|
"ticket_snapshot": {},
|
||||||
|
}
|
||||||
|
repository.fetch_execution_ticket_by_id.side_effect = lambda ticket_id: dict(ticket_row)
|
||||||
|
repository.fetch_signal_for_execution.return_value = _valid_signal()
|
||||||
|
repository.fetch_latest_execution.return_value = None
|
||||||
|
repository.insert_trade_execution.return_value = 101
|
||||||
|
repository.update_execution_ticket.side_effect = lambda **kwargs: ticket_row.update(
|
||||||
|
{
|
||||||
|
"status": kwargs["status"],
|
||||||
|
"ticket_snapshot": kwargs["ticket_snapshot"],
|
||||||
|
"execution_id": kwargs.get("execution_id"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
service = ExecutionService(repository=repository)
|
||||||
|
service.build_risk_context_for_signal = Mock(return_value=RiskContext())
|
||||||
|
|
||||||
|
result = service.submit_execution_ticket(91)
|
||||||
|
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
assert result is not None
|
||||||
|
self.assertEqual(result.status, "submitted")
|
||||||
|
self.assertEqual(result.execution_id, 101)
|
||||||
|
repository.insert_trade_execution.assert_called_once()
|
||||||
|
repository.update_execution_ticket.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
class ExecutionAuditTests(unittest.TestCase):
|
class ExecutionAuditTests(unittest.TestCase):
|
||||||
def test_classifies_lifecycle_states(self) -> None:
|
def test_classifies_lifecycle_states(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user