summaryrefslogtreecommitdiff
path: root/frontend/src/lib/format.ts
blob: 13e9d1e9d25fff24651f2c7761f130a01c05a1b3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import type {
  BacktestMetrics,
  Dataset,
  Instrument,
  Run
} from './types';

export function uuidLike(id: string): boolean {
  return /^[0-9a-f-]+$/i.test(id);
}

export function fmtDateTime(iso?: string | null): string {
  if (!iso) return '—';
  const d = new Date(iso);
  if (isNaN(d.getTime())) return iso;
  return d.toLocaleString('zh-CN', { hour12: false });
}

export function fmtDate(iso?: string | null): string {
  if (!iso) return '—';
  return iso.replace('T', ' ').replace(/Z$|\+08:00$/i, '');
}

export function fmtPct(v: number | null | undefined, digits = 2): string {
  if (v === null || v === undefined || Number.isNaN(v)) return '—';
  return `${(v * 100).toFixed(digits)}%`;
}

export function fmtNum(v: number | null | undefined, digits = 2): string {
  if (v === null || v === undefined || Number.isNaN(v)) return '—';
  if (Math.abs(v) >= 1e8) return `${(v / 1e8).toFixed(2)} 亿`;
  if (Math.abs(v) >= 1e4) return `${(v / 1e4).toFixed(2)} 万`;
  let r = v.toFixed(digits);
  if (r === '0.00' && v !== 0) r = '0';
  return r;
}

export function itemSourceLabel(itemSource?: string | null, fallback = ''): string {
  const s = itemSource || fallback;
  if (!s) return '—';
  if (s === 'catalog') return '目录';
  if (s === 'provider_suggest') return '供应商';
  if (s === 'eastmoney') return '东财';
  if (s === 'tencent') return '腾讯';
  if (s === 'sina') return '新浪';
  return s;
}

export const runStatusLabel: Record<Run['status'], string> = {
  queued: '排队中',
  running: '执行中',
  succeeded: '已完成',
  failed: '失败',
  cancelled: '已取消'
};

export const runStatusBadge: Record<Run['status'], 'grey' | 'warn' | 'ok' | 'danger' | 'teal'> = {
  queued: 'grey',
  running: 'warn',
  succeeded: 'ok',
  failed: 'danger',
  cancelled: 'grey'
};

export const datasetStatusLabel: Record<Dataset['status'], string> = {
  pending: '等待中',
  running: '获取数据',
  ready: '就绪',
  failed: '失败'
};

export const adjustmentLabel: Record<string, string> = {
  none: '不复权',
  qfq: '前复权',
  hfq: '后复权'
};

export const assetTypeLabel: Record<string, string> = {
  stock: '股票',
  etf: 'ETF',
  index: '指数'
};

export function instrumentLabel(i: Instrument): string {
  const name = i.name ? ` ${i.name}` : '';
  return `${i.market}.${i.symbol}${name}`;
}

export function formatMoney(v: number | null | undefined): string {
  if (v === null || v === undefined || Number.isNaN(v)) return '—';
  return `¥ ${fmtNum(v)}`;
}

export const versionSourceLabel: Record<string, string> = {
  manual: '手动快照',
  run: '回测快照',
  ai: 'AI 采用',
  restore: '恢复产生'
};

export function metricsLabels(): string[] {
  // trade_count semantics (worker/backtest.py): COUNT of closed round trips —
  // an open that is fully closed afterwards counts as 1; fills/orders and
  // cancellations do NOT count. Consistent label for the result card AND the
  // run comparison tables.
  return ['总收益率', '年化收益率', '最大回撤', '夏普比率', '平仓回合数', '期末权益'];
}

export function metricsRow(m: BacktestMetrics): string[] {
  return [
    fmtPct(m.total_return),
    fmtPct(m.annual_return),
    fmtPct(m.max_drawdown),
    m.sharpe === null || m.sharpe === undefined ? '—' : m.sharpe.toFixed(2),
    m.trade_count === null || m.trade_count === undefined ? '—' : String(m.trade_count),
    m.final_equity === null || m.final_equity === undefined ? '—' : fmtNum(m.final_equity)
  ];
}