feat(frontend): 终端化重构+策略管理(代码→实例→4运行)+撮合设计+回测参数 [nas]
CI/CD / test (push) Successful in 18s
CI/CD / nas-deploy (push) Failing after 17s
CI/CD / nas-verify (push) Has been skipped

- 终端化: 科幻色板(tokens/EP/reset/chips/echarts)+Layout(Cmd+K/时钟/状态灯)+mock全接口+Dashboard/Result/Monitor样板+6图表配色
- 策略管理: 策略库三层(代码→实例→4运行)+新建策略(组合/CTA模板+Monaco)+在线代码编辑(Monaco)+实例CRUD(参数反射/标的池type切/interval/match_session)
- 实例→运行关联: backtest/paper/live New页读instance预填
- 撮合设计: match_session(下一根K线开盘/收盘/集合竞价)+interval定频率+实走限日线/分钟走miniQMT
- 回测参数: 基准下拉+日期默认1年localStorage记忆+滑点/手续费UI
- 账户管理页+命名重整(模拟盘/实盘,消除撞名)
- spec §11-13(菜单/策略管理/撮合设计) + Monaco编辑器 + 策略实例删除/编辑入口 + 列表实例列
This commit is contained in:
2026-08-12 23:23:23 +08:00
parent 58b5d08280
commit 5c8a7e12c6
45 changed files with 3935 additions and 459 deletions
+425
View File
@@ -0,0 +1,425 @@
/* 业务假数据 —— 覆盖各接口的核心展示数据(样板用,连真实后端时不触发) */
import { genEquity, genBenchmark, genRisk, genDailyPnl, genPaperEquity, genDates } from './series'
const N = 117
/* ========== 策略 ========== */
export const strategiesMock = {
strategies: [
{ name: '双均线', class_name: 'AShareDoubleMaStrategy' },
{ name: '布林通道', class_name: 'AShareBollStrategy' },
{ name: 'ATR 突破', class_name: 'AShareAtrStrategy' },
{ name: '全天候(组合)', class_name: 'AllWeatherStrategy' },
],
}
export const strategyParamsMock: Record<string, { parameters: string[]; defaults: Record<string, number> }> = {
AShareDoubleMaStrategy: { parameters: ['fast_window', 'slow_window', 'size'], defaults: { fast_window: 10, slow_window: 30, size: 100 } },
AShareBollStrategy: { parameters: ['boll_window', 'boll_dev', 'size'], defaults: { boll_window: 20, boll_dev: 2.0, size: 100 } },
AShareAtrStrategy: { parameters: ['atr_window', 'atr_multiplier', 'size'], defaults: { atr_window: 14, atr_multiplier: 3.0, size: 100 } },
AllWeatherStrategy: { parameters: ['rebalance_day', 'top_n'], defaults: { rebalance_day: 1, top_n: 5 } },
}
/* ========== 回测历史任务列表 ========== */
export const taskListMock = {
tasks: [
{ id: 101, task_id: 'cta_7a92d1f6', type: 'cta', status: 'done', strategy: 'AShareDoubleMaStrategy', symbol: '600519.SH', start: '2024-01-02', end: '2024-06-28', instance: '茅台双均线(10,30)·15m' },
{ id: 100, task_id: 'cta_c287f4b5', type: 'cta', status: 'done', strategy: 'AShareBollStrategy', symbol: '000858.SZ', start: '2023-06-01', end: '2024-06-01', instance: '五粮液双均线(5,20)·日线' },
{ id: 99, task_id: 'opt_3f8a2c', type: 'optimize', status: 'done', strategy: 'AShareDoubleMaStrategy', symbol: '600519.SH', start: '2023-01-03', end: '2024-06-28', instance: '茅台双均线(10,30)·15m' },
{ id: 98, task_id: 'pf_a1b2c3', type: 'portfolio', status: 'done', strategy: 'AllWeatherStrategy', symbol: '中证1000成份', start: '2022-01-04', end: '2024-06-28', instance: '全天候·中证1000·周调仓' },
{ id: 97, task_id: 'cta_e5d4c3', type: 'cta', status: 'failed', strategy: 'AShareAtrStrategy', symbol: '300750.SZ', start: '2024-03-01', end: '2024-06-28', instance: '宁德布林(20,2)·15m' },
{ id: 96, task_id: 'fc_88aabb', type: 'factor', status: 'done', strategy: '动量因子MOM20', symbol: '全市场', start: '2020-01-02', end: '2024-06-28', instance: '' },
{ id: 95, task_id: 'cta_112233', type: 'cta', status: 'running', strategy: 'AShareDoubleMaStrategy', symbol: '510300.SH', start: '2024-01-02', end: '2024-08-12', instance: '茅台双均线(10,30)·15m' },
],
}
/* ========== CTA 回测结果 ========== */
export const ctaMetricsMock = {
relative_metrics: {
total_return: 0.3842,
annual_return: 0.3125,
alpha: 0.0821,
beta: 0.7128,
sharpe_ratio: 1.4203,
sortino_ratio: 1.8841,
information_ratio: 0.9635,
annual_volatility: 0.1822,
max_drawdown: -0.1278,
benchmark_return: 0.0721,
benchmark_volatility: 0.1513,
},
statistics: {
total_return: '0.3842',
annual_return: '0.3125',
sharpe_ratio: '1.4203',
sortino_ratio: '1.8841',
max_drawdown: '-0.1278',
total_trade_count: '48',
win_rate: '0.5417',
avg_return: '0.0080',
max_dd_duration: '42',
daily_net_pnl: '0',
total_net_pnl: '384200',
total_commission: '4128',
total_slippage: '1820',
total_turnover: '0.84',
},
task_id: 'cta_7a92d1f6',
symbol: '600519.SH',
start: '2024-01-02',
end: '2024-06-28',
strategy: 'AShareDoubleMaStrategy',
params: { fast_window: 10, slow_window: 30, size: 100 },
status: 'done',
}
export const equityCurveMock = { equity_curve: genEquity(N) }
export const benchmarkCurveMock = (() => {
const e = genEquity(N)
const b = genBenchmark(N)
return { dates: e.map((p) => p.date), strategy: e.map((p) => p.balance), benchmark: b.map((p) => p.balance) }
})()
export const riskSeriesMock = genRisk(N)
export const dailyPnlMock = { daily_pnl: genDailyPnl(N) }
export const tradesMock = {
trades: [
{ datetime: '2024-01-05 09:30:00', direction: 'LONG', offset: 'OPEN', price: 1685.2, volume: 100, vt_symbol: '600519.SH' },
{ datetime: '2024-01-18 14:50:00', direction: 'LONG', offset: 'OPEN', price: 1702.5, volume: 100, vt_symbol: '600519.SH' },
{ datetime: '2024-02-08 09:35:00', direction: 'SHORT', offset: 'CLOSE', price: 1748.0, volume: 200, vt_symbol: '600519.SH' },
{ datetime: '2024-03-04 09:30:00', direction: 'LONG', offset: 'OPEN', price: 1710.6, volume: 100, vt_symbol: '600519.SH' },
{ datetime: '2024-04-01 10:15:00', direction: 'SHORT', offset: 'CLOSE', price: 1756.3, volume: 100, vt_symbol: '600519.SH' },
{ datetime: '2024-04-22 09:30:00', direction: 'LONG', offset: 'OPEN', price: 1632.8, volume: 100, vt_symbol: '600519.SH' },
{ datetime: '2024-05-20 13:40:00', direction: 'SHORT', offset: 'CLOSE', price: 1718.9, volume: 100, vt_symbol: '600519.SH' },
{ datetime: '2024-06-12 09:30:00', direction: 'LONG', offset: 'OPEN', price: 1508.2, volume: 100, vt_symbol: '600519.SH' },
],
}
export const klineMock = {
kline: genDates('2024-01-02', 60).map((date, i) => ({
datetime: date,
open: 1680 + Math.sin(i / 5) * 30 + i * 0.4,
high: 1690 + Math.sin(i / 5) * 30 + i * 0.4,
low: 1670 + Math.sin(i / 5) * 30 + i * 0.4,
close: 1685 + Math.sin(i / 5) * 30 + i * 0.4,
volume: 1200000 + Math.sin(i / 3) * 400000,
})),
}
export const optResultsMock = {
results: [
{ params: { fast_window: 5, slow_window: 20 }, statistics: { sharpe_ratio: 1.12, total_return: 0.28, max_drawdown: -0.15 } },
{ params: { fast_window: 5, slow_window: 30 }, statistics: { sharpe_ratio: 1.35, total_return: 0.33, max_drawdown: -0.14 } },
{ params: { fast_window: 10, slow_window: 20 }, statistics: { sharpe_ratio: 1.48, total_return: 0.37, max_drawdown: -0.12 } },
{ params: { fast_window: 10, slow_window: 30 }, statistics: { sharpe_ratio: 1.42, total_return: 0.38, max_drawdown: -0.13 } },
{ params: { fast_window: 10, slow_window: 40 }, statistics: { sharpe_ratio: 1.51, total_return: 0.41, max_drawdown: -0.11 } },
{ params: { fast_window: 15, slow_window: 30 }, statistics: { sharpe_ratio: 1.36, total_return: 0.35, max_drawdown: -0.14 } },
{ params: { fast_window: 20, slow_window: 60 }, statistics: { sharpe_ratio: 1.23, total_return: 0.30, max_drawdown: -0.16 } },
],
}
export const logMock = { log: '[2024-06-28 09:30:00] load_data bars=117\n[09:30:01] run_backtesting start\n[09:30:04] run_backtesting done\n[09:30:04] calculate_result shape=(117,25)\n[09:30:04] compute_metrics 27ms\n[09:30:04] write metrics.json done\n' }
/* ========== 模拟盘 paper ========== */
export const paperListMock = {
accounts: [
{ id: 1, name: '茅台双均线·回放', mode: 'replay', interval: 'd', status: 'running', symbols: '600519.SH', initial_capital: 1000000, start_date: '2024-01-02', end_date: '2024-06-28', last_run_date: '2024-06-28', next_run_at: null, checkpoint_date: '2024-06-27', error_msg: null, latest_equity: 1384200, latest_date: '2024-06-28', total_return: 0.3842 },
{ id: 2, name: '15分钟动量·实走', mode: 'live', interval: '15m', status: 'running', symbols: '000858.SZ,002594.SZ', initial_capital: 500000, start_date: '2024-05-01', end_date: null, last_run_date: '2024-08-12', next_run_at: '2024-08-13 15:05', checkpoint_date: '2024-08-12', error_msg: null, latest_equity: 521800, latest_date: '2024-08-12', total_return: 0.0436 },
{ id: 3, name: '全天候组合·实走', mode: 'live', interval: 'd', status: 'pending', symbols: '中证1000池', initial_capital: 1000000, start_date: '2024-03-01', end_date: null, last_run_date: '2024-08-11', next_run_at: '2024-08-13 15:05', checkpoint_date: '2024-08-11', error_msg: null, latest_equity: 1046200, latest_date: '2024-08-11', total_return: 0.0462 },
{ id: 4, name: '布林通道测试', mode: 'replay', interval: 'd', status: 'failed', symbols: '300750.SZ', initial_capital: 300000, start_date: '2024-06-01', end_date: '2024-08-01', last_run_date: '2024-08-01', next_run_at: null, checkpoint_date: null, error_msg: 'load_data bars=0:区间无日线', latest_equity: null, latest_date: null, total_return: null },
],
}
export const paperDetailMock = paperListMock.accounts[0]
export const paperEquityMock = genPaperEquity(120)
export const paperTradesMock = tradesMock.trades.map((t, i) => ({
strategy_id: 'doublema_' + (i % 2),
symbol: '600519.SH',
direction: t.direction,
price: t.price,
volume: t.volume,
commission: Math.round(t.price * t.volume * 0.0003),
rejected: 0,
reject_reason: '',
bar_date: t.datetime.slice(0, 10),
}))
export const paperStrategiesMock = [
{ strategy_id: 'doublema_0', total_orders: 32, filled: 28, rejected: 4, commission: 1280 },
{ strategy_id: 'boll_1', total_orders: 16, filled: 16, rejected: 0, commission: 720 },
]
export const paperPositionsMock = [
{ symbol: '600519.SH', volume: 200, frozen: 0, avg_price: 1689.3 },
{ symbol: '000858.SZ', volume: 500, frozen: 0, avg_price: 168.2 },
]
export const paperPendingMock = [
{ strategy_id: 'doublema_0', symbol: '002594.SZ', side: 'BUY', price: 0, volume: 300, is_market: true, match_session: 'next_open', listing_days: 1200 },
]
/* ========== 实盘 live ========== */
export const liveListMock = {
accounts: [
{ id: 11, name: '茅台 miniQMT', account: '8888001100', vt_symbol: '600519.SH', strategy_class: 'AShareDoubleMaStrategy', strategy_name: '双均线', setting: '{"fast_window":10,"slow_window":30,"size":100}', status: 'running', interval: 'd', initial_capital: 1000000, connect_wait_sec: 15, init_wait_sec: 8, mini_path: 'D:\\miniQMTuserdata', error_msg: null, created_at: '2024-07-01 10:00:00', updated_at: '2024-08-12 15:05:00', latest_equity: 1384200, latest_date: '2024-08-12', total_return: 0.3842, position_count: 2 },
{ id: 12, name: '宁德 miniQMT', account: '8888001101', vt_symbol: '300750.SZ', strategy_class: 'AShareBollStrategy', strategy_name: '布林通道', setting: '{"boll_window":20,"boll_dev":2.0,"size":100}', status: 'stopped', interval: 'd', initial_capital: 500000, connect_wait_sec: 15, init_wait_sec: 8, mini_path: 'D:\\miniQMTuserdata', error_msg: null, created_at: '2024-07-10 14:00:00', updated_at: '2024-08-10 15:05:00', latest_equity: 485200, latest_date: '2024-08-10', total_return: -0.0296, position_count: 1 },
{ id: 13, name: '五粮液 miniQMT', account: '8888001102', vt_symbol: '000858.SZ', strategy_class: 'AShareAtrStrategy', strategy_name: 'ATR突破', setting: '{"atr_window":14,"atr_multiplier":3.0,"size":100}', status: 'error', interval: 'd', initial_capital: 300000, connect_wait_sec: 15, init_wait_sec: 8, mini_path: 'D:\\miniQMTuserdata', error_msg: 'xtquant connect timeout (mini_path 未登录)', created_at: '2024-08-01 09:00:00', updated_at: '2024-08-12 09:32:00', latest_equity: null, latest_date: null, total_return: null, position_count: 0 },
],
}
export const liveDetailMock = liveListMock.accounts[0]
export const livePositionsMock = [
{ symbol: '600519.SH', volume: 200, frozen: 0, avg_price: 1689.3, updated_at: '2024-08-12 15:00:00' },
{ symbol: '000858.SZ', volume: 500, frozen: 0, avg_price: 168.2, updated_at: '2024-08-12 15:00:00' },
]
export const liveTradesMock = tradesMock.trades.slice(0, 5).map((t, i) => ({
account_id: 11, strategy_name: '双均线', symbol: '600519.SH', direction: t.direction, offset: t.offset, price: t.price, volume: t.volume, traded_at: t.datetime, vt_tradeid: 'xt_' + i,
}))
export const liveAccountMock = { account_id: 11, date: '2024-08-12', cash: 612400, market_value: 771800, total: 1384200 }
export const liveStatusMock = { account_id: 11, status: 'running', name: '茅台 miniQMT', account: '8888001100', vt_symbol: '600519.SH', strategy_name: '双均线', updated_at: '2024-08-12 15:05:00', error_msg: null }
/* ========== 组合回测 portfolio ========== */
export const portfolioResultMock = {
strategy: 'AllWeatherStrategy',
period: { start: '2022-01-04', end: '2024-06-28', trading_days: 596 },
stocks_selected: [
{ code: '002594.SZ', name: '比亚迪', amount: 200, avg_cost: 245.3, price: 268.5, value: 53700 },
{ code: '300750.SZ', name: '宁德时代', amount: 50, avg_cost: 198.4, price: 212.6, value: 10630 },
{ code: '600519.SH', name: '贵州茅台', amount: 10, avg_cost: 1689.3, price: 1718.9, value: 17189 },
{ code: '000858.SZ', name: '五粮液', amount: 300, avg_cost: 168.2, price: 158.4, value: 47520 },
{ code: '601012.SH', name: '隆基绿能', amount: 800, avg_cost: 32.1, price: 28.6, value: 22880 },
],
trades: tradesMock.trades.slice(0, 6).map((t, i) => ({ datetime: t.datetime, code: '600519.SH', side: i % 2 === 0 ? 'BUY' : 'SELL', amount: 100, filled_amount: 100, price: t.price, filled_price: t.price, commission: 18.5, status: 'FILLED' })),
equity_curve: genEquity(180, { from: '2022-01-04' }).map((p) => ({ date: p.date, equity: p.balance })),
metrics: {
total_return: 0.1523, annual_return: 0.0582, max_drawdown: -0.168, sharpe: 0.78,
win_rate_daily: 0.521, win_rate_trade: 0.483, trading_days: 596,
},
}
/* ========== 策略配置(CRUD ========== */
export const strategyConfigsMock = {
strategies: [
{ id: 1, name: '茅台双均线(10,30)', strategy_class: 'AShareDoubleMaStrategy', params: { fast_window: 10, slow_window: 30, size: 100 }, symbol_or_pool: '600519.SH', benchmark: 'hs300', interval: 'd', remark: '茅台日线双均线', created_at: '2024-07-01', updated_at: '2024-08-10' },
{ id: 2, name: '宁德布林(20,2)', strategy_class: 'AShareBollStrategy', params: { boll_window: 20, boll_dev: 2, size: 100 }, symbol_or_pool: '300750.SZ', benchmark: 'zz500', interval: 'd', remark: '宁德时代布林通道', created_at: '2024-07-15', updated_at: '2024-08-09' },
{ id: 3, name: '全天候组合', strategy_class: 'AllWeatherStrategy', params: { rebalance_day: 1, top_n: 5 }, symbol_or_pool: '中证1000池', benchmark: 'zz500', interval: 'd', remark: '组合主线·月度调仓', created_at: '2024-06-01', updated_at: '2024-08-12' },
{ id: 4, name: '五粮液ATR突破', strategy_class: 'AShareAtrStrategy', params: { atr_window: 14, atr_multiplier: 3, size: 100 }, symbol_or_pool: '000858.SZ', benchmark: 'hs300', interval: 'd', remark: '', created_at: '2024-08-01', updated_at: '2024-08-11' },
],
}
/* ========== 策略代码文件(在线编辑 mock ========== */
const DOUBLE_MA_CODE = `"""双均线策略(A股适配 · 基于 vnpy StrategyTemplate)。
fast_window 周期均线上穿 slow_window 周期均线 → 买入;
下穿 → 卖出。定寸 size 股下单(A股 100 股一手)。
"""
from typing import List
from vnpy_ctastrategy import StrategyTemplate
from vnpy.trader.object import BarData
class DoubleMaStrategy(StrategyTemplate):
"""双均线 CTA 策略。"""
author = "sanguo"
fast_window = 10
slow_window = 30
size = 100
parameters = ["fast_window", "slow_window", "size"]
variables = ["fast_ma", "slow_ma"]
def __init__(self, cta_engine, strategy_name, vt_symbol, setting):
super().__init__(cta_engine, strategy_name, vt_symbol, setting)
self.fast_ma = 0.0
self.slow_ma = 0.0
def on_init(self) -> None:
self.write_log("策略初始化")
self.load_bar(self.slow_window)
def on_bar(self, bar: BarData) -> None:
am = self.am
am.update_bar(bar)
if not am.inited:
return
self.fast_ma = am.sma(self.fast_window, array=True)[-1]
self.slow_ma = am.sma(self.slow_window, array=True)[-1]
cross_over = self.fast_ma > self.slow_ma
cross_below = self.fast_ma < self.slow_ma
if cross_over and self.pos == 0:
self.buy(bar.close_price + 0.01, self.size)
elif cross_below and self.pos > 0:
self.sell(bar.close_price - 0.01, self.size)
self.put_event()
`
const ALL_WEATHER_CODE = `"""聚宽"全天候轮动"策略 · BulletTrade 框架移植。
选股 + 大小盘轮动 + 海外 ETF 兜底 + 涨停盯盘,月度调仓。
数据/下单全部走注入的 provider 和 broker_facade(聚宽风格 API)。
"""
import logging
import numpy as np
from dataclasses import dataclass, field
from typing import Any, Callable, List, Optional
logger = logging.getLogger(__name__)
@dataclass
class AllWeatherConfig:
stock_num: int = 3 # 持仓数
trend_window: int = 10 # 趋势涨幅窗口
trend_threshold: float = 10.0 # "无敌好行情"阈值
stop_loss_pct: float = 0.92 # 止损线
benchmark: str = "000300.XSHG"
class AllWeatherStrategy:
"""全天候轮动(组合主线策略)。"""
def __init__(self, provider: Any, broker: Any = None, config: Optional[AllWeatherConfig] = None):
self.provider = provider
self.broker = broker
self.config = config or AllWeatherConfig()
self.hold_list: List[str] = []
def initialize(self, context: Any) -> None:
b = self.broker
b.set_benchmark(self.config.benchmark)
b.run_daily(self.prepare_stock_list, "9:05")
b.run_monthly(self.monthly_adjustment, 1, "9:30")
b.run_daily(self.stop_loss, "14:00")
def monthly_adjustment(self, context: Any) -> None:
"""月度调仓:大小盘轮动 + 选股 + 调仓下单(详见完整源码)。"""
cfg = self.config
b_stocks = self._stock_pool("000300.XSHG", context.previous_date)
target = self._pick_big_universe(b_stocks, context)
for code in target:
self.broker.order_target_value(code, 0) # 占位:实际按 target 等权
logger.info("monthly_adjustment target=%s", target)
def stop_loss(self, context: Any) -> None:
"""持仓跌幅超 stop_loss_pct 止损。"""
pass # 详见完整实现
`
const BOLL_CODE = `"""布林带通道策略(A股)。价格突破上轨买入,跌破下轨卖出。"""
from vnpy_ctastrategy import StrategyTemplate
class BollChannelStrategy(StrategyTemplate):
author = "sanguo"
boll_window = 20
boll_dev = 2.0
size = 100
parameters = ["boll_window", "boll_dev", "size"]
variables = ["boll_up", "boll_down"]
def on_init(self):
self.load_bar(self.boll_window)
def on_bar(self, bar):
am = self.am
am.update_bar(bar)
if not am.inited:
return
self.boll_up, self.boll_down = am.boll(self.boll_window, self.boll_dev)
if am.close > self.boll_up and self.pos == 0:
self.buy(bar.close_price, self.size)
elif am.close < self.boll_down and self.pos > 0:
self.sell(bar.close_price, self.size)
`
const ATR_CODE = `"""ATR 通道突破策略(A股)。基于 ATR 的动态通道突破。"""
from vnpy_ctastrategy import StrategyTemplate
class AtrRsiStrategy(StrategyTemplate):
author = "sanguo"
atr_window = 14
atr_multiplier = 3.0
size = 100
parameters = ["atr_window", "atr_multiplier", "size"]
variables = ["atr_value", "entry_price"]
def on_init(self):
self.load_bar(self.atr_window + 5)
def on_bar(self, bar):
am = self.am
am.update_bar(bar)
if not am.inited:
return
self.atr_value = am.atr(self.atr_window)
entry = am.close[-2] + self.atr_value * self.atr_multiplier
if am.close > entry and self.pos == 0:
self.buy(entry, self.size)
self.entry_price = entry
`
export interface StrategyFile {
name: string
dir: string
class_name: string
type: 'portfolio' | 'cta'
lines: number
modified: string
code: string
}
export const strategyFilesMock: { files: StrategyFile[] } = {
files: [
{ name: 'all_weather.py', dir: 'sanguo_portfolio/strategies/', class_name: 'AllWeatherStrategy', type: 'portfolio', lines: 575, modified: '2024-08-12 14:30', code: ALL_WEATHER_CODE },
{ name: 'momentum_timing.py', dir: 'sanguo_portfolio/strategies/', class_name: 'MomentumTimingStrategy', type: 'portfolio', lines: 412, modified: '2024-08-10 10:20', code: ALL_WEATHER_CODE },
{ name: 'double_ma.py', dir: 'sanguo_live/strategies/', class_name: 'AShareDoubleMaStrategy', type: 'cta', lines: 68, modified: '2024-07-20 09:15', code: DOUBLE_MA_CODE },
{ name: 'boll_channel.py', dir: 'sanguo_live/strategies/', class_name: 'AShareBollStrategy', type: 'cta', lines: 32, modified: '2024-07-22 10:00', code: BOLL_CODE },
{ name: 'atr_breakout.py', dir: 'sanguo_live/strategies/', class_name: 'AShareAtrStrategy', type: 'cta', lines: 30, modified: '2024-08-01 11:20', code: ATR_CODE },
],
}
/* ========== 策略实例(实例层:代码下的参数变体,每实例一键 4 运行) ========== */
export interface StrategyInstance {
id: number
code_file: string
name: string
type: 'portfolio' | 'cta'
params: Record<string, unknown>
symbol_or_pool: string
interval: string
match_session: string
status: { backtest: string; replay: string; paper_live: string; live: string }
last_return: number | null
updated_at: string
}
export const strategyInstancesMock: { instances: StrategyInstance[] } = {
instances: [
{ id: 1, code_file: 'all_weather.py', name: '全天候·HS300·月调仓', type: 'portfolio', params: { stock_num: 3, trend_window: 10 }, symbol_or_pool: 'HS300', interval: 'd', match_session: 'next_open', status: { backtest: 'done', replay: 'done', paper_live: 'running', live: '-' }, last_return: 0.1523, updated_at: '2024-08-12' },
{ id: 2, code_file: 'all_weather.py', name: '全天候·中证1000·周调仓', type: 'portfolio', params: { stock_num: 5, trend_window: 5 }, symbol_or_pool: '中证1000', interval: 'd', match_session: 'next_open', status: { backtest: 'done', replay: '-', paper_live: '-', live: '-' }, last_return: 0.082, updated_at: '2024-08-10' },
{ id: 3, code_file: 'double_ma.py', name: '茅台双均线(10,30)·15m', type: 'cta', params: { fast_window: 10, slow_window: 30, size: 100 }, symbol_or_pool: '600519.SH', interval: '15m', match_session: 'next_open', status: { backtest: 'done', replay: 'done', paper_live: 'running', live: 'running' }, last_return: 0.3842, updated_at: '2024-08-12' },
{ id: 4, code_file: 'double_ma.py', name: '五粮液双均线(5,20)·日线', type: 'cta', params: { fast_window: 5, slow_window: 20, size: 100 }, symbol_or_pool: '000858.SZ', interval: 'd', match_session: 'current_close', status: { backtest: 'done', replay: '-', paper_live: '-', live: '-' }, last_return: -0.052, updated_at: '2024-08-08' },
{ id: 5, code_file: 'boll_channel.py', name: '宁德布林(20,2)·15m', type: 'cta', params: { boll_window: 20, boll_dev: 2, size: 100 }, symbol_or_pool: '300750.SZ', interval: '15m', match_session: 'next_open', status: { backtest: 'done', replay: '-', paper_live: '-', live: 'stopped' }, last_return: -0.0296, updated_at: '2024-08-10' },
],
}
/* ========== 因子 factor ========== */
export const factorListMock = {
factors: [
{ name: '动量因子 MOM20', category: '动量' },
{ name: '换手率 TURN5', category: '量价' },
{ name: '波动率 VOL20', category: '波动' },
{ name: '市值 SIZE', category: '风格' },
{ name: 'ROE_TTM', category: '质量' },
{ name: '毛利率 GROSS', category: '质量' },
],
}
export const icSummaryMock = {
ic_summary: {
MOM20: { IC_mean: 0.042, ICIR: 0.51, t_stat: 3.21, positive_ratio: 0.58 },
TURN5: { IC_mean: -0.028, ICIR: -0.33, t_stat: -2.05, positive_ratio: 0.41 },
VOL20: { IC_mean: -0.035, ICIR: -0.42, t_stat: -2.6, positive_ratio: 0.38 },
},
}
/* ========== 任务状态(progress 轮询) ========== */
export const taskStatusDoneMock = { task_id: 'cta_7a92d1f6', status: 'done', stage: 'compute_metrics' }
export const taskStatusRunningMock = { task_id: 'cta_running01', status: 'running', stage: 'run_backtesting' }
+97
View File
@@ -0,0 +1,97 @@
/* mock 路由匹配器 —— 按 method + URL 正则返回假数据
* 仅在后端完全不可达(无 HTTP response)时由 client 兜底调用;
* 连真实后端时(含 4xx/5xx)不触发,真实错误照常暴露。
* 样板阶段用于让每个页面都有内容可看。接真实后端时无需改这里。 */
import * as D from './data'
export const MOCK_ON = true
type Builder = (url: string) => unknown
interface Route {
m: string
re: RegExp
build: Builder
}
const routes: Route[] = [
// strategy
{ m: 'get', re: /^\/strategy\/list$/, build: () => D.strategiesMock },
// strategy instances(实例层 · 代码下参数变体)
{ m: 'get', re: /^\/strategy\/instances$/, build: () => D.strategyInstancesMock },
{ m: 'get', re: /^\/strategy\/instances\/[^/]+$/, build: (url) => { const id = Number(url.split('/')[3]); const inst = D.strategyInstancesMock.instances.find((i) => i.id === id); return { instance: inst ?? D.strategyInstancesMock.instances[0] } } },
{ m: 'post', re: /^\/strategy\/instances$/, build: () => ({ id: 99 }) },
{ m: 'delete', re: /^\/strategy\/instances\/[^/]+$/, build: () => ({ ok: true }) },
// strategy code files(在线编辑)
{ m: 'get', re: /^\/strategy\/files$/, build: () => D.strategyFilesMock },
{ m: 'post', re: /^\/strategy\/file\/[^/]+$/, build: () => ({ ok: true }) },
// strategy configsCRUD
{ m: 'get', re: /^\/strategy\/configs$/, build: () => D.strategyConfigsMock },
{ m: 'post', re: /^\/strategy\/configs$/, build: () => ({ id: 99 }) },
{ m: 'put', re: /^\/strategy\/configs\/[^/]+$/, build: () => ({ ok: true }) },
{ m: 'delete', re: /^\/strategy\/configs\/[^/]+$/, build: () => ({ ok: true }) },
// factor
{ m: 'get', re: /^\/factor\/list$/, build: () => D.factorListMock },
{ m: 'get', re: /^\/task\/[^/]+\/ic-summary$/, build: () => D.icSummaryMock },
{ m: 'post', re: /^\/factor\/analyze$/, build: () => ({ task_id: 'fc_mock_submit' }) },
{
m: 'get',
re: /^\/strategy\/[^/]+\/params$/,
build: (url) => D.strategyParamsMock[decodeURIComponent(url.split('/')[2])] ?? { parameters: [], defaults: {} },
},
// task
{ m: 'get', re: /^\/task$/, build: () => D.taskListMock },
{ m: 'get', re: /^\/task\/[^/]+$/, build: () => D.taskStatusDoneMock },
{ m: 'get', re: /^\/task\/[^/]+\/result$/, build: () => D.ctaMetricsMock },
{ m: 'get', re: /^\/task\/[^/]+\/equity-curve$/, build: () => D.equityCurveMock },
{ m: 'get', re: /^\/task\/[^/]+\/benchmark-curve$/, build: () => D.benchmarkCurveMock },
{ m: 'get', re: /^\/task\/[^/]+\/risk-series$/, build: () => D.riskSeriesMock },
{ m: 'get', re: /^\/task\/[^/]+\/daily-pnl$/, build: () => D.dailyPnlMock },
{ m: 'get', re: /^\/task\/[^/]+\/trades$/, build: () => D.tradesMock },
{ m: 'get', re: /^\/task\/[^/]+\/log$/, build: () => D.logMock },
{ m: 'get', re: /^\/task\/[^/]+\/optimization-results$/, build: () => D.optResultsMock },
// kline
{ m: 'get', re: /^\/kline$/, build: () => D.klineMock },
// paper
{ m: 'get', re: /^\/paper$/, build: () => D.paperListMock },
{ m: 'get', re: /^\/paper\/[^/]+$/, build: () => D.paperDetailMock },
{ m: 'get', re: /^\/paper\/[^/]+\/equity$/, build: () => D.paperEquityMock },
{ m: 'get', re: /^\/paper\/[^/]+\/trades$/, build: () => D.paperTradesMock },
{ m: 'get', re: /^\/paper\/[^/]+\/strategies$/, build: () => D.paperStrategiesMock },
{ m: 'get', re: /^\/paper\/[^/]+\/positions$/, build: () => D.paperPositionsMock },
{ m: 'get', re: /^\/paper\/[^/]+\/pending$/, build: () => D.paperPendingMock },
// live
{ m: 'get', re: /^\/live$/, build: () => D.liveListMock },
{ m: 'get', re: /^\/live\/[^/]+$/, build: () => D.liveDetailMock },
{ m: 'get', re: /^\/live\/[^/]+\/positions$/, build: () => D.livePositionsMock },
{ m: 'get', re: /^\/live\/[^/]+\/trades$/, build: () => D.liveTradesMock },
{ m: 'get', re: /^\/live\/[^/]+\/account$/, build: () => D.liveAccountMock },
{ m: 'get', re: /^\/live\/[^/]+\/status$/, build: () => D.liveStatusMock },
// portfolio
{ m: 'get', re: /^\/portfolio\/task\/[^/]+\/result$/, build: () => D.portfolioResultMock },
// POST 写操作 → 成功响应(样板)
{ m: 'post', re: /^\/auth\/login$/, build: () => ({ token: 'mock-token-sanguo-terminal' }) },
{ m: 'post', re: /^\/backtest\/cta$/, build: () => ({ task_id: 'cta_mock_submit' }) },
{ m: 'post', re: /^\/backtest\/optimize$/, build: () => ({ task_id: 'opt_mock_submit' }) },
{ m: 'post', re: /^\/portfolio\/backtest$/, build: () => ({ task_id: 'pf_mock_submit' }) },
{ m: 'post', re: /^\/paper\/create$/, build: () => ({ account_id: 99 }) },
{ m: 'post', re: /^\/live\/create$/, build: () => ({ account_id: 99, status: 'stopped' }) },
{
m: 'post',
re: /^\/live\/[^/]+\/start$/,
build: (url) => ({ account_id: Number(url.split('/')[2]), status: 'running' }),
},
{
m: 'post',
re: /^\/live\/[^/]+\/stop$/,
build: (url) => ({ account_id: Number(url.split('/')[2]), status: 'stopped' }),
},
]
export function matchMock(method: string, url: string): unknown | undefined {
if (!MOCK_ON) return undefined
const m = method.toLowerCase()
for (const r of routes) {
if (r.m === m && r.re.test(url)) return r.build(url)
}
return undefined
}
+86
View File
@@ -0,0 +1,86 @@
/* 时序假数据生成器 —— 确定性(基于 index 的函数),刷新不变,图表稳定 */
/** 从 start 起生成 n 个交易日(跳周末) */
export function genDates(start: string, n: number): string[] {
const d = new Date(start + 'T00:00:00')
const out: string[] = []
let guard = 0
while (out.length < n && guard < n * 2 + 20) {
const day = d.getDay()
if (day !== 0 && day !== 6) out.push(d.toISOString().slice(0, 10))
d.setDate(d.getDate() + 1)
guard++
}
return out
}
/** 策略净值:趋势 + 双频波动 + 周期回撤 */
export function genEquity(n: number, opts?: { start?: number; drift?: number; from?: string }):
{ date: string; balance: number }[] {
const start = opts?.start ?? 1
const drift = opts?.drift ?? 0.0009
const dates = genDates(opts?.from ?? '2024-01-02', n)
let v = start
return dates.map((date, i) => {
const wave = Math.sin(i / 8) * 0.006 + Math.sin(i / 21) * 0.004
const dd = i % 55 < 14 ? -0.004 * Math.sin(((i % 55) / 14) * Math.PI) : 0
v = v * (1 + drift + wave + dd)
return { date, balance: Number(v.toFixed(4)) }
})
}
/** 基准:弱趋势(跑输策略) */
export function genBenchmark(n: number, from = '2024-01-02'): { date: string; balance: number }[] {
const dates = genDates(from, n)
let v = 1
return dates.map((date, i) => {
v = v * (1 + 0.0003 + Math.sin(i / 13) * 0.004)
return { date, balance: Number(v.toFixed(4)) }
})
}
/** 每日盈亏(基点,偏正) */
export function genDailyPnl(n: number): { date: string; pnl: number }[] {
const eq = genEquity(n)
return eq.map((p, i) => {
const prev = i === 0 ? 1 : eq[i - 1].balance
return { date: p.date, pnl: Number(((p.balance - prev) * 10000).toFixed(0)) }
})
}
/** alpha/beta/drawdown/vol 时间序列 */
export function genRisk(n: number, from = '2024-01-02') {
const dates = genDates(from, n)
const eq = genEquity(n, { from }).map((p) => p.balance)
let runMax = 0
const drawdown = eq.map((v) => {
runMax = Math.max(runMax, v)
return Number((((v - runMax) / runMax) * 100).toFixed(2))
})
return {
dates,
alpha: dates.map((_, i) => Number((0.08 + Math.sin(i / 17) * 0.05).toFixed(3))),
beta: dates.map((_, i) => Number((0.72 + Math.sin(i / 25) * 0.12).toFixed(3))),
drawdown,
strategy_vol: dates.map((_, i) => Number((0.18 + Math.sin(i / 11) * 0.03).toFixed(3))),
benchmark_vol: dates.map((_, i) => Number((0.15 + Math.sin(i / 13) * 0.02).toFixed(3))),
}
}
/** 模拟盘/实盘净值(含 cash/market_value/总权益,带 checkpoint 标记) */
export function genPaperEquity(n: number): {
date: string; cash: number; market_value: number; total_equity: number; is_checkpoint: number
}[] {
const eq = genEquity(n, { start: 1000000, drift: 0.0006 })
return eq.map((p, i) => {
const total = p.balance * 1000000
const mv = total * (0.55 + Math.sin(i / 9) * 0.12)
return {
date: p.date,
cash: Number((total - mv).toFixed(0)),
market_value: Number(mv.toFixed(0)),
total_equity: Number(total.toFixed(0)),
is_checkpoint: i % 15 === 0 ? 1 : 0,
}
})
}