From 78e1e7c7cff07396b8419f9e0f4589f4ab4ed8f8 Mon Sep 17 00:00:00 2001 From: CoprDistGit Date: Thu, 18 May 2023 06:59:56 +0000 Subject: automatic import of python-testwise --- python-testwise.spec | 998 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 998 insertions(+) create mode 100644 python-testwise.spec (limited to 'python-testwise.spec') diff --git a/python-testwise.spec b/python-testwise.spec new file mode 100644 index 0000000..8924976 --- /dev/null +++ b/python-testwise.spec @@ -0,0 +1,998 @@ +%global _empty_manifest_terminate_build 0 +Name: python-testwise +Version: 0.0.63 +Release: 1 +Summary: A backtester (backtest helper) for testing my trading strategies. +License: MIT License +URL: https://github.com/aticio/testwise +Source0: https://mirrors.nju.edu.cn/pypi/web/packages/68/2f/a32200ddc5c3390dda0460de00b824bf742f67adc5c3cf028d87acb75788/testwise-0.0.63.tar.gz +BuildArch: noarch + +Requires: python3-matplotlib +Requires: python3-pytest + +%description +# Testwise + +![Publish Python 🐍 distributions 📦 to PyPI and TestPyPI](https://github.com/aticio/legitindicators/workflows/Publish%20Python%20%F0%9F%90%8D%20distributions%20%F0%9F%93%A6%20to%20PyPI%20and%20TestPyPI/badge.svg) + +A 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 +```python +# 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() +``` + +```python +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)} +``` + +## Important note: +Do not rely on a single test result. +At least do walkforward test with a few iterations. + +## Installation + +Run the following to install: + +```python +pip install testwise +``` + + + + +%package -n python3-testwise +Summary: A backtester (backtest helper) for testing my trading strategies. +Provides: python-testwise +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3-pip +%description -n python3-testwise +# Testwise + +![Publish Python 🐍 distributions 📦 to PyPI and TestPyPI](https://github.com/aticio/legitindicators/workflows/Publish%20Python%20%F0%9F%90%8D%20distributions%20%F0%9F%93%A6%20to%20PyPI%20and%20TestPyPI/badge.svg) + +A 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 +```python +# 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() +``` + +```python +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)} +``` + +## Important note: +Do not rely on a single test result. +At least do walkforward test with a few iterations. + +## Installation + +Run the following to install: + +```python +pip install testwise +``` + + + + +%package help +Summary: Development documents and examples for testwise +Provides: python3-testwise-doc +%description help +# Testwise + +![Publish Python 🐍 distributions 📦 to PyPI and TestPyPI](https://github.com/aticio/legitindicators/workflows/Publish%20Python%20%F0%9F%90%8D%20distributions%20%F0%9F%93%A6%20to%20PyPI%20and%20TestPyPI/badge.svg) + +A 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 +```python +# 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() +``` + +```python +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)} +``` + +## Important note: +Do not rely on a single test result. +At least do walkforward test with a few iterations. + +## Installation + +Run the following to install: + +```python +pip install testwise +``` + + + + +%prep +%autosetup -n testwise-0.0.63 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then + find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then + find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then + find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then + find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then + find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-testwise -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Thu May 18 2023 Python_Bot - 0.0.63-1 +- Package Spec generated -- cgit v1.2.3