diff --git a/README.md b/README.md index 204c3ca..3ae7e86 100644 --- a/README.md +++ b/README.md @@ -101,17 +101,19 @@ python scripts/generate_simulated_pair.py GBPUSD At this stage the script is hardcoded to create a single month's data for January 2014. That is, you will see individual files, of the format ```BBBQQQ_YYYYMMDD.csv``` (e.g. ```GBPUSD_20140112.csv```) appear in your ```CSV_DATA_DIR``` for all business days in that month. If you wish to change the month/year of the data output, simply modify the file and re-run. -7) Now that the historical data has been generated it is possible to carry out a backtest. The backtest file itself is stored in ```backtest/backtest.py```. It currently defaults to trading GBPUSD (so will expect GBPUSD data files in ```CSV_DATA_DIR```), but this can easily be changed. In addition a basic ```MovingAverageCrossStrategy``` is implemented (from ```strategy/strategy.py```), which will backtest on the data found in ```CSV_DATA_DIR```. +7) Now that the historical data has been generated it is possible to carry out a backtest. The backtest file itself is stored in ```backtest/backtest.py```, but this only contains the ```Backtest``` class. To actually execute a backtest you need to instantiate this class and provide it with the necessary modules. -To execute the backtest, simply run the following: +The best way to see how this is done is to look at the example Moving Average Crossover implementation in the ```examples/mac.py``` file and use this as a template. This makes use of the ```MovingAverageCrossStrategy``` which is found in ```strategy/strategy.py```. This defaults to trading both GBP/USD and EUR/USD to demonstrate multiple currency pair usage. It uses data found in ```CSV_DATA_DIR```. + +To execute the example backtest, simply run the following: ``` -python backtest/backtest.py +python examples/mac.py ``` **This will take some time.** On my Ubuntu desktop system at home, with the historical data generated via ```generate_simulated_pair.py```, it takes around 5-10 mins to run. A large part of this calculation occurs at the end of the actual backtest, when the drawdown is being calculated, so please remember that the code has not hung up! Please leave it until completion. -8) If you wish to view the performance of the backtest you can simply run ```output.py``` to view an equity curve, period returns (i.e. tick-to-tick returns) and a drawdown curve: +8) If you wish to view the performance of the backtest you can simply use ```output.py``` to view an equity curve, period returns (i.e. tick-to-tick returns) and a drawdown curve: ``` python backtest/output.py diff --git a/backtest/backtest.py b/backtest/backtest.py index 88446c7..249405c 100644 --- a/backtest/backtest.py +++ b/backtest/backtest.py @@ -18,7 +18,7 @@ class Backtest(object): self, pairs, data_handler, strategy, strategy_params, portfolio, execution, equity=100000.0, heartbeat=0.0, - max_iters=100000000 + max_iters=10000000000 ): """ Initialises the backtest. diff --git a/examples/mac.py b/examples/mac.py index 4a1a059..6ebed7f 100644 --- a/examples/mac.py +++ b/examples/mac.py @@ -4,13 +4,13 @@ from qsforex.backtest.backtest import Backtest from qsforex.execution.execution import SimulatedExecution from qsforex.portfolio.portfolio import Portfolio from qsforex import settings -from qsforex.strategy.strategy import TestStrategy, MovingAverageCrossStrategy +from qsforex.strategy.strategy import MovingAverageCrossStrategy from qsforex.data.price import HistoricCSVPriceHandler if __name__ == "__main__": - # Trade on the "Cable" - GBP/USD - pairs = ["GBPUSD"] + # Trade on GBP/USD and EUR/USD + pairs = ["GBPUSD", "EURUSD"] # Create the strategy parameters for the # MovingAverageCrossStrategy diff --git a/portfolio/portfolio.py b/portfolio/portfolio.py index 7b48035..faa14ed 100644 --- a/portfolio/portfolio.py +++ b/portfolio/portfolio.py @@ -14,9 +14,9 @@ from qsforex.settings import OUTPUT_RESULTS_DIR class Portfolio(object): def __init__( - self, ticker, events, home_currency="GBP", leverage=20, - equity=Decimal("100000.00"), risk_per_trade=Decimal("0.02"), - backtest=True + self, ticker, events, home_currency="GBP", + leverage=20, equity=Decimal("100000.00"), + risk_per_trade=Decimal("0.02"), backtest=True ): self.ticker = ticker self.events = events @@ -28,7 +28,8 @@ class Portfolio(object): self.backtest = backtest self.trade_units = self.calc_risk_position_size() self.positions = {} - self.backtest_file = self.create_equity_file() + if self.backtest: + self.backtest_file = self.create_equity_file() def calc_risk_position_size(self): return self.equity * self.risk_per_trade @@ -114,16 +115,16 @@ class Portfolio(object): if currency_pair in self.positions: ps = self.positions[currency_pair] ps.update_position_price() - out_line = "%s,%s" % (tick_event.time, self.balance) - for pair in self.ticker.pairs: - if pair in self.positions: - out_line += ",%s" % self.positions[currency_pair].profit_base - else: - out_line += ",0.00" - out_line += "\n" if self.backtest: + out_line = "%s,%s" % (tick_event.time, self.balance) + for pair in self.ticker.pairs: + if pair in self.positions: + out_line += ",%s" % self.positions[pair].profit_base + else: + out_line += ",0.00" + out_line += "\n" print(out_line[:-2]) - self.backtest_file.write(out_line) + self.backtest_file.write(out_line) def execute_signal(self, signal_event): side = signal_event.side diff --git a/strategy/strategy.py b/strategy/strategy.py index 5fb8157..0c1f874 100644 --- a/strategy/strategy.py +++ b/strategy/strategy.py @@ -1,3 +1,5 @@ +import copy + from qsforex.event.event import SignalEvent @@ -52,39 +54,49 @@ class MovingAverageCrossStrategy(object): short_window=500, long_window=2000 ): self.pairs = pairs - self.events = events - self.ticks = 0 - self.invested = False - + self.pairs_dict = self.create_pairs_dict() + self.events = events self.short_window = short_window self.long_window = long_window - self.short_sma = None - self.long_sma = None + + def create_pairs_dict(self): + attr_dict = { + "ticks": 0, + "invested": False, + "short_sma": None, + "long_sma": None + } + pairs_dict = {} + for p in self.pairs: + pairs_dict[p] = copy.deepcopy(attr_dict) + return pairs_dict def calc_rolling_sma(self, sma_m_1, window, price): return ((sma_m_1 * (window - 1)) + price) / window def calculate_signals(self, event): if event.type == 'TICK': + pair = event.instrument price = event.bid - if self.ticks == 0: - self.short_sma = price - self.long_sma = price + pd = self.pairs_dict[pair] + if pd["ticks"] == 0: + pd["short_sma"] = price + pd["long_sma"] = price else: - self.short_sma = self.calc_rolling_sma( - self.short_sma, self.short_window, price + pd["short_sma"] = self.calc_rolling_sma( + pd["short_sma"], self.short_window, price ) - self.long_sma = self.calc_rolling_sma( - self.long_sma, self.long_window, price + pd["long_sma"] = self.calc_rolling_sma( + pd["long_sma"], self.long_window, price ) # Only start the strategy when we have created an accurate short window - if self.ticks > self.short_window: - if self.short_sma > self.long_sma and not self.invested: - signal = SignalEvent(self.pairs[0], "market", "buy", event.time) + if pd["ticks"] > self.short_window: + if pd["short_sma"] > pd["long_sma"] and not pd["invested"]: + signal = SignalEvent(pair, "market", "buy", event.time) self.events.put(signal) - self.invested = True - if self.short_sma < self.long_sma and self.invested: - signal = SignalEvent(self.pairs[0], "market", "sell", event.time) + pd["invested"] = True + if pd["short_sma"] < pd["long_sma"] and pd["invested"]: + signal = SignalEvent(pair, "market", "sell", event.time) self.events.put(signal) - self.invested = False - self.ticks += 1 \ No newline at end of file + pd["invested"] = False + pd["ticks"] += 1 \ No newline at end of file