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
+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)