From 4f99c06cce0492c01b686cd4af804ef39e68d696 Mon Sep 17 00:00:00 2001 From: Chris Morgan Date: Fri, 24 Jul 2015 14:03:53 +0100 Subject: [PATCH] autopep8 -i -r --- backtest/backtest.py | 8 +++-- backtest/output.py | 5 +-- data/price.py | 22 +++++++------ data/streaming.py | 19 +++++++----- event/event.py | 9 ++++-- examples/mac.py | 14 ++++----- execution/execution.py | 20 +++++++----- performance/performance.py | 12 +++---- portfolio/portfolio.py | 29 ++++++++--------- portfolio/portfolio_test.py | 37 +++++++++++----------- portfolio/position.py | 20 +++++++----- portfolio/position_test.py | 50 ++++++++++++++++++++---------- scripts/generate_simulated_pair.py | 20 ++++++------ scripts/test_performance.py | 6 ++-- settings.py | 2 +- strategy/strategy.py | 19 ++++++++---- trading/trading.py | 12 +++---- 17 files changed, 176 insertions(+), 128 deletions(-) diff --git a/backtest/backtest.py b/backtest/backtest.py index 249405c..ca9a002 100644 --- a/backtest/backtest.py +++ b/backtest/backtest.py @@ -10,14 +10,16 @@ class Backtest(object): + """ Enscapsulates the settings and components for carrying out an event-driven backtest on the foreign exchange markets. """ + def __init__( - self, pairs, data_handler, strategy, - strategy_params, portfolio, execution, - equity=100000.0, heartbeat=0.0, + self, pairs, data_handler, strategy, + strategy_params, portfolio, execution, + equity=100000.0, heartbeat=0.0, max_iters=10000000000 ): """ diff --git a/backtest/output.py b/backtest/output.py index 60d1823..85b5cfe 100644 --- a/backtest/output.py +++ b/backtest/output.py @@ -1,4 +1,5 @@ -import os, os.path +import os +import os.path import pandas as pd import matplotlib @@ -31,7 +32,7 @@ # Plot three charts: Equity curve, period returns, drawdowns fig = plt.figure() fig.patch.set_facecolor('white') # Set the outer colour to white - + # Plot the equity curve ax1 = fig.add_subplot(311, ylabel='Portfolio value') equity["Equity"].plot(ax=ax1, color=sns.color_palette()[0]) diff --git a/data/price.py b/data/price.py index ec0d32c..134464c 100644 --- a/data/price.py +++ b/data/price.py @@ -15,6 +15,7 @@ class PriceHandler(object): + """ PriceHandler is an abstract base class providing an interface for all subsequent (inherited) data handlers (both live and historic). @@ -42,14 +43,14 @@ def _set_up_prices_dict(self): be more robust and straightforward to follow. """ prices_dict = dict( - (k, v) for k,v in [ + (k, v) for k, v in [ (p, {"bid": None, "ask": None, "time": None}) for p in self.pairs ] ) inv_prices_dict = dict( - (k, v) for k,v in [ + (k, v) for k, v in [ ( - "%s%s" % (p[3:], p[:3]), + "%s%s" % (p[3:], p[:3]), {"bid": None, "ask": None, "time": None} ) for p in self.pairs ] @@ -65,16 +66,17 @@ def invert_prices(self, pair, bid, ask): """ getcontext().rounding = ROUND_HALF_DOWN inv_pair = "%s%s" % (pair[3:], pair[:3]) - inv_bid = (Decimal("1.0")/bid).quantize( + inv_bid = (Decimal("1.0") / bid).quantize( Decimal("0.00001") ) - inv_ask = (Decimal("1.0")/ask).quantize( + inv_ask = (Decimal("1.0") / ask).quantize( Decimal("0.00001") ) return inv_pair, inv_bid, inv_ask class HistoricCSVPriceHandler(PriceHandler): + """ HistoricCSVPriceHandler is designed to read CSV files of tick data for each requested currency pair and stream those @@ -129,7 +131,7 @@ def _open_convert_csv_files_for_day(self, date_str): """ Opens the CSV files from the data directory, converting them into pandas DataFrames within a pairs dictionary. - + The function then concatenates all of the separate pairs for a single day into a single data frame that is time ordered, allowing tick data events to be added to the queue @@ -138,7 +140,7 @@ def _open_convert_csv_files_for_day(self, date_str): for p in self.pairs: pair_path = os.path.join(self.csv_dir, '%s_%s.csv' % (p, date_str)) self.pair_frames[p] = pd.io.parsers.read_csv( - pair_path, header=True, index_col=0, + pair_path, header=True, index_col=0, parse_dates=True, dayfirst=True, names=("Time", "Ask", "Bid", "AskVolume", "BidVolume") ) @@ -147,7 +149,7 @@ def _open_convert_csv_files_for_day(self, date_str): def _update_csv_for_day(self): try: - dt = self.file_dates[self.cur_date_idx+1] + dt = self.file_dates[self.cur_date_idx + 1] except IndexError: # End of file dates return False else: @@ -173,10 +175,10 @@ def stream_next_tick(self): # End of the current days data if self._update_csv_for_day(): index, row = next(self.cur_date_pairs) - else: # End of the data + else: # End of the data self.continue_backtest = False return - + getcontext().rounding = ROUND_HALF_DOWN pair = row["Pair"] bid = Decimal(str(row["Bid"])).quantize( diff --git a/data/streaming.py b/data/streaming.py index 3abe7dd..6a55c0b 100644 --- a/data/streaming.py +++ b/data/streaming.py @@ -11,8 +11,9 @@ class StreamingForexPrices(PriceHandler): + def __init__( - self, domain, access_token, + self, domain, access_token, account_id, pairs, events_queue ): self.domain = domain @@ -31,10 +32,10 @@ def invert_prices(self, pair, bid, ask): """ getcontext().rounding = ROUND_HALF_DOWN inv_pair = "%s%s" % (pair[3:], pair[:3]) - inv_bid = (Decimal("1.0")/bid).quantize( + inv_bid = (Decimal("1.0") / bid).quantize( Decimal("0.00001") ) - inv_ask = (Decimal("1.0")/ask).quantize( + inv_ask = (Decimal("1.0") / ask).quantize( Decimal("0.00001") ) return inv_pair, inv_bid, inv_ask @@ -46,8 +47,8 @@ def connect_to_stream(self): requests.packages.urllib3.disable_warnings() s = requests.Session() url = "https://" + self.domain + "/v1/prices" - headers = {'Authorization' : 'Bearer ' + self.access_token} - params = {'instruments' : pair_list, 'accountId' : self.account_id} + headers = {'Authorization': 'Bearer ' + self.access_token} + params = {'instruments': pair_list, 'accountId': self.account_id} req = requests.Request('GET', url, headers=headers, params=params) pre = req.prepare() resp = s.send(pre, stream=True, verify=False) @@ -67,12 +68,13 @@ def stream_to_queue(self): msg = json.loads(dline) except Exception as e: self.logger.error( - "Caught exception when converting message into json: %s" % str(e) + "Caught exception when converting message into json: %s" % str( + e) ) return if "instrument" in msg or "tick" in msg: self.logger.debug(msg) - getcontext().rounding = ROUND_HALF_DOWN + getcontext().rounding = ROUND_HALF_DOWN instrument = msg["tick"]["instrument"].replace("_", "") time = msg["tick"]["time"] bid = Decimal(str(msg["tick"]["bid"])).quantize( @@ -84,7 +86,8 @@ def stream_to_queue(self): self.prices[instrument]["bid"] = bid self.prices[instrument]["ask"] = ask # Invert the prices (GBP_USD -> USD_GBP) - inv_pair, inv_bid, inv_ask = self.invert_prices(instrument, bid, ask) + inv_pair, inv_bid, inv_ask = self.invert_prices( + instrument, bid, ask) self.prices[inv_pair]["bid"] = inv_bid self.prices[inv_pair]["ask"] = inv_ask self.prices[inv_pair]["time"] = time diff --git a/event/event.py b/event/event.py index 0a7c118..3db1d7c 100644 --- a/event/event.py +++ b/event/event.py @@ -3,6 +3,7 @@ class Event(object): class TickEvent(Event): + def __init__(self, instrument, time, bid, ask): self.type = 'TICK' self.instrument = instrument @@ -12,7 +13,7 @@ def __init__(self, instrument, time, bid, ask): def __str__(self): return "Type: %s, Instrument: %s, Time: %s, Bid: %s, Ask: %s" % ( - str(self.type), str(self.instrument), + str(self.type), str(self.instrument), str(self.time), str(self.bid), str(self.ask) ) @@ -21,6 +22,7 @@ def __repr__(self): class SignalEvent(Event): + def __init__(self, instrument, order_type, side, time): self.type = 'SIGNAL' self.instrument = instrument @@ -30,7 +32,7 @@ def __init__(self, instrument, order_type, side, time): def __str__(self): return "Type: %s, Instrument: %s, Order Type: %s, Side: %s" % ( - str(self.type), str(self.instrument), + str(self.type), str(self.instrument), str(self.order_type), str(self.side) ) @@ -39,6 +41,7 @@ def __repr__(self): class OrderEvent(Event): + def __init__(self, instrument, units, order_type, side): self.type = 'ORDER' self.instrument = instrument @@ -53,4 +56,4 @@ def __str__(self): ) def __repr__(self): - return str(self) \ No newline at end of file + return str(self) diff --git a/examples/mac.py b/examples/mac.py index 6ebed7f..72f35ec 100644 --- a/examples/mac.py +++ b/examples/mac.py @@ -11,19 +11,19 @@ if __name__ == "__main__": # Trade on GBP/USD and EUR/USD pairs = ["GBPUSD", "EURUSD"] - + # Create the strategy parameters for the # MovingAverageCrossStrategy strategy_params = { - "short_window": 500, + "short_window": 500, "long_window": 2000 } - + # Create and execute the backtest backtest = Backtest( - pairs, HistoricCSVPriceHandler, - MovingAverageCrossStrategy, strategy_params, - Portfolio, SimulatedExecution, + pairs, HistoricCSVPriceHandler, + MovingAverageCrossStrategy, strategy_params, + Portfolio, SimulatedExecution, equity=settings.EQUITY ) - backtest.simulate_trading() \ No newline at end of file + backtest.simulate_trading() diff --git a/execution/execution.py b/execution/execution.py index 3197dc7..005f566 100644 --- a/execution/execution.py +++ b/execution/execution.py @@ -15,6 +15,7 @@ class ExecutionHandler(object): + """ Provides an abstract base class to handle all execution in the backtesting and live trading system. @@ -31,6 +32,7 @@ def execute_order(self): class SimulatedExecution(object): + """ Provides a simulated execution handling environment. This class actually does nothing - it simply receives an order to execute. @@ -38,11 +40,13 @@ class SimulatedExecution(object): Instead, the Portfolio object actually provides fill handling. This will be modified in later versions. """ + def execute_order(self, event): pass class OANDAExecutionHandler(ExecutionHandler): + def __init__(self, domain, access_token, account_id): self.domain = domain self.access_token = access_token @@ -60,16 +64,16 @@ def execute_order(self, event): "Authorization": "Bearer " + self.access_token } params = urlencode({ - "instrument" : instrument, - "units" : event.units, - "type" : event.order_type, - "side" : event.side + "instrument": instrument, + "units": event.units, + "type": event.order_type, + "side": event.side }) self.conn.request( - "POST", - "/v1/accounts/%s/orders" % str(self.account_id), + "POST", + "/v1/accounts/%s/orders" % str(self.account_id), params, headers ) - response = self.conn.getresponse().read().decode("utf-8").replace("\n","").replace("\t","") + response = self.conn.getresponse().read().decode( + "utf-8").replace("\n", "").replace("\t", "") self.logger.debug(response) - \ No newline at end of file diff --git a/performance/performance.py b/performance/performance.py index 89a6371..c3a5ea5 100644 --- a/performance/performance.py +++ b/performance/performance.py @@ -15,18 +15,18 @@ def create_drawdowns(pnl): drawdown, duration - Highest peak-to-trough drawdown and duration. """ - # Calculate the cumulative returns curve + # Calculate the cumulative returns curve # and set up the High Water Mark hwm = [0] # Create the drawdown and duration series idx = pnl.index - drawdown = pd.Series(index = idx) - duration = pd.Series(index = idx) + drawdown = pd.Series(index=idx) + duration = pd.Series(index=idx) # Loop over the index range for t in range(1, len(idx)): - hwm.append(max(hwm[t-1], pnl.ix[t])) - drawdown.ix[t]= (hwm[t]-pnl.ix[t]) - duration.ix[t]= (0 if drawdown.ix[t] == 0 else duration.ix[t-1]+1) + hwm.append(max(hwm[t - 1], pnl.ix[t])) + drawdown.ix[t] = (hwm[t] - pnl.ix[t]) + duration.ix[t] = (0 if drawdown.ix[t] == 0 else duration.ix[t - 1] + 1) return drawdown, drawdown.max(), duration.max() diff --git a/portfolio/portfolio.py b/portfolio/portfolio.py index c86aafb..4107953 100644 --- a/portfolio/portfolio.py +++ b/portfolio/portfolio.py @@ -14,9 +14,10 @@ class Portfolio(object): + def __init__( - self, ticker, events, home_currency="GBP", - leverage=20, equity=Decimal("100000.00"), + self, ticker, events, home_currency="GBP", + leverage=20, equity=Decimal("100000.00"), risk_per_trade=Decimal("0.02"), backtest=True ): self.ticker = ticker @@ -40,7 +41,7 @@ def add_new_position( self, position_type, currency_pair, units, ticker ): ps = Position( - self.home_currency, position_type, + self.home_currency, position_type, currency_pair, units, ticker ) self.positions[currency_pair] = ps @@ -85,12 +86,12 @@ def create_equity_file(self): return out_file def output_results(self): - # Closes off the Backtest.csv file so it can be + # Closes off the Backtest.csv file so it can be # read via Pandas without problems self.backtest_file.close() - + in_filename = "backtest.csv" - out_filename = "equity.csv" + out_filename = "equity.csv" in_file = os.path.join(OUTPUT_RESULTS_DIR, in_filename) out_file = os.path.join(OUTPUT_RESULTS_DIR, out_filename) @@ -99,13 +100,13 @@ def output_results(self): df.dropna(inplace=True) df["Total"] = df.sum(axis=1) df["Returns"] = df["Total"].pct_change() - df["Equity"] = (1.0+df["Returns"]).cumprod() - + df["Equity"] = (1.0 + df["Returns"]).cumprod() + # Create drawdown statistics drawdown, max_dd, dd_duration = create_drawdowns(df["Equity"]) df["Drawdown"] = drawdown df.to_csv(out_file, index=True) - + print("Simulation complete and results exported to %s" % out_filename) def update_portfolio(self, tick_event): @@ -144,7 +145,7 @@ def execute_signal(self, signal_event): currency_pair = signal_event.instrument units = int(self.trade_units) time = signal_event.time - + # If there is no position, create one if currency_pair not in self.positions: if side == "buy": @@ -152,7 +153,7 @@ def execute_signal(self, signal_event): else: position_type = "short" self.add_new_position( - position_type, currency_pair, + position_type, currency_pair, units, self.ticker ) @@ -180,7 +181,7 @@ def execute_signal(self, signal_event): return elif units > ps.units: return - + elif side == "sell" and ps.position_type == "short": add_position_units(currency_pair, units) @@ -189,5 +190,5 @@ def execute_signal(self, signal_event): self.logger.info("Portfolio Balance: %s" % self.balance) else: - self.logger.info("Unable to execute order as price data was insufficient.") - \ No newline at end of file + self.logger.info( + "Unable to execute order as price data was insufficient.") diff --git a/portfolio/portfolio_test.py b/portfolio/portfolio_test.py index e6b8602..c08d389 100644 --- a/portfolio/portfolio_test.py +++ b/portfolio/portfolio_test.py @@ -7,6 +7,7 @@ class TestPortfolio(unittest.TestCase): + def setUp(self): home_currency = "GBP" leverage = 20 @@ -15,8 +16,8 @@ def setUp(self): ticker = TickerMock() events = {} self.port = Portfolio( - ticker, events, home_currency=home_currency, - leverage=leverage, equity=equity, + ticker, events, home_currency=home_currency, + leverage=leverage, equity=equity, risk_per_trade=risk_per_trade ) @@ -26,8 +27,8 @@ def test_add_position_long(self): units = Decimal("2000") ticker = TickerMock() self.port.add_new_position( - position_type, - currency_pair, + position_type, + currency_pair, units, ticker ) ps = self.port.positions[currency_pair] @@ -44,8 +45,8 @@ def test_add_position_short(self): units = Decimal("2000") ticker = TickerMock() self.port.add_new_position( - position_type, - currency_pair, + position_type, + currency_pair, units, ticker ) ps = self.port.positions[currency_pair] @@ -71,7 +72,7 @@ def test_add_position_units_long(self): # Add a position and test for real position self.port.add_new_position( - position_type, + position_type, currency_pair, units, ticker ) @@ -103,7 +104,7 @@ def test_add_position_units_short(self): # Add a position and test for real position self.port.add_new_position( - position_type, + position_type, currency_pair, units, ticker ) @@ -135,7 +136,7 @@ def test_remove_position_units_long(self): # Add a position and then add units to it self.port.add_new_position( - position_type, + position_type, currency_pair, units, ticker ) @@ -145,7 +146,7 @@ def test_remove_position_units_long(self): ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928") ticker.prices["USDGBP"]["bid"] = Decimal("0.65842") ticker.prices["USDGBP"]["ask"] = Decimal("0.65821") - + add_units = Decimal("8000") apu = self.port.add_position_units( currency_pair, add_units @@ -166,7 +167,7 @@ def test_remove_position_units_long(self): self.assertTrue(rpu) self.assertEqual(ps.units, Decimal("7000")) self.assertEqual(self.port.balance, Decimal("100007.99")) - + def test_remove_position_units_short(self): position_type = "short" currency_pair = "GBPUSD" @@ -182,7 +183,7 @@ def test_remove_position_units_short(self): # Add a position and then add units to it self.port.add_new_position( - position_type, + position_type, currency_pair, units, ticker ) @@ -192,7 +193,7 @@ def test_remove_position_units_short(self): ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928") ticker.prices["USDGBP"]["bid"] = Decimal("0.65842") ticker.prices["USDGBP"]["ask"] = Decimal("0.65821") - + add_units = Decimal("8000") apu = self.port.add_position_units( currency_pair, add_units @@ -229,7 +230,7 @@ def test_close_position_long(self): # Add a position and then add units to it self.port.add_new_position( - position_type, + position_type, currency_pair, units, ticker ) @@ -239,7 +240,7 @@ def test_close_position_long(self): ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928") ticker.prices["USDGBP"]["bid"] = Decimal("0.65842") ticker.prices["USDGBP"]["ask"] = Decimal("0.65821") - + add_units = Decimal("8000") apu = self.port.add_position_units( currency_pair, add_units @@ -282,7 +283,7 @@ def test_close_position_short(self): # Add a position and then add units to it self.port.add_new_position( - position_type, + position_type, currency_pair, units, ticker ) @@ -292,7 +293,7 @@ def test_close_position_short(self): ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928") ticker.prices["USDGBP"]["bid"] = Decimal("0.65842") ticker.prices["USDGBP"]["ask"] = Decimal("0.65821") - + add_units = Decimal("8000") apu = self.port.add_position_units( currency_pair, add_units @@ -322,4 +323,4 @@ def test_close_position_short(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/portfolio/position.py b/portfolio/position.py index def814b..5926079 100644 --- a/portfolio/position.py +++ b/portfolio/position.py @@ -2,8 +2,9 @@ class Position(object): + def __init__( - self, home_currency, position_type, + self, home_currency, position_type, currency_pair, units, ticker ): self.home_currency = home_currency # Account denomination (e.g. GBP) @@ -16,15 +17,18 @@ def __init__( self.profit_perc = self.calculate_profit_perc() def set_up_currencies(self): - self.base_currency = self.currency_pair[:3] # For EUR/USD, this is EUR - self.quote_currency = self.currency_pair[3:] # For EUR/USD, this is USD + # For EUR/USD, this is EUR + self.base_currency = self.currency_pair[:3] + # For EUR/USD, this is USD + self.quote_currency = self.currency_pair[3:] # For EUR/USD, with account denominated in GBP, this is USD/GBP - self.quote_home_currency_pair = "%s%s" % (self.quote_currency, self.home_currency) + self.quote_home_currency_pair = "%s%s" % ( + self.quote_currency, self.home_currency) ticker_cur = self.ticker.prices[self.currency_pair] if self.position_type == "long": self.avg_price = Decimal(str(ticker_cur["ask"])) - self.cur_price = Decimal(str(ticker_cur["bid"])) + self.cur_price = Decimal(str(ticker_cur["bid"])) else: self.avg_price = Decimal(str(ticker_cur["bid"])) self.cur_price = Decimal(str(ticker_cur["ask"])) @@ -50,7 +54,7 @@ def calculate_profit_base(self): profit = pips * qh_close * self.units return profit.quantize( Decimal("0.00001"), ROUND_HALF_DOWN - ) + ) def calculate_profit_perc(self): return (self.profit_base / self.units * Decimal("100.00")).quantize( @@ -73,8 +77,8 @@ def add_units(self, units): else: add_price = cp["bid"] new_total_units = self.units + units - new_total_cost = self.avg_price*self.units + add_price*units - self.avg_price = new_total_cost/new_total_units + new_total_cost = self.avg_price * self.units + add_price * units + self.avg_price = new_total_cost / new_total_units self.units = new_total_units self.update_position_price() diff --git a/portfolio/position_test.py b/portfolio/position_test.py index f428a9b..9dd4135 100644 --- a/portfolio/position_test.py +++ b/portfolio/position_test.py @@ -5,6 +5,7 @@ class TickerMock(object): + """ A mock object that allows a representation of the ticker/pricing handler. @@ -19,16 +20,17 @@ def __init__(self): } - # ===================================== # GBP Home Currency with GBP/USD traded # ===================================== class TestLongGBPUSDPosition(unittest.TestCase): + """ Unit tests that cover going long GBP/USD with an account denominated currency of GBP, using 2,000 units of GBP/USD. """ + def setUp(self): home_currency = "GBP" position_type = "long" @@ -36,7 +38,7 @@ def setUp(self): units = Decimal("2000") ticker = TickerMock() self.position = Position( - home_currency, position_type, + home_currency, position_type, currency_pair, units, ticker ) @@ -58,8 +60,10 @@ def test_calculate_updated_values(self): pips, profit and percentage profit calculations are correct. """ prices = self.position.ticker.prices - prices["GBPUSD"] = {"bid": Decimal("1.50486"), "ask": Decimal("1.50586")} - prices["USDGBP"] = {"bid": Decimal("0.66451"), "ask": Decimal("0.66407")} + prices["GBPUSD"] = { + "bid": Decimal("1.50486"), "ask": Decimal("1.50586")} + prices["USDGBP"] = { + "bid": Decimal("0.66451"), "ask": Decimal("0.66407")} self.position.update_position_price() # Check pips @@ -74,10 +78,12 @@ def test_calculate_updated_values(self): class TestShortGBPUSDPosition(unittest.TestCase): + """ Unit tests that cover going short GBP/USD with an account denominated currency of GBP, using 2,000 units of GBP/USD. """ + def setUp(self): home_currency = "GBP" position_type = "short" @@ -85,7 +91,7 @@ def setUp(self): units = Decimal("2000") ticker = TickerMock() self.position = Position( - home_currency, position_type, + home_currency, position_type, currency_pair, units, ticker ) @@ -107,8 +113,10 @@ def test_calculate_updated_values(self): pips, profit and percentage profit calculations are correct. """ prices = self.position.ticker.prices - prices["GBPUSD"] = {"bid": Decimal("1.50486"), "ask": Decimal("1.50586")} - prices["USDGBP"] = {"bid": Decimal("0.66451"), "ask": Decimal("0.66407")} + prices["GBPUSD"] = { + "bid": Decimal("1.50486"), "ask": Decimal("1.50586")} + prices["USDGBP"] = { + "bid": Decimal("0.66451"), "ask": Decimal("0.66407")} self.position.update_position_price() # Check pips @@ -127,10 +135,12 @@ def test_calculate_updated_values(self): # ===================================== class TestLongEURUSDPosition(unittest.TestCase): + """ Unit tests that cover going long EUR/USD with an account denominated currency of GBP, using 2,000 units of EUR/USD. """ + def setUp(self): home_currency = "GBP" position_type = "long" @@ -138,7 +148,7 @@ def setUp(self): units = Decimal("2000") ticker = TickerMock() self.position = Position( - home_currency, position_type, + home_currency, position_type, currency_pair, units, ticker ) @@ -160,9 +170,12 @@ def test_calculate_updated_values(self): pips, profit and percentage profit calculations are correct. """ prices = self.position.ticker.prices - prices["GBPUSD"] = {"bid": Decimal("1.50486"), "ask": Decimal("1.50586")} - prices["USDGBP"] = {"bid": Decimal("0.66451"), "ask": Decimal("0.66407")} - prices["EURUSD"] = {"bid": Decimal("1.07811"), "ask": Decimal("1.07827")} + prices["GBPUSD"] = { + "bid": Decimal("1.50486"), "ask": Decimal("1.50586")} + prices["USDGBP"] = { + "bid": Decimal("0.66451"), "ask": Decimal("0.66407")} + prices["EURUSD"] = { + "bid": Decimal("1.07811"), "ask": Decimal("1.07827")} self.position.update_position_price() # Check pips @@ -177,10 +190,12 @@ def test_calculate_updated_values(self): class TestLongEURUSDPosition(unittest.TestCase): + """ Unit tests that cover going short EUR/USD with an account denominated currency of GBP, using 2,000 units of EUR/USD. """ + def setUp(self): home_currency = "GBP" position_type = "short" @@ -188,7 +203,7 @@ def setUp(self): units = Decimal("2000") ticker = TickerMock() self.position = Position( - home_currency, position_type, + home_currency, position_type, currency_pair, units, ticker ) @@ -210,9 +225,12 @@ def test_calculate_updated_values(self): pips, profit and percentage profit calculations are correct. """ prices = self.position.ticker.prices - prices["GBPUSD"] = {"bid": Decimal("1.50486"), "ask": Decimal("1.50586")} - prices["USDGBP"] = {"bid": Decimal("0.66451"), "ask": Decimal("0.66407")} - prices["EURUSD"] = {"bid": Decimal("1.07811"), "ask": Decimal("1.07827")} + prices["GBPUSD"] = { + "bid": Decimal("1.50486"), "ask": Decimal("1.50586")} + prices["USDGBP"] = { + "bid": Decimal("0.66451"), "ask": Decimal("0.66407")} + prices["EURUSD"] = { + "bid": Decimal("1.07811"), "ask": Decimal("1.07827")} self.position.update_position_price() # Check pips @@ -227,4 +245,4 @@ def test_calculate_updated_values(self): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/scripts/generate_simulated_pair.py b/scripts/generate_simulated_pair.py index d9623ae..947c7bf 100644 --- a/scripts/generate_simulated_pair.py +++ b/scripts/generate_simulated_pair.py @@ -3,7 +3,8 @@ import calendar import copy import datetime -import os, os.path +import os +import os.path import sys import numpy as np @@ -19,7 +20,7 @@ def month_weekdays(year_int, month_int): """ cal = calendar.Calendar() return [ - d for d in cal.itermonthdates(year_int, month_int) + d for d in cal.itermonthdates(year_int, month_int) if d.weekday() < 5 and d.year == year_int ] @@ -28,7 +29,8 @@ def month_weekdays(year_int, month_int): try: pair = sys.argv[1] except IndexError: - print("You need to enter a currency pair, e.g. GBPUSD, as a command line parameter.") + print( + "You need to enter a currency pair, e.g. GBPUSD, as a command line parameter.") else: np.random.seed(42) # Fix the randomness @@ -50,14 +52,14 @@ def month_weekdays(year_int, month_int): current_time = current_time.replace(day=d.day) outfile = open( os.path.join( - settings.CSV_DATA_DIR, + settings.CSV_DATA_DIR, "%s_%s.csv" % ( pair, d.strftime("%Y%m%d") ) - ), - "w") - outfile.write("Time,Ask,Bid,AskVolume,BidVolume\n") - + ), + "w") + outfile.write("Time,Ask,Bid,AskVolume,BidVolume\n") + # Create the random walk for the bid/ask prices # with fixed spread between them while True: @@ -73,7 +75,7 @@ def month_weekdays(year_int, month_int): ask_volume = 1.0 + np.random.uniform(0.0, 2.0) bid_volume = 1.0 + np.random.uniform(0.0, 2.0) line = "%s,%s,%s,%s,%s\n" % ( - current_time.strftime("%d.%m.%Y %H:%M:%S.%f")[:-3], + current_time.strftime("%d.%m.%Y %H:%M:%S.%f")[:-3], "%0.5f" % ask, "%0.5f" % bid, "%0.2f00" % ask_volume, "%0.2f00" % bid_volume ) diff --git a/scripts/test_performance.py b/scripts/test_performance.py index 7aa1a63..76e89af 100644 --- a/scripts/test_performance.py +++ b/scripts/test_performance.py @@ -18,7 +18,7 @@ if __name__ == "__main__": in_filename = "backtest.csv" - out_filename = "equity.csv" + out_filename = "equity.csv" in_file = os.path.join(OUTPUT_RESULTS_DIR, in_filename) out_file = os.path.join(OUTPUT_RESULTS_DIR, out_filename) @@ -27,9 +27,9 @@ df.dropna(inplace=True) df["Total"] = df.sum(axis=1) df["Returns"] = df["Total"].pct_change() - df["Equity"] = (1.0+df["Returns"]).cumprod() + df["Equity"] = (1.0 + df["Returns"]).cumprod() # Create drawdown statistics drawdown, max_dd, dd_duration = create_drawdowns(df["Equity"]) df["Drawdown"] = drawdown - df.to_csv(out_file, index=True) \ No newline at end of file + df.to_csv(out_file, index=True) diff --git a/settings.py b/settings.py index bfd0e78..01c78a6 100644 --- a/settings.py +++ b/settings.py @@ -2,7 +2,7 @@ import os -ENVIRONMENTS = { +ENVIRONMENTS = { "streaming": { "real": "stream-fxtrade.oanda.com", "practice": "stream-fxpractice.oanda.com", diff --git a/strategy/strategy.py b/strategy/strategy.py index 248e592..bc82b45 100644 --- a/strategy/strategy.py +++ b/strategy/strategy.py @@ -4,6 +4,7 @@ class TestStrategy(object): + """ A testing strategy that alternates between buying and selling a currency pair on every 5th tick. This has the effect of @@ -13,6 +14,7 @@ class TestStrategy(object): It is used to test that the backtester/live trading system is behaving as expected. """ + def __init__(self, pairs, events): self.pairs = pairs self.events = events @@ -23,17 +25,20 @@ def calculate_signals(self, event): if event.type == 'TICK' and event.instrument == self.pairs[0]: if self.ticks % 5 == 0: if self.invested == False: - signal = SignalEvent(self.pairs[0], "market", "buy", event.time) + signal = SignalEvent( + self.pairs[0], "market", "buy", event.time) self.events.put(signal) self.invested = True else: - signal = SignalEvent(self.pairs[0], "market", "sell", event.time) + signal = SignalEvent( + self.pairs[0], "market", "sell", event.time) self.events.put(signal) self.invested = False self.ticks += 1 class MovingAverageCrossStrategy(object): + """ A basic Moving Average Crossover strategy that generates two simple moving averages (SMA), with default windows @@ -49,13 +54,14 @@ class MovingAverageCrossStrategy(object): increase efficiency by eliminating the need to call two full moving average calculations on each tick. """ + def __init__( - self, pairs, events, + self, pairs, events, short_window=500, long_window=2000 ): self.pairs = pairs self.pairs_dict = self.create_pairs_dict() - self.events = events + self.events = events self.short_window = short_window self.long_window = long_window @@ -89,7 +95,8 @@ def calculate_signals(self, event): 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 + # Only start the strategy when we have created an accurate short + # window if pd["ticks"] > self.short_window: if pd["short_sma"] > pd["long_sma"] and not pd["invested"]: signal = SignalEvent(pair, "market", "buy", event.time) @@ -99,4 +106,4 @@ def calculate_signals(self, event): signal = SignalEvent(pair, "market", "sell", event.time) self.events.put(signal) pd["invested"] = False - pd["ticks"] += 1 \ No newline at end of file + pd["ticks"] += 1 diff --git a/trading/trading.py b/trading/trading.py index e418896..95137e7 100644 --- a/trading/trading.py +++ b/trading/trading.py @@ -62,11 +62,11 @@ def trade(events, strategy, portfolio, execution, heartbeat): # Create the OANDA market price streaming class # making sure to provide authentication commands prices = StreamingForexPrices( - settings.STREAM_DOMAIN, settings.ACCESS_TOKEN, + settings.STREAM_DOMAIN, settings.ACCESS_TOKEN, settings.ACCOUNT_ID, pairs, events ) - # Create the strategy/signal generator, passing the + # Create the strategy/signal generator, passing the # instrument and the events queue strategy = TestStrategy(pairs, events) @@ -80,11 +80,11 @@ def trade(events, strategy, portfolio, execution, heartbeat): # Create the execution handler making sure to # provide authentication commands execution = OANDAExecutionHandler( - settings.API_DOMAIN, - settings.ACCESS_TOKEN, + settings.API_DOMAIN, + settings.ACCESS_TOKEN, settings.ACCOUNT_ID ) - + # Create two separate threads: One for the trading loop # and another for the market price streaming class trade_thread = threading.Thread( @@ -93,7 +93,7 @@ def trade(events, strategy, portfolio, execution, heartbeat): ) ) price_thread = threading.Thread(target=prices.stream_to_queue, args=[]) - + # Start both threads logger.info("Starting trading thread") trade_thread.start()