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