{"id":23675499,"url":"https://github.com/aticio/testwise","last_synced_at":"2025-12-25T06:30:13.660Z","repository":{"id":45987551,"uuid":"258052776","full_name":"aticio/testwise","owner":"aticio","description":"A backtester (backtest helper) for testing my trading strategies.","archived":false,"fork":false,"pushed_at":"2021-12-29T22:43:48.000Z","size":158,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-02-19T09:32:23.860Z","etag":null,"topics":["algorithmic-trading","algotrade","backtesting","cryptocurrencies","indicators","python","trading"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aticio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-04-23T00:29:39.000Z","updated_at":"2022-05-06T08:54:09.000Z","dependencies_parsed_at":"2022-09-01T05:21:24.221Z","dependency_job_id":null,"html_url":"https://github.com/aticio/testwise","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aticio%2Ftestwise","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aticio%2Ftestwise/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aticio%2Ftestwise/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aticio%2Ftestwise/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aticio","download_url":"https://codeload.github.com/aticio/testwise/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239708990,"owners_count":19684168,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["algorithmic-trading","algotrade","backtesting","cryptocurrencies","indicators","python","trading"],"created_at":"2024-12-29T14:00:18.918Z","updated_at":"2025-12-25T06:30:13.597Z","avatar_url":"https://github.com/aticio.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Testwise\n\n![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)\n\nA backtester (backtest helper) for testing my trading strategies.\n\nIt 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.\nThere may still be errors. It is pretty difficult to check but I'm trying to improve the usage.\n\n## Example Usage\n```python\n# Testwise is a backtester library that requires some coding knowledge\n# There is no cli or interface. \n# You should directly execute necessary functions like enter_long() or exit_short()\n# This is a backtesting example of Exponential Moving Average cross strategy.\n# There is a 1.5 ATR stop loss level and a 1 ATR take profit level for every position. \n# Commission rate is 0.1000%. \n# Margin usage is allowed up to 5 times the main capital.\nfrom datetime import datetime, timedelta\nfrom testwise import Testwise\nimport requests\nfrom legitindicators import ema, atr\n\n# In this example, daily BTCUSDT kline data is used from binance\n# Let's say you want to backtest your strategy for about 450 days.\n# It would be useful to add some extra days to the specified time interval\n# for the indicators to work properly.\n# (For example 10 days of EMA won't be calculated for the first 9 days of time range)\n# In this example I add 40 extra days. This value can be determined by assigning the TRIM variable\nTRIM = 40\nBINANCE_URL = \"https://api.binance.com/api/v3/klines\"\nSYMBOL = \"BTCUSDT\"\nINTERVAL = \"1d\"\n\n# These are the initial paramters for backtester.\n# You can find a more detailed explanation where the Testwise definition is given below.\nCOMMISSION = 0.001\nDYNAMIC_POSITIONING = True\nMARGIN_FACTOR = 5\nLIMIT_FACTOR = 1\nRISK_FACTOR = 1.5\n\n\ndef main():\n    # Here we define the start time and end time of backtesting.\n    # Notice usage of TRIM variable to start to backtest a few days earlier for proper indicator use.\n    start_time = datetime(2020, 6, 1, 0, 0, 0)\n    start_time = start_time - timedelta(days=TRIM)\n\n    end_time = datetime(2021, 9, 1, 0, 0, 0)\n\n    # In this example, timestamps are used. (Because binance accept timestamp)\n    start_time_ts = int(datetime.timestamp(start_time) * 1000)\n    end_time_ts = int(datetime.timestamp(end_time) * 1000)\n\n    backtest(start_time_ts, end_time_ts)\n\n\ndef backtest(start_time, end_time):\n    # Getting OHLC data\n    # Example binance kline response\n    # [\n    #     [\n    #         1499040000000,      // Open time\n    #         \"0.01634790\",       // Open\n    #         \"0.80000000\",       // High\n    #         \"0.01575800\",       // Low\n    #         \"0.01577100\",       // Close\n    #         \"148976.11427815\",  // Volume\n    #         1499644799999,      // Close time\n    #         \"2434.19055334\",    // Quote asset volume\n    #         308,                // Number of trades\n    #         \"1756.87402397\",    // Taker buy base asset volume\n    #         \"28.46694368\",      // Taker buy quote asset volume\n    #         \"17928899.62484339\" // Ignore.\n    #     ]\n    # ]\n    params = {\"symbol\": SYMBOL, \"interval\": INTERVAL, \"startTime\": start_time, \"endTime\": end_time}\n    data = get_data(params)\n    opn, high, low, close = get_ohlc(data)\n\n    # Again for proper indicator usage number of bars to work on is defined as lookback\n    lookback = len(data) - TRIM\n\n    # These are simply trimmed OHLC data\n    data = data[-lookback:]\n    # Here, a list of close prices kept under different naming conventions than other OHL data\n    # That is because I will use this close data as a parameter \n    # for Exponential Moving Average indicator and then trim the list of EMA values afterward.\n    close_tmp = close[-lookback:]\n    opn = opn[-lookback:]\n    high = high[-lookback:]\n    low = low[-lookback:]\n\n    # Here is the calculation of ATR values historically. I use legitindicators library.\n    atr_input = []\n    for i, _ in enumerate(data):\n        ohlc = [opn[i], high[i], low[i], close_tmp[i]]\n        atr_input.append(ohlc)\n    atrng = atr(atr_input, 14)\n\n    # Backtesting operation starts here.\n    # Following two for loops will check two EMA crosses in the range of 10 to 30\n    for ema_length1 in range(10, 11):\n        for ema_length2 in range(ema_length1 + 1, 30):\n            # When the dynamic_positioning is set to True, \n            # the backtester will work as if the margin usage is available for use.\n            # margin_factor indicates the margin ratio. (In this example, it is 5 times the main capital)\n            # limit_factor is an ATR based take profit level. (it is 1 ATR from the position price)\n            # risk_factor is an ATR based stop loss level. (it is 1.5 ATR from the position price)\n            twise = Testwise(\n                commission=COMMISSION,\n                dynamic_positioning=DYNAMIC_POSITIONING,\n                margin_factor=MARGIN_FACTOR,\n                limit_factor=LIMIT_FACTOR,\n                risk_factor=RISK_FACTOR\n            )\n\n            # Here, two EMA indicators are defined. I use legitindicators library.\n            ema_first = ema(close, ema_length1)\n            ema_second = ema(close, ema_length2)\n            # List of indicator values trimmed accordingly\n            ema_first = ema_first[-lookback:]\n            ema_second = ema_second[-lookback:]\n\n            # Notice that at this point:\n            # open, high, low, close, ema_first and ema_second lists are all trimmed\n            #  and all have the same length\n            # Ready for testing\n\n            # Start walking on the data taken from the binance.\n            for i, _ in enumerate(data):\n                # Exclude first price data\n                if i \u003e 1 and i \u003c len(data) - 1:\n                    # Here, data[n][0] is the open time of price data\n                    # date_open is kept for use if there will be a pose to be opened the next day\n                    # date_close is kept for use if the current open position is closed in this iteration\n                    date_open = datetime.fromtimestamp(int(data[i+1][0] / 1000)).strftime(\"%Y-%m-%d %H\")\n                    date_close = datetime.fromtimestamp(int(data[i][0] / 1000)).strftime(\"%Y-%m-%d %H\")\n\n                    # Position exits\n                    # On every iteration, position exits checked firstly \n                    # Below, if the current position is long (1 means long) and\n                    # the ema_first crosses below the ema_second, position exit function triggered\n                    if twise.pos == 1 and (ema_first[i] \u003c ema_second[i]):\n                        # exit_long function takes closing date, \n                        # closing price as open price of next day opn[i + 1],\n                        # and amount to close the position. \n                        # This amount already kept in twise.current_open_pos[\"qty\"].\n                        # This value is set when opening the positions\n                        twise.exit_long(date_close, opn[i + 1], twise.current_open_pos[\"qty\"])\n\n                    # Closing short position(-1 means short)\n                    if twise.pos == -1 and (ema_first[i] \u003e ema_second[i]):\n                        twise.exit_short(date_close, opn[i + 1], twise.current_open_pos[\"qty\"])\n\n                    # The following if condition simulates price movements inside the bar. \n                    # This is crucial if you want to add take profit and stop loss logic to the backtester.\n                    # This pine script broker emulator documentation will explain this condition more clearly:\n                    # https://www.tradingview.com/pine-script-docs/en/v5/concepts/Strategies.html?highlight=strategy#broker-emulator\n                    if abs(high[i] - opn[i]) \u003c abs(low[i] - opn[i]):\n                        # Simply, If the bar’s high is closer to bar’s open than the bar’s low, \n                        # bar movement will be like: \n                        # open - high - low - close\n\n                        # In this movement, take profit operation will be checked before stop loss. \n                        # This is because, it is assumed that the price will go up first. \n                        # For example, if both take profit and stop loss prices are exceeded, \n                        # it is assumed that first, take profit is taken, than stop loss price is reached.\n\n                        # if current position is long, here is take profit logic:\n                        # if current position is long and high is \n                        # higher than take proift price (twise.current_open_pos[\"tp\"]) \n                        # and take profit is not taken (twise.current_open_pos[\"tptaken\"] is False)\n                        if twise.pos == 1 and high[i] \u003e twise.current_open_pos[\"tp\"] and twise.current_open_pos[\"tptaken\"] is False:\n                            # Stop loss price will be set to break even with break_even() function\n                            twise.break_even()\n                            # Take profit operation is simply a partially position closing operation. \n                            # Here, half of the position is closed. (twise.current_open_pos[\"qty\"] / 2)  \n                            twise.exit_long(date_close, twise.current_open_pos[\"tp\"], twise.current_open_pos[\"qty\"] / 2, True)\n\n                        # if current position is long, here is stop loss logic:\n                        # if current position is long and low is \n                        # lower than stop loss price (twise.current_open_pos[\"sl\"])\n                        if twise.pos == 1 and low[i] \u003c twise.current_open_pos[\"sl\"]:\n                            twise.exit_long(date_close, twise.current_open_pos[\"sl\"], twise.current_open_pos[\"qty\"])\n\n                        # if current position is short, here is take profit logic:\n                        if twise.pos == -1 and high[i] \u003e twise.current_open_pos[\"sl\"]:\n                            twise.exit_short(date_close, twise.current_open_pos[\"sl\"], twise.current_open_pos[\"qty\"])\n\n                        # if current position is short, here is stop loss logic:\n                        if twise.pos == -1 and low[i] \u003c twise.current_open_pos[\"tp\"] and twise.current_open_pos[\"tptaken\"] is False:\n                            twise.break_even()\n                            twise.exit_short(date_close, twise.current_open_pos[\"tp\"], twise.current_open_pos[\"qty\"] / 2, True)\n                    else:\n                        # If the bar’s low is closer to bar’s open than the bar’s high, \n                        # bar movement will be like: \n                        # open - low - high - close\n\n                        # In this movement, stop loss operation will be checked before take profit. \n                        # This is because, it is assumed that the price will go down firstly. \n                        # For example, if both take profit and stop loss prices are exceeded,\n                        # it is assumed that first, stop loss is executed, \n                        # then take profit will never be reached because \n                        # if the position is fully closed with exit_long, \n                        # twise.pos value will be 0 (which means there is no open position).\n\n                        # if the current position is long, here is stop loss logic:\n                        if twise.pos == 1 and low[i] \u003c twise.current_open_pos[\"sl\"]:\n                            twise.exit_long(date_close, twise.current_open_pos[\"sl\"], twise.current_open_pos[\"qty\"])\n\n                        # if current position is long, here is take profit logic:\n                        if twise.pos == 1 and high[i] \u003e twise.current_open_pos[\"tp\"] and twise.current_open_pos[\"tptaken\"] is False:\n                            twise.break_even()\n                            twise.exit_long(date_close, twise.current_open_pos[\"tp\"], twise.current_open_pos[\"qty\"] / 2, True)\n\n                        # if current position is short, here is take profit logic:\n                        if twise.pos == -1 and low[i] \u003c twise.current_open_pos[\"tp\"] and twise.current_open_pos[\"tptaken\"] is False:\n                            twise.break_even()\n                            twise.exit_short(date_close, twise.current_open_pos[\"tp\"], twise.current_open_pos[\"qty\"] / 2, True)\n\n                        # if current position is short, here is stop loss logic:\n                        if twise.pos == -1 and high[i] \u003e twise.current_open_pos[\"sl\"]:\n                            twise.exit_short(date_close, twise.current_open_pos[\"sl\"], twise.current_open_pos[\"qty\"])\n\n                    # Opening long position\n                    # If there is no long positions open\n                    if twise.pos != 1:\n                        # If ema_first crosses over ema_second\n                        if ema_first[i] \u003e ema_second[i]:\n                            # You can manually set the amount to open position. \n                            # But there will be a calculation overhead.\n                            # Testwise has a built-in share calculation funciton\n                            # In tihs function, share is calculated as: \n                            # share = (equity * position risk) / (atr * risk factor)\n                            share = twise.calculate_share(atrng[i], custom_position_risk=0.02)\n                            # Opening long position with opening date (date_open), \n                            # opening price of next day (opn[i + 1]),\n                            # amount to buy, and current atr value to define take profit and stop loss prices\n                            twise.entry_long(date_open, opn[i + 1], share, atrng[i])\n\n                    if twise.pos != -1:\n                        if ema_first[i] \u003c ema_second[i]:\n                            share = twise.calculate_share(atrng[i], custom_position_risk=0.02)\n                            # Opening short position with opening date (date_open), \n                            # opening price of next day (opn[i + 1]),\n                            # amount to buy, and current atr value to define take profit and stop loss prices\n                            twise.entry_short(date_open, opn[i + 1], share, atrng[i])\n            # get_result() function will give you the backtest results\n            print(twise.get_result())\n\n\ndef get_data(params):\n    r = requests.get(url=BINANCE_URL, params=params)\n    data = r.json()\n    return data\n\n\ndef get_ohlc(data):\n    opn = [float(o[1]) for o in data]\n    close = [float(d[4]) for d in data]\n    high = [float(h[2]) for h in data]\n    low = [float(lo[3]) for lo in data]\n\n    return opn, high, low, close\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n```python\nExample backtest result:\n{\n    'net_profit': 30557.012567638478, \n    'net_profit_percent': 30.557012567638477, \n    'gross_profit': 69163.31181062985, \n    'gross_loss': 36783.34343506002, \n    'max_drawdown': -13265.365111723615, \n    'max_drawdown_rate': 2.3035183962356918, \n    'win_rate': 53.48837209302326, \n    'risk_reward_ratio': 1.6350338129618904, \n    'profit_factor': 1.880288884906174, \n    'ehlers_ratio': 0.1311829454585705, \n    'return_on_capital': 0.26978249297565415, \n    'max_capital_required': 113265.36511172361, \n    'total_trades': 43, \n    'pearsonsr': 0.8022110890986095, \n    'number_of_winning_trades': 23, \n    'number_of_losing_trades': 20, \n    'largest_winning_trade': ('2021-01-23 03', 34417.71907928039), \n    'largest_losing_trade': ('2020-09-21 03', -4627.351985682239)}\n```\n\n## Important note: \nDo not rely on a single test result. \nAt least do walkforward test with a few iterations.\n\n## Installation\n\nRun the following to install:\n\n```python\npip install testwise\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faticio%2Ftestwise","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faticio%2Ftestwise","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faticio%2Ftestwise/lists"}