mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-11 06:46:59 +08:00
Dev backtest (#1134)
This commit is contained in:
1080
web/src/App.tsx
1080
web/src/App.tsx
File diff suppressed because it is too large
Load Diff
1273
web/src/components/BacktestPage.tsx
Normal file
1273
web/src/components/BacktestPage.tsx
Normal file
File diff suppressed because it is too large
Load Diff
177
web/src/components/DecisionCard.tsx
Normal file
177
web/src/components/DecisionCard.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { useState } from 'react'
|
||||
import type { DecisionRecord } from '../types'
|
||||
import { t, type Language } from '../i18n/translations'
|
||||
|
||||
interface DecisionCardProps {
|
||||
decision: DecisionRecord
|
||||
language: Language
|
||||
}
|
||||
|
||||
export function DecisionCard({ decision, language }: DecisionCardProps) {
|
||||
const [showInputPrompt, setShowInputPrompt] = useState(false)
|
||||
const [showCoT, setShowCoT] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded p-5 transition-all duration-300 hover:translate-y-[-2px]"
|
||||
style={{
|
||||
border: '1px solid #2B3139',
|
||||
background: '#1E2329',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{t('cycle', language)} #{decision.cycle_number}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{new Date(decision.timestamp).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="px-3 py-1 rounded text-xs font-bold"
|
||||
style={
|
||||
decision.success
|
||||
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81' }
|
||||
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }
|
||||
}
|
||||
>
|
||||
{t(decision.success ? 'success' : 'failed', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{decision.input_prompt && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowInputPrompt(!showInputPrompt)}
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
style={{ color: '#60a5fa' }}
|
||||
>
|
||||
<span className="font-semibold">
|
||||
📥 {t('inputPrompt', language)}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{showInputPrompt ? t('collapse', language) : t('expand', language)}
|
||||
</span>
|
||||
</button>
|
||||
{showInputPrompt && (
|
||||
<div
|
||||
className="mt-2 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
{decision.input_prompt}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{decision.cot_trace && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowCoT(!showCoT)}
|
||||
className="flex items-center gap-2 text-sm transition-colors"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
<span className="font-semibold">
|
||||
📤 {t('aiThinking', language)}
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
{showCoT ? t('collapse', language) : t('expand', language)}
|
||||
</span>
|
||||
</button>
|
||||
{showCoT && (
|
||||
<div
|
||||
className="mt-2 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
{decision.cot_trace}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{decision.decisions && decision.decisions.length > 0 && (
|
||||
<div className="space-y-2 mb-3">
|
||||
{decision.decisions.map((action, index) => (
|
||||
<div
|
||||
key={`${action.symbol}-${index}`}
|
||||
className="flex items-center gap-2 text-sm rounded px-3 py-2"
|
||||
style={{ background: '#0B0E11' }}
|
||||
>
|
||||
<span
|
||||
className="font-mono font-bold"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{action.symbol}
|
||||
</span>
|
||||
<span
|
||||
className="px-2 py-0.5 rounded text-xs font-bold"
|
||||
style={
|
||||
action.action.includes('open')
|
||||
? {
|
||||
background: 'rgba(96, 165, 250, 0.1)',
|
||||
color: '#60a5fa',
|
||||
}
|
||||
: action.action.includes('close')
|
||||
? {
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(248, 113, 113, 0.1)',
|
||||
color: '#F87171',
|
||||
}
|
||||
}
|
||||
>
|
||||
{action.action}
|
||||
</span>
|
||||
{action.reasoning && (
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: '#848E9C', flex: 1 }}
|
||||
>
|
||||
{action.reasoning}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{decision.execution_log && decision.execution_log.length > 0 && (
|
||||
<div
|
||||
className="rounded p-3 text-xs font-mono space-y-1"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
{decision.execution_log.map((log, index) => (
|
||||
<div key={`${log}-${index}`} style={{ color: '#EAECEF' }}>
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{decision.error_message && (
|
||||
<div
|
||||
className="rounded p-3 mt-3 text-sm"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
border: '1px solid rgba(246, 70, 93, 0.4)',
|
||||
color: '#F6465D',
|
||||
}}
|
||||
>
|
||||
❌ {decision.error_message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,16 +6,25 @@ import { t, type Language } from '../i18n/translations'
|
||||
import { Container } from './Container'
|
||||
import { useSystemConfig } from '../hooks/useSystemConfig'
|
||||
|
||||
type Page =
|
||||
| 'competition'
|
||||
| 'traders'
|
||||
| 'trader'
|
||||
| 'backtest'
|
||||
| 'faq'
|
||||
| 'login'
|
||||
| 'register'
|
||||
|
||||
interface HeaderBarProps {
|
||||
onLoginClick?: () => void
|
||||
isLoggedIn?: boolean
|
||||
isHomePage?: boolean
|
||||
currentPage?: string
|
||||
currentPage?: Page
|
||||
language?: Language
|
||||
onLanguageChange?: (lang: Language) => void
|
||||
user?: { email: string } | null
|
||||
onLogout?: () => void
|
||||
onPageChange?: (page: string) => void
|
||||
onPageChange?: (page: Page) => void
|
||||
}
|
||||
|
||||
export default function HeaderBar({
|
||||
@@ -207,6 +216,47 @@ export default function HeaderBar({
|
||||
{t('dashboardNav', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (onPageChange) {
|
||||
onPageChange('backtest')
|
||||
}
|
||||
navigate('/backtest')
|
||||
}}
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color:
|
||||
currentPage === 'backtest'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'backtest') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'backtest') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currentPage === 'backtest' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Backtest
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (onPageChange) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const translations = {
|
||||
details: 'Details',
|
||||
tradingPanel: 'Trading Panel',
|
||||
competition: 'Competition',
|
||||
backtest: 'Backtest',
|
||||
running: 'RUNNING',
|
||||
stopped: 'STOPPED',
|
||||
adminMode: 'Admin Mode',
|
||||
@@ -82,6 +83,168 @@ export const translations = {
|
||||
currentGap: 'Current Gap',
|
||||
count: '{count} pts',
|
||||
|
||||
// Backtest Page
|
||||
backtestPage: {
|
||||
title: 'Backtest Lab',
|
||||
subtitle: 'Pick a model + time range to replay the full AI decision loop.',
|
||||
start: 'Start Backtest',
|
||||
starting: 'Starting...',
|
||||
quickRanges: {
|
||||
h24: '24h',
|
||||
d3: '3d',
|
||||
d7: '7d',
|
||||
},
|
||||
actions: {
|
||||
pause: 'Pause',
|
||||
resume: 'Resume',
|
||||
stop: 'Stop',
|
||||
},
|
||||
states: {
|
||||
running: 'Running',
|
||||
paused: 'Paused',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
liquidated: 'Liquidated',
|
||||
},
|
||||
form: {
|
||||
aiModelLabel: 'AI Model',
|
||||
selectAiModel: 'Select AI model',
|
||||
providerLabel: 'Provider',
|
||||
statusLabel: 'Status',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
noModelWarning:
|
||||
'Please add and enable an AI model on the Model Config page first.',
|
||||
runIdLabel: 'Run ID',
|
||||
runIdPlaceholder: 'Leave blank to auto-generate',
|
||||
decisionTfLabel: 'Decision TF',
|
||||
cadenceLabel: 'Decision cadence (bars)',
|
||||
timeRangeLabel: 'Time range',
|
||||
symbolsLabel: 'Symbols (comma-separated)',
|
||||
customTfPlaceholder: 'Custom TFs (comma separated, e.g. 2h,6h)',
|
||||
initialBalanceLabel: 'Initial balance (USDT)',
|
||||
feeLabel: 'Fee (bps)',
|
||||
slippageLabel: 'Slippage (bps)',
|
||||
btcEthLeverageLabel: 'BTC/ETH leverage (x)',
|
||||
altcoinLeverageLabel: 'Altcoin leverage (x)',
|
||||
fillPolicies: {
|
||||
nextOpen: 'Next open',
|
||||
barVwap: 'Bar VWAP',
|
||||
midPrice: 'Mid price',
|
||||
},
|
||||
promptPresets: {
|
||||
baseline: 'Baseline',
|
||||
aggressive: 'Aggressive',
|
||||
conservative: 'Conservative',
|
||||
scalping: 'Scalping',
|
||||
},
|
||||
cacheAiLabel: 'Reuse AI cache',
|
||||
replayOnlyLabel: 'Replay only',
|
||||
overridePromptLabel: 'Use only custom prompt',
|
||||
customPromptLabel: 'Custom prompt (optional)',
|
||||
customPromptPlaceholder:
|
||||
'Append or fully customize the strategy prompt',
|
||||
},
|
||||
runList: {
|
||||
title: 'Runs',
|
||||
count: 'Total {count} records',
|
||||
},
|
||||
filters: {
|
||||
allStates: 'All states',
|
||||
searchPlaceholder: 'Run ID / label',
|
||||
},
|
||||
tableHeaders: {
|
||||
runId: 'Run ID',
|
||||
label: 'Label',
|
||||
state: 'State',
|
||||
progress: 'Progress',
|
||||
equity: 'Equity',
|
||||
lastError: 'Last Error',
|
||||
updated: 'Updated',
|
||||
},
|
||||
emptyStates: {
|
||||
noRuns: 'No runs yet',
|
||||
selectRun: 'Select a run to view details',
|
||||
},
|
||||
detail: {
|
||||
tfAndSymbols: 'TF: {tf} · Symbols {count}',
|
||||
labelPlaceholder: 'Label note',
|
||||
saveLabel: 'Save',
|
||||
deleteLabel: 'Delete',
|
||||
exportLabel: 'Export',
|
||||
errorLabel: 'Error',
|
||||
},
|
||||
toasts: {
|
||||
selectModel: 'Please select an AI model first.',
|
||||
modelDisabled: 'AI model {name} is disabled.',
|
||||
invalidRange: 'End time must be later than start time.',
|
||||
startSuccess: 'Backtest {id} started.',
|
||||
startFailed: 'Failed to start. Please try again later.',
|
||||
actionSuccess: '{action} {id} succeeded.',
|
||||
actionFailed: 'Operation failed. Please try again later.',
|
||||
labelSaved: 'Label updated.',
|
||||
labelFailed: 'Failed to update label.',
|
||||
confirmDelete: 'Delete backtest {id}? This action cannot be undone.',
|
||||
deleteSuccess: 'Backtest record deleted.',
|
||||
deleteFailed: 'Failed to delete. Please try again later.',
|
||||
traceFailed: 'Failed to fetch AI trace.',
|
||||
exportSuccess: 'Exported data for {id}.',
|
||||
exportFailed: 'Failed to export.',
|
||||
},
|
||||
aiTrace: {
|
||||
title: 'AI Trace',
|
||||
clear: 'Clear',
|
||||
cyclePlaceholder: 'Cycle',
|
||||
fetch: 'Fetch',
|
||||
prompt: 'Prompt',
|
||||
cot: 'Chain of thought',
|
||||
output: 'Output',
|
||||
cycleTag: 'Cycle #{cycle}',
|
||||
},
|
||||
decisionTrail: {
|
||||
title: 'AI Decision Trail',
|
||||
subtitle: 'Showing last {count} cycles',
|
||||
empty: 'No records yet',
|
||||
emptyHint: 'The AI thought & execution log will appear once the run starts.',
|
||||
},
|
||||
charts: {
|
||||
equityTitle: 'Equity Curve',
|
||||
equityEmpty: 'No data yet',
|
||||
},
|
||||
metrics: {
|
||||
title: 'Metrics',
|
||||
totalReturn: 'Total Return %',
|
||||
maxDrawdown: 'Max Drawdown %',
|
||||
sharpe: 'Sharpe',
|
||||
profitFactor: 'Profit Factor',
|
||||
pending: 'Calculating...',
|
||||
realized: 'Realized PnL',
|
||||
unrealized: 'Unrealized PnL',
|
||||
},
|
||||
trades: {
|
||||
title: 'Trade Events',
|
||||
headers: {
|
||||
time: 'Time',
|
||||
symbol: 'Symbol',
|
||||
action: 'Action',
|
||||
qty: 'Qty',
|
||||
leverage: 'Leverage',
|
||||
pnl: 'PnL',
|
||||
},
|
||||
empty: 'No trades yet',
|
||||
},
|
||||
metadata: {
|
||||
title: 'Metadata',
|
||||
created: 'Created',
|
||||
updated: 'Updated',
|
||||
processedBars: 'Processed Bars',
|
||||
maxDrawdown: 'Max DD',
|
||||
liquidated: 'Liquidated',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
},
|
||||
},
|
||||
|
||||
// Competition Page
|
||||
aiCompetition: 'AI Competition',
|
||||
traders: 'traders',
|
||||
@@ -872,6 +1035,7 @@ export const translations = {
|
||||
details: '详情',
|
||||
tradingPanel: '交易面板',
|
||||
competition: '竞赛',
|
||||
backtest: '回测',
|
||||
running: '运行中',
|
||||
stopped: '已停止',
|
||||
adminMode: '管理员模式',
|
||||
@@ -945,6 +1109,166 @@ export const translations = {
|
||||
currentGap: '当前差距',
|
||||
count: '{count} 个',
|
||||
|
||||
// Backtest Page
|
||||
backtestPage: {
|
||||
title: '回测实验室',
|
||||
subtitle: '选择模型与时间范围,快速复盘 AI 决策链路。',
|
||||
start: '启动回测',
|
||||
starting: '启动中...',
|
||||
quickRanges: {
|
||||
h24: '24小时',
|
||||
d3: '3天',
|
||||
d7: '7天',
|
||||
},
|
||||
actions: {
|
||||
pause: '暂停',
|
||||
resume: '恢复',
|
||||
stop: '停止',
|
||||
},
|
||||
states: {
|
||||
running: '运行中',
|
||||
paused: '已暂停',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
liquidated: '已爆仓',
|
||||
},
|
||||
form: {
|
||||
aiModelLabel: 'AI 模型',
|
||||
selectAiModel: '选择AI模型',
|
||||
providerLabel: 'Provider',
|
||||
statusLabel: '状态',
|
||||
enabled: '已启用',
|
||||
disabled: '未启用',
|
||||
noModelWarning: '请先在「模型配置」页面添加并启用AI模型。',
|
||||
runIdLabel: 'Run ID',
|
||||
runIdPlaceholder: '留空则自动生成',
|
||||
decisionTfLabel: '决策周期',
|
||||
cadenceLabel: '决策节奏(根数)',
|
||||
timeRangeLabel: '时间范围',
|
||||
symbolsLabel: '交易标的(逗号分隔)',
|
||||
customTfPlaceholder: '自定义周期(逗号分隔,例如 2h,6h)',
|
||||
initialBalanceLabel: '初始资金 (USDT)',
|
||||
feeLabel: '手续费 (bps)',
|
||||
slippageLabel: '滑点 (bps)',
|
||||
btcEthLeverageLabel: 'BTC/ETH 杠杆 (倍)',
|
||||
altcoinLeverageLabel: '山寨币杠杆 (倍)',
|
||||
fillPolicies: {
|
||||
nextOpen: '下一根开盘价',
|
||||
barVwap: 'K线 VWAP',
|
||||
midPrice: '中间价',
|
||||
},
|
||||
promptPresets: {
|
||||
baseline: '基础版',
|
||||
aggressive: '激进版',
|
||||
conservative: '稳健版',
|
||||
scalping: '剥头皮',
|
||||
},
|
||||
cacheAiLabel: '复用AI缓存',
|
||||
replayOnlyLabel: '仅回放记录',
|
||||
overridePromptLabel: '仅使用自定义提示词',
|
||||
customPromptLabel: '自定义提示词(可选)',
|
||||
customPromptPlaceholder: '追加或完全自定义策略提示词',
|
||||
},
|
||||
runList: {
|
||||
title: '运行列表',
|
||||
count: '共 {count} 条记录',
|
||||
},
|
||||
filters: {
|
||||
allStates: '全部状态',
|
||||
searchPlaceholder: 'Run ID / 标签',
|
||||
},
|
||||
tableHeaders: {
|
||||
runId: 'Run ID',
|
||||
label: '标签',
|
||||
state: '状态',
|
||||
progress: '进度',
|
||||
equity: '净值',
|
||||
lastError: '最后错误',
|
||||
updated: '更新时间',
|
||||
},
|
||||
emptyStates: {
|
||||
noRuns: '暂无记录',
|
||||
selectRun: '请选择一个运行查看详情',
|
||||
},
|
||||
detail: {
|
||||
tfAndSymbols: '周期: {tf} · 币种 {count}',
|
||||
labelPlaceholder: '备注标签',
|
||||
saveLabel: '保存',
|
||||
deleteLabel: '删除',
|
||||
exportLabel: '导出',
|
||||
errorLabel: '错误',
|
||||
},
|
||||
toasts: {
|
||||
selectModel: '请先选择一个AI模型。',
|
||||
modelDisabled: 'AI模型 {name} 尚未启用。',
|
||||
invalidRange: '结束时间必须晚于开始时间。',
|
||||
startSuccess: '回测 {id} 已启动。',
|
||||
startFailed: '启动失败,请稍后再试。',
|
||||
actionSuccess: '{action} {id} 成功。',
|
||||
actionFailed: '操作失败,请稍后再试。',
|
||||
labelSaved: '标签已更新。',
|
||||
labelFailed: '更新标签失败。',
|
||||
confirmDelete: '确认删除回测 {id} 吗?该操作不可恢复。',
|
||||
deleteSuccess: '回测记录已删除。',
|
||||
deleteFailed: '删除失败,请稍后再试。',
|
||||
traceFailed: '获取AI思维链失败。',
|
||||
exportSuccess: '已导出 {id} 的数据。',
|
||||
exportFailed: '导出失败。',
|
||||
},
|
||||
aiTrace: {
|
||||
title: 'AI 思维链',
|
||||
clear: '清除',
|
||||
cyclePlaceholder: '循环编号',
|
||||
fetch: '获取',
|
||||
prompt: '提示词',
|
||||
cot: '思考链',
|
||||
output: '输出',
|
||||
cycleTag: '周期 #{cycle}',
|
||||
},
|
||||
decisionTrail: {
|
||||
title: 'AI 决策轨迹',
|
||||
subtitle: '展示最近 {count} 次循环',
|
||||
empty: '暂无记录',
|
||||
emptyHint: '回测运行后将自动记录每次 AI 思考与执行',
|
||||
},
|
||||
charts: {
|
||||
equityTitle: '净值曲线',
|
||||
equityEmpty: '暂无数据',
|
||||
},
|
||||
metrics: {
|
||||
title: '指标',
|
||||
totalReturn: '总收益率 %',
|
||||
maxDrawdown: '最大回撤 %',
|
||||
sharpe: '夏普比率',
|
||||
profitFactor: '盈亏因子',
|
||||
pending: '计算中...',
|
||||
realized: '已实现盈亏',
|
||||
unrealized: '未实现盈亏',
|
||||
},
|
||||
trades: {
|
||||
title: '交易事件',
|
||||
headers: {
|
||||
time: '时间',
|
||||
symbol: '币种',
|
||||
action: '操作',
|
||||
qty: '数量',
|
||||
leverage: '杠杆',
|
||||
pnl: '盈亏',
|
||||
},
|
||||
empty: '暂无交易',
|
||||
},
|
||||
metadata: {
|
||||
title: '元信息',
|
||||
created: '创建时间',
|
||||
updated: '更新时间',
|
||||
processedBars: '已处理K线',
|
||||
maxDrawdown: '最大回撤',
|
||||
liquidated: '是否爆仓',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
},
|
||||
},
|
||||
|
||||
// Competition Page
|
||||
aiCompetition: 'AI竞赛',
|
||||
traders: '交易员',
|
||||
|
||||
@@ -12,12 +12,53 @@ import type {
|
||||
UpdateModelConfigRequest,
|
||||
UpdateExchangeConfigRequest,
|
||||
CompetitionData,
|
||||
BacktestRunsResponse,
|
||||
BacktestStartConfig,
|
||||
BacktestStatusPayload,
|
||||
BacktestEquityPoint,
|
||||
BacktestTradeEvent,
|
||||
BacktestMetrics,
|
||||
BacktestRunMetadata,
|
||||
} from '../types'
|
||||
import { CryptoService } from './crypto'
|
||||
import { httpClient } from './httpClient'
|
||||
|
||||
const API_BASE = '/api'
|
||||
|
||||
// Helper function to get auth headers
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
const token = localStorage.getItem('auth_token')
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
async function handleJSONResponse<T>(res: Response): Promise<T> {
|
||||
const text = await res.text()
|
||||
if (!res.ok) {
|
||||
let message = text || res.statusText
|
||||
try {
|
||||
const data = text ? JSON.parse(text) : null
|
||||
if (data && typeof data === 'object') {
|
||||
message = data.error || data.message || message
|
||||
}
|
||||
} catch {
|
||||
/* ignore JSON parse errors */
|
||||
}
|
||||
throw new Error(message || '请求失败')
|
||||
}
|
||||
if (!text) {
|
||||
return {} as T
|
||||
}
|
||||
return JSON.parse(text) as T
|
||||
}
|
||||
|
||||
export const api = {
|
||||
// AI交易员管理接口
|
||||
async getTraders(): Promise<TraderInfo[]> {
|
||||
@@ -106,6 +147,16 @@ export const api = {
|
||||
return result.data!
|
||||
},
|
||||
|
||||
async getPromptTemplates(): Promise<string[]> {
|
||||
const res = await fetch(`${API_BASE}/prompt-templates`)
|
||||
if (!res.ok) throw new Error('获取提示词模板失败')
|
||||
const data = await res.json()
|
||||
if (Array.isArray(data.templates)) {
|
||||
return data.templates.map((item: { name: string }) => item.name)
|
||||
}
|
||||
return []
|
||||
},
|
||||
|
||||
async updateModelConfigs(request: UpdateModelConfigRequest): Promise<void> {
|
||||
// 获取RSA公钥
|
||||
const publicKey = await CryptoService.fetchPublicKey()
|
||||
@@ -341,4 +392,175 @@ export const api = {
|
||||
if (!result.success) throw new Error('获取服务器IP失败')
|
||||
return result.data!
|
||||
},
|
||||
|
||||
// Backtest APIs
|
||||
async getBacktestRuns(params?: {
|
||||
state?: string
|
||||
search?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<BacktestRunsResponse> {
|
||||
const query = new URLSearchParams()
|
||||
if (params?.state) query.set('state', params.state)
|
||||
if (params?.search) query.set('search', params.search)
|
||||
if (params?.limit) query.set('limit', String(params.limit))
|
||||
if (params?.offset) query.set('offset', String(params.offset))
|
||||
const res = await fetch(
|
||||
`${API_BASE}/backtest/runs${query.toString() ? `?${query}` : ''}`,
|
||||
{
|
||||
headers: getAuthHeaders(),
|
||||
}
|
||||
)
|
||||
return handleJSONResponse<BacktestRunsResponse>(res)
|
||||
},
|
||||
|
||||
async startBacktest(config: BacktestStartConfig): Promise<BacktestRunMetadata> {
|
||||
const res = await fetch(`${API_BASE}/backtest/start`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ config }),
|
||||
})
|
||||
return handleJSONResponse<BacktestRunMetadata>(res)
|
||||
},
|
||||
|
||||
async pauseBacktest(runId: string): Promise<BacktestRunMetadata> {
|
||||
const res = await fetch(`${API_BASE}/backtest/pause`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ run_id: runId }),
|
||||
})
|
||||
return handleJSONResponse<BacktestRunMetadata>(res)
|
||||
},
|
||||
|
||||
async resumeBacktest(runId: string): Promise<BacktestRunMetadata> {
|
||||
const res = await fetch(`${API_BASE}/backtest/resume`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ run_id: runId }),
|
||||
})
|
||||
return handleJSONResponse<BacktestRunMetadata>(res)
|
||||
},
|
||||
|
||||
async stopBacktest(runId: string): Promise<BacktestRunMetadata> {
|
||||
const res = await fetch(`${API_BASE}/backtest/stop`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ run_id: runId }),
|
||||
})
|
||||
return handleJSONResponse<BacktestRunMetadata>(res)
|
||||
},
|
||||
|
||||
async updateBacktestLabel(
|
||||
runId: string,
|
||||
label: string
|
||||
): Promise<BacktestRunMetadata> {
|
||||
const res = await fetch(`${API_BASE}/backtest/label`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ run_id: runId, label }),
|
||||
})
|
||||
return handleJSONResponse<BacktestRunMetadata>(res)
|
||||
},
|
||||
|
||||
async deleteBacktestRun(runId: string): Promise<void> {
|
||||
const res = await fetch(`${API_BASE}/backtest/delete`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ run_id: runId }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(await res.text())
|
||||
}
|
||||
},
|
||||
|
||||
async getBacktestStatus(runId: string): Promise<BacktestStatusPayload> {
|
||||
const res = await fetch(`${API_BASE}/backtest/status?run_id=${runId}`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
return handleJSONResponse<BacktestStatusPayload>(res)
|
||||
},
|
||||
|
||||
async getBacktestEquity(
|
||||
runId: string,
|
||||
timeframe?: string,
|
||||
limit?: number
|
||||
): Promise<BacktestEquityPoint[]> {
|
||||
const query = new URLSearchParams({ run_id: runId })
|
||||
if (timeframe) query.set('tf', timeframe)
|
||||
if (limit) query.set('limit', String(limit))
|
||||
const res = await fetch(`${API_BASE}/backtest/equity?${query}`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
return handleJSONResponse<BacktestEquityPoint[]>(res)
|
||||
},
|
||||
|
||||
async getBacktestTrades(
|
||||
runId: string,
|
||||
limit = 200
|
||||
): Promise<BacktestTradeEvent[]> {
|
||||
const query = new URLSearchParams({
|
||||
run_id: runId,
|
||||
limit: String(limit),
|
||||
})
|
||||
const res = await fetch(`${API_BASE}/backtest/trades?${query}`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
return handleJSONResponse<BacktestTradeEvent[]>(res)
|
||||
},
|
||||
|
||||
async getBacktestMetrics(runId: string): Promise<BacktestMetrics> {
|
||||
const res = await fetch(`${API_BASE}/backtest/metrics?run_id=${runId}`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
return handleJSONResponse<BacktestMetrics>(res)
|
||||
},
|
||||
|
||||
async getBacktestTrace(
|
||||
runId: string,
|
||||
cycle?: number
|
||||
): Promise<DecisionRecord> {
|
||||
const query = new URLSearchParams({ run_id: runId })
|
||||
if (cycle) query.set('cycle', String(cycle))
|
||||
const res = await fetch(`${API_BASE}/backtest/trace?${query}`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
return handleJSONResponse<DecisionRecord>(res)
|
||||
},
|
||||
|
||||
async getBacktestDecisions(
|
||||
runId: string,
|
||||
limit = 20,
|
||||
offset = 0
|
||||
): Promise<DecisionRecord[]> {
|
||||
const query = new URLSearchParams({
|
||||
run_id: runId,
|
||||
limit: String(limit),
|
||||
offset: String(offset),
|
||||
})
|
||||
const res = await fetch(`${API_BASE}/backtest/decisions?${query}`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
return handleJSONResponse<DecisionRecord[]>(res)
|
||||
},
|
||||
|
||||
async exportBacktest(runId: string): Promise<Blob> {
|
||||
const res = await fetch(`${API_BASE}/backtest/export?run_id=${runId}`, {
|
||||
headers: getAuthHeaders(),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
try {
|
||||
const data = text ? JSON.parse(text) : null
|
||||
throw new Error(
|
||||
data?.error || data?.message || text || '导出失败,请稍后再试'
|
||||
)
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message) {
|
||||
throw err
|
||||
}
|
||||
throw new Error(text || '导出失败,请稍后再试')
|
||||
}
|
||||
}
|
||||
return res.blob()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,24 +3,27 @@ import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import { Toaster } from 'sonner'
|
||||
import './index.css'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Toaster
|
||||
theme="dark"
|
||||
richColors
|
||||
closeButton
|
||||
position="top-center"
|
||||
duration={2200}
|
||||
toastOptions={{
|
||||
className: 'nofx-toast',
|
||||
style: {
|
||||
background: '#0b0e11',
|
||||
border: '1px solid var(--panel-border)',
|
||||
color: 'var(--text-primary)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<Toaster
|
||||
theme="dark"
|
||||
richColors
|
||||
closeButton
|
||||
position="top-center"
|
||||
duration={2200}
|
||||
toastOptions={{
|
||||
className: 'nofx-toast',
|
||||
style: {
|
||||
background: '#0b0e11',
|
||||
border: '1px solid var(--panel-border)',
|
||||
color: 'var(--text-primary)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
134
web/src/types.ts
134
web/src/types.ts
@@ -50,6 +50,7 @@ export interface DecisionAction {
|
||||
timestamp: string
|
||||
success: boolean
|
||||
error?: string
|
||||
reasoning?: string
|
||||
}
|
||||
|
||||
export interface AccountSnapshot {
|
||||
@@ -213,3 +214,136 @@ export interface TraderConfigData {
|
||||
scan_interval_minutes: number
|
||||
is_running: boolean
|
||||
}
|
||||
|
||||
// Backtest types
|
||||
export interface BacktestRunSummary {
|
||||
symbol_count: number;
|
||||
decision_tf: string;
|
||||
processed_bars: number;
|
||||
progress_pct: number;
|
||||
equity_last: number;
|
||||
max_drawdown_pct: number;
|
||||
liquidated: boolean;
|
||||
liquidation_note?: string;
|
||||
}
|
||||
|
||||
export interface BacktestRunMetadata {
|
||||
run_id: string;
|
||||
label?: string;
|
||||
user_id?: string;
|
||||
last_error?: string;
|
||||
version: number;
|
||||
state: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
summary: BacktestRunSummary;
|
||||
}
|
||||
|
||||
export interface BacktestRunsResponse {
|
||||
total: number;
|
||||
items: BacktestRunMetadata[];
|
||||
}
|
||||
|
||||
export interface BacktestStatusPayload {
|
||||
run_id: string;
|
||||
state: string;
|
||||
progress_pct: number;
|
||||
processed_bars: number;
|
||||
current_time: number;
|
||||
decision_cycle: number;
|
||||
equity: number;
|
||||
unrealized_pnl: number;
|
||||
realized_pnl: number;
|
||||
note?: string;
|
||||
last_error?: string;
|
||||
last_updated_iso: string;
|
||||
}
|
||||
|
||||
export interface BacktestEquityPoint {
|
||||
ts: number;
|
||||
equity: number;
|
||||
available: number;
|
||||
pnl: number;
|
||||
pnl_pct: number;
|
||||
dd_pct: number;
|
||||
cycle: number;
|
||||
}
|
||||
|
||||
export interface BacktestTradeEvent {
|
||||
ts: number;
|
||||
symbol: string;
|
||||
action: string;
|
||||
side?: string;
|
||||
qty: number;
|
||||
price: number;
|
||||
fee: number;
|
||||
slippage: number;
|
||||
order_value: number;
|
||||
realized_pnl: number;
|
||||
leverage?: number;
|
||||
cycle: number;
|
||||
position_after: number;
|
||||
liquidation: boolean;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface BacktestMetrics {
|
||||
total_return_pct: number;
|
||||
max_drawdown_pct: number;
|
||||
sharpe_ratio: number;
|
||||
profit_factor: number;
|
||||
win_rate: number;
|
||||
trades: number;
|
||||
avg_win: number;
|
||||
avg_loss: number;
|
||||
best_symbol: string;
|
||||
worst_symbol: string;
|
||||
liquidated: boolean;
|
||||
symbol_stats?: Record<
|
||||
string,
|
||||
{
|
||||
total_trades: number;
|
||||
winning_trades: number;
|
||||
losing_trades: number;
|
||||
total_pnl: number;
|
||||
avg_pnl: number;
|
||||
win_rate: number;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export interface BacktestStartConfig {
|
||||
run_id?: string;
|
||||
ai_model_id?: string;
|
||||
symbols: string[];
|
||||
timeframes: string[];
|
||||
decision_timeframe: string;
|
||||
decision_cadence_nbars: number;
|
||||
start_ts: number;
|
||||
end_ts: number;
|
||||
initial_balance: number;
|
||||
fee_bps: number;
|
||||
slippage_bps: number;
|
||||
fill_policy: string;
|
||||
prompt_variant?: string;
|
||||
prompt_template?: string;
|
||||
custom_prompt?: string;
|
||||
override_prompt?: boolean;
|
||||
cache_ai?: boolean;
|
||||
replay_only?: boolean;
|
||||
checkpoint_interval_bars?: number;
|
||||
checkpoint_interval_seconds?: number;
|
||||
replay_decision_dir?: string;
|
||||
shared_ai_cache_path?: string;
|
||||
ai?: {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
key?: string;
|
||||
secret_key?: string;
|
||||
base_url?: string;
|
||||
};
|
||||
leverage?: {
|
||||
btc_eth_leverage?: number;
|
||||
altcoin_leverage?: number;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user