最近有许多典型的非典型的策略,想要回测历史表现。市面上的回测服务存在繁琐、限制较多的问题,不能满足需要。粗略想了下回测框架的实现并不复杂,用熟悉的编程语言,自己动手也许是成本最低的方式。
核心元素
以单一股票的策略为例,输入「股票代码、回测时间段、策略逻辑」,输出「盈利结果、历史交易动作」即可。多只股票组合亦同理。
代码实现
最简版
包含注释在内的最简版的核心代码不到 50 行
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
| export class BackTesting { public readonly options: Options
constructor(options: Options) { this.options = options }
public async execute(period: KLinePeriod, klineCallback: KLineCallback) { const accountInfo: AccountInfo = { balance: 0, position: 0, tradingItems: [], profit: 0 } const kLines = await this.getKLines(period) for (const curKLine of kLines) { this.tradeForAccount(accountInfo, klineCallback({ kline: curKLine, account: accountInfo })) } accountInfo.profit = accountInfo.balance + accountInfo.position * kLines[kLines.length - 1].close return accountInfo }
public tradeForAccount(accountInfo: AccountInfo, tradingItems: TradingItem[]) { tradingItems.forEach((tradingItem) => { switch (tradingItem.action) { case 'BUY': accountInfo.position += tradingItem.quantity accountInfo.balance -= tradingItem.quantity * tradingItem.price break case 'SELL': accountInfo.position -= tradingItem.quantity accountInfo.balance += tradingItem.quantity * tradingItem.price break } }) accountInfo.tradingItems.push(...tradingItems) }
public async getKLines(period: KLinePeriod): Promise<Classical_KLine[]> { const kLines = await KLineTools.getStockKLines(this.options.stockCode, { period: period, beginTime: this.options.startTime, endTime: this.options.endTime, }) return kLines } }
|
均线策略框架
均线相关的交易策略非常常见。于是可以在核心函数基础上实现一个新的均线策略测试函数,使回调函数携带均线信息供策略开发者直接使用。
为了让每条回调数据都尽可能拥有均值信息,如 MA20 需要额外向前获取 20 条数据,于是可将 getKLines
方法微调,使其支持自定义的开始时间
1 2 3 4 5 6 7 8
| public async getKLines(period: KLinePeriod, startTime?: string): Promise<Classical_KLine[]> { const kLines = await KLineTools.getStockKLines(this.options.stockCode, { period: period, beginTime: startTime || this.options.startTime, endTime: this.options.endTime, }) return kLines }
|
testMA
支持 OptionsMA
传递,如 20 日均线为 { period: KLinePeriod.DAY, length: 20 }
- 回调方法额外携带
prevMA
、curMA
信息,供策略判断现价与均价的关系,以及均线朝向
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
| public async testMA(optionsMA: OptionsMA, klineCallback: MACallback) { const accountInfo: AccountInfo = { balance: 0, position: 0, tradingItems: [], profit: 0 } const { startTime } = this.options
const realStartTime = ((): string => { if (optionsMA.period.endsWith('minute') || optionsMA.period.endsWith('hour')) { return startTime } const period = optionsMA.period.split('/')![1] as any return moment(startTime) .subtract(optionsMA.length * 2, period) .startOf(period) .format('YYYY-MM-DD') })()
const kLines = await this.getKLines(optionsMA.period, realStartTime)
const startIndex = ((): number => { const keyTs = moment(startTime).unix() return Math.max( kLines.findIndex((item) => moment(item.time).unix() >= keyTs), optionsMA.length ) })()
if (startIndex < optionsMA.length) { return accountInfo }
let prevSum = kLines.slice(startIndex - optionsMA.length, startIndex).reduce((result, cur) => result + cur.close, 0)
for (let i = startIndex; i < kLines.length; ++i) { const curKLine = kLines[i] const curSum = prevSum - kLines[i - optionsMA.length].close + curKLine.close
const items = klineCallback({ kline: curKLine, prevMA: prevSum / optionsMA.length, curMA: curSum / optionsMA.length, account: accountInfo, }) this.tradeForAccount(accountInfo, items) prevSum = curSum } accountInfo.profit = accountInfo.balance + accountInfo.position * kLines[kLines.length - 1].close return accountInfo }
|
回测示例
回测恒指期货 2022-01-01
~ 2024-01-24
“收盘价站上 MA20 日线做多,跌破 MA20 日线平仓” 的示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const backTesting = new BackTesting({ stockCode: 'HSImain', startTime: '2022-01-01', endTime: '2024-01-24' }) const accountInfo = await backTesting.testMA( { period: KLinePeriod.DAY, length: 20 }, ({ kline, curMA, account }) => { const tradingItems: TradingItem[] = [] if (kline.close > curMA && account.position <= 0) { tradingItems.push({ date: kline.time, action: 'BUY', price: kline.close, quantity: 1 }) } else if (kline.close < curMA && account.position > 0) { tradingItems.push({ date: kline.time, action: 'SELL', price: kline.close, quantity: account.position }) } return tradingItems } ) console.info(accountInfo) console.info(`Trade ${accountInfo.tradingItems.length} times`)
|
输出结果为:交易 52 次,亏损 2111
😀
1 2 3 4 5 6 7 8 9 10 11 12 13
| { balance: -2111, position: 0, tradingItems: [ { date: '2022-01-07', action: 'BUY', price: 23467, quantity: 1 }, { date: '2022-01-27', action: 'SELL', price: 23837, quantity: 1 }, …… { date: '2023-12-27', action: 'BUY', price: 16657, quantity: 1 }, { date: '2024-01-05', action: 'SELL', price: 16589, quantity: 1 } ], profit: -2111 } Trade 52 times
|
后续利用
- 有了易编程可复用的框架后,可将经典及突发奇想的策略落实为可执行代码,输入全市场全周期数据逐一验证
- 输出结果包含交易明细,可用于绘制策略资产净值曲线,计算最大回撤
- 对典型结果及非典型结果,可进行相关性归纳
附录(数据结构定义)
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
| export interface Classical_KLine { time: string
open: number close: number low: number high: number
prev_close: number close_p: number
volume: number turnover: number }
export enum KLinePeriod { MIN_1 = '1/minute', MIN_5 = '5/minute', MIN_15 = '15/minute', MIN_30 = '30/minute', MIN_60 = '60/minute', HOUR_1 = '1/hour', HOUR_4 = '4/hour', DAY = '1/day', WEEK = '1/week', MONTH = '1/month', QUARTER = '1/quarter', YEAR = '1/year', }
export interface TradingItem { date: string action: 'BUY' | 'SELL' price: number quantity: number }
export interface AccountInfo { balance: number position: number tradingItems: TradingItem[] profit: number }
interface CallbackCoreData { kline: Classical_KLine account: AccountInfo }
export interface MACallbackData extends CallbackCoreData { kline: Classical_KLine account: AccountInfo prevMA: number curMA: number }
export type KLineCallback<T extends CallbackCoreData = CallbackCoreData> = (data: T) => TradingItem[]
export type MACallback = (data: MACallbackData) => TradingItem[]
|