├── helpers ├── __init__.py └── helpers.py ├── backtesting ├── __init__.py ├── Backtester.py ├── ContrarianBacktest.py ├── MomentumBacktest.py ├── IterativeBase.py ├── MLClassificationBacktest.py ├── MultipleRegressionModelPredictor.py ├── SMABacktest.py ├── IterativeBacktest.py └── BollingerBandsBacktest.py ├── livetrading ├── __init__.py ├── MomentumLive.py ├── ContrarianLive.py ├── SMALive.py ├── BollingerBandsLive.py ├── MLClassificationLive.py └── LiveTrader.py ├── .gitmodules ├── requirements.txt ├── .idea ├── misc.xml ├── .gitignore ├── inspectionProfiles │ ├── profiles_settings.xml │ └── Project_Default.xml ├── modules.xml └── FXBot.iml ├── .gitignore ├── README.md └── main.py /helpers/__init__.py: -------------------------------------------------------------------------------- 1 | """Livetrading public package.""" 2 | -------------------------------------------------------------------------------- /backtesting/__init__.py: -------------------------------------------------------------------------------- 1 | """Backtesting public package.""" 2 | -------------------------------------------------------------------------------- /livetrading/__init__.py: -------------------------------------------------------------------------------- 1 | """Livetrading public package.""" 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tpqoa"] 2 | path = tpqoa 3 | url = https://github.com/yhilpisch/tpqoa.git 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | v20==3.0.25.0 2 | pytz==2019.3 3 | matplotlib==3.4.2 4 | numpy==1.21.0 5 | pandas==1.2.5 6 | scikit_learn==0.24.2 7 | yfinance==0.1.59 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/FXBot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /helpers/helpers.py: -------------------------------------------------------------------------------- 1 | 2 | import tpqoa 3 | 4 | import matplotlib.pyplot as plt 5 | plt.style.use("seaborn") 6 | 7 | 8 | class helpers(): 9 | 10 | def find_optimal_trading_time(cfg, instrument, start, end, granularity="M5"): 11 | 12 | # WARNING: the smaller the granularity, the less frequently the price change will 13 | # be able to cover the trading costs 14 | 15 | oanda = tpqoa.tpqoa(cfg) 16 | 17 | bid_price = oanda.get_history(instrument=instrument, start=start, end=end, granularity="M5", price="B", localize=False).c.dropna().to_frame() 18 | ask_price = oanda.get_history(instrument=instrument, start=start, end=end, granularity="M5", price="A", localize=False).c.dropna().to_frame() 19 | 20 | if granularity != "M5": 21 | bid_price = bid_price.resample(granularity).last().dropna() 22 | ask_price = ask_price.resample(granularity).last().dropna() 23 | 24 | spread = ask_price - bid_price 25 | data = bid_price 26 | 27 | data.rename(columns={"c": "bid_price"}, inplace=True) 28 | 29 | data["ask_price"] = ask_price 30 | data["mid_price"] = ask_price - spread 31 | data["spread"] = spread 32 | 33 | data["hour"] = data.index.hour 34 | 35 | data["price_change"] = data["mid_price"].diff().abs() 36 | 37 | data["covered_costs"] = data["price_change"] > data["spread"] 38 | 39 | hourly_grouping = data.groupby("hour")["covered_costs"].mean() 40 | 41 | hourly_grouping.plot(kind="bar", figsize=(12,8), fontsize=13) 42 | plt.xlabel("UTC Hour") 43 | plt.ylabel("Percentage of Costs Covered") 44 | plt.title(f"Granularity = {granularity}") 45 | 46 | plt.show() 47 | 48 | return hourly_grouping 49 | 50 | -------------------------------------------------------------------------------- /livetrading/MomentumLive.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from livetrading.LiveTrader import LiveTrader 4 | 5 | 6 | class MomentumLive(LiveTrader): 7 | def __init__( 8 | self, 9 | cfg, 10 | instrument, 11 | bar_length, 12 | window, 13 | units, 14 | stop_datetime=None, 15 | stop_loss=None, 16 | stop_profit=None, 17 | ): 18 | """ 19 | Initializes the MomentumLive object. 20 | 21 | Args: 22 | cfg (object): An object representing the OANDA connection 23 | instrument (string): A string holding the ticker instrument of instrument to be tested 24 | bar_length (string): Length of each candlestick for the respective instrument 25 | window (int): Length of sliding window to consider 26 | units (int): Amount of units to take positions with 27 | stop_datetime (object) : A datetime object that when passed stops trading 28 | stop_loss (float) : A stop loss that when profit goes below stops trading 29 | stop_profit (float) : A stop profit that when profit goes above stops trading 30 | """ 31 | self._window = window 32 | 33 | # passes params to the parent class 34 | super().__init__( 35 | cfg, 36 | instrument, 37 | bar_length, 38 | units, 39 | stop_datetime=stop_datetime, 40 | stop_loss=stop_loss, 41 | stop_profit=stop_profit, 42 | ) 43 | 44 | def define_strategy(self): 45 | data = self._raw_data.copy() 46 | data["returns"] = np.log(data["mid_price"].div(data["mid_price"].shift(1))) 47 | data["position"] = np.sign(data["returns"].rolling(self._window).mean()) 48 | self._data = data.dropna().copy() 49 | -------------------------------------------------------------------------------- /livetrading/ContrarianLive.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from livetrading.LiveTrader import LiveTrader 4 | 5 | 6 | class ContrarianLive(LiveTrader): 7 | def __init__( 8 | self, 9 | cfg, 10 | instrument, 11 | bar_length, 12 | window, 13 | units, 14 | stop_datetime=None, 15 | stop_loss=None, 16 | stop_profit=None, 17 | ): 18 | """ 19 | Initializes the ContrarianLive object. 20 | 21 | Args: 22 | cfg (object): An object representing the OANDA connection 23 | instrument (string): A string holding the ticker instrument of instrument to be tested 24 | bar_length (string): Length of each candlestick for the respective instrument 25 | window (int): Length of sliding window to consider 26 | units (int): Amount of units to take positions with 27 | stop_datetime (object) : A datetime object that when passed stops trading 28 | stop_loss (float) : A stop loss that when profit goes below stops trading 29 | stop_profit (float) : A stop profit that when profit goes above stops trading 30 | """ 31 | self._window = window 32 | 33 | # passes params to the parent class 34 | super().__init__( 35 | cfg, 36 | instrument, 37 | bar_length, 38 | units, 39 | stop_datetime=stop_datetime, 40 | stop_loss=stop_loss, 41 | stop_profit=stop_profit, 42 | ) 43 | 44 | def define_strategy(self): 45 | data = self._raw_data.copy() 46 | data["returns"] = np.log(data["mid_price"].div(data["mid_price"].shift(1))) 47 | data["position"] = -np.sign(data["returns"].rolling(self._window).mean()) 48 | self._data = data.dropna().copy() 49 | -------------------------------------------------------------------------------- /livetrading/SMALive.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from livetrading.LiveTrader import LiveTrader 4 | 5 | 6 | class SMALive(LiveTrader): 7 | def __init__( 8 | self, 9 | cfg, 10 | instrument, 11 | bar_length, 12 | smas, 13 | smal, 14 | units, 15 | stop_datetime=None, 16 | stop_loss=None, 17 | stop_profit=None, 18 | ): 19 | """ 20 | Initializes the SMALive object. 21 | 22 | Args: 23 | cfg (object): An object representing the OANDA connection 24 | instrument (string): A string holding the ticker instrument of instrument to be tested 25 | bar_length (string): Length of each candlestick for the respective instrument 26 | smas (int): A value for the # of days the Simple Moving Average lags (Shorter) should consider 27 | smal (int): A value for the # of days the Simple Moving Average lags (Longer) should consider 28 | stop_datetime (object) : A datetime object that when passed stops trading 29 | stop_loss (float) : A stop loss that when profit goes below stops trading 30 | stop_profit (float) : A stop profit that when profit goes above stops trading 31 | """ 32 | # these should be in terms of minutes 33 | self._smas = smas 34 | self._smal = smal 35 | 36 | # passes params to the parent class 37 | super().__init__( 38 | cfg, 39 | instrument, 40 | bar_length, 41 | units, 42 | stop_datetime=stop_datetime, 43 | stop_loss=stop_loss, 44 | stop_profit=stop_profit, 45 | ) 46 | 47 | def define_strategy(self): 48 | data = self._raw_data.copy() 49 | data["smas"] = data["mid_price"].rolling(self._smas).mean() 50 | data["smal"] = data["mid_price"].rolling(self._smal).mean() 51 | data["position"] = np.where(data["smas"] > data["smal"], 1, -1) 52 | 53 | self._data = data.dropna().copy() 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # PyCharm 141 | .idea/ 142 | */.idea/ 143 | **/.idea/ 144 | *.xml 145 | *.iml 146 | -------------------------------------------------------------------------------- /livetrading/BollingerBandsLive.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from livetrading.LiveTrader import LiveTrader 4 | 5 | 6 | class BollingerBandsLive(LiveTrader): 7 | def __init__( 8 | self, 9 | cfg, 10 | instrument, 11 | bar_length, 12 | sma, 13 | deviation, 14 | units, 15 | stop_datetime=None, 16 | stop_loss=None, 17 | stop_profit=None, 18 | ): 19 | """ 20 | Initializes the BollingerBandsLive object. 21 | 22 | Args: 23 | cfg (object): An object representing the OANDA connection 24 | instrument (string): A string holding the ticker instrument of instrument to be tested 25 | bar_length (string): Length of each candlestick for the respective instrument 26 | sma (int): Length of simple moving average to consider 27 | deviation (int): Standard deviation multiplier for upper and lower bands 28 | units (int): Amount of units to take positions with 29 | stop_datetime (object) : A datetime object that when passed stops trading 30 | stop_loss (float) : A stop loss that when profit goes below stops trading 31 | stop_profit (float) : A stop profit that when profit goes above stops trading 32 | """ 33 | self._sma = sma 34 | self._deviation = deviation 35 | 36 | # passes params to the parent class 37 | super().__init__( 38 | cfg, 39 | instrument, 40 | bar_length, 41 | units, 42 | stop_datetime=stop_datetime, 43 | stop_loss=stop_loss, 44 | stop_profit=stop_profit, 45 | ) 46 | 47 | def define_strategy(self): 48 | data = self._raw_data.copy() 49 | 50 | data["sma"] = data["mid_price"].rolling(self._sma).mean() 51 | data["lower"] = data["sma"] - ( 52 | data["mid_price"].rolling(self._sma).std() * self._deviation 53 | ) 54 | data["upper"] = data["sma"] + ( 55 | data["mid_price"].rolling(self._sma).std() * self._deviation 56 | ) 57 | data["distance"] = data["mid_price"] - data["sma"] 58 | 59 | # if price is lower than lower band, indicates oversold, and to go long 60 | data["position"] = np.where(data["mid_price"] < data["lower"], 1, np.nan) 61 | # if price is higher than upper band, indicates overbought, and to go short 62 | data["position"] = np.where( 63 | data["mid_price"] > data["upper"], -1, data["position"] 64 | ) 65 | # if we have crossed the sma line, we want to close our current position (be neutral, position=0) 66 | data["position"] = np.where( 67 | data["distance"] * data["distance"].shift(1) < 0, 0, data["position"] 68 | ) 69 | # clean up any NAN values/holiday vacancies 70 | data["position"] = data.position.ffill().fillna(0) 71 | 72 | self._data = data.dropna().copy() 73 | -------------------------------------------------------------------------------- /livetrading/MLClassificationLive.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from sklearn.linear_model import LogisticRegression 4 | from datetime import datetime, timedelta 5 | 6 | from livetrading.LiveTrader import LiveTrader 7 | import tpqoa 8 | 9 | 10 | class MLClassificationLive(LiveTrader): 11 | def __init__( 12 | self, 13 | cfg, 14 | instrument, 15 | bar_length, 16 | lags, 17 | units, 18 | history_days=7, 19 | stop_datetime=None, 20 | stop_loss=None, 21 | stop_profit=None, 22 | ): 23 | """ 24 | Initializes the MLClassificationLive object. 25 | 26 | Args: 27 | cfg (object): An object representing the OANDA connection 28 | instrument (string): A string holding the ticker instrument of instrument to be tested 29 | bar_length (string): Length of each candlestick for the respective instrument 30 | lags (int): Amount of lagged returns to consider 31 | units (int): Amount of units to take positions with 32 | history_days (int) : Amount of prior days to train the model on 33 | stop_datetime (object) : A datetime object that when passed stops trading 34 | stop_loss (float) : A stop loss that when profit goes below stops trading 35 | stop_profit (float) : A stop profit that when profit goes above stops trading 36 | """ 37 | # some of this info is needed by fit_model(), so we must set it in the child class 38 | self._instrument = instrument 39 | self._bar_length = pd.to_timedelta(bar_length) 40 | self._lags = lags 41 | self._model = None 42 | self.fit_model() 43 | 44 | # passes params to the parent class 45 | super().__init__( 46 | cfg, 47 | instrument, 48 | bar_length, 49 | units, 50 | history_days, 51 | stop_datetime=stop_datetime, 52 | stop_loss=stop_loss, 53 | stop_profit=stop_profit, 54 | ) 55 | 56 | def fit_model(self): 57 | print("Fitting model on past 7 days...") 58 | now = datetime.utcnow() 59 | now = now.replace(microsecond=0) 60 | past = now - timedelta(days=7) 61 | 62 | oanda = tpqoa.tpqoa("oanda.cfg") 63 | 64 | mid_price = oanda.get_history(instrument=self._instrument, start=past, end=now, granularity="S5", price="M", localize=False).c.dropna().to_frame() 65 | 66 | data = mid_price 67 | data.rename(columns={"c": "mid_price"}, inplace=True) 68 | 69 | data = data.resample(self._bar_length, label="right").last().dropna().iloc[:-1] 70 | 71 | data["returns"] = np.log(data.div(data.shift(1))) 72 | data.dropna(inplace=True) 73 | 74 | data["direction"] = np.sign(data["returns"]) 75 | 76 | pd.set_option("max_columns", None) 77 | 78 | feature_columns = [] 79 | 80 | for lag in range(1, self._lags + 1): 81 | data[f"lag{lag}"] = data["returns"].shift(lag) 82 | feature_columns.append(f"lag{lag}") 83 | 84 | data.dropna(inplace=True) 85 | 86 | model = LogisticRegression(C=1e6, max_iter=100000, multi_class="ovr") 87 | model.fit(data[feature_columns], data["direction"]) 88 | 89 | self._model = model 90 | 91 | data["pred"] = self._model.predict(data[feature_columns]) 92 | 93 | print("Model fitted.") 94 | 95 | def define_strategy(self): 96 | data = self._raw_data.copy() 97 | 98 | data = data.append(self._tick_data.iloc[-1]) 99 | data["returns"] = np.log(data["mid_price"] / data["mid_price"].shift()) 100 | 101 | feature_columns = [] 102 | 103 | for lag in range(1, self._lags + 1): 104 | data[f"lag{lag}"] = data["returns"].shift(lag) 105 | feature_columns.append(f"lag{lag}") 106 | 107 | data.dropna(inplace=True) 108 | 109 | # use the passed model to predict our positions 110 | data["position"] = self._model.predict(data[feature_columns]) 111 | 112 | self._data = data.dropna().copy() 113 | -------------------------------------------------------------------------------- /backtesting/Backtester.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import tpqoa 4 | 5 | 6 | class Backtester: 7 | """Class implementing a vectorized back-testing framework.""" 8 | def __init__(self, instrument, start, end, granularity="D", trading_cost=0): 9 | """ 10 | Initializes the Backtester object. 11 | 12 | Args: 13 | instrument (string): A string holding the ticker of instrument to be tested 14 | start (string): The start date of the testing period 15 | end (string): The end date of the testing period 16 | granularity (string) : Length of each candlestick for the respective instrument 17 | trading_cost (float) : A static trading cost considered when calculating returns 18 | """ 19 | self._instrument = instrument 20 | self._start = start 21 | self._end = end 22 | self._granularity = granularity 23 | self._tc = trading_cost 24 | 25 | self._results = None 26 | 27 | self._data = self.acquire_data() 28 | self._data = self.prepare_data() 29 | 30 | def acquire_data(self): 31 | """ 32 | A general function to acquire data of instrument from a source. 33 | 34 | Returns: 35 | Returns a Pandas dataframe containing downloaded info. 36 | """ 37 | print("Downloading historical data...") 38 | 39 | oanda = tpqoa.tpqoa("oanda.cfg") 40 | 41 | df = oanda.get_history( 42 | self._instrument, self._start, self._end, self._granularity, "M" 43 | ) 44 | 45 | # only care for the closing price 46 | df = df.c.to_frame() 47 | df.rename(columns={"c": "price"}, inplace=True) 48 | 49 | df.dropna(inplace=True) 50 | 51 | df["returns"] = np.log(df.div(df.shift(1))) 52 | 53 | print("Download complete.") 54 | 55 | return df 56 | 57 | def prepare_data(self): 58 | """ 59 | Prepares data for strategy-specific information. 60 | Returns: 61 | Returns a Pandas dataframe 62 | """ 63 | return self._data.copy() 64 | 65 | def resample(self, granularity): 66 | """ 67 | Resamples the instruments' dataset to be in buckets of the passed granularity (IE "W", "D", "H"). 68 | 69 | Args: 70 | granularity (string): The new granularity for the dataset 71 | """ 72 | self._granularity = granularity 73 | self._data.resample(granularity) 74 | return 75 | 76 | def get_data(self): 77 | """ 78 | Getter function to retrieve current instrument's dataframe. 79 | 80 | Returns: 81 | Returns the stored Pandas dataframe with information regarding the instrument 82 | """ 83 | return self._data 84 | 85 | def get_results(self): 86 | """ 87 | Getter function to retrieve current instrument's dataframe after testing the strategy. 88 | 89 | Returns: 90 | Returns a Pandas dataframe with results from back-testing the strategy 91 | """ 92 | if self._results is not None: 93 | return self._results 94 | else: 95 | print("Please run .test() first.") 96 | 97 | def test(self): 98 | """ 99 | Executes the back-testing of the strategy on the set instrument. 100 | 101 | Returns: 102 | Returns a tuple, (float: performance, float: out_performance) 103 | -> "performance" is the percentage of return on the interval [start, end] 104 | -> "out_performance" is the performance when compared to a buy & hold on the same interval 105 | IE, if out_performance is greater than one, the strategy outperformed B&H. 106 | """ 107 | pass 108 | 109 | def optimize(self): 110 | """ 111 | Optimizes the strategy on the interval [start,end] which allows for the greatest return. 112 | """ 113 | pass 114 | 115 | def plot_results(self): 116 | """ 117 | Plots the results of test() or optimize(). 118 | Also plots the results of the buy and hold strategy on the interval [start,end] to compare to the results. 119 | """ 120 | if self._results is not None: 121 | print("Plotting Results.") 122 | title = f"{self._instrument}" 123 | self._results[["creturns", "cstrategy"]].plot(title=title, figsize=(12, 8)) 124 | plt.show() 125 | else: 126 | print("Please run test() or optimize().") 127 | -------------------------------------------------------------------------------- /backtesting/ContrarianBacktest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from backtesting.Backtester import Backtester 4 | 5 | 6 | class ContrarianBacktest(Backtester): 7 | """Class implementing vectorized back-testing of a Contrarian Strategy.""" 8 | def __init__(self, instrument, start, end, window=1, granularity="D", trading_cost=0): 9 | """ 10 | Initializes the ContrarianBacktest object. 11 | 12 | Args: 13 | instrument (string): A string holding the ticker symbol of instrument to be tested 14 | start (string): The start date of the testing period 15 | end (string): The end date of the testing period 16 | window (int) : Length of lags that drives the position. 17 | granularity (string) : Length of each candlestick for the respective instrument 18 | trading_cost (float) : A static trading cost considered when calculating returns 19 | """ 20 | self._window = window 21 | 22 | # passes params to the parent class 23 | super().__init__( 24 | instrument, 25 | start, 26 | end, 27 | granularity, 28 | trading_cost 29 | ) 30 | 31 | def __repr__(self): 32 | """Custom Representation.""" 33 | return f"ContrarianBacktest( symbol={self._instrument}, start={self._start}, end={self._end}, granularity={self._granularity}, lags={self._window}, trading_cost={self._tc} )" 34 | 35 | def test(self, window=1, mute=False): 36 | """ 37 | Executes the back-testing of the Contrarian strategy on the set instrument. 38 | 39 | Returns: 40 | Returns a tuple, (float: performance, float: out_performance) 41 | -> "performance" is the percentage of return on the interval [start, end] 42 | -> "out_performance" is the performance when compared to a buy & hold on the same interval 43 | IE, if out_performance is greater than one, the strategy outperformed B&H. 44 | """ 45 | if not mute: 46 | print(f"Testing strategy with window = {window} ...") 47 | 48 | # IE if no lags parameter included and we have a stored lags, use that as the lags instead 49 | if self._window != 1 and window == 1: 50 | window = self._window 51 | 52 | data = self._data.copy() 53 | 54 | data["position"] = -np.sign(data["returns"].rolling(window).mean()) 55 | data["strategy"] = data["position"].shift(1) * data["returns"] 56 | 57 | data.dropna(inplace=True) 58 | 59 | # running total of amount of trades, each change of position is 2 trades 60 | data["trades"] = data.position.diff().fillna(0).abs() 61 | 62 | # correct strategy returns based on trading costs 63 | data.strategy = data.strategy - data.trades * self._tc 64 | 65 | data["creturns"] = data["returns"].cumsum().apply(np.exp) 66 | data["cstrategy"] = data["strategy"].cumsum().apply(np.exp) 67 | 68 | self._results = data 69 | 70 | performance = data["cstrategy"].iloc[-1] 71 | # out_performance is our strats performance vs a buy and hold on the interval 72 | out_performance = performance - data["creturns"].iloc[-1] 73 | 74 | if not mute: 75 | print(f"Return: {round(performance*100 - 100,2) }%, Out Performance: {round(out_performance*100,2)}%") 76 | 77 | return performance, out_performance 78 | 79 | def optimize(self, window_range=(1, 252)): 80 | """ 81 | Optimizes the lags on the interval [start,end] which allows for the greatest return. 82 | 83 | Args: 84 | window_range (tuple(int, int)) =(1,252): Range of values for optimization of sliding lags 85 | 86 | Returns: 87 | Returns a tuple, (float: max_return, int: best_window) 88 | -> "max_return" is the optimized (maximum) return rate of the instrument on the interval [start,end] 89 | -> "best_window" is the optimized lags that enables a maximum return 90 | """ 91 | if window_range[0] >= window_range[1]: 92 | print("The range must satisfy: (X,Y) -> X < Y") 93 | return 94 | 95 | print("Optimizing strategy...") 96 | 97 | max_return = float("-inf") 98 | best_window = 1 99 | 100 | for window in range(window_range[0], window_range[1]): 101 | 102 | if window == (window_range[1] / 4): 103 | print("25%...") 104 | if window == (window_range[1] / 2): 105 | print("50%...") 106 | if window == (window_range[1] / 1.5): 107 | print("75%...") 108 | 109 | current_return = self.test(window, mute=True)[0] 110 | 111 | if current_return > max_return: 112 | max_return = current_return 113 | best_window = window 114 | 115 | # save the optimized lags 116 | self._window = best_window 117 | 118 | # run the final test to store results 119 | self.test(self._window, mute=True) 120 | 121 | print(f"Strategy optimized on interval {self._start} - {self._end}") 122 | print(f"Max Return: {round(max_return * 100 - 100,2) - 100}%, Best Window: {best_window} ({self._granularity})") 123 | 124 | return max_return, best_window 125 | -------------------------------------------------------------------------------- /backtesting/MomentumBacktest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from backtesting.Backtester import Backtester 4 | 5 | 6 | class MomentumBacktest(Backtester): 7 | 8 | """ 9 | Class implementing vectorized back-testing of a Momentum Strategy. 10 | This strategy should only be used alongside other strategies, on it's own it is very sensitive 11 | to the value of the moving lags set for the strategy. 12 | Also note, these strategies change positions many times, which can lead to trading costs diminishing your 13 | profits, or magnifying your losses. 14 | """ 15 | def __init__(self, instrument, start, end, window=1, granularity="D", trading_cost=0): 16 | """ 17 | Initializes the ContrarianBacktest object. 18 | 19 | Args: 20 | instrument (string): A string holding the ticker instrument of instrument to be tested 21 | start (string): The start date of the testing period 22 | end (string): The end date of the testing period 23 | window (int): Length of lags that drives the position. 24 | granularity (string) : Length of each candlestick for the respective instrument 25 | trading_cost (float) : A static trading cost considered when calculating returns 26 | """ 27 | self._window = window 28 | 29 | # passes params to the parent class 30 | super().__init__( 31 | instrument, 32 | start, 33 | end, 34 | granularity, 35 | trading_cost 36 | ) 37 | 38 | def __repr__(self): 39 | """Custom Representation.""" 40 | return f"MomentumBacktest( instrument={self._instrument}, start={self._start}, end={self._end}, granularity={self._granularity}, trading_cost={self._tc} )" 41 | 42 | def test(self, window=1, mute=False): 43 | """ 44 | Executes the back-testing of the Momentum strategy on the set instrument. 45 | 46 | Returns: 47 | Returns a tuple, (float: performance, float: out_performance) 48 | -> "performance" is the percentage of return on the interval [start, end] 49 | -> "out_performance" is the performance when compared to a buy & hold on the same interval 50 | IE, if out_performance is greater than one, the strategy outperformed B&H. 51 | """ 52 | if not mute: 53 | print(f"Testing strategy with window = {self._window} ...") 54 | 55 | # IE if no lags parameter included and we have a stored lags, use that as the lags instead 56 | if self._window != 1 and window == 1: 57 | window = self._window 58 | 59 | data = self._data.copy() 60 | 61 | data["position"] = np.sign(data["returns"].rolling(window).mean()) 62 | 63 | data["strategy"] = data["position"].shift(1) * data["returns"] 64 | 65 | data.dropna(inplace=True) 66 | 67 | data["trades"] = data.position.diff().fillna(0).abs() 68 | 69 | # correct strategy returns based on trading costs 70 | data.strategy = data.strategy - data.trades * self._tc 71 | 72 | data["creturns"] = data["returns"].cumsum().apply(np.exp) 73 | data["cstrategy"] = data["strategy"].cumsum().apply(np.exp) 74 | 75 | self._results = data 76 | 77 | performance = data["cstrategy"].iloc[-1] 78 | # out_performance is our strats performance vs a buy and hold on the interval 79 | out_performance = performance - data["creturns"].iloc[-1] 80 | 81 | if not mute: 82 | print(f"Return: {round(performance*100 - 100,2)}%, Out Performance: {round(out_performance*100,2)}%") 83 | 84 | return performance, out_performance 85 | 86 | def optimize(self, window_range=(1, 252)): 87 | """ 88 | Optimizes the lags on the interval [start,end] which allows for the greatest return. 89 | 90 | Args: 91 | window_range (tuple(int, int)) =(1,252): Range of values for optimization of sliding lags 92 | 93 | Returns: 94 | Returns a tuple, (float: max_return, int: best_window) 95 | -> "max_return" is the optimized (maximum) return rate of the instrument on the interval [start,end] 96 | -> "best_window" is the optimized lags that enables a maximum return 97 | """ 98 | if window_range[0] >= window_range[1]: 99 | print("The range must satisfy: (X,Y) -> X < Y") 100 | return 101 | 102 | print("Optimizing strategy...") 103 | 104 | max_return = float("-inf") 105 | best_window = 1 106 | 107 | for window in range(window_range[0], window_range[1]): 108 | 109 | if window == (window_range[1] / 4): 110 | print("25%...") 111 | if window == (window_range[1] / 2): 112 | print("50%...") 113 | if window == (window_range[1] / 1.5): 114 | print("75%...") 115 | 116 | current_return = self.test(window, mute=True)[0] 117 | 118 | if current_return > max_return: 119 | max_return = current_return 120 | best_window = window 121 | 122 | # save the optimized lags 123 | self._window = best_window 124 | 125 | # run the final test to store results 126 | self.test(mute=True) 127 | 128 | print(f"Strategy optimized on interval {self._start} - {self._end}") 129 | print(f"Max Return: {round(max_return * 100 - 100, 2)}%, Best Window: {best_window} ({self._granularity})") 130 | 131 | return max_return, best_window 132 | -------------------------------------------------------------------------------- /backtesting/IterativeBase.py: -------------------------------------------------------------------------------- 1 | import tpqoa 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | class IterativeBase: 7 | 8 | """Class allowing iterative backtesting functionalities""" 9 | def __init__( 10 | self, cfg, instrument, start, end, amount, granularity="D", use_spread=True 11 | ): 12 | """ 13 | Initializes the IterativeBase object. 14 | 15 | Args: 16 | cfg (object): An object representing the OANDA connection 17 | instrument (string): A string holding the ticker instrument of instrument to be tested 18 | start (string): Length of each candlestick for the respective instrument 19 | start (string): The start date of the testing period 20 | end (string): The end date of the testing period 21 | amount (int): Amount of units to take positions with 22 | granularity (string) : Length of each candlestick for the respective instrument 23 | use_spread (bool) : An option to consider trading costs or to neglect them 24 | """ 25 | self._cfg = cfg 26 | self._instrument = instrument 27 | self._start = start 28 | self._end = end 29 | self._initial_balance = amount 30 | self._current_balance = amount 31 | self._granularity = granularity 32 | self._use_spread = use_spread 33 | 34 | self._units = 0 35 | self._trades = 0 36 | self._position = 0 37 | 38 | self.acquire_data() 39 | 40 | def acquire_data(self): 41 | """A general function to acquire data of an instrument from a source.""" 42 | oanda = tpqoa.tpqoa(self._cfg) 43 | 44 | bid_df = oanda.get_history( 45 | self._instrument, self._start, self._end, self._granularity, "B" 46 | ) 47 | ask_df = oanda.get_history( 48 | self._instrument, self._start, self._end, self._granularity, "A" 49 | ) 50 | 51 | bid_price = bid_df.c.to_frame() 52 | ask_price = ask_df.c.to_frame() 53 | 54 | spread = ask_price - bid_price 55 | 56 | # create the new dataframe with relevent info 57 | df = bid_price 58 | df.rename(columns={"c": "bid_price"}, inplace=True) 59 | df["ask_price"] = ask_price 60 | df["mid_price"] = ask_price - spread 61 | df["spread"] = spread 62 | 63 | df.dropna(inplace=True) 64 | 65 | df["returns"] = np.log(df.bid_price.div(df.bid_price.shift(1))) 66 | 67 | self._data = df 68 | 69 | def bar_info(self, bar): 70 | date = str(self._data.index[bar].date()) 71 | # round price to 5 as OANDA only gives 5 decimal places 72 | price = round(self._data.bid_price.iloc[bar], 5) 73 | spread = round(self._data.spread.iloc[bar], 5) 74 | 75 | return date, price, spread 76 | 77 | def print_current_balance(self, bar): 78 | date = self.bar_info(bar) 79 | print(f"{date} | Current Balance: ${round(self._current_balance,2)}") 80 | 81 | def print_current_nav(self, bar): 82 | date, price = self.bar_info(bar) 83 | nav = self._current_balance + (self._units * price) 84 | print(f"{date} | Current NAV: ${round(nav,2)}") 85 | 86 | def print_current_position_value(self, bar): 87 | date, price = self.bar_info(bar) 88 | 89 | curr_value = self._units * price 90 | print(f"{date} | Current Position Value: ${round(curr_value,2)}") 91 | 92 | def buy(self, bar, units=None, amount=None): 93 | date, price, spread = self.bar_info(bar) 94 | 95 | # since price is bid price, to get ask price we add the spread 96 | if self._use_spread: 97 | price += spread 98 | 99 | if amount is not None: 100 | units = int(amount / price) 101 | 102 | if self._current_balance < units * price: 103 | print("Not enough balance.") 104 | return 105 | 106 | self._current_balance -= units * price 107 | self._units += units 108 | self._trades += 1 109 | print( 110 | f"{date} | Bought {units} units of {self._instrument} @ ${round(price,2)}/unit, total=${round(units*price,2)}" 111 | ) 112 | 113 | def sell(self, bar, units=None, amount=None): 114 | date, price, spread = self.bar_info(bar) 115 | 116 | # since price is bid price, no need to adjust for spread 117 | 118 | if amount is not None: 119 | units = int(amount / price) 120 | 121 | self._current_balance += units * price 122 | self._units -= units 123 | self._trades += 1 124 | print( 125 | f"{date} | Sold {units} units of {self._instrument} @ ${round(price,2)}/unit, total=${round(units*price,2)}" 126 | ) 127 | 128 | def close_position(self, bar): 129 | date, price, spread = self.bar_info(bar) 130 | print("=" * 50) 131 | print(f"Closing Position ({self._instrument}, {date})") 132 | 133 | if self._units > 0: 134 | # closing long position by selling (bid price) 135 | self._current_balance += self._units * price 136 | else: 137 | # closing short position by buying (ask price = bid_price + spread) 138 | self._current_balance -= abs(self._units * (price + spread)) 139 | 140 | print(f"{date} | closed position of {self._units} units @ {price}") 141 | self._units = 0 142 | self._trades += 1 143 | performance = ( 144 | (self._current_balance - self._initial_balance) / self._initial_balance 145 | ) * 100 146 | self.print_current_balance(bar) 147 | print(f"Performance %: {round(performance,2)}") 148 | print(f"Trades Executed: {self._trades}") 149 | print("=" * 50) 150 | 151 | def plot_data(self, columns="bid_price"): 152 | self._data[columns].plot(figsize=(12, 8), title=self._instrument) 153 | plt.show() 154 | -------------------------------------------------------------------------------- /backtesting/MLClassificationBacktest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from sklearn.linear_model import LogisticRegression 3 | 4 | from backtesting.Backtester import Backtester 5 | 6 | 7 | class MLClassificationBacktest(Backtester): 8 | 9 | """ 10 | Class implementing the vectorized backtesting of machine learning strategies. 11 | In this case, Classification. 12 | """ 13 | def __init__(self, instrument, start, end, granularity="D", trading_cost=0): 14 | """ 15 | Initializes the MLClassificationBacktest object. 16 | 17 | Args: 18 | instrument (string): A string holding the ticker instrument of instrument to be tested 19 | start (string): The start date for the instrument 20 | end (string): The end date for the instrument 21 | granularity (string) : Length of each candlestick for the respective instrument 22 | trading_cost (float) : A static trading cost considered when calculating returns 23 | """ 24 | # low regularization 25 | self._model = LogisticRegression(C=1e6, max_iter=100000, multi_class="ovr") 26 | 27 | # passes params to the parent class 28 | super().__init__( 29 | instrument, 30 | start, 31 | end, 32 | granularity, 33 | trading_cost 34 | ) 35 | 36 | def __repr__(self): 37 | """Custom Representation.""" 38 | return f"MLClassificationBacktest( instrument={self._instrument}, start={self._start}, end={self._end}, granularity={self._granularity}, trading_cost={self._tc} )" 39 | 40 | def get_hitratio(self): 41 | """ 42 | Getter function to retrieve the forward test's hit ratio. 43 | 44 | Returns: 45 | Returns the hit ratio 46 | """ 47 | print(self._hitratio) 48 | return self._hitratio 49 | 50 | def split_data(self, start, end): 51 | """ 52 | Splits the full data into a subset of itself. 53 | 54 | Args: 55 | start (string): The start date of split 56 | end (string): The end date of split 57 | """ 58 | return self._data.loc[start:end].copy() 59 | 60 | def fit_model(self, start, end): 61 | """ 62 | Fits the ML model based on the training set. 63 | 64 | Args: 65 | start (string): The start date to fit model on 66 | end (string): The end date to fit model on 67 | """ 68 | self.prepare_features(start, end) 69 | self._model.fit( 70 | self._data_subset[self._feature_columns], 71 | np.sign(self._data_subset["returns"]), 72 | ) 73 | 74 | def prepare_features(self, start, end): 75 | """ 76 | Prepares the feature columns for training/testing. 77 | 78 | Args: 79 | start (string): The start date to prepare model on 80 | end (string): The end date to prepare model on 81 | """ 82 | self._data_subset = self.split_data(start, end) 83 | feature_columns = [] 84 | 85 | for lag in range(1, self._lags + 1): 86 | self._data_subset[f"lag{lag}"] = self._data_subset["returns"].shift(lag) 87 | feature_columns.append(f"lag{lag}") 88 | 89 | self._data_subset.dropna(inplace=True) 90 | 91 | self._feature_columns = feature_columns 92 | 93 | def test(self, train_ratio=0.7, lags=5): 94 | """ 95 | Backtests the model and then forward tests the strategy. 96 | 97 | Args: 98 | train_ratio (float [0, 1.0]) : Splits the dataset into backtesting set (train_ratio) and test set (1-train_ratio) 99 | lags (int) : The number of return lags serving as model features 100 | """ 101 | print(f"Testing strategy with train_ratio = {train_ratio}, lags = {lags} ...") 102 | 103 | self._lags = lags 104 | 105 | df = self._data.copy() 106 | 107 | # splits data 108 | split_index = int(len(df) * train_ratio) 109 | split_date = df.index[split_index - 1] 110 | backtest_start = df.index[0] 111 | test_end = df.index[-1] 112 | 113 | # fits the model on the backtest data 114 | self.fit_model(backtest_start, split_date) 115 | 116 | # prepares the test set 117 | self.prepare_features(split_date, test_end) 118 | 119 | # makes predictions on the test set 120 | self._data_subset["prediction"] = self._model.predict( 121 | self._data_subset[self._feature_columns] 122 | ) 123 | 124 | # strat returns 125 | self._data_subset["strategy"] = ( 126 | self._data_subset["prediction"] * self._data_subset["returns"] 127 | ) 128 | 129 | # number of trades 130 | self._data_subset["trades"] = ( 131 | self._data_subset["prediction"].diff().fillna(0).abs() 132 | ) 133 | 134 | # adjust strat returns based on trading cost 135 | self._data_subset["strategy"] = self._data_subset["strategy"] - ( 136 | self._data_subset["trades"] * self._tc 137 | ) 138 | 139 | self._data_subset["creturns"] = ( 140 | self._data_subset["returns"].cumsum().apply(np.exp) 141 | ) 142 | self._data_subset["cstrategy"] = ( 143 | self._data_subset["strategy"].cumsum().apply(np.exp) 144 | ) 145 | 146 | self._results = self._data_subset 147 | 148 | # stores the number of times we are correct or wrong with the prediction 149 | self._hits = np.sign( 150 | self._data_subset.returns * self._data_subset.prediction 151 | ).value_counts() 152 | # see how often we are correct 153 | self._hitratio = self._hits[1.0] / sum(self._hits) 154 | 155 | # absolute performance of strat 156 | performance = self._results["cstrategy"].iloc[-1] 157 | 158 | # outperformance of strat vs buy and hold on interval 159 | out_performance = performance - self._results["creturns"].iloc[-1] 160 | 161 | print(f"Return: {round(performance * 100 - 100, 2)}%, Out Performance: {round(out_performance * 100, 2)}%") 162 | 163 | return performance, out_performance 164 | -------------------------------------------------------------------------------- /backtesting/MultipleRegressionModelPredictor.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import tpqoa 3 | from sklearn.linear_model import LinearRegression 4 | 5 | from backtesting.Backtester import Backtester 6 | 7 | 8 | class MultipleRegressionModelPredictor(Backtester): 9 | 10 | """ 11 | Predicts the direction of returns for each granularity time stamp, by fitting to a known time range 12 | and then predicting a future time range. 13 | """ 14 | def __init__(self, instrument, backtest_range, forwardtest_range, lags=3, granularity="D", trading_cost=0): 15 | """ 16 | Initializes the MultipleRegressionModelPredictor object. 17 | 18 | Args: 19 | instrument (string): A string holding the ticker instrument of instrument to be tested 20 | backtest_range (tuple: str): The date range of the backtesting testing period 21 | forwardtest_range (tuple: str): The date range of period to predict 22 | lags (int): Number of lags to consider when fitting 23 | granularity (string) : Length of each candlestick for the respective instrument 24 | trading_cost (float) : A static trading cost considered when calculating returns 25 | """ 26 | 27 | if backtest_range[0] > backtest_range[1] or forwardtest_range[0] > forwardtest_range[1] or backtest_range[1] > forwardtest_range[0]: 28 | raise ValueError("Please ensure that the start date for each date range is earlier than the end date, and also ensure the backtest range is completely before the forwardtest range.") 29 | 30 | self._startb = backtest_range[0] 31 | self._endb = backtest_range[1] 32 | self._startf = forwardtest_range[0] 33 | self._endf = forwardtest_range[1] 34 | self._lags = lags 35 | 36 | # passes params to the parent class 37 | super().__init__( 38 | instrument, 39 | forwardtest_range[0], 40 | forwardtest_range[1], 41 | granularity, 42 | trading_cost 43 | ) 44 | 45 | def acquire_data(self): 46 | """ 47 | Sets up the backtest data as well as the forward test data 48 | """ 49 | oanda = tpqoa.tpqoa('oanda.cfg') 50 | 51 | # get data of both periods 52 | backtestdf = oanda.get_history(self._instrument, self._startb, self._endb, self._granularity, "M") 53 | forwardtestdf = oanda.get_history(self._instrument, self._startf, self._endf, self._granularity, "M") 54 | 55 | # only care for the closing price 56 | backtestdf = backtestdf.c.to_frame() 57 | backtestdf.rename(columns={"c": "price"}, inplace=True) 58 | 59 | backtestdf.dropna(inplace=True) 60 | 61 | backtestdf["returns"] = np.log(backtestdf.div(backtestdf.shift(1))) 62 | 63 | self._backtest_df = backtestdf 64 | 65 | # only care for the closing price 66 | forwardtestdf = forwardtestdf.c.to_frame() 67 | forwardtestdf.rename(columns={"c": "price"}, inplace=True) 68 | 69 | forwardtestdf.dropna(inplace=True) 70 | 71 | forwardtestdf["returns"] = np.log(forwardtestdf.div(forwardtestdf.shift(1))) 72 | 73 | self._forwardtest_df = forwardtestdf 74 | 75 | 76 | def prepare_data(self): 77 | """ 78 | Prepares data for strategy-specific information. 79 | """ 80 | backtestdf = self._backtest_df.copy() 81 | forwardtestdf = self._forwardtest_df.copy() 82 | 83 | # we have multiple lagging columns (depending on lags length > 1) 84 | columns = [] 85 | for lag in range(1, self._lags + 1): 86 | column = f"lag{lag}" 87 | backtestdf[column] = backtestdf.returns.shift(lag) 88 | forwardtestdf[column] = forwardtestdf.returns.shift(lag) 89 | columns.append(column) 90 | 91 | backtestdf.dropna(inplace=True) 92 | forwardtestdf.dropna(inplace=True) 93 | 94 | self._lm = LinearRegression(fit_intercept=True) 95 | 96 | # has multiple independent variables 97 | self._lm.fit(backtestdf[columns], backtestdf.returns) 98 | 99 | forwardtestdf["prediction"] = self._lm.predict(forwardtestdf[columns].values) 100 | 101 | # only interested in the direction of returns, not the magnitude 102 | forwardtestdf["prediction"] = np.sign(forwardtestdf["prediction"]) 103 | 104 | self._forwardtest_df = forwardtestdf 105 | 106 | # stores the number of times we are correct or wrong with the prediction 107 | self._hits = np.sign(forwardtestdf.returns * forwardtestdf.prediction).value_counts() 108 | # see how often we are correct 109 | self._hitratio = self._hits[1.0] / sum(self._hits) 110 | 111 | def get_hitratio(self): 112 | """ 113 | Getter function to retrieve the forward test's hit ratio. 114 | 115 | Returns: 116 | Returns the hit ratio 117 | """ 118 | print(self._hitratio) 119 | return self._hitratio 120 | 121 | def test(self): 122 | """ 123 | Computes the strategies returns over the forwardtest interval. 124 | 125 | Returns: 126 | Returns a tuple, (float: performance, float: out_performance) 127 | -> "performance" is the percentage of return on the interval [start, end] 128 | -> "out_performance" is the performance when compared to a buy & hold on the same interval 129 | IE, if out_performance is greater than one, the strategy outperformed B&H. 130 | """ 131 | print("Testing strategy...") 132 | 133 | data = self._forwardtest_df.copy() 134 | 135 | data["strategy"] = data.prediction * data.returns 136 | 137 | data["trades"] = data.prediction.diff().fillna(0).abs() 138 | 139 | # correct strategy returns based on trading costs 140 | data.strategy = data.strategy - data.trades * self._tc 141 | 142 | data["creturns"] = data["returns"].cumsum().apply(np.exp) 143 | data["cstrategy"] = data["strategy"].cumsum().apply(np.exp) 144 | 145 | self._results = data 146 | 147 | performance = data["cstrategy"].iloc[-1] 148 | # out_performance is our strats performance vs a buy and hold on the interval 149 | out_performance = performance - data["creturns"].iloc[-1] 150 | 151 | print(f"Return: {round(performance * 100 - 100, 2)}%, Out Performance: {round(out_performance * 100, 2)}%") 152 | 153 | return performance, out_performance 154 | -------------------------------------------------------------------------------- /backtesting/SMABacktest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from backtesting.Backtester import Backtester 4 | 5 | 6 | class SMABacktest(Backtester): 7 | """Class implementing vectorized back-testing of a SMA Cross trading strategy.""" 8 | def __init__(self, instrument, start, end, smas, smal, granularity="D", trading_cost=0): 9 | """ 10 | Initializes the SMABacktest object. 11 | 12 | Args: 13 | instrument (string): A string holding the ticker instrument of instrument to be tested 14 | start (string): The start date of the testing period 15 | end (string): The end date of the testing period 16 | smas (int): A value for the # of days the Simple Moving Average lags (Shorter) should consider 17 | smal (int): A value for the # of days the Simple Moving Average lags (Longer) should consider 18 | granularity (string) : Length of each candlestick for the respective instrument 19 | trading_cost (float) : A static trading cost considered when calculating returns 20 | """ 21 | self._smas = smas 22 | self._smal = smal 23 | 24 | # passes params to the parent class 25 | super().__init__( 26 | instrument, 27 | start, 28 | end, 29 | granularity, 30 | trading_cost 31 | ) 32 | 33 | def __repr__(self): 34 | """Custom Representation.""" 35 | return f"SMABacktest( instrument={self._instrument}, start={self._start}, end={self._end}, smas={self._smas}, smal={self._smal}, granularity={self._granularity}, trading_cost={self._tc} )" 36 | 37 | def prepare_data(self): 38 | """ 39 | Prepares data for strategy-specific information. 40 | 41 | Returns: 42 | Returns a Pandas dataframe which is simply the original dataframe after acquiring instrument data 43 | but with the smas & smal rolling lags values for each dataframe entry added 44 | """ 45 | df = self._data.copy() 46 | df["smas"] = df.price.rolling(self._smas).mean() 47 | df["smal"] = df.price.rolling(self._smal).mean() 48 | return df 49 | 50 | def set_params(self, SMAS=None, SMAL=None): 51 | """ 52 | Allows the caller to reset/override the current SMA (Short and Long) values individually or together, 53 | which also updates the prepared dataset (rolling SMA values) associated with the instrument. 54 | 55 | Args: 56 | SMAS (int): The new shorter SMA 57 | SMAL (int): The new longer SMA 58 | """ 59 | if SMAS is not None and SMAL is not None: 60 | if SMAS >= SMAL: 61 | print("The smas value must be smaller than the smal value.") 62 | return 63 | 64 | if SMAS is not None: 65 | self._smas = SMAS 66 | self._data["smas"] = self._data["price"].rolling(self._smas).mean() 67 | if SMAL is not None: 68 | self._smal = SMAL 69 | self._data["smal"] = self._data["price"].rolling(self._smal).mean() 70 | 71 | def test(self, mute=False): 72 | """ 73 | Executes the back-testing of the SMA Cross strategy on the set instrument. 74 | 75 | Returns: 76 | Returns a tuple, (float: performance, float: out_performance) 77 | -> "performance" is the percentage of return on the interval [start, end] 78 | -> "out_performance" is the performance when compared to a buy & hold on the same interval 79 | IE, if out_performance is greater than one, the strategy outperformed B&H. 80 | """ 81 | 82 | if not mute: 83 | print(f"Testing strategy with smas = {self._smas}, smal = {self._smal} ...") 84 | 85 | data = self._data.copy() 86 | 87 | data["position"] = np.where(data["smas"] > data["smal"], 1, -1) 88 | data["strategy"] = data.position.shift(1) * data["returns"] 89 | data.dropna(inplace=True) 90 | 91 | data["trades"] = data.position.diff().fillna(0).abs() 92 | 93 | # correct strategy returns based on trading costs 94 | data.strategy = data.strategy - data.trades * self._tc 95 | 96 | data["creturns"] = data["returns"].cumsum().apply(np.exp) 97 | data["cstrategy"] = data["strategy"].cumsum().apply(np.exp) 98 | self._results = data 99 | 100 | performance = data["cstrategy"].iloc[-1] 101 | # out_performance is our strats performance vs a buy and hold on the interval 102 | out_performance = performance - data["creturns"].iloc[-1] 103 | 104 | if not mute: 105 | print(f"Return: {round(performance*100 - 100,2)}%, Out Performance: {round(out_performance*100,2)}%") 106 | 107 | return performance, out_performance 108 | 109 | def optimize(self): 110 | """ 111 | Optimizes the smas and smal on the interval [start,end] which allows for the greatest return. 112 | This function attempts all combinations of: smas Days [10,50] & smal Days [100,252], so depending on the 113 | length of the interval, it can take some time to compute. 114 | 115 | Returns: 116 | Returns a tuple, (float: max_return, int: GSMAS, int: GSMAL) 117 | -> "max_return" is the optimized (maximum) return rate of the instrument on the interval [start,end] 118 | -> "GSMAS" is the optimized global smas value that maximizes return 119 | -> "GSMAL" is the optimized global smal value that maximizes return 120 | """ 121 | 122 | print("Optimizing strategy...") 123 | 124 | max_return = float("-inf") 125 | GSMAS = -1 126 | GSMAL = -1 127 | 128 | for SMAS in range(10, 50): 129 | 130 | if SMAS == 13: 131 | print("25%...") 132 | if SMAS == 25: 133 | print("50%...") 134 | if SMAS == 38: 135 | print("75%...") 136 | 137 | for SMAL in range(100, 252): 138 | 139 | self.set_params(SMAS, SMAL) 140 | current_return = self.test(mute=True)[0] 141 | 142 | if current_return > max_return: 143 | max_return = current_return 144 | GSMAS = SMAS 145 | GSMAL = SMAL 146 | 147 | self.set_params(GSMAS, GSMAL) 148 | self.test(mute=True) 149 | 150 | print(f"Strategy optimized on interval {self._start} - {self._end}") 151 | print(f"Max Return: {round(max_return * 100 - 100, 2)}%, Best SMAS: {GSMAS} ({self._granularity}), Best SMAL: {GSMAL} ({self._granularity})") 152 | 153 | return max_return, GSMAS, GSMAL 154 | -------------------------------------------------------------------------------- /backtesting/IterativeBacktest.py: -------------------------------------------------------------------------------- 1 | from backtesting.IterativeBase import IterativeBase 2 | 3 | 4 | class IterativeBacktest(IterativeBase): 5 | 6 | """Class implementing strategy specific iterative testing functions""" 7 | def go_long(self, bar, units=None, amount=None): 8 | if self._position == -1: 9 | # if short, go neutral first 10 | self.buy(bar, units=-self._units) 11 | if units: 12 | self.buy(bar, units=units) 13 | elif amount: 14 | if amount == "all": 15 | amount = self._current_balance 16 | self.buy(bar, amount=amount) 17 | 18 | def go_short(self, bar, units=None, amount=None): 19 | if self._position == 1: 20 | self.sell(bar, units=self._units) 21 | if units: 22 | self.sell(bar, units=units) 23 | elif amount: 24 | if amount == "all": 25 | amount = self._current_balance 26 | self.sell(bar, amount=amount) 27 | 28 | def reset(self): 29 | # reset instrument attributes 30 | self._position = 0 31 | self._trades = 0 32 | self._current_balance = self._initial_balance 33 | self.acquire_data() 34 | 35 | # TODO: Make this inheritable by the strategy, make this file more abstract 36 | def test_sma(self, smas, smal): 37 | print( 38 | f"Testing SMA strategy on {self._symbol} with smas={smas} and smal={smal}" 39 | ) 40 | 41 | self.reset() 42 | 43 | self._data["smas"] = self._data.bid_price.rolling(smas).mean() 44 | self._data["smal"] = self._data.bid_price.rolling(smal).mean() 45 | self._data.dropna(inplace=True) 46 | 47 | # sma crossover strategy 48 | for bar in range(len(self._data) - 1): 49 | if self._data["smas"].iloc[bar] > self._data["smal"].iloc[bar]: 50 | # go long 51 | if self._position in [0, -1]: 52 | # go long with entire balance to switch position 53 | self.go_long(bar, amount="all") 54 | self._position = 1 55 | elif self._data["smas"].iloc[bar] < self._data["smal"].iloc[bar]: 56 | # go short 57 | if self._position in [0, 1]: 58 | # go short with entire balance to switch position 59 | self.go_short(bar, amount="all") 60 | self._position = -1 61 | 62 | self.close_position(bar + 1) 63 | 64 | def test_contrarian(self, window=1): 65 | print(f"Testing Contrarian strategy on {self._symbol} with window={window}") 66 | 67 | self.reset() 68 | 69 | # prepares the data 70 | self._data["rolling_returns"] = self._data["returns"].rolling(window).mean() 71 | self._data.dropna(inplace=True) 72 | 73 | for bar in range(len(self._data) - 1): 74 | if self._data["rolling_returns"].iloc[bar] <= 0: 75 | # go long 76 | if self._position in [0, -1]: 77 | self.go_long(bar, amount="all") 78 | self._position = 1 79 | else: 80 | # go short 81 | if self._position in [0, 1]: 82 | self.go_short(bar, amount="all") 83 | self._position = -1 84 | 85 | self.close_position(bar + 1) 86 | 87 | def test_momentum(self, window=1): 88 | print(f"Testing Momentum strategy on {self._symbol} with window={window}") 89 | 90 | self.reset() 91 | 92 | # prepares the data 93 | self._data["rolling_returns"] = self._data["returns"].rolling(window).mean() 94 | self._data.dropna(inplace=True) 95 | 96 | for bar in range(len(self._data) - 1): 97 | if self._data["rolling_returns"].iloc[bar] <= 0: 98 | # go short 99 | if self._position in [0, 1]: 100 | self.go_short(bar, amount="all") 101 | self._position = -1 102 | else: 103 | # go long 104 | if self._position in [0, -1]: 105 | self.go_long(bar, amount="all") 106 | self._position = 1 107 | 108 | self.close_position(bar + 1) 109 | 110 | def test_bollinger_bands(self, sma, std=2): 111 | print( 112 | f"Testing Bollinger Bands strategy on {self._symbol} with sma={sma}, std={std}" 113 | ) 114 | 115 | self.reset() 116 | 117 | # prepares the data 118 | self._data["sma"] = self._data.bid_price.rolling(sma).mean() 119 | self._data["lower"] = self._data["sma"] - ( 120 | self._data.bid_price.rolling(sma).std() * std 121 | ) 122 | self._data["upper"] = self._data["sma"] + ( 123 | self._data.bid_price.rolling(sma).std() * std 124 | ) 125 | 126 | self._data.dropna(inplace=True) 127 | 128 | for bar in range(len(self._data) - 1): 129 | 130 | if self._position == 0: 131 | 132 | if self._data["bid_price"].iloc[bar] < self._data["lower"].iloc[bar]: 133 | # if price is lower than lower band, indicates oversold, and to go long 134 | self.go_long(bar, amount="all") 135 | self._position = 1 136 | elif self._data["bid_price"].iloc[bar] > self._data["upper"].iloc[bar]: 137 | # if price is higher than upper band, indicates overbought, and to go short 138 | self.go_short(bar, amount="all") 139 | self._position = -1 140 | 141 | elif self._position == 1: 142 | if self._data["bid_price"].iloc[bar] > self._data["sma"].iloc[bar]: 143 | 144 | # if price crosses upper band, signal to go short 145 | if ( 146 | self._data["bid_price"].iloc[bar] 147 | > self._data["upper"].iloc[bar] 148 | ): 149 | self.go_short(bar, amount="all") 150 | self._position = -1 151 | else: 152 | # if price is between sma and upper, just go neutral 153 | self.sell(bar, units=self._units) 154 | self._position = 0 155 | 156 | elif self._position == -1: 157 | if self._data["bid_price"].iloc[bar] < self._data["sma"].iloc[bar]: 158 | 159 | # if price crosses lower band, signal to go long 160 | if ( 161 | self._data["bid_price"].iloc[bar] 162 | < self._data["lower"].iloc[bar] 163 | ): 164 | self.go_long(bar, amount="all") 165 | self._position = 1 166 | else: 167 | # if price is between lower and sma, just go neutral 168 | self.buy(bar, units=-self._units) 169 | self._position = 0 170 | 171 | self.close_position(bar + 1) 172 | -------------------------------------------------------------------------------- /backtesting/BollingerBandsBacktest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from backtesting.Backtester import Backtester 3 | 4 | 5 | class BollingerBandsBacktest(Backtester): 6 | 7 | """Class implementing vectorized back-testing of a Bollinger Bands trading strategy.""" 8 | def __init__( 9 | self, instrument, start, end, sma=20, deviation=2, granularity="D", trading_cost=0 10 | ): 11 | """ 12 | Initializes the BollingerBandsBacktest object. 13 | 14 | Args: 15 | instrument (string): A string holding the ticker instrument of instrument to be tested 16 | start (string): The start date of the testing period 17 | end (string): The end date of the testing period 18 | sma (int) : Length of sliding average lags 19 | deviation (int) : Standard deviation multiplier for upper and lower bands 20 | granularity (string) : Length of each candlestick for the respective instrument 21 | trading_cost (float) : A static trading cost considered when calculating returns 22 | """ 23 | self._sma = sma 24 | self._deviation = deviation 25 | 26 | # passes params to the parent class 27 | super().__init__( 28 | instrument, 29 | start, 30 | end, 31 | granularity, 32 | trading_cost 33 | ) 34 | 35 | def __repr__(self): 36 | """Custom Representation.""" 37 | return f"BollingerBandsBacktest( instrument={self._instrument}, start={self._start}, end={self._end}, sma={self._sma}, deviation={self._deviation}, granularity={self._granularity}, trading_cost={self._tc} )" 38 | 39 | def prepare_data(self): 40 | """ 41 | Prepares data for strategy-specific information. 42 | Returns: 43 | Returns a Pandas dataframe 44 | """ 45 | df = self._data.copy() 46 | df["sma"] = df.price.rolling(self._sma).mean() 47 | 48 | df["lower"] = df["sma"] - (df.price.rolling(self._sma).std() * self._deviation) 49 | df["upper"] = df["sma"] + (df.price.rolling(self._sma).std() * self._deviation) 50 | 51 | return df 52 | 53 | def set_params(self, sma=None, deviation=None): 54 | """ 55 | Allows the caller to reset/override the current sma value and the deviation value, 56 | which also updates the prepared dataset associated with the instrument. 57 | 58 | Args: 59 | sma (int): The new sma 60 | deviation (int): The new deviation 61 | """ 62 | if sma is not None: 63 | self._sma = sma 64 | self._data["sma"] = self._data.price.rolling(self._sma).mean() 65 | 66 | # error with python... https://github.com/pandas-dev/pandas/issues/21786 .std() doesnt work 67 | self._data["lower"] = ( 68 | self._data["sma"] 69 | - self._data.price.rolling(self._sma).apply(lambda x: np.std(x)) 70 | * self._deviation 71 | ) 72 | self._data["upper"] = ( 73 | self._data["sma"] 74 | + self._data.price.rolling(self._sma).apply(lambda x: np.std(x)) 75 | * self._deviation 76 | ) 77 | 78 | if deviation is not None: 79 | self._deviation = deviation 80 | self._data["lower"] = self._data["sma"] - ( 81 | self._data.price.rolling(self._sma).apply(lambda x: np.std(x)) 82 | * deviation 83 | ) 84 | self._data["upper"] = self._data["sma"] + ( 85 | self._data.price.rolling(self._sma).apply(lambda x: np.std(x)) 86 | * deviation 87 | ) 88 | 89 | def test(self, mute=False): 90 | """ 91 | Executes the back-testing of the Bollinger Bands strategy on the set instrument. 92 | 93 | Returns: 94 | Returns a tuple, (float: performance, float: out_performance) 95 | -> "performance" is the percentage of return on the interval [start, end] 96 | -> "out_performance" is the performance when compared to a buy & hold on the same interval 97 | IE, if out_performance is greater than one, the strategy outperformed B&H. 98 | """ 99 | if not mute: 100 | print(f"Testing strategy with sma = {self._sma}, deviation = {self._deviation} ...") 101 | 102 | data = self._data.copy().dropna() 103 | 104 | data["distance"] = data["price"] - data["sma"] 105 | 106 | # if price is lower than lower band, indicates oversold, and to go long 107 | data["position"] = np.where(data["price"] < data["lower"], 1, np.nan) 108 | 109 | # if price is higher than upper band, indicates overbought, and to go short 110 | data["position"] = np.where(data["price"] > data["upper"], -1, data["position"]) 111 | 112 | # if we have crossed the sma line, we want to close our current position (be neutral, position=0) 113 | data["position"] = np.where( 114 | data["distance"] * data["distance"].shift(1) < 0, 0, data["position"] 115 | ) 116 | 117 | # clean up any NAN values/holiday vacancies 118 | data["position"] = data.position.ffill().fillna(0) 119 | 120 | data["strategy"] = data.position.shift(1) * data["returns"] 121 | 122 | data.dropna(inplace=True) 123 | 124 | data["trades"] = data.position.diff().fillna(0).abs() 125 | 126 | # correct strategy returns based on trading costs (only applicable if self._tc > 0) 127 | data.strategy = data.strategy - data.trades * self._tc 128 | 129 | data["creturns"] = data["returns"].cumsum().apply(np.exp) 130 | data["cstrategy"] = data["strategy"].cumsum().apply(np.exp) 131 | self._results = data 132 | 133 | performance = data["cstrategy"].iloc[-1] 134 | # out_performance is our strats performance vs a buy and hold on the interval 135 | out_performance = performance - data["creturns"].iloc[-1] 136 | 137 | if not mute: 138 | print(f"Return: {round(performance*100 - 100,2)}%, Out Performance: {round(out_performance*100,2)}%") 139 | 140 | return performance, out_performance 141 | 142 | def optimize(self, sma_range=(1, 252), dev_range=(1, 3)): 143 | """ 144 | Optimizes the sma and deviation on the interval [start,end] which allows for the greatest return. 145 | 146 | Returns: 147 | Returns a tuple, (float: max_return, int: best_sma, int: best_dev) 148 | -> "max_return" is the optimized (maximum) return rate of the instrument on the interval [start,end] 149 | -> "best_sma" is the optimized global best_sma value that maximizes return 150 | -> "best_dev" is the optimized global best_dev value that maximizes return 151 | """ 152 | ############################################### 153 | print("Warning: There is a current issue that will cause this optimization to take a long time.") 154 | ############################################### 155 | 156 | if sma_range[0] >= sma_range[1] or dev_range[0] >= dev_range[1]: 157 | print("The ranges must satisfy: (X,Y) -> X < Y") 158 | return 159 | 160 | print("Optimizing strategy...") 161 | 162 | max_return = float("-inf") 163 | best_sma = -1 164 | best_dev = -1 165 | for sma in range(sma_range[0], sma_range[1]): 166 | 167 | if sma == sma_range[1] / 4: 168 | print("25%...") 169 | if sma == sma_range[1] / 2: 170 | print("50%...") 171 | if sma == sma_range[1] / 1.5: 172 | print("75%...") 173 | 174 | for dev in range(dev_range[0], dev_range[1]): 175 | self.set_params(sma, dev) 176 | current_return = self.test(mute=True)[0] 177 | 178 | if current_return > max_return: 179 | max_return = current_return 180 | best_sma = sma 181 | best_dev = dev 182 | 183 | self.set_params(best_sma, best_dev) 184 | self.test(mute=True) 185 | 186 | print(f"Strategy optimized on interval {self._start} - {self._end}") 187 | print(f"Max Return: {round(max_return * 100 - 100, 2) - 100}%, Best SMA: {best_sma} ({self._granularity}), Best Deviation: {best_dev}") 188 | 189 | return max_return, best_sma, best_dev 190 | 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/4d81d46fe74d40ba8d405550e644a812)](https://www.codacy.com/gh/trentstauff/FXBot/dashboard?utm_source=github.com&utm_medium=referral&utm_content=trentstauff/FXBot&utm_campaign=Badge_Grade) 2 | [![PRs Welcome](https://img.shields.io/badge/PRs%20-welcome-brightgreen.svg)](#contributing) 3 | 4 | # Important: 5 | ### This repo relies on submodules. To clone it properly, run: 6 | 7 | ``` 8 | git clone --recurse-submodules -j8 https://github.com/trentstauff/FXBot 9 | ``` 10 | # FXBot 11 | 12 | ![image](https://user-images.githubusercontent.com/53923200/128397947-04711cb7-0b16-4ed6-8c84-3fdacdc2fadc.png) 13 | 14 | **FXBot** is just what you guessed- a **Forex trading bot!** It's been developed in Python, enabled by the OANDA V20 API. 15 | 16 | This trading bot allows users to **backtest** and **analyze** their favourite strategies executed on the most popular currency pairs, while also enabling users to dive straight into trading these forex pairs in **real-time, through algorithmic live trading.** 17 | 18 | As we all know, algorithmic trading is the **future of finance**. When looking at the top trading firms in the world, all of them are making a shift towards automated trading, and are investing heavily in the space. The ones who don't automate, are at risk of falling behind their competitors! 19 | 20 | That's what inspired me to make this bot, and to expose to the public how really anyone this day and age can jump headfirst into the algorithmic trading world. 21 | 22 | ## Disclaimer 23 | 24 | Before downloading and using this bot, please make sure to understand the following: 25 | 26 | Through OANDA, you do NOT need to trade real money, and the same is true with respect to using this bot. OANDA offers practice accounts, which this bot is **highly recommended** to utilize. 27 | 28 | If you do decide to trade real money, this disclaimer is for you. 29 | 30 | ### Understand the Risk 31 | Trading Forex involves a risk of loss. Please consider carefully if such trading is appropriate for you. Past performance is not indicative of future results. FXBot has been created solely for educational purposes only and its calculations do not constitute investment recommendations or advice, and it is strongly recommended that you **use this bot as a learning tool.** 32 | 33 | If you are to trade using this bot, understand that algorithmic trading involves a high level of risk and is not appropriate for everyone. No guarantee is being made that by using this bot, the algorithmic trading strategies will result in profitable trading or be free of risk of loss. There is a possibility that you could lose some or all of your investment. 34 | 35 | ## What FXBot Can Do For You 36 | 37 | When you first run FXBot, you will be prompted to enter the following: 38 | 39 | 1) The currency pair you would like to analyze/trade 40 | 2) Whether to conduct backtesting or live trading on said currency pair 41 | 42 | ![image](https://user-images.githubusercontent.com/53923200/128404575-c66b07cc-fc77-4d28-8d35-58d7ae2d120c.png) 43 | 44 | ### Backtesting 45 | 46 | Backtesting is a method for seeing how well a strategy would have performed on historical data. This powerful technique can gather a lot of important information about the strategy- such as when is the best time for the strategy to operate, which currency pairs it should execute on, and much more. 47 | If the strategy performs well during backtesting, then individuals can look into putting the strategy into a production environment and try to beat the market. 48 | 49 | FXBot enables users to backtest their strategies, alongside giving flexibility and customization surrounding the parameters passed to the backtester. 50 | 51 | When running the backtesting section of the bot, users will be prompted to specify the following: 52 | 53 | 1) The strategy to backtest 54 | 2) The date range the backtest should occur over 55 | 3) Whether the strategy should consider trading costs 56 | 4) The granularity for the backtest session (IE how often should the bot analyze the data and consider positions)? 57 | 5) Values unique to the strategy (if these values are with respect to time, such as moving averages, the time unit is the same as your specified granularity) 58 | 59 | Note: **For Backtesting,** here are the accepted granularity values you can choose from. You must enter a choice from this list. **For live trading,** please follow the on screen prompts to enter something along the lines of "1hr" or "30s". 60 | ![image](https://user-images.githubusercontent.com/53923200/148832211-edd63f5a-8ca5-4d58-8325-661e0e71ebc2.png) 61 | 62 | 63 | ![image](https://user-images.githubusercontent.com/53923200/128403680-72bfc834-aa76-4f1f-a1a0-295f24c9ebcf.png) 64 | 65 | After entering this, the bot will go ahead and conduct the backtesting. 66 | 67 | Not only will it test the user's specified parameter choices onto the currency pair, but the bot will also find the most **optimal** parameter values for that time period that allows for the highest ROI. This optimization gives critical information that can be further analyzed by the user to find the best values for their trading situation. 68 | 69 | Finally, the bot will plot the results so that the user can tangibly see the performance of the strategy. 70 | 71 | ### Live Trading 72 | 73 | Once you believe you have found a good strategy and have optimized its unique parameters, you can jump into live trading. 74 | 75 | Live trading is exactly how it sounds, it utilizes algorithmic, event-driven trading that allows the user to execute the strategy on data as it happens in real-time. 76 | This is where you can realize the full potential of your strategy and see how it performs against the market. 77 | 78 | When running the live trading section of the bot, users will be prompted to specify the following: 79 | 80 | 1) The strategy to backtest 81 | 2) The date range the backtest should occur over 82 | 3) The granularity for the trading session (IE how often should the bot analyze the data and consider positions)? 83 | 4) The number of units to trade with (IE the size you want your positions to be) 84 | 5) OPTIONAL: A "stop profit" to halt trading if you reach 85 | 6) OPTIONAL: A "stop-loss" to halt trading if you go below 86 | 7) Values unique to the strategy (if these values are with respect to time, such as moving averages, the time unit is the same as your specified granularity) 87 | 88 | ![image](https://user-images.githubusercontent.com/53923200/128405363-900d8ddb-6b43-4dae-bfdf-7fdfbe1b4007.png) 89 | 90 | Once the bot is set up and ready to trade, the trading stream will open. For the duration of the session, the console will continuously output each "tick" of data that is being streamed back to the bot, which contains the time of the tick, the bid price, and the ask price. 91 | 92 | Every "granularity", the bot will analyze the current market and determine if it should open, close, modify, or hold a position, which is based on the underlying strategy. 93 | 94 | ![image](https://user-images.githubusercontent.com/53923200/128405765-15760e5f-1807-42a5-997a-06d975a5ea1f.png) 95 | 96 | If any of the stop thresholds have been crossed, or if the user terminates the session, the bot will automatically exit all of its current positions, and the console will default back to the start, where the user can start over. 97 | 98 | ![image](https://user-images.githubusercontent.com/53923200/128405987-c60bbea3-7c1c-4cae-8b5f-c5e0dab8fea4.png) 99 | 100 | 101 | ## Current Strategies 102 | 103 | - SMA https://www.google.com/search?q=sma+strategy&oq=SMA+strategy&aqs=chrome.0.0i512j0i67j0i512l2j0i22i30l6.1680j0j7&sourceid=chrome&ie=UTF-8 104 | - Bollinger Bands https://www.investopedia.com/trading/using-bollinger-bands-to-gauge-trends/ 105 | - Contrarian https://www.investopedia.com/terms/c/contrarian.asp 106 | - Momentum https://www.investopedia.com/terms/m/momentum_investing.asp 107 | - Machine Learning Classification Analysis 108 | - Machine Learning Regression Analysis 109 | - And much more to come! 110 | 111 | ## How to Setup FXBot 112 | 113 | You can start off by cloning the repo by running `git clone --recurse-submodules -j8 https://github.com/trentstauff/FXBot` 114 | 115 | ### Requirements 116 | 117 | First, you need to have at least a practice account with Oanda (https://oanda.com/). Once logged in, you must create an API token and copy your account number. 118 | 119 | #### API Token 120 | ![image](https://user-images.githubusercontent.com/53923200/128407124-08f22bff-a82e-4c47-b150-a3f743e7a38b.png) 121 | 122 | This green button will say "Generate". Click it, and copy the API Token. 123 | ![image](https://user-images.githubusercontent.com/53923200/128407767-7e8e738a-2a88-42c1-9744-273b31f63dfc.png) 124 | 125 | Navigate back to your account, so you can get your account number. 126 | ![image](https://user-images.githubusercontent.com/53923200/128407881-538e26ac-6e17-46b8-a07b-ce025d4cf527.png) 127 | 128 | #### Account Number 129 | 130 | Click "Add Account". 131 | ![image](https://user-images.githubusercontent.com/53923200/128408490-e0aa2fe3-daac-4f04-8301-0654b9993f37.png) 132 | 133 | Make an account. 134 | **Make sure to select 'v20 fxTrade'** 135 | ![image](https://user-images.githubusercontent.com/53923200/128408746-e17439b8-e97b-4513-9691-c23643040552.png) 136 | 137 | Once you have made an account, grab your account number. 138 | ![image](https://user-images.githubusercontent.com/53923200/128408956-b9bc4f3f-a939-4945-b3a5-2b6736d75548.png) 139 | 140 | **Now that you have an OANDA account and these values, you'll need to store them for the bot to use.** 141 | 142 | These values need to be put into a configuration file, with the name `oanda.cfg`, as follows: 143 | 144 | (You can make it a .txt file first, type the data, and then rename the file to .cfg) 145 | 146 | [oanda] 147 | account_id = XYZ-ABC-... 148 | access_token = ZYXCAB... 149 | account_type = practice (default) or live 150 | 151 | **Place the oanda.cfg file into the same directory as the main.py file in the cloned repository folder.** 152 | 153 | After cloning the repo, run `pip install -r requirements.txt` to get the required packages. 154 | 155 | ### Running the application 156 | 157 | After installing the requirements, open a command prompt and you can start up the program by typing `python main.py` (python3 on Linux, if applicable) while in its directory. 158 | 159 | ### Thats all! I hope that this bot helps you to see how awesome algorithmic trading can be. 160 | ### Have a good one! 161 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import tpqoa 2 | from datetime import datetime 3 | 4 | from livetrading.BollingerBandsLive import BollingerBandsLive 5 | from livetrading.ContrarianLive import ContrarianLive 6 | from livetrading.MLClassificationLive import MLClassificationLive 7 | from livetrading.MomentumLive import MomentumLive 8 | from livetrading.SMALive import SMALive 9 | 10 | from backtesting.ContrarianBacktest import ContrarianBacktest 11 | from backtesting.BollingerBandsBacktest import BollingerBandsBacktest 12 | from backtesting.MomentumBacktest import MomentumBacktest 13 | from backtesting.SMABacktest import SMABacktest 14 | from backtesting.MLClassificationBacktest import MLClassificationBacktest 15 | 16 | 17 | if __name__ == "__main__": 18 | 19 | while True: 20 | 21 | # step 1 ensure they have this 22 | cfg = "oanda.cfg" 23 | 24 | # step 1.5 open oanda connection 25 | oanda = tpqoa.tpqoa("oanda.cfg") 26 | 27 | # step 2 decide instrument 28 | 29 | print("Enter an instrument to trade (index or pair name): \n") 30 | choices = [] 31 | 32 | for index, instrument in enumerate(oanda.get_instruments()): 33 | temp = instrument[1] 34 | choices.append(temp) 35 | print(f"({index}: {temp})", end=", ") 36 | 37 | print("") 38 | 39 | choice = input("\n") 40 | 41 | while True: 42 | if choice not in choices: 43 | 44 | try: 45 | val = int(choice) 46 | if val < len(choices) and val >= 0: 47 | choice = choices[val] 48 | break 49 | except: 50 | pass 51 | 52 | choice = input("Please choose an instrument from the list above: ") 53 | else: 54 | break 55 | 56 | instrument = choice 57 | 58 | print(f"Instrument: {instrument}") 59 | 60 | # step 3 decide if live or backtesting 61 | 62 | choice = input("Live Trading (1) or Backtesting (2)? \n") 63 | 64 | while choice not in ["1", "2"]: 65 | choice = input("Please choose between \"1\" or \"2\": \n") 66 | 67 | # step 4, depending on decision, showcase available strategies 68 | 69 | if choice == "1": 70 | live_strategies = ["sma", "bollinger_bands", "contrarian", "momentum", "ml_classification"] 71 | 72 | print("Please choose the strategy you would like to utilize: \n") 73 | 74 | for strategy in live_strategies: 75 | print(strategy, end=", ") 76 | 77 | choice = input("\n").lower() 78 | 79 | while choice not in live_strategies: 80 | choice = input("Please choose a strategy listed above. \n").lower() 81 | 82 | strategy = choice 83 | 84 | print("Please enter the granularity for your session (NOT from list in README, try \"1hr\", \"1m\", \"30s\" (less strict): \n") 85 | 86 | granularity = input("") 87 | 88 | print("Please enter the number of units you'd like to trade with (integer, IE 200000 units): \n") 89 | 90 | units = int(input("")) 91 | 92 | print("Enter stop profit dollars to halt trading at (float, IE 25, 15.34, etc) (enter \"n\" if not applicable): \n") 93 | 94 | # TODO: Enable stop datetime 95 | 96 | stop_profit = input("") 97 | 98 | if stop_profit == "n": 99 | stop_profit = None 100 | else: 101 | stop_profit = float(stop_profit) 102 | 103 | print("Enter a negative stop loss to halt trading at (float, IE -25, -1.32, etc) (enter \"n\" if not applicable): \n") 104 | 105 | stop_loss = input("") 106 | 107 | if stop_loss == "n": 108 | stop_loss = None 109 | else: 110 | stop_loss = float(stop_loss) 111 | 112 | ################################################################################### 113 | # Strategies 114 | ################################################################################### 115 | 116 | if strategy == "sma": 117 | 118 | print("Enter SMAS value (integer, IE 9): \n") 119 | 120 | smas = int(input("")) 121 | 122 | print("Enter SMAL value (integer, IE 20): \n") 123 | 124 | smal = int(input("")) 125 | 126 | while smal < smas: 127 | smal = int(input("SMAL must be larger or equal to SMAS: \n")) 128 | 129 | trader = SMALive(cfg, instrument, granularity, smas, smal, units, stop_loss=stop_loss, stop_profit=stop_profit) 130 | 131 | # TODO: Post trading analysis, maybe some graphs etc 132 | 133 | elif strategy == "bollinger_bands": 134 | 135 | print("Enter SMA value (integer, IE 9): \n") 136 | 137 | sma = int(input("")) 138 | 139 | print("Enter deviation value (integer, IE 2): \n") 140 | 141 | deviation = int(input("")) 142 | 143 | trader = BollingerBandsLive(cfg, instrument, granularity, sma, deviation, units, stop_loss=stop_loss, 144 | stop_profit=stop_profit) 145 | 146 | # TODO: Post trading analysis, maybe some graphs etc 147 | 148 | elif strategy == "momentum": 149 | 150 | print("Enter window value (integer, IE 3): \n") 151 | 152 | window = int(input("")) 153 | 154 | trader = MomentumLive(cfg, instrument, granularity, window, units, stop_loss=stop_loss, 155 | stop_profit=stop_profit) 156 | 157 | # TODO: Post trading analysis, maybe some graphs etc 158 | 159 | elif strategy == "contrarian": 160 | 161 | print("Enter window value (integer, IE 3): \n") 162 | 163 | window = int(input("")) 164 | 165 | trader = ContrarianLive(cfg, instrument, granularity, window, units, stop_loss=stop_loss, 166 | stop_profit=stop_profit) 167 | 168 | # TODO: Post trading analysis, maybe some graphs etc 169 | 170 | elif strategy == "ml_classification": 171 | 172 | print("Enter number of lags (integer, IE 6): \n") 173 | 174 | lags = int(input("")) 175 | 176 | trader = MLClassificationLive(cfg, instrument, granularity, lags, units, stop_loss=stop_loss, 177 | stop_profit=stop_profit) 178 | 179 | # TODO: Post trading analysis, maybe some graphs etc 180 | 181 | else: 182 | 183 | backtesting_strategies = ["sma", "bollinger_bands", "contrarian", "momentum", "ml_classification", "ml_regression"] 184 | 185 | print("Please choose the strategy you would like to backtest: \n") 186 | 187 | for strategy in backtesting_strategies: 188 | print(strategy, end=", ") 189 | 190 | choice = input("\n").lower() 191 | 192 | while choice not in backtesting_strategies: 193 | choice = input("Please choose a strategy listed above. \n").lower() 194 | 195 | strategy = choice 196 | 197 | print("Enter the start date for the backtest (string, in form of \"YYYY-MM-DD\"): \n") 198 | 199 | start = input("") 200 | 201 | print("Enter the end date for the backtest (string, in form of \"YYYY-MM-DD\"): \n") 202 | 203 | end = input("") 204 | 205 | while datetime.strptime(start, '%Y-%m-%d') > datetime.strptime(end, '%Y-%m-%d'): 206 | end = input("End date must be after start date. \n") 207 | 208 | print("Enter the trading cost to consider: (float, IE 0.0, 0.00007): \n") 209 | 210 | trading_cost = float(input("")) 211 | 212 | print("Please enter the granularity for your session (See list in README, IE \"S30\", \"M1\", \"H1\"): \n") 213 | 214 | granularity = input("") 215 | 216 | if strategy == "sma": 217 | 218 | print("Enter SMAS value (integer, IE 9): \n") 219 | 220 | smas = int(input("")) 221 | 222 | print("Enter SMAL value (integer, IE 20): \n") 223 | 224 | smal = int(input("")) 225 | 226 | while smal < smas: 227 | smal = int(input("SMAL must be larger or equal to SMAS: \n")) 228 | 229 | trader = SMABacktest(instrument, start, end, smas, smal, granularity, trading_cost) 230 | 231 | trader.test() 232 | trader.optimize() 233 | trader.plot_results() 234 | 235 | # TODO: Post trading analysis, maybe some graphs etc 236 | 237 | elif strategy == "bollinger_bands": 238 | 239 | print("Enter SMA value (integer, IE 9): \n") 240 | 241 | sma = int(input("")) 242 | 243 | print("Enter deviation value (integer, IE 2): \n") 244 | 245 | deviation = int(input("")) 246 | 247 | trader = BollingerBandsBacktest(instrument, start, end, sma, deviation, granularity, trading_cost) 248 | 249 | trader.test() 250 | trader.optimize() 251 | trader.plot_results() 252 | 253 | # TODO: Post trading analysis, maybe some graphs etc 254 | 255 | elif strategy == "momentum": 256 | 257 | print("Enter window value (integer, IE 3): \n") 258 | 259 | window = int(input("")) 260 | 261 | trader = MomentumBacktest(instrument, start, end, window, granularity, trading_cost) 262 | 263 | trader.test() 264 | trader.optimize() 265 | trader.plot_results() 266 | 267 | # TODO: Post trading analysis, maybe some graphs etc 268 | 269 | elif strategy == "contrarian": 270 | 271 | print("Enter window value (integer, IE 3): \n") 272 | 273 | window = int(input("")) 274 | 275 | trader = ContrarianBacktest(instrument, start, end, window, granularity, trading_cost) 276 | 277 | trader.test() 278 | trader.optimize() 279 | trader.plot_results() 280 | 281 | # TODO: Post trading analysis, maybe some graphs etc 282 | 283 | elif strategy == "ml_classification": 284 | 285 | trader = MLClassificationBacktest(instrument, start, end, granularity, trading_cost) 286 | 287 | trader.test() 288 | trader.plot_results() 289 | 290 | # TODO: Post trading analysis, maybe some graphs etc 291 | -------------------------------------------------------------------------------- /livetrading/LiveTrader.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from datetime import datetime, timedelta 3 | 4 | import pytz 5 | 6 | import tpqoa 7 | import matplotlib.pyplot as plt 8 | 9 | plt.style.use("seaborn") 10 | 11 | 12 | class LiveTrader(tpqoa.tpqoa): 13 | def __init__( 14 | self, 15 | cfg, 16 | instrument, 17 | bar_length, 18 | units, 19 | history_days=1, 20 | stop_datetime=None, 21 | stop_loss=None, 22 | stop_profit=None, 23 | ): 24 | """ 25 | Initializes the LiveTrader object. 26 | 27 | Args: 28 | cfg (object): An object representing the OANDA connection 29 | instrument (string): A string holding the ticker instrument of instrument to be tested 30 | bar_length (string): Length of each candlestick for the respective instrument 31 | units (int): Amount of units to take positions with 32 | history_days (int): Amount of prior days history to download 33 | stop_datetime (object) : A datetime object that when passed stops trading 34 | stop_loss (float) : A stop loss that when profit goes below stops trading 35 | stop_profit (float) : A stop profit that when profit goes above stops trading 36 | """ 37 | # TODO: More rigorous handling of markets being closed (this is EST dependent, must ensure that is what the datetime is giving) 38 | if datetime.today().weekday() >= 6 and datetime.today().hour >= 17: 39 | print("Markets are open.") 40 | elif datetime.today().weekday() == 6 and datetime.today().hour < 17: 41 | raise Exception("Sorry, markets are closed") 42 | elif datetime.today().weekday() == 5: 43 | raise Exception("Sorry, markets are closed") 44 | elif datetime.today().weekday() >= 4 and datetime.today().hour >= 5: 45 | raise Exception("Sorry, markets are closed") 46 | else: 47 | print("Markets are open, beginning trading session.") 48 | 49 | # passes the config file to tpqoa 50 | super().__init__(cfg) 51 | self._instrument = instrument 52 | self._bar_length = pd.to_timedelta(bar_length) 53 | self._tick_data = pd.DataFrame() 54 | self._raw_data = None 55 | self._data = None 56 | self._last_tick = None 57 | self._units = units 58 | 59 | if stop_datetime: 60 | utc_datetime = stop_datetime.astimezone(pytz.utc) 61 | 62 | self._stop_datetime = utc_datetime 63 | else: 64 | self._stop_datetime = None 65 | 66 | self._stop_loss = stop_loss 67 | self._stop_profit = stop_profit 68 | 69 | self._position = 0 70 | 71 | self._profits = [] 72 | self._profit = 0 73 | 74 | # set up history used by some trades 75 | self.setup_history(history_days) 76 | 77 | self.stream_data(self._instrument) 78 | 79 | def __del__(self): 80 | """Destructor used to ensure closing of position when object expires.""" 81 | # close out position 82 | self.close_position() 83 | 84 | # used to gather historical data used by some strategies 85 | def setup_history(self, days=1): 86 | print("Setting up history...") 87 | if days != 0: 88 | # while loop to combat missing bar on boundary of historical and streamed data 89 | while True: 90 | 91 | now = datetime.utcnow() 92 | now = now.replace(microsecond=0) 93 | past = now - timedelta(days=days) 94 | 95 | mid_price = ( 96 | self.get_history( 97 | instrument=self._instrument, 98 | start=past, 99 | end=now, 100 | granularity="S5", 101 | price="M", 102 | localize=False, 103 | ) 104 | .c.dropna() 105 | .to_frame() 106 | ) 107 | 108 | df = mid_price 109 | df.rename(columns={"c": "mid_price"}, inplace=True) 110 | 111 | df = ( 112 | df.resample(self._bar_length, label="right") 113 | .last() 114 | .dropna() 115 | .iloc[:-1] 116 | ) 117 | 118 | # uncomment if we need ask,bid,spread pricings 119 | # bid_price = self.get_history(instrument=self._instrument, start=past, end=now, granularity="S5", price="B", localize=False).c.dropna().to_frame() 120 | # ask_price = self.get_history(instrument=self._instrument, start=past, end=now, granularity="S5", price="A", localize=False).c.dropna().to_frame() 121 | # spread = ask_price - bid_price 122 | # create the new dataframe with relevent info 123 | # df = bid_price 124 | # df.rename(columns={"c": "bid_price"}, inplace=True) 125 | # df["ask_price"] = ask_price 126 | # df["mid_price"] = ask_price - spread 127 | # df["spread"] = spread 128 | # df = df.resample(self._bar_length, label="right").last().dropna().iloc[:-1] 129 | 130 | self._raw_data = df.copy() 131 | self._last_tick = self._raw_data.index[-1] 132 | 133 | # set the data if less than _bar_length time as elapsed since the last full historical bar 134 | # this way we never have a missing boundary bar between historical and stream 135 | if ( 136 | pd.to_datetime(datetime.utcnow()).tz_localize("UTC") 137 | - self._last_tick 138 | ) < self._bar_length: 139 | print("History set up. Opening trading stream.") 140 | break 141 | 142 | # called when new streamed data is successful 143 | def on_success(self, time, bid, ask): 144 | print(time, bid, ask) 145 | 146 | recent_tick = pd.to_datetime(time) 147 | 148 | stopped = False 149 | 150 | if self._stop_datetime: 151 | if recent_tick >= self._stop_datetime: 152 | self.stop_stream = True 153 | self.close_position() 154 | stopped = True 155 | 156 | if self._stop_loss: 157 | if self._profit < self._stop_loss: 158 | self.stop_stream = True 159 | self.close_position() 160 | stopped = True 161 | 162 | if self._stop_profit: 163 | if self._profit > self._stop_profit: 164 | self.stop_stream = True 165 | self.close_position() 166 | stopped = True 167 | 168 | if stopped: 169 | print("Stop triggered, ending stream.") 170 | 171 | if not stopped: 172 | df = pd.DataFrame( 173 | { 174 | "bid_price": bid, 175 | "ask_price": ask, 176 | "mid_price": (ask + bid) / 2, 177 | "spread": ask - bid, 178 | }, 179 | index=[recent_tick], 180 | ) 181 | self._tick_data = self._tick_data.append(df) 182 | # resamples the tick data (if applicable), while also dropping 183 | # the last row (as it can be far off the resampled granularity) 184 | if (recent_tick - self._last_tick) >= self._bar_length: 185 | 186 | # append the most recent resampled ticks to self._data 187 | self._raw_data = self._raw_data.append( 188 | self._tick_data.resample(self._bar_length, label="right") 189 | .last() 190 | .ffill() 191 | .iloc[:-1] 192 | ) 193 | 194 | # only keep the last tick bar (which is a pandas DataFrame) 195 | self._tick_data = self._tick_data.iloc[-1:] 196 | self._last_tick = self._raw_data.index[-1] 197 | 198 | self.define_strategy() 199 | self.trade() 200 | 201 | def define_strategy(self): 202 | pass 203 | 204 | def trade(self): 205 | # if most recent bar in position of strat says to go long, do it 206 | if self._data["position"].iloc[-1] == 1: 207 | # if we were neutral, only need to go long "units" 208 | if self._position == 0: 209 | order = self.create_order( 210 | self._instrument, self._units, suppress=True, ret=True 211 | ) 212 | self.trade_report(order, 1) 213 | # if we were short, need to go long 2 * "units" 214 | elif self._position == -1: 215 | order = self.create_order( 216 | self._instrument, self._units * 2, suppress=True, ret=True 217 | ) 218 | self.trade_report(order, 1) 219 | 220 | self._position = 1 221 | 222 | # short position 223 | elif self._data["position"].iloc[-1] == -1: 224 | # if we were neutral, only need to go short "units" 225 | if self._position == 0: 226 | order = self.create_order( 227 | self._instrument, -self._units, suppress=True, ret=True 228 | ) 229 | self.trade_report(order, -1) 230 | # if we were short, need to go short 2 * "units" 231 | elif self._position == 1: 232 | order = self.create_order( 233 | self._instrument, -(self._units * 2), suppress=True, ret=True 234 | ) 235 | self.trade_report(order, -1) 236 | 237 | self._position = -1 238 | 239 | # if we want to go neutral, close out current open position 240 | elif self._data["position"].iloc[-1] == 0: 241 | 242 | if self._position == 1: 243 | order = self.create_order( 244 | self._instrument, -self._units, suppress=True, ret=True 245 | ) 246 | self.trade_report(order, 0) 247 | 248 | elif self._position == -1: 249 | order = self.create_order( 250 | self._instrument, self._units, suppress=True, ret=True 251 | ) 252 | self.trade_report(order, 0) 253 | 254 | self._position = 0 255 | 256 | def close_position(self): 257 | if self._position != 0: 258 | order = self.create_order( 259 | self._instrument, 260 | units=-(self._position * self._units), 261 | suppress=True, 262 | ret=True, 263 | ) 264 | self.trade_report(order, 0) 265 | self._position = 0 266 | 267 | def trade_report(self, order, position): 268 | 269 | time = order["time"] 270 | units = order["units"] 271 | price = order["price"] 272 | profit = float(order["pl"]) 273 | 274 | self._profits.append(profit) 275 | 276 | cum_profits = sum(self._profits) 277 | self._profit = cum_profits 278 | 279 | print( 280 | f"{time} : {position} --- {units} units, price of ${price}, profit of ${profit}, cum profit of ${cum_profits}" 281 | ) 282 | --------------------------------------------------------------------------------