[Add] backtesting function for spread trading

This commit is contained in:
vn.py 2019-11-10 16:09:43 +08:00
parent ddbf62d47a
commit 687bdbc66d
5 changed files with 297 additions and 95 deletions

View File

@ -0,0 +1,120 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"#%%\n",
"from vnpy.app.spread_trading.backtesting import BacktestingEngine\n",
"from vnpy.app.spread_trading.strategies.statistical_arbitrage_strategy import (\n",
" StatisticalArbitrageStrategy\n",
")\n",
"from vnpy.app.spread_trading.base import LegData, SpreadData\n",
"from datetime import datetime"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"spread = SpreadData(\n",
" name=\"IF-Spread\",\n",
" legs=[LegData(\"IF1911.CFFEX\"), LegData(\"IF1912.CFFEX\")],\n",
" price_multipliers={\"IF1911.CFFEX\": 1, \"IF1912.CFFEX\": -1},\n",
" trading_multipliers={\"IF1911.CFFEX\": 1, \"IF1912.CFFEX\": -1},\n",
" active_symbol=\"IF1911.CFFEX\",\n",
" inverse_contracts={\"IF1911.CFFEX\": False, \"IF1912.CFFEX\": False},\n",
" min_volume=1\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"#%%\n",
"engine = BacktestingEngine()\n",
"engine.set_parameters(\n",
" spread=spread,\n",
" interval=\"1m\",\n",
" start=datetime(2019, 6, 10),\n",
" end=datetime(2019, 11, 10),\n",
" rate=0,\n",
" slippage=0,\n",
" size=300,\n",
" pricetick=0.2,\n",
" capital=1_000_000,\n",
")\n",
"engine.add_strategy(StatisticalArbitrageStrategy, {})"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"2019-11-10 16:09:03.822440\t开始加载历史数据\n"
]
}
],
"source": [
"#%%\n",
"engine.load_data()\n",
"engine.run_backtesting()\n",
"df = engine.calculate_result()\n",
"engine.calculate_statistics()\n",
"engine.show_chart()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"for trade in engine.trades.values():\n",
" print(trade)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.1"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@ -3,7 +3,9 @@ from pathlib import Path
from vnpy.trader.app import BaseApp
from vnpy.trader.object import (
OrderData,
TradeData
TradeData,
TickData,
BarData
)
from .engine import (

View File

@ -1,6 +1,6 @@
from collections import defaultdict
from datetime import date, datetime, timedelta
from typing import Callable
from typing import Callable, Type, Dict, List
from functools import lru_cache
import numpy as np
@ -14,7 +14,7 @@ from vnpy.trader.database import database_manager
from vnpy.trader.object import TradeData, BarData, TickData
from vnpy.trader.utility import round_to
from .template import SpreadStrategyTemplate
from .template import SpreadStrategyTemplate, SpreadAlgoTemplate
from .base import SpreadData, BacktestingMode
sns.set_style("whitegrid")
@ -38,8 +38,8 @@ class BacktestingEngine:
self.capital = 1_000_000
self.mode = BacktestingMode.BAR
self.strategy_class = None
self.strategy = None
self.strategy_class: Type[SpreadStrategyTemplate] = None
self.strategy: SpreadStrategyTemplate = None
self.tick: TickData = None
self.bar: BarData = None
self.datetime = None
@ -138,7 +138,8 @@ class BacktestingEngine:
self.spread,
self.interval,
self.start,
self.end
self.end,
self.pricetick
)
else:
self.history_datas = load_tick_data(
@ -208,8 +209,7 @@ class BacktestingEngine:
start_pos,
self.size,
self.rate,
self.slippage,
self.inverse
self.slippage
)
pre_close = daily_result.close_price
@ -427,10 +427,9 @@ class BacktestingEngine:
""""""
self.bar = bar
self.datetime = bar.datetime
self.cross_algo()
self.cross_limit_order()
self.cross_stop_order()
self.strategy.on_bar(bar)
self.strategy.on_spread_bar(bar)
self.update_daily_close(bar.close_price)
@ -438,44 +437,39 @@ class BacktestingEngine:
""""""
self.tick = tick
self.datetime = tick.datetime
self.cross_algo()
self.cross_limit_order()
self.cross_stop_order()
self.strategy.on_tick(tick)
self.spread.bid_price = tick.bid_price_1
self.spread.bid_volume = tick.bid_volume_1
self.spread.ask_price = tick.ask_price_1
self.spread.ask_volume = tick.ask_volume_1
self.strategy.on_spread_data()
self.update_daily_close(tick.last_price)
def cross_limit_order(self):
def cross_algo(self):
"""
Cross limit order with last bar/tick data.
"""
if self.mode == BacktestingMode.BAR:
long_cross_price = self.bar.low_price
short_cross_price = self.bar.high_price
long_best_price = self.bar.open_price
short_best_price = self.bar.open_price
long_cross_price = self.bar.close_price
short_cross_price = self.bar.close_price
else:
long_cross_price = self.tick.ask_price_1
short_cross_price = self.tick.bid_price_1
long_best_price = long_cross_price
short_best_price = short_cross_price
for order in list(self.active_limit_orders.values()):
# Push order update with status "not traded" (pending).
if order.status == Status.SUBMITTING:
order.status = Status.NOTTRADED
self.strategy.on_order(order)
for algo in list(self.active_algos.values()):
# Check whether limit orders can be filled.
long_cross = (
order.direction == Direction.LONG
and order.price >= long_cross_price
algo.direction == Direction.LONG
and algo.price >= long_cross_price
and long_cross_price > 0
)
short_cross = (
order.direction == Direction.SHORT
and order.price <= short_cross_price
algo.direction == Direction.SHORT
and algo.price <= short_cross_price
and short_cross_price > 0
)
@ -483,49 +477,49 @@ class BacktestingEngine:
continue
# Push order udpate with status "all traded" (filled).
order.traded = order.volume
order.status = Status.ALLTRADED
self.strategy.on_order(order)
algo.traded = algo.volume
algo.status = Status.ALLTRADED
self.strategy.update_spread_algo(algo)
self.active_limit_orders.pop(order.vt_orderid)
self.active_algos.pop(algo.algoid)
# Push trade update
self.trade_count += 1
if long_cross:
trade_price = min(order.price, long_best_price)
pos_change = order.volume
trade_price = long_cross_price
pos_change = algo.volume
else:
trade_price = max(order.price, short_best_price)
pos_change = -order.volume
trade_price = short_cross_price
pos_change = -algo.volume
trade = TradeData(
symbol=order.symbol,
exchange=order.exchange,
orderid=order.orderid,
symbol=self.spread.name,
exchange=Exchange.LOCAL,
orderid=algo.algoid,
tradeid=str(self.trade_count),
direction=order.direction,
offset=order.offset,
direction=algo.direction,
offset=algo.offset,
price=trade_price,
volume=order.volume,
volume=algo.volume,
time=self.datetime.strftime("%H:%M:%S"),
gateway_name=self.gateway_name,
)
trade.datetime = self.datetime
self.strategy.pos += pos_change
self.strategy.on_trade(trade)
self.spread.net_pos += pos_change
self.strategy.on_spread_pos()
self.trades[trade.vt_tradeid] = trade
def load_bar(
self, spread: str, days: int, interval: Interval, callback: Callable
self, spread: SpreadData, days: int, interval: Interval, callback: Callable
):
""""""
self.days = days
self.callback = callback
def load_tick(self, spread: str, days: int, callback: Callable):
def load_tick(self, spread: SpreadData, days: int, callback: Callable):
""""""
self.days = days
self.callback = callback
@ -543,14 +537,39 @@ class BacktestingEngine:
lock: bool
) -> str:
""""""
pass
self.algo_count += 1
algoid = str(self.algo_count)
algo = SpreadAlgoTemplate(
self,
algoid,
self.spread,
direction,
offset,
price,
volume,
payup,
interval,
lock
)
self.algos[algoid] = algo
self.active_algos[algoid] = algo
return algoid
def stop_algo(
self,
strategy: SpreadStrategyTemplate,
algoid: str
):
""""""
pass
if algoid not in self.active_algos:
return
algo = self.active_algos.pop(algoid)
algo.status = Status.CANCELLED
self.strategy.update_spread_algo(algo)
def send_order(
self,
@ -563,23 +582,15 @@ class BacktestingEngine:
lock: bool
):
""""""
price = round_to(price, self.pricetick)
if stop:
vt_orderid = self.send_stop_order(direction, offset, price, volume)
else:
vt_orderid = self.send_limit_order(direction, offset, price, volume)
return [vt_orderid]
pass
def cancel_order(self, strategy: SpreadStrategyTemplate, vt_orderid: str):
"""
Cancel order by vt_orderid.
"""
if vt_orderid.startswith(STOPORDER_PREFIX):
self.cancel_stop_order(strategy, vt_orderid)
else:
self.cancel_limit_order(strategy, vt_orderid)
pass
def write_log(self, msg: str, strategy: SpreadStrategyTemplate = None):
def write_strategy_log(self, strategy: SpreadStrategyTemplate, msg: str):
"""
Write log message.
"""
@ -598,6 +609,10 @@ class BacktestingEngine:
"""
pass
def write_algo_log(self, algo: SpreadAlgoTemplate, msg: str):
""""""
pass
class DailyResult:
""""""
@ -633,8 +648,7 @@ class DailyResult:
start_pos: float,
size: int,
rate: float,
slippage: float,
inverse: bool
slippage: float
):
""""""
# If no pre_close provided on the first day,
@ -648,12 +662,7 @@ class DailyResult:
self.start_pos = start_pos
self.end_pos = start_pos
if not inverse: # For normal contract
self.holding_pnl = self.start_pos * \
(self.close_price - self.pre_close) * size
else: # For crypto currency inverse contract
self.holding_pnl = self.start_pos * \
(1 / self.pre_close - 1 / self.close_price) * size
self.holding_pnl = self.start_pos * (self.close_price - self.pre_close) * size
# Trading pnl is the pnl from new trade during the day
self.trade_count = len(self.trades)
@ -666,18 +675,10 @@ class DailyResult:
self.end_pos += pos_change
# For normal contract
if not inverse:
turnover = trade.volume * size * trade.price
self.trading_pnl += pos_change * \
(self.close_price - trade.price) * size
self.slippage += trade.volume * size * slippage
# For crypto currency inverse contract
else:
turnover = trade.volume * size / trade.price
self.trading_pnl += pos_change * \
(1 / trade.price - 1 / self.close_price) * size
self.slippage += trade.volume * size * slippage / (trade.price ** 2)
turnover = trade.volume * size * trade.price
self.trading_pnl += pos_change * \
(self.close_price - trade.price) * size
self.slippage += trade.volume * size * slippage
self.turnover += turnover
self.commission += turnover * rate
@ -687,30 +688,72 @@ class DailyResult:
self.net_pnl = self.total_pnl - self.commission - self.slippage
@lru_cache(maxsize=999)
def load_bar_data(
symbol: str,
exchange: Exchange,
spread: SpreadData,
interval: Interval,
start: datetime,
end: datetime
end: datetime,
pricetick: float
):
""""""
return database_manager.load_bar_data(
symbol, exchange, interval, start, end
)
# Load bar data of each spread leg
leg_bars: Dict[str, Dict] = {}
for vt_symbol in spread.legs.keys():
symbol, exchange_str = vt_symbol.split(".")
exchange = Exchange(exchange_str)
bar_data: List[BarData] = database_manager.load_bar_data(
symbol, exchange, interval, start, end
)
bars: Dict[datetime, BarData] = {bar.datetime: bar for bar in bar_data}
leg_bars[vt_symbol] = bars
# Calculate spread bar data
spread_bars: List[BarData] = []
for dt in bars.keys():
spread_price = 0
spread_available = True
for leg in spread.legs.values():
leg_bar = leg_bars[leg.vt_symbol].get(dt, None)
if leg_bar:
price_multiplier = spread.price_multipliers[leg.vt_symbol]
spread_price += price_multiplier * leg_bar.close_price
else:
spread_available = False
if spread_available:
spread_price = round_to(spread_price, pricetick)
spread_bar = BarData(
symbol=spread.name,
exchange=exchange.LOCAL,
datetime=dt,
interval=interval,
open_price=spread_price,
high_price=spread_price,
low_price=spread_price,
close_price=spread_price,
gateway_name=BacktestingEngine.gateway_name,
)
spread_bars.append(spread_bar)
return spread_bars
@lru_cache(maxsize=999)
def load_tick_data(
symbol: str,
exchange: Exchange,
spread: SpreadData,
start: datetime,
end: datetime
):
""""""
return database_manager.load_tick_data(
symbol, exchange, start, end
spread.name, Exchange.LOCAL, start, end
)

View File

@ -1,5 +1,6 @@
from typing import Dict, List
from datetime import datetime
from enum import Enum
from vnpy.trader.object import TickData, PositionData, TradeData, ContractData
from vnpy.trader.constant import Direction, Offset, Exchange

View File

@ -1,10 +1,12 @@
from collections import defaultdict
from typing import Dict, List, Set
from typing import Dict, List, Set, Callable
from copy import copy
from vnpy.trader.object import TickData, TradeData, OrderData, ContractData
from vnpy.trader.constant import Direction, Status, Offset
from vnpy.trader.object import (
TickData, TradeData, OrderData, ContractData, BarData
)
from vnpy.trader.constant import Direction, Status, Offset, Interval
from vnpy.trader.utility import virtual, floor_to, ceil_to, round_to
from .base import SpreadData, calculate_inverse_volume
@ -434,6 +436,20 @@ class SpreadStrategyTemplate:
"""
pass
@virtual
def on_spread_tick(self, tick: TickData):
"""
Callback when new spread tick data is generated.
"""
pass
@virtual
def on_spread_bar(self, bar: BarData):
"""
Callback when new spread bar data is generated.
"""
pass
@virtual
def on_spread_pos(self):
"""
@ -635,3 +651,23 @@ class SpreadStrategyTemplate:
"""
if self.inited:
self.strategy_engine.send_email(msg, self)
def load_bar(
self,
days: int,
interval: Interval = Interval.MINUTE,
callback: Callable = None,
):
"""
Load historical bar data for initializing strategy.
"""
if not callback:
callback = self.on_spread_bar
self.strategy_engine.load_bar(self.spread, days, interval, callback)
def load_tick(self, days: int):
"""
Load historical tick data for initializing strategy.
"""
self.strategy_engine.load_tick(self.spread, days, self.on_spread_tick)