├── 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 |
4 |
5 |
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 |
4 |
5 |
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 | [](https://www.codacy.com/gh/trentstauff/FXBot/dashboard?utm_source=github.com&utm_medium=referral&utm_content=trentstauff/FXBot&utm_campaign=Badge_Grade)
2 | [](#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 | 
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 | 
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 | 
61 |
62 |
63 | 
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 | 
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 | 
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 | 
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 | 
121 |
122 | This green button will say "Generate". Click it, and copy the API Token.
123 | 
124 |
125 | Navigate back to your account, so you can get your account number.
126 | 
127 |
128 | #### Account Number
129 |
130 | Click "Add Account".
131 | 
132 |
133 | Make an account.
134 | **Make sure to select 'v20 fxTrade'**
135 | 
136 |
137 | Once you have made an account, grab your account number.
138 | 
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 |
--------------------------------------------------------------------------------