Dev backtest (#1134)

This commit is contained in:
Rick
2025-11-28 21:34:27 +08:00
committed by GitHub
parent 64a5734011
commit 7eebb4e218
39 changed files with 9293 additions and 125 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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>
)
}

View File

@@ -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) {

View File

@@ -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: '交易员',

View File

@@ -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()
},
}

View File

@@ -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>
)

View File

@@ -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;
};
}