Package: testwise
TestwiseA backtester (backtest helper) for testing my trading strategies.It requires a lot of manual processing and coding. Difficult to comprehend. I tried to explain the use of the library with examples as best I could. But writing such automation is quite complex.
There may still be errors. It is pretty difficult to check but I'm trying to improve the usage.Example Usage# 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.fromdatetimeimportdatetime,timedeltafromtestwiseimportTestwiseimportrequestsfromlegitindicatorsimportema,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 variableTRIM=40BINANCE_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.001DYNAMIC_POSITIONING=TrueMARGIN_FACTOR=5LIMIT_FACTOR=1RISK_FACTOR=1.5defmain():# 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)defbacktest(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 lookbacklookback=len(data)-TRIM# These are simply trimmed OHLC datadata=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=[]fori,_inenumerate(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 30forema_length1inrange(10,11):forema_length2inrange(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 accordinglyema_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.fori,_inenumerate(data):# Exclude first price dataifi>1andi<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 iterationdate_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 triggerediftwise.pos==1and(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 positionstwise.exit_long(date_close,opn[i+1],twise.current_open_pos["qty"])# Closing short position(-1 means short)iftwise.pos==-1and(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-emulatorifabs(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)iftwise.pos==1andhigh[i]>twise.current_open_pos["tp"]andtwise.current_open_pos["tptaken"]isFalse:# Stop loss price will be set to break even with break_even() functiontwise.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"])iftwise.pos==1andlow[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:iftwise.pos==-1andhigh[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:iftwise.pos==-1andlow[i]<twise.current_open_pos["tp"]andtwise.current_open_pos["tptaken"]isFalse: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:iftwise.pos==1andlow[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:iftwise.pos==1andhigh[i]>twise.current_open_pos["tp"]andtwise.current_open_pos["tptaken"]isFalse: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:iftwise.pos==-1andlow[i]<twise.current_open_pos["tp"]andtwise.current_open_pos["tptaken"]isFalse: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:iftwise.pos==-1andhigh[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 openiftwise.pos!=1:# If ema_first crosses over ema_secondifema_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 pricestwise.entry_long(date_open,opn[i+1],share,atrng[i])iftwise.pos!=-1:ifema_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 pricestwise.entry_short(date_open,opn[i+1],share,atrng[i])# get_result() function will give you the backtest resultsprint(twise.get_result())defget_data(params):r=requests.get(url=BINANCE_URL,params=params)data=r.json()returndatadefget_ohlc(data):opn=[float(o[1])foroindata]close=[float(d[4])fordindata]high=[float(h[2])forhindata]low=[float(lo[3])forloindata]returnopn,high,low,closeif__name__=="__main__":main()Examplebacktestresult:{'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)}Important note:Do not rely on a single test result.
At least do walkforward test with a few iterations.InstallationRun the following to install:pipinstalltestwise