backtester (backtest helper) สำหรับการทดสอบกลยุทธ์การซื้อขายของฉัน
มันต้องใช้การประมวลผลและการเข้ารหัสด้วยตนเองจำนวนมาก ยากที่จะเข้าใจ ฉันพยายามอธิบายการใช้ห้องสมุดด้วยตัวอย่างที่ดีที่สุดเท่าที่จะทำได้ แต่การเขียนระบบอัตโนมัติดังกล่าวค่อนข้างซับซ้อน อาจมีข้อผิดพลาด มันค่อนข้างยากที่จะตรวจสอบ แต่ฉันพยายามปรับปรุงการใช้งาน
# Testwise is a backtester library that requires some coding knowledge
# There is no cli or interface.
# You should directly execute necessary functions like enter_long() or exit_short()
# This is a backtesting example of Exponential Moving Average cross strategy.
# There is a 1.5 ATR stop loss level and a 1 ATR take profit level for every position.
# Commission rate is 0.1000%.
# Margin usage is allowed up to 5 times the main capital.
from datetime import datetime , timedelta
from testwise import Testwise
import requests
from legitindicators import ema , atr
# In this example, daily BTCUSDT kline data is used from binance
# Let's say you want to backtest your strategy for about 450 days.
# It would be useful to add some extra days to the specified time interval
# for the indicators to work properly.
# (For example 10 days of EMA won't be calculated for the first 9 days of time range)
# In this example I add 40 extra days. This value can be determined by assigning the TRIM variable
TRIM = 40
BINANCE_URL = "https://api.binance.com/api/v3/klines"
SYMBOL = "BTCUSDT"
INTERVAL = "1d"
# These are the initial paramters for backtester.
# You can find a more detailed explanation where the Testwise definition is given below.
COMMISSION = 0.001
DYNAMIC_POSITIONING = True
MARGIN_FACTOR = 5
LIMIT_FACTOR = 1
RISK_FACTOR = 1.5
def main ():
# Here we define the start time and end time of backtesting.
# Notice usage of TRIM variable to start to backtest a few days earlier for proper indicator use.
start_time = datetime ( 2020 , 6 , 1 , 0 , 0 , 0 )
start_time = start_time - timedelta ( days = TRIM )
end_time = datetime ( 2021 , 9 , 1 , 0 , 0 , 0 )
# In this example, timestamps are used. (Because binance accept timestamp)
start_time_ts = int ( datetime . timestamp ( start_time ) * 1000 )
end_time_ts = int ( datetime . timestamp ( end_time ) * 1000 )
backtest ( start_time_ts , end_time_ts )
def backtest ( start_time , end_time ):
# Getting OHLC data
# Example binance kline response
# [
# [
# 1499040000000, // Open time
# "0.01634790", // Open
# "0.80000000", // High
# "0.01575800", // Low
# "0.01577100", // Close
# "148976.11427815", // Volume
# 1499644799999, // Close time
# "2434.19055334", // Quote asset volume
# 308, // Number of trades
# "1756.87402397", // Taker buy base asset volume
# "28.46694368", // Taker buy quote asset volume
# "17928899.62484339" // Ignore.
# ]
# ]
params = { "symbol" : SYMBOL , "interval" : INTERVAL , "startTime" : start_time , "endTime" : end_time }
data = get_data ( params )
opn , high , low , close = get_ohlc ( data )
# Again for proper indicator usage number of bars to work on is defined as lookback
lookback = len ( data ) - TRIM
# These are simply trimmed OHLC data
data = data [ - lookback :]
# Here, a list of close prices kept under different naming conventions than other OHL data
# That is because I will use this close data as a parameter
# for Exponential Moving Average indicator and then trim the list of EMA values afterward.
close_tmp = close [ - lookback :]
opn = opn [ - lookback :]
high = high [ - lookback :]
low = low [ - lookback :]
# Here is the calculation of ATR values historically. I use legitindicators library.
atr_input = []
for i , _ in enumerate ( data ):
ohlc = [ opn [ i ], high [ i ], low [ i ], close_tmp [ i ]]
atr_input . append ( ohlc )
atrng = atr ( atr_input , 14 )
# Backtesting operation starts here.
# Following two for loops will check two EMA crosses in the range of 10 to 30
for ema_length1 in range ( 10 , 11 ):
for ema_length2 in range ( ema_length1 + 1 , 30 ):
# When the dynamic_positioning is set to True,
# the backtester will work as if the margin usage is available for use.
# margin_factor indicates the margin ratio. (In this example, it is 5 times the main capital)
# limit_factor is an ATR based take profit level. (it is 1 ATR from the position price)
# risk_factor is an ATR based stop loss level. (it is 1.5 ATR from the position price)
twise = Testwise (
commission = COMMISSION ,
dynamic_positioning = DYNAMIC_POSITIONING ,
margin_factor = MARGIN_FACTOR ,
limit_factor = LIMIT_FACTOR ,
risk_factor = RISK_FACTOR
)
# Here, two EMA indicators are defined. I use legitindicators library.
ema_first = ema ( close , ema_length1 )
ema_second = ema ( close , ema_length2 )
# List of indicator values trimmed accordingly
ema_first = ema_first [ - lookback :]
ema_second = ema_second [ - lookback :]
# Notice that at this point:
# open, high, low, close, ema_first and ema_second lists are all trimmed
# and all have the same length
# Ready for testing
# Start walking on the data taken from the binance.
for i , _ in enumerate ( data ):
# Exclude first price data
if i > 1 and i < len ( data ) - 1 :
# Here, data[n][0] is the open time of price data
# date_open is kept for use if there will be a pose to be opened the next day
# date_close is kept for use if the current open position is closed in this iteration
date_open = datetime . fromtimestamp ( int ( data [ i + 1 ][ 0 ] / 1000 )). strftime ( "%Y-%m-%d %H" )
date_close = datetime . fromtimestamp ( int ( data [ i ][ 0 ] / 1000 )). strftime ( "%Y-%m-%d %H" )
# Position exits
# On every iteration, position exits checked firstly
# Below, if the current position is long (1 means long) and
# the ema_first crosses below the ema_second, position exit function triggered
if twise . pos == 1 and ( ema_first [ i ] < ema_second [ i ]):
# exit_long function takes closing date,
# closing price as open price of next day opn[i + 1],
# and amount to close the position.
# This amount already kept in twise.current_open_pos["qty"].
# This value is set when opening the positions
twise . exit_long ( date_close , opn [ i + 1 ], twise . current_open_pos [ "qty" ])
# Closing short position(-1 means short)
if twise . pos == - 1 and ( ema_first [ i ] > ema_second [ i ]):
twise . exit_short ( date_close , opn [ i + 1 ], twise . current_open_pos [ "qty" ])
# The following if condition simulates price movements inside the bar.
# This is crucial if you want to add take profit and stop loss logic to the backtester.
# This pine script broker emulator documentation will explain this condition more clearly:
# https://www.tradingview.com/pine-script-docs/en/v5/concepts/Strategies.html?highlight=strategy#broker-emulator
if abs ( high [ i ] - opn [ i ]) < abs ( low [ i ] - opn [ i ]):
# Simply, If the bar’s high is closer to bar’s open than the bar’s low,
# bar movement will be like:
# open - high - low - close
# In this movement, take profit operation will be checked before stop loss.
# This is because, it is assumed that the price will go up first.
# For example, if both take profit and stop loss prices are exceeded,
# it is assumed that first, take profit is taken, than stop loss price is reached.
# if current position is long, here is take profit logic:
# if current position is long and high is
# higher than take proift price (twise.current_open_pos["tp"])
# and take profit is not taken (twise.current_open_pos["tptaken"] is False)
if twise . pos == 1 and high [ i ] > twise . current_open_pos [ "tp" ] and twise . current_open_pos [ "tptaken" ] is False :
# Stop loss price will be set to break even with break_even() function
twise . break_even ()
# Take profit operation is simply a partially position closing operation.
# Here, half of the position is closed. (twise.current_open_pos["qty"] / 2)
twise . exit_long ( date_close , twise . current_open_pos [ "tp" ], twise . current_open_pos [ "qty" ] / 2 , True )
# if current position is long, here is stop loss logic:
# if current position is long and low is
# lower than stop loss price (twise.current_open_pos["sl"])
if twise . pos == 1 and low [ i ] < twise . current_open_pos [ "sl" ]:
twise . exit_long ( date_close , twise . current_open_pos [ "sl" ], twise . current_open_pos [ "qty" ])
# if current position is short, here is take profit logic:
if twise . pos == - 1 and high [ i ] > twise . current_open_pos [ "sl" ]:
twise . exit_short ( date_close , twise . current_open_pos [ "sl" ], twise . current_open_pos [ "qty" ])
# if current position is short, here is stop loss logic:
if twise . pos == - 1 and low [ i ] < twise . current_open_pos [ "tp" ] and twise . current_open_pos [ "tptaken" ] is False :
twise . break_even ()
twise . exit_short ( date_close , twise . current_open_pos [ "tp" ], twise . current_open_pos [ "qty" ] / 2 , True )
else :
# If the bar’s low is closer to bar’s open than the bar’s high,
# bar movement will be like:
# open - low - high - close
# In this movement, stop loss operation will be checked before take profit.
# This is because, it is assumed that the price will go down firstly.
# For example, if both take profit and stop loss prices are exceeded,
# it is assumed that first, stop loss is executed,
# then take profit will never be reached because
# if the position is fully closed with exit_long,
# twise.pos value will be 0 (which means there is no open position).
# if the current position is long, here is stop loss logic:
if twise . pos == 1 and low [ i ] < twise . current_open_pos [ "sl" ]:
twise . exit_long ( date_close , twise . current_open_pos [ "sl" ], twise . current_open_pos [ "qty" ])
# if current position is long, here is take profit logic:
if twise . pos == 1 and high [ i ] > twise . current_open_pos [ "tp" ] and twise . current_open_pos [ "tptaken" ] is False :
twise . break_even ()
twise . exit_long ( date_close , twise . current_open_pos [ "tp" ], twise . current_open_pos [ "qty" ] / 2 , True )
# if current position is short, here is take profit logic:
if twise . pos == - 1 and low [ i ] < twise . current_open_pos [ "tp" ] and twise . current_open_pos [ "tptaken" ] is False :
twise . break_even ()
twise . exit_short ( date_close , twise . current_open_pos [ "tp" ], twise . current_open_pos [ "qty" ] / 2 , True )
# if current position is short, here is stop loss logic:
if twise . pos == - 1 and high [ i ] > twise . current_open_pos [ "sl" ]:
twise . exit_short ( date_close , twise . current_open_pos [ "sl" ], twise . current_open_pos [ "qty" ])
# Opening long position
# If there is no long positions open
if twise . pos != 1 :
# If ema_first crosses over ema_second
if ema_first [ i ] > ema_second [ i ]:
# You can manually set the amount to open position.
# But there will be a calculation overhead.
# Testwise has a built-in share calculation funciton
# In tihs function, share is calculated as:
# share = (equity * position risk) / (atr * risk factor)
share = twise . calculate_share ( atrng [ i ], custom_position_risk = 0.02 )
# Opening long position with opening date (date_open),
# opening price of next day (opn[i + 1]),
# amount to buy, and current atr value to define take profit and stop loss prices
twise . entry_long ( date_open , opn [ i + 1 ], share , atrng [ i ])
if twise . pos != - 1 :
if ema_first [ i ] < ema_second [ i ]:
share = twise . calculate_share ( atrng [ i ], custom_position_risk = 0.02 )
# Opening short position with opening date (date_open),
# opening price of next day (opn[i + 1]),
# amount to buy, and current atr value to define take profit and stop loss prices
twise . entry_short ( date_open , opn [ i + 1 ], share , atrng [ i ])
# get_result() function will give you the backtest results
print ( twise . get_result ())
def get_data ( params ):
r = requests . get ( url = BINANCE_URL , params = params )
data = r . json ()
return data
def get_ohlc ( data ):
opn = [ float ( o [ 1 ]) for o in data ]
close = [ float ( d [ 4 ]) for d in data ]
high = [ float ( h [ 2 ]) for h in data ]
low = [ float ( lo [ 3 ]) for lo in data ]
return opn , high , low , close
if __name__ == "__main__" :
main ()
Example backtest result :
{
'net_profit' : 30557.012567638478 ,
'net_profit_percent' : 30.557012567638477 ,
'gross_profit' : 69163.31181062985 ,
'gross_loss' : 36783.34343506002 ,
'max_drawdown' : - 13265.365111723615 ,
'max_drawdown_rate' : 2.3035183962356918 ,
'win_rate' : 53.48837209302326 ,
'risk_reward_ratio' : 1.6350338129618904 ,
'profit_factor' : 1.880288884906174 ,
'ehlers_ratio' : 0.1311829454585705 ,
'return_on_capital' : 0.26978249297565415 ,
'max_capital_required' : 113265.36511172361 ,
'total_trades' : 43 ,
'pearsonsr' : 0.8022110890986095 ,
'number_of_winning_trades' : 23 ,
'number_of_losing_trades' : 20 ,
'largest_winning_trade' : ( '2021-01-23 03' , 34417.71907928039 ),
'largest_losing_trade' : ( '2020-09-21 03' , - 4627.351985682239 )}
อย่าพึ่งพาผลการทดสอบเพียงครั้งเดียว อย่างน้อยก็ทำการทดสอบ Walkforward ด้วยการทำซ้ำสองสามครั้ง
รันต่อไปนี้เพื่อติดตั้ง:
pip install testwise