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:
+20
-23
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user