Productize strategy backtest surface
This commit is contained in:
+6
-2
@@ -2,7 +2,7 @@
|
||||
|
||||
## Current Goal
|
||||
|
||||
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase is a dense `Decision Assistant` operator workspace with grouped recall, broader replay baselines, note templates, quick-save shortcuts, read-only market context, and a continuous operator cockpit. The previous `鏈畬鎴愰渶姹傛竻鍗昤 has been archived, and the current documentation baseline now uses `docs/AI_ICT_闇€姹傛枃妗e墿浣欏姛鑳戒笌寤舵湡椤规竻鍗昣2026-05-14_v1.md` to distinguish true remaining enhancement work from explicitly deferred real-trading scope.
|
||||
Automation execution-layer PXX remains closed and its known scope stays frozen. Under explicit user direction, the active product phase has shifted from broad Decision Assistant expansion to mainline correction: follow `docs/AI_ICT_主线纠偏与功能裁剪方案_2026-05-20_v1.md`, keep only Trader Cockpit / Strategy Backtest / Trading Plan / Journal / System Ops in the trader-facing shell, and productize existing ICT backtest evidence before adding any new feature surface.
|
||||
|
||||
## Current Branch
|
||||
|
||||
@@ -128,6 +128,10 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
|
||||
- latest_frontend_navigation_prune_status: implemented_deployed_remote_smoke_passed
|
||||
- latest_frontend_navigation_prune_closure: trader-facing left nav now only exposes Trader Cockpit, Strategy Backtest, Trading Plan, Journal, and System Ops; previous Decision Assistant/runtime/archive/alert/detail panels remain reachable only from System Ops advanced links; stale legacy localStorage advanced-section defaults fall back to Trader Cockpit unless the operator explicitly opens a hash.
|
||||
- latest_frontend_navigation_prune_validation: node --check app/i18n, focused static/docs tests, script static checks, full unit 790 tests, deploy smoke 11 endpoints, remote ops smoke, readonly live analysis smoke, and Playwright browser audit passed; browser audit confirmed zh-CN nav labels, System Ops advanced drill-down, active nav mapping, hidden generic filters on main slices, and zero console errors.
|
||||
- latest_strategy_backtest_slice: readonly_strategy_backtest_surface
|
||||
- latest_strategy_backtest_status: implemented_deployed_remote_smoke_passed
|
||||
- latest_strategy_backtest_closure: Strategy Backtest now has `/trading/backtests`, a read-only workbench page with instrument/run controls, latest stored backtest, quality gate, sample/executed counts, total R, expectancy, profit factor, drawdown, loss streak, equity curve, top sessions, failure reasons, and recent run list. The page catches candle/backtest endpoint failures so one local DB permission issue does not blank the whole workbench.
|
||||
- latest_strategy_backtest_validation: node --check app/i18n, focused API/static tests, `npm run build`, `npm run lint` full unit discovery 791 tests, `git diff --check` with only LF/CRLF warnings, deploy smoke 11 endpoints, remote ops smoke, readonly live analysis smoke, remote `/trading/backtests` field smoke, and browser audit on `http://192.168.3.90:8000/#strategy-backtest` passed. Remote latest run `#489` is correctly blocked by quality gate (`sample_size_too_small`, executed sample, expectancy, profit factor, consecutive-loss reasons) and the Chinese UI renders those as trader-readable reasons.
|
||||
- latest_decision_assistant_workflow_queue_slice: assistant_loop_queue
|
||||
- latest_decision_assistant_workspace_loop_slice: workflow_section_navigation_loop_queue
|
||||
- latest_decision_assistant_learning_focus_slice: learning_focus_queue
|
||||
@@ -820,7 +824,7 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
|
||||
|
||||
## Exact Next Step
|
||||
|
||||
Next continue the real Trader Mode usage loop over future live sessions: record more actual operator feedback until `operator_feedback_count >= 5`, wait for a real executable candidate before creating any paper ticket, only paper-execute a confirmed paper ticket, and convert only real execution-derived journal entries into weekly review evidence. Do not add broad product features until the trader reports which first-screen phrase still fails during live use. Keep `execution_allowed`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged.
|
||||
Next continue Step 4 of `docs/AI_ICT_主线纠偏与功能裁剪方案_2026-05-20_v1.md`: compress Trader Cockpit further so the first screen uses the new backtest evidence to answer only `now / reason / wait / risk / historical edge / boundary`. Do not restart external-context/archive/digest long-tail work, do not create order-submission features, and do not fabricate R2 paper samples; R2 evidence collection remains live but secondary to the current trader-facing mainline correction. Keep `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged.
|
||||
|
||||
## Validation Commands Used Recently
|
||||
|
||||
|
||||
@@ -927,6 +927,8 @@
|
||||
- [x] Existing UI i18n polish: phrase replacement no longer corrupts `not` / `enough`, existing Trader Mode/duty/chart/R2/runtime labels are more readable in Chinese, browser audit dropped visible English remnants from 3396 to 881, and deploy smoke / remote ops / readonly live smoke / gstack console check passed
|
||||
- [x] Existing UI i18n/R2 gate polish follow-up: existing Trader Mode/readiness/shadow-paper labels now translate runtime/gate jargon such as `Auto refresh`, `All Trust`, `Incident Journal`, `Live Readiness`, `paper_ticket_sample_gate`, and `operator_feedback_count`; R2 evidence window now follows canonical `failed_criteria`, remote browser audit has no repeated first-screen Chinese phrases and only 13 active-view English tokens left; deploy smoke, remote ops smoke, read-only live smoke, focused tests, static checks, and full unit (`790`) passed
|
||||
- [x] Session Focus dynamic Chinese label follow-up: existing i18n now translates reported dynamic session-focus labels/values (`ASIA`, `NY_PM`, `status`, `window`, `candidates`, `executable`, `open executions`, `alerts`, `recommendation`, `action hint`, `incomplete`, `observe @ n/a`, `avoid_new_risk`, `review_alerts`); remote browser audit on `/trader-mode#session-focus` passed with no reported English leftovers and no console errors
|
||||
- [x] Trader mainline Step 3 Strategy Backtest productization: added read-only `/trading/backtests`, Strategy Backtest workbench controls, latest-run summary, quality gate explanation, R metrics, equity curve, top sessions, failure reasons, recent run list, Chinese labels for backtest gate reasons, and resilient candle/backtest failure fallback; full unit (`791`), deploy smoke, remote ops smoke, readonly live smoke, remote backtest endpoint smoke, and browser audit passed
|
||||
- [ ] Trader mainline Step 4 cockpit recompression: use Strategy Backtest evidence on Trader Cockpit first screen and reduce it to now / reason / wait / risk / historical edge / boundary
|
||||
- [ ] R2 paper-ticket sample gate is waiting on a real executable candidate; do not fabricate paper tickets from blocked candidates
|
||||
- [ ] R2 operator feedback sample gate still needs 2 more real feedback samples (`operator_feedback_count=3/5`)
|
||||
- [ ] R2 weekly review visible gate still needs a real execution-derived journal/weekly review entry (`weekly_review_entry_count=0`)
|
||||
|
||||
@@ -276,6 +276,14 @@ ICT 书本规则
|
||||
- 曲线和样本表
|
||||
- 回测质量门解释
|
||||
|
||||
状态更新(2026-05-20):
|
||||
|
||||
- [x] 已补 `/trading/backtests` 只读查询面。
|
||||
- [x] 已在 `Strategy Backtest` 页面展示最新已存回测、质量门、样本数、执行样本、总 R、期望 R、盈利因子、最大回撤、连续亏损、权益曲线、优势时段、失败原因、最近回测运行。
|
||||
- [x] 已补中文/英文前端文案,质量门阻塞原因不再直接暴露 snake_case 给交易员。
|
||||
- [x] 已完成本地完整单元测试、远端部署、远端 smoke、只读实盘分析 smoke、远端 backtest endpoint smoke 和浏览器页面审计。
|
||||
- [ ] 样本表的逐笔明细仍可后续增强,但不阻塞 Step 3 作为交易员可读回测页面闭环。
|
||||
|
||||
### Step 4:Trader Cockpit 重新压缩
|
||||
|
||||
Trader Cockpit 不再展示复杂系统状态。
|
||||
@@ -336,4 +344,3 @@ Trader Cockpit 不再展示复杂系统状态。
|
||||
下一轮应停止继续补 UI 文案和复杂面板,直接进入:
|
||||
|
||||
> 回到《ICT交易全书》主线,裁剪导航,产品化 Strategy Backtest,并把 Trader Cockpit 压缩成极简交易员读数。
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
"""Lightweight internal API query helpers."""
|
||||
|
||||
from src.api.backtest import get_recent_backtest_summaries
|
||||
from src.api.backtest import get_recent_backtest_summaries, get_strategy_backtest_surface
|
||||
from src.api.broker_state import get_broker_state
|
||||
from src.api.candles import get_recent_candles
|
||||
from src.api.execution_tickets import get_recent_execution_tickets
|
||||
@@ -141,6 +141,7 @@ __all__ = [
|
||||
"close_execution",
|
||||
"sync_execution",
|
||||
"get_recent_backtest_summaries",
|
||||
"get_strategy_backtest_surface",
|
||||
"dispatch_pending_alerts",
|
||||
"get_alert_deliveries",
|
||||
"get_alert_quality_summary",
|
||||
|
||||
+81
-3
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import text
|
||||
@@ -6,11 +7,39 @@ from src.db.session import SessionLocal
|
||||
from src.services.backtest import evaluate_backtest_quality
|
||||
|
||||
|
||||
def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: int = 10) -> dict:
|
||||
"""Return the read-only Strategy Backtest cockpit surface."""
|
||||
safe_limit = max(1, min(int(limit or 10), 50))
|
||||
items = get_recent_backtest_summaries(instrument_id=instrument_id, limit=safe_limit)
|
||||
latest = items[0] if items else None
|
||||
latest_summary = dict(latest.get("summary") or {}) if latest else {}
|
||||
quality_gate = latest_summary.get("quality_gate") if latest else None
|
||||
return {
|
||||
"instrument_id": instrument_id,
|
||||
"item_count": len(items),
|
||||
"latest": latest,
|
||||
"quality_gate": quality_gate,
|
||||
"operator_summary": _build_operator_summary(latest),
|
||||
"items": items,
|
||||
}
|
||||
|
||||
|
||||
def get_recent_backtest_summaries(instrument_id: Optional[int] = None, limit: int = 10) -> list[dict]:
|
||||
where_clause = "where instrument_id = :instrument_id" if instrument_id is not None else ""
|
||||
sql = text(
|
||||
f"""
|
||||
select id, model_code, instrument_id, timeframe_set, started_ts, ended_ts, config, summary
|
||||
select
|
||||
id,
|
||||
subject_type,
|
||||
model_code,
|
||||
instrument_id,
|
||||
trading_plan_id,
|
||||
candidate_model_code,
|
||||
timeframe_set,
|
||||
started_ts,
|
||||
ended_ts,
|
||||
config,
|
||||
summary
|
||||
from backtest_runs
|
||||
{where_clause}
|
||||
order by started_ts desc
|
||||
@@ -26,7 +55,56 @@ def get_recent_backtest_summaries(instrument_id: Optional[int] = None, limit: in
|
||||
|
||||
|
||||
def normalize_backtest_summary_row(row: dict) -> dict:
|
||||
summary = dict(row.get("summary") or {})
|
||||
summary = _coerce_json_dict(row.get("summary"))
|
||||
if "quality_gate" not in summary:
|
||||
summary["quality_gate"] = evaluate_backtest_quality(summary)
|
||||
return {**row, "summary": summary}
|
||||
return {
|
||||
**row,
|
||||
"timeframe_set": _coerce_json_value(row.get("timeframe_set")),
|
||||
"config": _coerce_json_dict(row.get("config")),
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
|
||||
def _build_operator_summary(latest: Optional[dict]) -> dict:
|
||||
if not latest:
|
||||
return {
|
||||
"status": "missing_backtest",
|
||||
"decision": "do_not_trust_strategy_yet",
|
||||
"reason": "no_stored_no_lookahead_replay",
|
||||
"next_action": "run_backtest_replay_no_lookahead",
|
||||
}
|
||||
summary = dict(latest.get("summary") or {})
|
||||
quality_gate = dict(summary.get("quality_gate") or {})
|
||||
passed = bool(quality_gate.get("passed"))
|
||||
if passed:
|
||||
status = "quality_gate_passed"
|
||||
decision = "eligible_for_trading_plan_review"
|
||||
reason = "sample_and_risk_metrics_passed"
|
||||
else:
|
||||
status = "quality_gate_blocked"
|
||||
decision = "do_not_promote_strategy"
|
||||
reasons = quality_gate.get("reasons") or []
|
||||
reason = str(reasons[0]) if reasons else "quality_gate_not_passed"
|
||||
return {
|
||||
"status": status,
|
||||
"decision": decision,
|
||||
"reason": reason,
|
||||
"next_action": "review_failure_reasons_and_session_edges",
|
||||
}
|
||||
|
||||
|
||||
def _coerce_json_dict(value: object) -> dict:
|
||||
parsed = _coerce_json_value(value)
|
||||
return dict(parsed) if isinstance(parsed, dict) else {}
|
||||
|
||||
|
||||
def _coerce_json_value(value: object) -> object:
|
||||
if value is None:
|
||||
return {}
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
return value
|
||||
return value
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Optional
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from src.api.broker_state import get_broker_state
|
||||
from src.api.backtest import get_strategy_backtest_surface
|
||||
from src.api.candles import get_recent_candles
|
||||
from src.api.execution_tickets import get_recent_execution_tickets
|
||||
from src.api.health import get_health
|
||||
@@ -219,6 +220,7 @@ def build_api_dependencies() -> dict:
|
||||
"trading_journal_generate": generate_trading_journal,
|
||||
"trading_weekly_review": get_weekly_review,
|
||||
"trading_plans": get_recent_trading_plans,
|
||||
"trading_backtests": get_strategy_backtest_surface,
|
||||
"training_cases": get_training_cases,
|
||||
"training_case": get_training_case,
|
||||
"training_export_cases": export_training_case_package,
|
||||
@@ -836,6 +838,10 @@ def route_request(
|
||||
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 path == "/trading/backtests":
|
||||
instrument_id = _get_optional_int(query, "instrument_id")
|
||||
limit = _get_int(query, "limit", 10)
|
||||
return 200, deps["trading_backtests"](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:
|
||||
|
||||
+160
-2
@@ -51,6 +51,10 @@ const tradeCandidatesPanel = document.getElementById('tradeCandidatesPanel');
|
||||
const executionTicketsPanel = document.getElementById('executionTicketsPanel');
|
||||
const alertHistoryPanel = document.getElementById('alertHistoryPanel');
|
||||
const alertQualityPanel = document.getElementById('alertQualityPanel');
|
||||
const strategyBacktestPanel = document.getElementById('strategyBacktestPanel');
|
||||
const backtestInstrumentIdInput = document.getElementById('backtestInstrumentId');
|
||||
const backtestLimitInput = document.getElementById('backtestLimit');
|
||||
const backtestRefreshBtn = document.getElementById('backtestRefreshBtn');
|
||||
const visualReviewsPanel = document.getElementById('visualReviewsPanel');
|
||||
const positionManagementPanel = document.getElementById('positionManagementPanel');
|
||||
const operatorConsolePanel = document.getElementById('operatorConsolePanel');
|
||||
@@ -5481,6 +5485,150 @@ function renderTrustScore(payload) {
|
||||
trustScorePanel.appendChild(grid);
|
||||
}
|
||||
|
||||
function renderStrategyBacktests(payload) {
|
||||
if (!strategyBacktestPanel) return;
|
||||
clearNode(strategyBacktestPanel);
|
||||
if (payload?.unavailable) {
|
||||
strategyBacktestPanel.appendChild(createElement('div', 'empty-state error-state', 'Strategy backtest data unavailable.'));
|
||||
return;
|
||||
}
|
||||
const latest = payload?.latest || (payload?.items || [])[0] || null;
|
||||
if (!latest) {
|
||||
strategyBacktestPanel.appendChild(createElement('div', 'empty-state', 'No stored backtest runs yet.'));
|
||||
strategyBacktestPanel.appendChild(
|
||||
buildInfoGrid([
|
||||
['Next Action', 'Run replay no-lookahead backtest'],
|
||||
['Boundary', 'Read-only analysis; no order submission'],
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
const summary = latest.summary || {};
|
||||
const qualityGate = payload?.quality_gate || summary.quality_gate || {};
|
||||
const passed = Boolean(qualityGate.passed);
|
||||
const operatorSummary = payload?.operator_summary || {};
|
||||
|
||||
strategyBacktestPanel.appendChild(
|
||||
buildInfoGrid([
|
||||
['Latest Backtest', `#${latest.id ?? 'n/a'} ${latest.model_code || ''}`.trim()],
|
||||
['Quality Gate', passed ? 'Passed' : 'Blocked'],
|
||||
['Decision', formatBacktestDecision(operatorSummary.decision)],
|
||||
['Reason', formatBacktestDecision(operatorSummary.reason)],
|
||||
['Instrument ID', latest.instrument_id ?? 'n/a'],
|
||||
['Timeframe', formatJsonInline(latest.timeframe_set || {})],
|
||||
['Started', formatTimestamp(latest.started_ts)],
|
||||
['Ended', formatTimestamp(latest.ended_ts)],
|
||||
['Mode', latest.config?.mode || latest.config?.backtest_mode || 'replay_no_lookahead'],
|
||||
])
|
||||
);
|
||||
|
||||
const cards = createElement('div', 'cards');
|
||||
cards.appendChild(
|
||||
buildMetricCard('Sample Count', String(summary.total_signals ?? summary.candidate_count ?? 0), 'total replay samples')
|
||||
);
|
||||
cards.appendChild(
|
||||
buildMetricCard('Executed Samples', String(summary.executed_signals ?? 0), `executed rate ${formatPercent(summary.executed_rate)}`)
|
||||
);
|
||||
cards.appendChild(
|
||||
buildMetricCard('Total R', formatNumber(summary.total_r, 2), `expectancy ${formatNumber(summary.expectancy_r, 2)} R`)
|
||||
);
|
||||
cards.appendChild(
|
||||
buildMetricCard('Profit Factor', formatNumber(summary.profit_factor_r, 2), `win rate ${formatPercent(summary.win_rate)}`)
|
||||
);
|
||||
cards.appendChild(
|
||||
buildMetricCard('Max Drawdown R', formatNumber(summary.max_drawdown_r, 2), `loss streak ${summary.max_consecutive_losses ?? 0}`)
|
||||
);
|
||||
cards.appendChild(
|
||||
buildMetricCard('Quality Gate', passed ? 'Passed' : 'Blocked', formatList((qualityGate.reasons || []).map(formatBacktestDecision), 'No gate blockers'))
|
||||
);
|
||||
strategyBacktestPanel.appendChild(cards);
|
||||
|
||||
strategyBacktestPanel.appendChild(buildBacktestCurve(summary.cumulative_r_curve || []));
|
||||
|
||||
const detailGrid = createElement('div', 'cards compact-cards');
|
||||
detailGrid.appendChild(
|
||||
buildItemCard(
|
||||
'Top Sessions',
|
||||
'review',
|
||||
topBacktestBuckets(summary.session_performance || summary.by_session || {}).map((item) => [
|
||||
item.key,
|
||||
`${formatNumber(item.expectancy_r, 2)} R / ${formatPercent(item.win_rate)} / ${item.count ?? 0} samples`,
|
||||
])
|
||||
)
|
||||
);
|
||||
detailGrid.appendChild(
|
||||
buildItemCard(
|
||||
'Failure Reasons',
|
||||
'review',
|
||||
topCountEntries(summary.failure_reasons || {}).map((item) => [formatBacktestDecision(item.key), String(item.value)])
|
||||
)
|
||||
);
|
||||
detailGrid.appendChild(
|
||||
buildItemCard(
|
||||
'Recent Backtest Runs',
|
||||
`${payload?.item_count ?? (payload?.items || []).length} runs`,
|
||||
(payload?.items || []).slice(0, 5).map((item) => [
|
||||
`#${item.id}`,
|
||||
`${item.model_code || 'model'} / ${formatTimestamp(item.started_ts)} / ${item.summary?.quality_gate?.passed ? 'Passed' : 'Blocked'}`,
|
||||
])
|
||||
)
|
||||
);
|
||||
strategyBacktestPanel.appendChild(detailGrid);
|
||||
}
|
||||
|
||||
function buildBacktestCurve(curve) {
|
||||
const panel = createElement('div', 'backtest-curve-panel');
|
||||
panel.appendChild(createElement('div', 'assistant-section-title', 'Equity Curve'));
|
||||
if (!Array.isArray(curve) || !curve.length) {
|
||||
panel.appendChild(createElement('div', 'empty-state', 'No equity curve points recorded.'));
|
||||
return panel;
|
||||
}
|
||||
const numbers = curve.map((value) => Number(value)).filter((value) => Number.isFinite(value));
|
||||
if (!numbers.length) {
|
||||
panel.appendChild(createElement('div', 'empty-state', 'No equity curve points recorded.'));
|
||||
return panel;
|
||||
}
|
||||
const min = Math.min(...numbers);
|
||||
const max = Math.max(...numbers);
|
||||
const range = Math.max(1, max - min);
|
||||
const chart = createElement('div', 'backtest-curve');
|
||||
numbers.slice(-80).forEach((value) => {
|
||||
const bar = createElement('span', `backtest-curve-bar${value < 0 ? ' is-negative' : ''}`);
|
||||
bar.style.height = `${16 + ((value - min) / range) * 64}px`;
|
||||
bar.title = `${formatNumber(value, 2)} R`;
|
||||
chart.appendChild(bar);
|
||||
});
|
||||
panel.appendChild(chart);
|
||||
panel.appendChild(
|
||||
buildInfoGrid([
|
||||
['Curve Points', String(numbers.length)],
|
||||
['Last Total R', formatNumber(numbers[numbers.length - 1], 2)],
|
||||
['Best Total R', formatNumber(max, 2)],
|
||||
['Worst Total R', formatNumber(min, 2)],
|
||||
])
|
||||
);
|
||||
return panel;
|
||||
}
|
||||
|
||||
function topBacktestBuckets(bucketMap) {
|
||||
return Object.entries(bucketMap || {})
|
||||
.map(([key, value]) => ({ key, ...(value || {}) }))
|
||||
.sort((left, right) => Number(right.expectancy_r || 0) - Number(left.expectancy_r || 0))
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
function topCountEntries(countMap) {
|
||||
return Object.entries(countMap || {})
|
||||
.map(([key, value]) => ({ key, value: Number(value || 0) }))
|
||||
.sort((left, right) => right.value - left.value)
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
function formatBacktestDecision(value) {
|
||||
const text = String(value || 'n/a').replace(/_/g, ' ');
|
||||
return text.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
function renderLiveReadiness(payload) {
|
||||
clearNode(liveReadinessPanel);
|
||||
const okxSandboxPreflight = payload?.okx_sandbox_preflight_summary || null;
|
||||
@@ -6485,6 +6633,7 @@ function showErrorPanel(message) {
|
||||
launchReadinessPanel,
|
||||
sessionFocusPanel,
|
||||
preMarketPlanPanel,
|
||||
strategyBacktestPanel,
|
||||
tradeCandidatesPanel,
|
||||
executionTicketsPanel,
|
||||
alertHistoryPanel,
|
||||
@@ -6521,6 +6670,7 @@ function renderDashboardData({
|
||||
liveReadiness,
|
||||
launchReadiness,
|
||||
sessionView,
|
||||
strategyBacktests,
|
||||
tradeCandidates,
|
||||
executionTickets,
|
||||
tradingExecutions,
|
||||
@@ -6541,6 +6691,7 @@ function renderDashboardData({
|
||||
renderLiveReadiness(liveReadiness);
|
||||
renderLaunchReadiness(launchReadiness);
|
||||
renderSessionFocus(sessionView);
|
||||
renderStrategyBacktests(strategyBacktests);
|
||||
renderPreMarketPlan(tradingPlan);
|
||||
renderCandidates(tradeCandidates.items || []);
|
||||
renderTickets(executionTickets.items || []);
|
||||
@@ -6559,12 +6710,14 @@ async function refreshDashboard() {
|
||||
const instrumentId = instrumentIdInput.value || '1';
|
||||
const timeframe = timeframeInput.value || '1m';
|
||||
const limit = limitInput.value || '20';
|
||||
const backtestInstrumentId = backtestInstrumentIdInput?.value || instrumentId;
|
||||
const backtestLimit = backtestLimitInput?.value || '10';
|
||||
const profileKey = assistantDigestProfileKey || assistantPreferenceDraft?.profile_key || 'local_operator';
|
||||
|
||||
try {
|
||||
const [health, candles, tradingPlan, marketState, runtimeStatus, runtimeIncidents, maintenanceSummary, decisionPacket, decisionAlerts, alertQualitySummary, operatorDigest, trustScore, liveReadiness, launchReadiness, sessionView, tradeCandidates, executionTickets, tradingExecutions, journalSummary, operatorNotes, reviewPacket, journalItems, weeklyReview] = await Promise.all([
|
||||
const [health, candles, tradingPlan, marketState, runtimeStatus, runtimeIncidents, maintenanceSummary, decisionPacket, decisionAlerts, alertQualitySummary, operatorDigest, trustScore, liveReadiness, launchReadiness, sessionView, strategyBacktests, tradeCandidates, executionTickets, tradingExecutions, journalSummary, operatorNotes, reviewPacket, journalItems, weeklyReview] = await Promise.all([
|
||||
fetchJson('/health'),
|
||||
fetchJson(`/candles?${queryString({ instrument_id: instrumentId, timeframe, limit })}`),
|
||||
fetchJson(`/candles?${queryString({ instrument_id: instrumentId, timeframe, limit })}`).catch(() => ({ items: [] })),
|
||||
fetchJson(`/trading/plans/latest?${queryString({ instrument_id: instrumentId })}`).catch(() => null),
|
||||
fetchJson(`/trading/market-state?${queryString({ instrument_id: instrumentId, timeframe })}`).catch(() => ({ market_state: {} })),
|
||||
fetchJson('/trading/runtime-status').catch(() => ({ scheduler_state: 'unavailable', tasks: {}, current_blockers: ['runtime_status_unavailable'] })),
|
||||
@@ -6578,6 +6731,7 @@ async function refreshDashboard() {
|
||||
fetchJson(`/trading/live-readiness?${queryString({ instrument_id: instrumentId, timeframe })}`).catch(() => ({ ready_for_real_live: false, blocking_reasons: ['live_readiness_unavailable'], checklist: [] })),
|
||||
fetchJson(`/trading/launch-readiness?${queryString({ instrument_id: instrumentId, timeframe })}`).catch(() => ({ assisted_trading_ready: false, real_live_ready: false, blocking_reasons: ['launch_readiness_unavailable'], warning_reasons: [], checklist: [], next_actions: [] })),
|
||||
fetchJson(`/trading/session-view?${queryString({ instrument_id: instrumentId, limit: 12 })}`).catch(() => ({ items: [] })),
|
||||
fetchJson(`/trading/backtests?${queryString({ instrument_id: backtestInstrumentId, limit: backtestLimit })}`).catch(() => ({ unavailable: true, items: [], latest: null })),
|
||||
fetchJson(`/trading/candidates?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
|
||||
fetchJson(`/trading/tickets?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
|
||||
fetchJson(`/trading/executions?${queryString({ instrument_id: instrumentId, limit })}`).catch(() => ({ items: [] })),
|
||||
@@ -6610,6 +6764,7 @@ async function refreshDashboard() {
|
||||
liveReadiness,
|
||||
launchReadiness,
|
||||
sessionView,
|
||||
strategyBacktests,
|
||||
tradeCandidates,
|
||||
executionTickets,
|
||||
tradingExecutions,
|
||||
@@ -6636,6 +6791,9 @@ function configureAutoRefresh() {
|
||||
}
|
||||
|
||||
refreshBtn.addEventListener('click', refreshDashboard);
|
||||
backtestRefreshBtn?.addEventListener('click', refreshDashboard);
|
||||
backtestInstrumentIdInput?.addEventListener('change', refreshDashboard);
|
||||
backtestLimitInput?.addEventListener('change', refreshDashboard);
|
||||
executionStatusFilterInput.addEventListener('change', refreshDashboard);
|
||||
sessionFilterInput.addEventListener('change', refreshDashboard);
|
||||
searchTextInput.addEventListener('input', refreshDashboard);
|
||||
|
||||
@@ -41,6 +41,46 @@
|
||||
'Replay / no-lookahead': '重放 / 无未来函数',
|
||||
'Primary Metrics': '核心指标',
|
||||
'Expectancy R, profit factor, drawdown': '期望 R、盈利因子、回撤',
|
||||
'Quality Gate': '质量门',
|
||||
'Sample, expectancy, drawdown': '样本、期望、回撤',
|
||||
'Stored Runs': '已存回测',
|
||||
'Latest replay evidence': '最新重放证据',
|
||||
'Strategy Backtest Filters': '策略回测筛选',
|
||||
'Recent Runs': '最近回测',
|
||||
'Reload Backtests': '重新加载回测',
|
||||
'Strategy backtest data unavailable.': '策略回测数据不可用。',
|
||||
'No stored backtest runs yet.': '还没有已保存的回测运行。',
|
||||
'Run replay no-lookahead backtest': '运行无未来函数重放回测',
|
||||
'Latest Backtest': '最新回测',
|
||||
'Decision': '判断',
|
||||
'Reason': '原因',
|
||||
'Started': '开始时间',
|
||||
'Ended': '结束时间',
|
||||
'Mode': '模式',
|
||||
'Passed': '已通过',
|
||||
'Blocked': '被阻塞',
|
||||
'Sample Count': '样本数量',
|
||||
'Executed Samples': '执行样本',
|
||||
'total replay samples': '重放样本总数',
|
||||
'Total R': '总 R',
|
||||
'Profit Factor': '盈利因子',
|
||||
'Max Drawdown R': '最大回撤 R',
|
||||
'No gate blockers': '无质量门阻塞',
|
||||
'Equity Curve': '权益曲线',
|
||||
'No equity curve points recorded.': '未记录权益曲线点。',
|
||||
'Curve Points': '曲线点数',
|
||||
'Last Total R': '最新总 R',
|
||||
'Best Total R': '最佳总 R',
|
||||
'Worst Total R': '最差总 R',
|
||||
'Top Sessions': '优势时段',
|
||||
'Failure Reasons': '失败原因',
|
||||
'Recent Backtest Runs': '最近回测运行',
|
||||
'Eligible For Trading Plan Review': '可进入交易计划复核',
|
||||
'Do Not Promote Strategy': '不要晋级策略',
|
||||
'Do Not Trust Strategy Yet': '暂不信任策略',
|
||||
'Quality Gate Not Passed': '质量门未通过',
|
||||
'Sample And Risk Metrics Passed': '样本与风险指标已通过',
|
||||
'Review Failure Reasons And Session Edges': '复核失败原因与时段优势',
|
||||
'Next Cut': '下一刀',
|
||||
'Trader-facing controls': '交易员控制面',
|
||||
'Instrument ID': '品种 ID',
|
||||
@@ -450,6 +490,35 @@
|
||||
['avoid @', '回避 @'],
|
||||
['review_alerts', '复核提醒'],
|
||||
['avoid_new_risk', '回避新增风险'],
|
||||
['bias_tf', '方向周期'],
|
||||
['entry_then_outcome', '入场后结果评估'],
|
||||
['Entry Then Outcome', '入场后结果评估'],
|
||||
['Sample Size Too Small', '样本数量太少'],
|
||||
['Executed Sample Too Small', '执行样本太少'],
|
||||
['Expectancy Below Threshold', '期望值低于门槛'],
|
||||
['Profit Factor Below Threshold', '盈利因子低于门槛'],
|
||||
['Consecutive Losses Above Threshold', '连续亏损超出门槛'],
|
||||
['Stopped Out', '止损出场'],
|
||||
['Outside Session Window', '不在允许时段'],
|
||||
['Same Candle Entry Exit Ambiguous', '同一K线入场出场不清晰'],
|
||||
['Blocked', '被阻塞'],
|
||||
['Passed', '已通过'],
|
||||
['executed rate', '执行率'],
|
||||
['expectancy', '期望'],
|
||||
['win rate', '胜率'],
|
||||
['loss streak', '连续亏损'],
|
||||
['samples', '样本'],
|
||||
['runs', '次运行'],
|
||||
['replay_no_lookahead', '无未来函数重放'],
|
||||
['quality gate passed', '质量门已通过'],
|
||||
['quality gate blocked', '质量门被阻塞'],
|
||||
['quality gate not passed', '质量门未通过'],
|
||||
['sample and risk metrics passed', '样本与风险指标已通过'],
|
||||
['eligible for trading plan review', '可进入交易计划复核'],
|
||||
['do not promote strategy', '不要晋级策略'],
|
||||
['do not trust strategy yet', '暂不信任策略'],
|
||||
['run backtest replay no lookahead', '运行无未来函数重放回测'],
|
||||
['missing backtest', '缺少回测'],
|
||||
['incomplete', '未完成'],
|
||||
['open executions', '未平仓执行'],
|
||||
['action hint', '动作提示'],
|
||||
|
||||
+10
-4
@@ -235,14 +235,20 @@
|
||||
<div>Replay / no-lookahead</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="metric">Primary Metrics</div>
|
||||
<div>Expectancy R, profit factor, drawdown</div>
|
||||
<div class="metric">Quality Gate</div>
|
||||
<div>Sample, expectancy, drawdown</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="metric">Next Cut</div>
|
||||
<div>Trader-facing controls</div>
|
||||
<div class="metric">Stored Runs</div>
|
||||
<div>Latest replay evidence</div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="controls strategy-backtest-controls" aria-label="Strategy Backtest Filters">
|
||||
<label>Instrument ID <input id="backtestInstrumentId" type="number" value="1" min="1" /></label>
|
||||
<label>Recent Runs <input id="backtestLimit" type="number" value="10" min="1" max="50" /></label>
|
||||
<button id="backtestRefreshBtn" type="button">Reload Backtests</button>
|
||||
</section>
|
||||
<div id="strategyBacktestPanel" class="detail-panel"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel panel-wide workspace-section" data-workbench-section="system-ops" data-workbench-label="System Ops">
|
||||
|
||||
@@ -374,6 +374,37 @@ th { color: #9db0d3; }
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
gap: 9px;
|
||||
}
|
||||
.strategy-backtest-controls {
|
||||
margin: 14px 0 12px;
|
||||
}
|
||||
.compact-cards {
|
||||
margin-top: 12px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
.backtest-curve-panel {
|
||||
margin-top: 14px;
|
||||
}
|
||||
.backtest-curve {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 3px;
|
||||
min-height: 92px;
|
||||
padding: 10px;
|
||||
border: 1px solid #24304f;
|
||||
border-radius: 8px;
|
||||
background: #0b1228;
|
||||
}
|
||||
.backtest-curve-bar {
|
||||
flex: 1 1 4px;
|
||||
min-width: 4px;
|
||||
max-width: 18px;
|
||||
border-radius: 5px 5px 0 0;
|
||||
background: #4f7cff;
|
||||
opacity: 0.88;
|
||||
}
|
||||
.backtest-curve-bar.is-negative {
|
||||
background: #eab308;
|
||||
}
|
||||
.assistant-scan-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
|
||||
}
|
||||
|
||||
@@ -79,6 +79,29 @@ class ApiServerRoutingTests(unittest.TestCase):
|
||||
self.assertEqual(status_alerts, 200)
|
||||
self.assertEqual(payload_alerts["items"], [{"id": 1, "alert_type": "candidate_actionable"}])
|
||||
|
||||
def test_routes_trading_backtests(self) -> None:
|
||||
captured = {}
|
||||
|
||||
def fake_backtests(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {
|
||||
"instrument_id": kwargs["instrument_id"],
|
||||
"latest": {"id": 7, "summary": {"quality_gate": {"passed": True}}},
|
||||
"quality_gate": {"passed": True},
|
||||
"items": [{"id": 7}],
|
||||
}
|
||||
|
||||
status, payload = route_request(
|
||||
"/trading/backtests",
|
||||
{"instrument_id": ["1"], "limit": ["3"]},
|
||||
dependencies={"trading_backtests": fake_backtests},
|
||||
)
|
||||
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(captured, {"instrument_id": 1, "limit": 3})
|
||||
self.assertEqual(payload["latest"]["id"], 7)
|
||||
self.assertTrue(payload["quality_gate"]["passed"])
|
||||
|
||||
def test_routes_trading_alert_quality_summary(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/alerts/quality-summary",
|
||||
|
||||
@@ -156,6 +156,9 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
'data-workbench-section="journal"',
|
||||
'data-workbench-section="system-ops"',
|
||||
'id="traderStatusPanel"',
|
||||
'id="strategyBacktestPanel"',
|
||||
'id="backtestInstrumentId"',
|
||||
'id="backtestRefreshBtn"',
|
||||
'data-system-ops-target="decision-assistant"',
|
||||
'data-workbench-section="decision-assistant"',
|
||||
'data-workbench-section="readiness"',
|
||||
@@ -202,6 +205,7 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
"data-active-workbench-section",
|
||||
".language-switcher",
|
||||
".system-ops-link-grid",
|
||||
".backtest-curve",
|
||||
):
|
||||
self.assertIn(expected, styles_css)
|
||||
|
||||
@@ -221,6 +225,8 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
"交易员模式",
|
||||
"交易员驾驶舱",
|
||||
"策略回测",
|
||||
"质量门",
|
||||
"重新加载回测",
|
||||
"系统运维",
|
||||
"决策助手",
|
||||
"仅观察",
|
||||
@@ -299,6 +305,7 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
"/trading/runtime-incidents",
|
||||
"/trading/maintenance-summary",
|
||||
"/trading/visual-watch/cleanup-captures",
|
||||
"/trading/backtests",
|
||||
"/trading/tickets",
|
||||
"/trading/executions",
|
||||
"/trading/journal",
|
||||
@@ -313,7 +320,7 @@ class ApiStaticServerTests(unittest.TestCase):
|
||||
"/execution-tickets",
|
||||
"/trading-journal",
|
||||
"/trading-journal-summary",
|
||||
"/backtests",
|
||||
"fetchJson('/backtests",
|
||||
"fetchJson('/executions",
|
||||
"/training/",
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user