Productize strategy backtest surface

This commit is contained in:
Codex
2026-05-21 00:12:52 +08:00
parent 3e5bb071a4
commit 02c31262f9
12 changed files with 406 additions and 14 deletions
+6 -2
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+69
View File
@@ -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
View File
@@ -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">
+31
View File
@@ -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));
}
+23
View File
@@ -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",
+8 -1
View File
@@ -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/",
):