Lots of changes. Modified the Position object to handle more of the actual position calculations instead of the Portfolio. Added more unit tests for both Position and Portfolio. Allowed Positions to trade in currencies other than GBPUSD and in base/quotes which aren't the home currency. Modified the backtester to be single-threaded and added a basic Moving Average Crossover strategy. Also added a basic equity curve output script.

This commit is contained in:
Michael Halls-Moore
2015-04-21 13:01:20 +01:00
parent e74777802b
commit e84512e1e7
11 changed files with 700 additions and 376 deletions
+20 -23
View File
@@ -7,19 +7,25 @@ from decimal import Decimal, getcontext
from qsforex.execution.execution import SimulatedExecution
from qsforex.portfolio.portfolio import Portfolio
from qsforex import settings
from qsforex.strategy.strategy import TestStrategy
from qsforex.strategy.strategy import TestStrategy, MovingAverageCrossStrategy
from qsforex.data.price import HistoricCSVPriceHandler
def trade(events, strategy, portfolio, execution, heartbeat):
def backtest(
events, ticker, strategy, portfolio,
execution, heartbeat, max_iters=200000
):
"""
Carries out an infinite while loop that polls the
events queue and directs each event to either the
strategy component of the execution handler. The
loop will then pause for "heartbeat" seconds and
continue.
continue unti the maximum number of iterations is
exceeded.
"""
while True:
iters = 0
while True and iters < max_iters:
ticker.stream_next_tick()
try:
event = events.get(False)
except Queue.Empty:
@@ -33,13 +39,12 @@ def trade(events, strategy, portfolio, execution, heartbeat):
elif event.type == 'ORDER':
execution.execute_order(event)
time.sleep(heartbeat)
iters += 1
portfolio.output_results()
if __name__ == "__main__":
# Set the number of decimal places to 2
getcontext().prec = 2
heartbeat = 0.0 # Half a second between polling
heartbeat = 0.0
events = Queue.Queue()
equity = settings.EQUITY
@@ -51,27 +56,19 @@ if __name__ == "__main__":
sys.exit()
# Create the historic tick data streaming class
prices = HistoricCSVPriceHandler(pairs, events, csv_dir)
ticker = HistoricCSVPriceHandler(pairs, events, csv_dir)
# Create the strategy/signal generator, passing the
# instrument and the events queue
strategy = TestStrategy(pairs[0], events)
strategy = MovingAverageCrossStrategy(
pairs, events, 500, 2000
)
# Create the portfolio object to track trades
portfolio = Portfolio(prices, events, equity=equity)
portfolio = Portfolio(ticker, events, equity=equity)
# Create the simulated execution handler
execution = SimulatedExecution()
# Create two separate threads: One for the trading loop
# and another for the market price streaming class
trade_thread = threading.Thread(
target=trade, args=(
events, strategy, portfolio, execution, heartbeat
)
)
price_thread = threading.Thread(target=prices.stream_to_queue, args=[])
# Start both threads
trade_thread.start()
price_thread.start()
# Carry out the backtest loop
backtest(events, ticker, strategy, portfolio, execution, heartbeat)
+24
View File
@@ -0,0 +1,24 @@
import os, os.path
import pandas as pd
import matplotlib.pyplot as plt
from qsforex.settings import OUTPUT_RESULTS_DIR
if __name__ == "__main__":
"""
A simple script to plot the balance of the portfolio, or
"equity curve", as a function of time.
It requires OUTPUT_RESULTS_DIR to be set in the project
settings.
"""
equity_file = os.path.join(OUTPUT_RESULTS_DIR, "equity.csv")
equity = pd.io.parsers.read_csv(
equity_file, header=True,
names=["time", "balance"],
parse_dates=True, index_col=0
)
equity["balance"].plot()
plt.show()
+104 -11
View File
@@ -3,6 +3,8 @@ import datetime
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
import os
import os.path
import time
import numpy as np
import pandas as pd
@@ -59,28 +61,119 @@ class HistoricCSVPriceHandler(PriceHandler):
self.pairs = pairs
self.events_queue = events_queue
self.csv_dir = csv_dir
self.cur_bid = None
self.cur_ask = None
self.prices = self._set_up_prices_dict()
self.pair_frames = {}
self._open_convert_csv_files()
def _set_up_prices_dict(self):
"""
Due to the way that the Position object handles P&L
calculation, it is necessary to include values for not
only base/quote currencies but also their reciprocals.
This means that this class will contain keys for, e.g.
"GBPUSD" and "USDGBP".
At this stage they are calculated in an ad-hoc manner,
but a future TODO is to modify the following code to
be more robust and straightforward to follow.
"""
prices_dict = dict(
(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 [
(
"%s%s" % (p[3:], p[:3]),
{"bid": None, "ask": None, "time": None}
) for p in self.pairs
]
)
prices_dict.update(inv_prices_dict)
return prices_dict
def _open_convert_csv_files(self):
"""
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
in a chronological fashion.
"""
pair_path = os.path.join(self.csv_dir, '%s.csv' % self.pairs[0])
self.pair = pd.io.parsers.read_csv(
pair_path, header=True, index_col=0, parse_dates=True,
names=("Time", "Ask", "Bid", "AskVolume", "BidVolume")
).iterrows()
for p in self.pairs:
pair_path = os.path.join(self.csv_dir, '%s.csv' % p)
self.pair_frames[p] = pd.io.parsers.read_csv(
pair_path, header=True, index_col=0, parse_dates=True,
names=("Time", "Ask", "Bid", "AskVolume", "BidVolume")
)
self.pair_frames[p]["Pair"] = p
self.all_pairs = pd.concat(self.pair_frames.values()).sort().iterrows()
def invert_prices(self, row):
"""
Simply inverts the prices for a particular currency pair.
This will turn the bid/ask of "GBPUSD" into bid/ask for
"USDGBP" and place them in the prices dictionary.
"""
pair = row["Pair"]
bid = row["Bid"]
ask = row["Ask"]
inv_pair = "%s%s" % (pair[3:], pair[:3])
inv_bid = Decimal(str(1.0/bid)).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
inv_ask = Decimal(str(1.0/ask)).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
return inv_pair, inv_bid, inv_ask
def stream_next_tick(self):
"""
The Backtester has now moved over to a single-threaded
model in order to fully reproduce results on each run.
This means that the stream_to_queue method is unable to
be used and a replacement, called stream_next_tick, is
used instead.
This method is called by the backtesting function outside
of this class and places a single tick onto the queue, as
well as updating the current bid/ask and inverse bid/ask.
"""
try:
index, row = self.all_pairs.next()
except StopIteration:
return
else:
self.prices[row["Pair"]]["bid"] = Decimal(str(row["Bid"])).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
self.prices[row["Pair"]]["ask"] = Decimal(str(row["Ask"])).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
self.prices[row["Pair"]]["time"] = index
inv_pair, inv_bid, inv_ask = self.invert_prices(row)
self.prices[inv_pair]["bid"] = inv_bid
self.prices[inv_pair]["ask"] = inv_ask
self.prices[inv_pair]["time"] = index
tev = TickEvent(row["Pair"], index, row["Bid"], row["Ask"])
self.events_queue.put(tev)
def stream_to_queue(self):
self._open_convert_csv_files()
for index, row in self.pair:
self.cur_bid = Decimal(str(row["Bid"])).quantize(
for index, row in self.all_pairs:
self.prices[row["Pair"]]["bid"] = Decimal(str(row["Bid"])).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
self.cur_ask = Decimal(str(row["Ask"])).quantize(
self.prices[row["Pair"]]["ask"] = Decimal(str(row["Ask"])).quantize(
Decimal("0.00001", ROUND_HALF_DOWN)
)
tev = TickEvent(self.pairs[0], index, row["Bid"], row["Ask"])
self.prices[row["Pair"]]["time"] = index
inv_pair, inv_bid, inv_ask = self.invert_prices(row)
self.prices[inv_pair]["bid"] = inv_bid
self.prices[inv_pair]["ask"] = inv_ask
self.prices[inv_pair]["time"] = index
tev = TickEvent(row["Pair"], index, row["Bid"], row["Ask"])
self.events_queue.put(tev)
+2 -1
View File
@@ -12,11 +12,12 @@ class TickEvent(Event):
class SignalEvent(Event):
def __init__(self, instrument, order_type, side):
def __init__(self, instrument, order_type, side, time):
self.type = 'SIGNAL'
self.instrument = instrument
self.order_type = order_type
self.side = side
self.time = time # Time of the last tick that generated the signal
class OrderEvent(Event):
+51 -66
View File
@@ -1,123 +1,107 @@
from copy import deepcopy
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
import os
import pandas as pd
from qsforex.event.event import OrderEvent
from qsforex.portfolio.position import Position
from qsforex.settings import OUTPUT_RESULTS_DIR
class Portfolio(object):
def __init__(
self, ticker, events, base="GBP", leverage=20,
self, ticker, events, home_currency="GBP", leverage=20,
equity=Decimal("100000.00"), risk_per_trade=Decimal("0.02")
):
self.ticker = ticker
self.events = events
self.base = base
self.home_currency = home_currency
self.leverage = leverage
self.equity = equity
self.balance = deepcopy(self.equity)
self.risk_per_trade = risk_per_trade
self.trade_units = self.calc_risk_position_size()
self.positions = {}
self.equity = []
def calc_risk_position_size(self):
return self.equity * self.risk_per_trade
def add_new_position(
self, position_type, market, units,
exposure, bid, ask
self, position_type, currency_pair, units, ticker
):
ps = Position(
position_type, market, units,
exposure, bid, ask
self.home_currency, position_type,
currency_pair, units, ticker
)
self.positions[market] = ps
self.positions[currency_pair] = ps
def add_position_units(
self, market, units,
exposure, bid, ask
):
if market not in self.positions:
def add_position_units(self, currency_pair, units):
if currency_pair not in self.positions:
return False
else:
ps = self.positions[market]
if ps.position_type == "long":
add_price = ask
else:
add_price = bid
new_total_units = ps.units + units
new_total_cost = ps.avg_price*ps.units + add_price*units
ps.exposure += exposure
ps.avg_price = new_total_cost/new_total_units
ps.units = new_total_units
ps.update_position_price(bid, ask, exposure)
ps = self.positions[currency_pair]
ps.add_units(units)
return True
def remove_position_units(
self, market, units, bid, ask
):
if market not in self.positions:
def remove_position_units(self, currency_pair, units):
if currency_pair not in self.positions:
return False
else:
ps = self.positions[market]
if ps.position_type == "long":
remove_price = bid
else:
remove_price = ask
ps.units -= units
exposure = Decimal(str(units))
ps.exposure -= exposure
ps.update_position_price(bid, ask, exposure)
pnl = ps.calculate_pips() * exposure / remove_price
self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
ps = self.positions[currency_pair]
pnl = ps.remove_units(units)
self.balance += pnl
return True
def close_position(
self, market, bid, ask
):
if market not in self.positions:
def close_position(self, currency_pair):
if currency_pair not in self.positions:
return False
else:
ps = self.positions[market]
ps.update_position_price(bid, ask, ps.exposure)
if ps.position_type == "long":
remove_price = bid
else:
remove_price = ask
pnl = ps.calculate_pips() * ps.exposure / remove_price
self.balance += pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
del[self.positions[market]]
ps = self.positions[currency_pair]
pnl = ps.close_position()
self.balance += pnl
del[self.positions[currency_pair]]
return True
def append_equity_row(self, time, balance):
d = {"time": time, "balance": balance}
self.equity.append(d)
def output_results(self):
filename = "equity.csv"
out_file = os.path.join(OUTPUT_RESULTS_DIR, filename)
df_equity = pd.DataFrame.from_records(self.equity, index='time')
df_equity.to_csv(out_file)
print "Simulation complete and results exported to %s" % filename
def execute_signal(self, signal_event):
side = signal_event.side
market = signal_event.instrument
currency_pair = signal_event.instrument
units = int(self.trade_units)
exposure = Decimal(str(units))
bid = Decimal(str(self.ticker.cur_bid))
ask = Decimal(str(self.ticker.cur_ask))
time = signal_event.time
# If there is no position, create one
if market not in self.positions:
if currency_pair not in self.positions:
if side == "buy":
position_type = "long"
else:
position_type = "short"
self.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type, currency_pair,
units, self.ticker
)
# If a position exists add or remove units
else:
ps = self.positions[market]
ps = self.positions[currency_pair]
if side == "buy" and ps.position_type == "long":
add_position_units(market, units, exposure, bid, ask)
add_position_units(currency_pair, units)
elif side == "sell" and ps.position_type == "long":
if units == ps.units:
self.close_position(market, bid, ask)
self.close_position(currency_pair)
# TODO: Allow units to be added/removed
elif units < ps.units:
return
@@ -126,7 +110,7 @@ class Portfolio(object):
elif side == "buy" and ps.position_type == "short":
if units == ps.units:
self.close_position(market, bid, ask)
self.close_position(currency_pair)
# TODO: Allow units to be added/removed
elif units < ps.units:
return
@@ -134,9 +118,10 @@ class Portfolio(object):
return
elif side == "sell" and ps.position_type == "short":
add_position_units(market, units, exposure, bid, ask)
add_position_units(currency_pair, units)
order = OrderEvent(market, units, "market", side)
order = OrderEvent(currency_pair, units, "market", side)
self.events.put(order)
print "Balance: %0.2f" % self.balance
print "Balance: %0.2f" % self.balance
self.append_equity_row(time, self.balance)
+173 -212
View File
@@ -1,364 +1,325 @@
from decimal import Decimal, getcontext, ROUND_HALF_DOWN
import unittest
from portfolio import Portfolio
from qsforex.portfolio.portfolio import Portfolio
from qsforex.portfolio.position_test import TickerMock
from qsforex.portfolio.position import Position
class TestPortfolio(unittest.TestCase):
def setUp(self):
base = "GBP"
home_currency = "GBP"
leverage = 20
equity = Decimal("100000.00")
risk_per_trade = Decimal("0.02")
ticker = {}
ticker = TickerMock()
events = {}
self.port = Portfolio(
ticker, events, base=base, leverage=leverage,
equity=equity, risk_per_trade=risk_per_trade
ticker, events, home_currency=home_currency,
leverage=leverage, equity=equity,
risk_per_trade=risk_per_trade
)
def test_add_position_long(self):
position_type = "long"
market = "GBP/USD"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
ps = self.port.positions[currency_pair]
self.assertEquals(ps.position_type, position_type)
self.assertEquals(ps.market, market)
self.assertEquals(ps.currency_pair, currency_pair)
self.assertEquals(ps.units, units)
self.assertEquals(ps.exposure, exposure)
self.assertEquals(ps.avg_price, ask)
self.assertEquals(ps.cur_price, bid)
self.assertEquals(ps.avg_price, ticker.prices[currency_pair]["ask"])
self.assertEquals(ps.cur_price, ticker.prices[currency_pair]["bid"])
def test_add_position_short(self):
position_type = "short"
market = "GBP/USD"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
ps = self.port.positions[currency_pair]
self.assertEquals(ps.position_type, position_type)
self.assertEquals(ps.market, market)
self.assertEquals(ps.currency_pair, currency_pair)
self.assertEquals(ps.units, units)
self.assertEquals(ps.exposure, exposure)
self.assertEquals(ps.avg_price, bid)
self.assertEquals(ps.cur_price, ask)
self.assertEquals(ps.avg_price, ticker.prices[currency_pair]["bid"])
self.assertEquals(ps.cur_price, ticker.prices[currency_pair]["ask"])
def test_add_position_units_long(self):
position_type = "long"
market = "GBP/USD"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
alt_currency_pair = "USDCAD"
apu = self.port.add_position_units(
market, units, exposure,
bid, ask
alt_currency_pair, units
)
self.assertFalse(apu)
# Add a position and test for real position
market = "GBP/USD"
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
ps = self.port.positions[currency_pair]
# Test for addition of units
bid = Decimal("1.51878")
ask = Decimal("1.51928")
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65842")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65821")
apu = self.port.add_position_units(
market, units, exposure,
bid, ask
currency_pair, units
)
self.assertTrue(apu)
self.assertEqual(ps.avg_price, Decimal("1.518735"))
self.assertEqual(ps.avg_price, Decimal("1.511385"))
def test_add_position_units_short(self):
position_type = "short"
market = "GBP/USD"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
alt_currency_pair = "USDCAD"
apu = self.port.add_position_units(
market, units, exposure,
bid, ask
alt_currency_pair, units
)
self.assertFalse(apu)
# Add a position and test for real position
market = "GBP/USD"
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
ps = self.port.positions[currency_pair]
# Test for addition of units
bid = Decimal("1.51878")
ask = Decimal("1.51928")
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.51928")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65842")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65821")
apu = self.port.add_position_units(
market, units, exposure,
bid, ask
currency_pair, units
)
self.assertTrue(apu)
self.assertEqual(ps.avg_price, Decimal("1.51824"))
self.assertEqual(ps.avg_price, Decimal("1.51103"))
def test_remove_position_units_long(self):
position_type = "long"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
alt_currency_pair = "USDCAD"
apu = self.port.remove_position_units(
market, units, bid, ask
alt_currency_pair, units
)
self.assertFalse(apu)
# Add a position and then add units to it
market = "GBP/USD"
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
bid = Decimal("1.51878")
ask = Decimal("1.51928")
add_units = 8000
add_exposure = Decimal(str(add_units))
ps = self.port.positions[currency_pair]
# Test for addition of units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
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(
market, add_units, add_exposure,
bid, ask
currency_pair, add_units
)
self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.519062"))
self.assertEqual(ps.avg_price, Decimal("1.516122"))
# Test removal of (some) of the units
bid = Decimal("1.52017")
ask = Decimal("1.52134")
remove_units = 3000
ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units(
market, remove_units, bid, ask
currency_pair, remove_units
)
self.assertTrue(rpu)
self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("2.19054"))
self.assertEqual(self.port.balance, Decimal("100002.19"))
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"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
alt_currency_pair = "USDCAD"
apu = self.port.remove_position_units(
market, units, bid, ask
alt_currency_pair, units
)
self.assertFalse(apu)
# Add a position and then add units to it
market = "GBP/USD"
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
bid = Decimal("1.51878")
ask = Decimal("1.51928")
add_units = 8000
add_exposure = Decimal(str(add_units))
ps = self.port.positions[currency_pair]
# Test for addition of units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
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(
market, add_units, add_exposure,
bid, ask
currency_pair, add_units
)
self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.518564"))
self.assertEqual(ps.avg_price, Decimal("1.51568"))
# Test removal of (some) of the units
bid = Decimal("1.52017")
ask = Decimal("1.52134")
remove_units = 3000
ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units(
market, remove_units, bid, ask
currency_pair, remove_units
)
self.assertTrue(rpu)
self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("-5.48201"))
self.assertEqual(self.port.balance, Decimal("99994.52"))
self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(self.port.balance, Decimal("99988.84"))
def test_close_position_long(self):
position_type = "long"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
cp = self.port.close_position(
market, bid, ask
alt_currency_pair = "USDCAD"
apu = self.port.remove_position_units(
alt_currency_pair, units
)
self.assertFalse(cp)
self.assertFalse(apu)
# Add a position and then close it
# Will lose money on the spread
market = "GBP/USD"
# Add a position and then add units to it
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
cp = self.port.close_position(
market, bid, ask
)
self.assertTrue(cp)
self.assertRaises(ps) # Key doesn't exist
self.assertEqual(self.port.balance, Decimal("99999.35"))
# Add 2000, add another 8000, remove 3000 and then
# close the position. Balance should be as expected
# for a multi-leg transaction.
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
)
ps = self.port.positions[market]
# Add 8000 units
bid = Decimal("1.51878")
ask = Decimal("1.51928")
add_units = 8000
add_exposure = Decimal(str(add_units))
ps = self.port.positions[currency_pair]
# Test for addition of units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
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(
market, add_units,
add_exposure, bid, ask
currency_pair, add_units
)
self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.519062"))
self.assertEqual(ps.avg_price, Decimal("1.516122"))
# Remove 3000 units
bid = Decimal("1.52017")
ask = Decimal("1.52134")
remove_units = 3000
# Test removal of (some) of the units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units(
market, remove_units, bid, ask
currency_pair, remove_units
)
self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("2.19054"))
self.assertEqual(self.port.balance, Decimal("100001.54"))
self.assertTrue(rpu)
self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(self.port.balance, Decimal("100007.99"))
# Close the position
cp = self.port.close_position(
market, bid, ask
)
cp = self.port.close_position(currency_pair)
self.assertTrue(cp)
self.assertRaises(ps) # Key doesn't exist
self.assertEqual(self.port.balance, Decimal("100006.65"))
self.assertEqual(self.port.balance, Decimal("100026.64"))
def test_close_position_short(self):
position_type = "short"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.51770")
ask = Decimal("1.51819")
ticker = TickerMock()
# Test for no position
market = "EUR/USD"
cp = self.port.close_position(
market, bid, ask
alt_currency_pair = "USDCAD"
apu = self.port.remove_position_units(
alt_currency_pair, units
)
self.assertFalse(cp)
self.assertFalse(apu)
# Add a position and then close it
# Will lose money on the spread
market = "GBP/USD"
# Add a position and then add units to it
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
position_type,
currency_pair,
units, ticker
)
ps = self.port.positions[market]
cp = self.port.close_position(
market, bid, ask
)
self.assertTrue(cp)
self.assertRaises(ps) # Key doesn't exist
self.assertEqual(self.port.balance, Decimal("99999.35"))
# Add 2000, add another 8000, remove 3000 and then
# close the position. Balance should be as expected
# for a multi-leg transaction.
self.port.add_new_position(
position_type, market, units,
exposure, bid, ask
)
ps = self.port.positions[market]
# Add 8000 units
bid = Decimal("1.51878")
ask = Decimal("1.51928")
add_units = 8000
add_exposure = Decimal(str(add_units))
ps = self.port.positions[currency_pair]
# Test for addition of units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.51878")
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(
market, add_units,
add_exposure, bid, ask
currency_pair, add_units
)
self.assertEqual(ps.units, 10000)
self.assertEqual(ps.exposure, Decimal("10000.00"))
self.assertEqual(ps.avg_price, Decimal("1.518564"))
self.assertEqual(ps.avg_price, Decimal("1.51568"))
# Remove 3000 units
bid = Decimal("1.52017")
ask = Decimal("1.52134")
remove_units = 3000
# Test removal of (some) of the units
ticker.prices["GBPUSD"]["bid"] = Decimal("1.52017")
ticker.prices["GBPUSD"]["ask"] = Decimal("1.52134")
ticker.prices["USDGBP"]["bid"] = Decimal("0.65782")
ticker.prices["USDGBP"]["ask"] = Decimal("0.65732")
remove_units = Decimal("3000")
rpu = self.port.remove_position_units(
market, remove_units, bid, ask
currency_pair, remove_units
)
self.assertEqual(ps.units, 7000)
self.assertEqual(ps.exposure, Decimal("7000.00"))
self.assertEqual(ps.profit_base, Decimal("-5.48201"))
self.assertEqual(self.port.balance, Decimal("99993.87"))
self.assertTrue(rpu)
self.assertEqual(ps.units, Decimal("7000"))
self.assertEqual(self.port.balance, Decimal("99988.84"))
# Close the position
cp = self.port.close_position(
market, bid, ask
)
cp = self.port.close_position(currency_pair)
self.assertTrue(cp)
self.assertRaises(ps) # Key doesn't exist
self.assertEqual(self.port.balance, Decimal("99981.08"))
self.assertEqual(self.port.balance, Decimal("99962.80"))
if __name__ == "__main__":
unittest.main()
+86 -30
View File
@@ -3,51 +3,107 @@ from decimal import Decimal, getcontext, ROUND_HALF_DOWN
class Position(object):
def __init__(
self, position_type, market,
units, exposure, bid, ask
self, home_currency, position_type,
currency_pair, units, ticker
):
self.home_currency = home_currency # Account denomination (e.g. GBP)
self.position_type = position_type # Long or short
self.market = market
self.currency_pair = currency_pair # Intended traded currency pair
self.units = units
self.exposure = Decimal(str(exposure))
self.ticker = ticker
self.set_up_currencies()
self.profit_base = self.calculate_profit_base()
self.profit_perc = self.calculate_profit_perc()
# Long or short
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, with account denominated in GBP, this is USD/GBP
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(ask))
self.cur_price = Decimal(str(bid))
self.avg_price = Decimal(str(ticker_cur["ask"]))
self.cur_price = Decimal(str(ticker_cur["bid"]))
else:
self.avg_price = Decimal(str(bid))
self.cur_price = Decimal(str(ask))
self.profit_base = self.calculate_profit_base(self.exposure)
self.profit_perc = self.calculate_profit_perc(self.exposure)
self.avg_price = Decimal(str(ticker_cur["bid"]))
self.cur_price = Decimal(str(ticker_cur["ask"]))
def calculate_pips(self):
getcontext.prec = 6
mult = Decimal("1")
if self.position_type == "long":
mult = Decimal("1")
elif self.position_type == "short":
mult = Decimal("-1")
return (mult * (self.cur_price - self.avg_price)).quantize(
pips = (mult * (self.cur_price - self.avg_price)).quantize(
Decimal("0.00001"), ROUND_HALF_DOWN
)
return pips
def calculate_profit_base(self, exposure):
pips = self.calculate_pips()
return (pips * exposure / self.cur_price).quantize(
Decimal("0.00001"), ROUND_HALF_DOWN
)
def calculate_profit_perc(self, exposure):
return (self.profit_base / exposure * Decimal("100.00")).quantize(
Decimal("0.00001"), ROUND_HALF_DOWN
)
def update_position_price(self, bid, ask, exposure):
def calculate_profit_base(self):
pips = self.calculate_pips()
ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
if self.position_type == "long":
self.cur_price = Decimal(str(bid))
qh_close = ticker_qh["bid"]
else:
self.cur_price = Decimal(str(ask))
self.profit_base = self.calculate_profit_base(exposure)
self.profit_perc = self.calculate_profit_perc(exposure)
qh_close = ticker_qh["ask"]
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(
Decimal("0.00001"), ROUND_HALF_DOWN
)
def update_position_price(self):
ticker_cur = self.ticker.prices[self.currency_pair]
if self.position_type == "long":
self.cur_price = Decimal(str(ticker_cur["bid"]))
else:
self.cur_price = Decimal(str(ticker_cur["ask"]))
self.profit_base = self.calculate_profit_base()
self.profit_perc = self.calculate_profit_perc()
def add_units(self, units):
cp = self.ticker.prices[self.currency_pair]
if self.position_type == "long":
add_price = cp["ask"]
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
self.units = new_total_units
self.update_position_price()
def remove_units(self, units):
dec_units = Decimal(str(units))
ticker_cp = self.ticker.prices[self.currency_pair]
ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
if self.position_type == "long":
remove_price = ticker_cp["ask"]
qh_close = ticker_qh["bid"]
else:
remove_price = ticker_cp["bid"]
qh_close = ticker_qh["ask"]
self.units -= dec_units
self.update_position_price()
# Calculate PnL
pnl = self.calculate_pips() * qh_close * dec_units
return pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
def close_position(self):
ticker_cp = self.ticker.prices[self.currency_pair]
ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
if self.position_type == "long":
remove_price = ticker_cp["ask"]
qh_close = ticker_qh["bid"]
else:
remove_price = ticker_cp["bid"]
qh_close = ticker_qh["ask"]
self.update_position_price()
# Calculate PnL
pnl = self.calculate_pips() * qh_close * self.units
return pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
+162 -28
View File
@@ -4,18 +4,39 @@ import unittest
from position import Position
class TickerMock(object):
"""
A mock object that allows a representation of the
ticker/pricing handler.
"""
def __init__(self):
self.prices = {
"GBPUSD": {"bid": Decimal("1.50328"), "ask": Decimal("1.50349")},
"USDGBP": {"bid": Decimal("0.66521"), "ask": Decimal("0.66512")},
"EURUSD": {"bid": Decimal("1.07832"), "ask": Decimal("1.07847")}
}
# =====================================
# 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):
getcontext.prec = 2
home_currency = "GBP"
position_type = "long"
market = "GBP/USD"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.50328")
ask = Decimal("1.50349")
ticker = TickerMock()
self.position = Position(
position_type, market,
units, exposure, bid, ask
home_currency, position_type,
currency_pair, units, ticker
)
def test_calculate_init_pips(self):
@@ -23,11 +44,11 @@ class TestLongGBPUSDPosition(unittest.TestCase):
self.assertEqual(pos_pips, Decimal("-0.00021"))
def test_calculate_init_profit_base(self):
profit_base = self.position.calculate_profit_base(self.position.exposure)
profit_base = self.position.calculate_profit_base()
self.assertEqual(profit_base, Decimal("-0.27939"))
def test_calculate_init_profit_perc(self):
profit_perc = self.position.calculate_profit_perc(self.position.exposure)
profit_perc = self.position.calculate_profit_perc()
self.assertEqual(profit_perc, Decimal("-0.01397"))
def test_calculate_updated_values(self):
@@ -35,32 +56,37 @@ class TestLongGBPUSDPosition(unittest.TestCase):
Check that after the bid/ask prices move, that the updated
pips, profit and percentage profit calculations are correct.
"""
bid = Decimal("1.50486")
ask = Decimal("1.50586")
self.position.update_position_price(bid, ask, self.position.exposure)
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")}
self.position.update_position_price()
# Check pips
pos_pips = self.position.calculate_pips()
self.assertEqual(pos_pips, Decimal("0.00137"))
# Check profit base
profit_base = self.position.calculate_profit_base(self.position.exposure)
self.assertEqual(profit_base, Decimal("1.82077"))
profit_base = self.position.calculate_profit_base()
self.assertEqual(profit_base, Decimal("1.82076"))
# Check profit percentage
profit_perc = self.position.calculate_profit_perc(self.position.exposure)
profit_perc = self.position.calculate_profit_perc()
self.assertEqual(profit_perc, Decimal("0.09104"))
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):
getcontext.prec = 2
home_currency = "GBP"
position_type = "short"
market = "GBP/USD"
currency_pair = "GBPUSD"
units = Decimal("2000")
exposure = Decimal("2000.00")
bid = Decimal("1.50328")
ask = Decimal("1.50349")
ticker = TickerMock()
self.position = Position(
position_type, market,
units, exposure, bid, ask
home_currency, position_type,
currency_pair, units, ticker
)
def test_calculate_init_pips(self):
@@ -68,11 +94,11 @@ class TestShortGBPUSDPosition(unittest.TestCase):
self.assertEqual(pos_pips, Decimal("-0.00021"))
def test_calculate_init_profit_base(self):
profit_base = self.position.calculate_profit_base(self.position.exposure)
profit_base = self.position.calculate_profit_base()
self.assertEqual(profit_base, Decimal("-0.27935"))
def test_calculate_init_profit_perc(self):
profit_perc = self.position.calculate_profit_perc(self.position.exposure)
profit_perc = self.position.calculate_profit_perc()
self.assertEqual(profit_perc, Decimal("-0.01397"))
def test_calculate_updated_values(self):
@@ -80,19 +106,127 @@ class TestShortGBPUSDPosition(unittest.TestCase):
Check that after the bid/ask prices move, that the updated
pips, profit and percentage profit calculations are correct.
"""
bid = Decimal("1.50486")
ask = Decimal("1.50586")
self.position.update_position_price(bid, ask, self.position.exposure)
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")}
self.position.update_position_price()
# Check pips
pos_pips = self.position.calculate_pips()
self.assertEqual(pos_pips, Decimal("-0.00258"))
# Check profit base
profit_base = self.position.calculate_profit_base(self.position.exposure)
self.assertEqual(profit_base, Decimal("-3.42661"))
profit_base = self.position.calculate_profit_base()
self.assertEqual(profit_base, Decimal("-3.42660"))
# Check profit percentage
profit_perc = self.position.calculate_profit_perc(self.position.exposure)
profit_perc = self.position.calculate_profit_perc()
self.assertEqual(profit_perc, Decimal("-0.17133"))
# =====================================
# GBP Home Currency with EUR/USD traded
# =====================================
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):
getcontext.prec = 2
home_currency = "GBP"
position_type = "long"
currency_pair = "EURUSD"
units = Decimal("2000")
ticker = TickerMock()
self.position = Position(
home_currency, position_type,
currency_pair, units, ticker
)
def test_calculate_init_pips(self):
pos_pips = self.position.calculate_pips()
self.assertEqual(pos_pips, Decimal("-0.00015"))
def test_calculate_init_profit_base(self):
profit_base = self.position.calculate_profit_base()
self.assertEqual(profit_base, Decimal("-0.19956"))
def test_calculate_init_profit_perc(self):
profit_perc = self.position.calculate_profit_perc()
self.assertEqual(profit_perc, Decimal("-0.00998"))
def test_calculate_updated_values(self):
"""
Check that after the bid/ask prices move, that the updated
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")}
self.position.update_position_price()
# Check pips
pos_pips = self.position.calculate_pips()
self.assertEqual(pos_pips, Decimal("-0.00036"))
# Check profit base
profit_base = self.position.calculate_profit_base()
self.assertEqual(profit_base, Decimal("-0.47845"))
# Check profit percentage
profit_perc = self.position.calculate_profit_perc()
self.assertEqual(profit_perc, Decimal("-0.02392"))
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):
getcontext.prec = 2
home_currency = "GBP"
position_type = "short"
currency_pair = "EURUSD"
units = Decimal("2000")
ticker = TickerMock()
self.position = Position(
home_currency, position_type,
currency_pair, units, ticker
)
def test_calculate_init_pips(self):
pos_pips = self.position.calculate_pips()
self.assertEqual(pos_pips, Decimal("-0.00015"))
def test_calculate_init_profit_base(self):
profit_base = self.position.calculate_profit_base()
self.assertEqual(profit_base, Decimal("-0.19954"))
def test_calculate_init_profit_perc(self):
profit_perc = self.position.calculate_profit_perc()
self.assertEqual(profit_perc, Decimal("-0.00998"))
def test_calculate_updated_values(self):
"""
Check that after the bid/ask prices move, that the updated
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")}
self.position.update_position_price()
# Check pips
pos_pips = self.position.calculate_pips()
self.assertEqual(pos_pips, Decimal("0.00005"))
# Check profit base
profit_base = self.position.calculate_profit_base()
self.assertEqual(profit_base, Decimal("0.06641"))
# Check profit percentage
profit_perc = self.position.calculate_profit_perc()
self.assertEqual(profit_perc, Decimal("0.00332"))
if __name__ == "__main__":
unittest.main()
+5 -1
View File
@@ -1,11 +1,15 @@
argparse==1.2.1
ipython==2.3.1
matplotlib==1.4.3
mock==1.0.1
nose==1.3.6
numpy==1.9.1
pandas==0.15.2
pyparsing==2.0.3
python-dateutil==2.4.0
pytz==2014.10
requests==2.5.1
scikit-learn==0.15.2
scipy==0.15.1
six==1.9.0
wsgiref==0.1.2
wsgiref==0.1.2
+1
View File
@@ -16,6 +16,7 @@ ENVIRONMENTS = {
}
CSV_DATA_DIR = os.environ.get('QSFOREX_CSV_DATA_DIR', None)
OUTPUT_RESULTS_DIR = os.environ.get('QSFOREX_OUTPUT_RESULTS_DIR', None)
DOMAIN = "practice"
STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN]
+72 -4
View File
@@ -2,8 +2,17 @@ from qsforex.event.event import SignalEvent
class TestStrategy(object):
def __init__(self, instrument, events):
self.instrument = instrument
"""
A testing strategy that alternates between buying and selling
a currency pair on every 5th tick. This has the effect of
continuously "crossing the spread" and so will be loss-making
strategy.
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
self.ticks = 0
self.invested = False
@@ -12,11 +21,70 @@ class TestStrategy(object):
if event.type == 'TICK':
if self.ticks % 5 == 0:
if self.invested == False:
signal = SignalEvent(self.instrument, "market", "buy")
signal = SignalEvent(self.pairs[0], "market", "buy", event.time)
self.events.put(signal)
self.invested = True
else:
signal = SignalEvent(self.instrument, "market", "sell")
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
of 500 ticks for the short SMA and 2,000 ticks for the
long SMA.
The strategy is "long only" in the sense it will only
open a long position once the short SMA exceeds the long
SMA. It will close the position (by taking a corresponding
sell order) when the long SMA recrosses the short SMA.
The strategy uses a rolling SMA calculation in order to
increase efficiency by eliminating the need to call two
full moving average calculations on each tick.
"""
def __init__(
self, pairs, events,
short_window=500, long_window=2000
):
self.pairs = pairs
self.events = events
self.ticks = 0
self.invested = False
self.short_window = short_window
self.long_window = long_window
self.short_sma = None
self.long_sma = None
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':
price = event.bid
if self.ticks == 0:
self.short_sma = price
self.long_sma = price
else:
self.short_sma = self.calc_rolling_sma(
self.short_sma, self.short_window, price
)
self.long_sma = self.calc_rolling_sma(
self.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)
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)
self.events.put(signal)
self.invested = False
self.ticks += 1