Skip to content

Commit

Permalink
Merge pull request #286 from mhallsmoore/elim-forced-defaults
Browse files Browse the repository at this point in the history
Eliminated certain 'forced' defaults in BacktestTradingSession. Added…
  • Loading branch information
mhallsmoore committed Nov 20, 2019
2 parents 6e57191 + 6c08c6f commit 4af4515
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 35 deletions.
1 change: 1 addition & 0 deletions examples/buy_and_hold.py
Expand Up @@ -19,5 +19,6 @@
assets,
alpha_model,
rebalance='buy_and_hold',
cash_buffer_percentage=0.05
)
backtest.run()
4 changes: 4 additions & 0 deletions examples/sixty_forty.py
Expand Up @@ -19,5 +19,9 @@
assets,
alpha_model,
rebalance='end_of_month',
account_name='Strategic Asset Allocation Account',
portfolio_id='SAA001',
portfolio_name='Strategic Asset Allocation - 60/40 US Equities/Bonds (SPY/AGG)',
cash_buffer_percentage=0.05
)
backtest.run()
141 changes: 141 additions & 0 deletions qstrader/broker/portfolio/portfolio.py
@@ -1,5 +1,6 @@
import datetime
import logging
import sys

import pandas as pd

Expand All @@ -8,6 +9,9 @@
from qstrader.broker.portfolio.portfolio_event import PortfolioEvent
from qstrader.broker.portfolio.position import Position
from qstrader.broker.portfolio.position_handler import PositionHandler
from qstrader.utils.console import (
string_colour, GREEN, RED, CYAN, WHITE
)


class Portfolio(object):
Expand Down Expand Up @@ -127,6 +131,29 @@ def total_non_cash_equity(self):
"""
return self.total_equity - self.total_cash

@property
def total_non_cash_unrealised_gain(self):
"""
Calculate the sum of all the positions'
unrealised gains.
"""
return sum(
pos.unrealised_gain
for asset, pos in self.pos_handler.positions.items()
if not asset.startswith('CASH')
)

@property
def total_non_cash_unrealised_percentage_gain(self):
"""
Calculate the total unrealised percentage gain
on the positions.
"""
tbc = self.pos_handler.total_book_cost()
if tbc == 0.0:
return 0.0
return (self.total_non_cash_equity - tbc) / tbc * 100.0

def subscribe_funds(self, dt, amount):
"""
Credit funds to the portfolio.
Expand Down Expand Up @@ -353,3 +380,117 @@ def history_to_df(self):
"date", "type", "description", "debit", "credit", "balance"
]
).set_index(keys=["date"])

def holdings_to_console(self):
"""
Output the portfolio holdings information to the console.
"""
def print_row_divider(repeats, symbol="=", cap="*"):
"""
Prints a row divider for the table.
"""
sys.stdout.write(
"%s%s%s\n" % (cap, symbol * repeats, cap)
)

# Sort the assets based on their name, not ticker symbol
pos_sorted = sorted(
self.pos_handler.positions.items(),
key=lambda x: x[0]
)

# Output the name and ID of the portfolio
sys.stdout.write(
string_colour(
"\nPortfolio Holdings | %s - %s\n\n" % (
self.portfolio_id, self.name
), colour=CYAN
)
)

# Create the header row and dividers
repeats = 99
print_row_divider(repeats)
sys.stdout.write(
"| Holding | Quantity | Price | Change |"
" Book Cost | Market Value | "
" Unrealised Gain | \n"
)
print_row_divider(repeats)

# Create the asset holdings rows for each ticker
ticker_format = '| {0:>8} | {1:>8d} | {2:>5} | ' \
'{3:>6} | {4:>14} | {5:>14} |'
for asset, pos in pos_sorted:
if asset.startswith('CASH'):
pos_quantity = 0
pos_book_cost = pos.market_value
pos_unrealised_gain = '0.00'
pos_unrealised_percentage_gain = '0.00%'
else:
pos_quantity = int(pos.quantity)
pos_book_cost = pos.book_cost
pos_unrealised_gain = "%0.2f" % pos.unrealised_gain
pos_unrealised_percentage_gain = "%0.2f%%" % pos.unrealised_percentage_gain
sys.stdout.write(
ticker_format.format(
asset, pos_quantity, "-", "-",
"%0.2f" % pos_book_cost,
"%0.2f" % pos.market_value
)
)
# Colour the gain as red, green or white depending upon
# whether it is negative, positive or breakeven
colour = WHITE
if pos.unrealised_gain > 0.0:
colour = GREEN
elif pos.unrealised_gain < 0.0:
colour = RED
gain_str = string_colour(
pos_unrealised_gain,
colour=colour
)
perc_gain_str = string_colour(
pos_unrealised_percentage_gain,
colour=colour
)
sys.stdout.write(" " * (25 - len(gain_str)))
sys.stdout.write(gain_str)
sys.stdout.write(" " * (22 - len(perc_gain_str)))
sys.stdout.write(str(perc_gain_str))
sys.stdout.write(" |\n")

# Create the totals row
print_row_divider(repeats)
total_format = '| {0:>8} | {1:25} | {2:>14} | {3:>14} |'
sys.stdout.write(
total_format.format(
"Total", " ",
"%0.2f" % self.pos_handler.total_book_cost(),
"%0.2f" % self.pos_handler.total_market_value()
)
)
# Utilise the correct colour for the totals
# of gain and percentage gain
colour = WHITE
total_gain = self.pos_handler.total_unrealised_gain()
perc_total_gain = self.pos_handler.total_unrealised_percentage_gain()
if total_gain > 0.0:
colour = GREEN
elif total_gain < 0.0:
colour = RED
gain_str = string_colour(
"%0.2f" % total_gain,
colour=colour
)
perc_gain_str = string_colour(
"%0.2f%%" % perc_total_gain,
colour=colour
)
sys.stdout.write(" " * (25 - len(gain_str)))
sys.stdout.write(gain_str)
sys.stdout.write(" " * (22 - len(perc_gain_str)))
sys.stdout.write(str(perc_gain_str))
sys.stdout.write(" |\n")
print_row_divider(repeats)
sys.stdout.write("\n")
54 changes: 48 additions & 6 deletions qstrader/broker/portfolio/position_handler.py
Expand Up @@ -77,38 +77,80 @@ def update_commission(self, asset, commission):
asset, commission
)

def total_book_cost(self):
def total_non_cash_book_cost(self):
"""
Calculate the sum of all the Positions' book costs.
Calculate the sum of all the Positions' book costs
excluding cash.
"""
return sum(
pos.book_cost
for asset, pos in self.positions.items()
)

def total_book_cost(self):
"""
Calculate the sum of all the Positions' book cost
including cash.
"""
return sum(
pos.book_cost if not asset.startswith('CASH') else pos.market_value
for asset, pos in self.positions.items()
)

def total_non_cash_market_value(self):
"""
Calculate the sum of all the positions' market values
excluding cash.
"""
return sum(
0.0 if asset.startswith('CASH') else pos.market_value
for asset, pos in self.positions.items()
)

def total_market_value(self):
"""
Calculate the sum of all the positions' market values.
Calculate the sum of all the positions' market values
including cash.
"""
return sum(
pos.market_value
for asset, pos in self.positions.items()
)

def total_unrealised_gain(self):
def total_non_cash_unrealised_gain(self):
"""
Calculate the sum of all the positions'
unrealised gains.
unrealised gains excluding cash.
"""
return sum(
pos.unrealised_gain
for asset, pos in self.positions.items()
)

def total_unrealised_gain(self):
"""
Calculate the sum of all the positions'
unrealised gains including cash.
"""
return sum(
0.0 if asset.startswith('CASH') else pos.unrealised_gain
for asset, pos in self.positions.items()
)

def total_non_cash_unrealised_percentage_gain(self):
"""
Calculate the total unrealised percentage gain
on the positions excluding cash.
"""
tbc = self.total_non_cash_book_cost()
if tbc == 0.0:
return 0.0
return (self.total_non_cash_market_value() - tbc) / tbc * 100.0

def total_unrealised_percentage_gain(self):
"""
Calculate the total unrealised percentage gain
on the positions.
on the positions including cash.
"""
tbc = self.total_book_cost()
if tbc == 0.0:
Expand Down
4 changes: 2 additions & 2 deletions qstrader/statistics/performance.py
Expand Up @@ -84,12 +84,12 @@ def create_drawdowns(returns):

# Create the high water mark
for t in range(1, len(idx)):
hwm[t] = max(hwm[t - 1], returns.ix[t])
hwm[t] = max(hwm[t - 1], returns.iloc[t])

# Calculate the drawdown and duration statistics
perf = pd.DataFrame(index=idx)
perf["Drawdown"] = (hwm - returns) / hwm
perf["Drawdown"].ix[0] = 0.0
perf["Drawdown"].iloc[0] = 0.0
perf["DurationCheck"] = np.where(perf["Drawdown"] == 0, 0, 1)
duration = max(
sum(1 for i in g if i == 1)
Expand Down
7 changes: 6 additions & 1 deletion qstrader/system/qts.py
Expand Up @@ -32,6 +32,8 @@ class QuantTradingSystem(object):
The specific broker portfolio to send orders to.
data_handler : `DataHandler`
The data handler instance used for all market/fundamental data.
cash_buffer_percentage : `float`, optional
The percentage of the portfolio to retain in cash.
submit_orders : `Boolean`, optional
Whether to actually submit generated orders. Defaults to no submission.
"""
Expand All @@ -43,13 +45,15 @@ def __init__(
broker_portfolio_id,
data_handler,
alpha_model,
cash_buffer_percentage=0.05,
submit_orders=False
):
self.universe = universe
self.broker = broker
self.broker_portfolio_id = broker_portfolio_id
self.data_handler = data_handler
self.alpha_model = alpha_model
self.cash_buffer_percentage = cash_buffer_percentage
self.submit_orders = submit_orders
self._initialise_models()

Expand All @@ -67,7 +71,8 @@ def _initialise_models(self):
order_sizer = DollarWeightedCashBufferedOrderSizeGeneration(
self.broker,
self.broker_portfolio_id,
self.data_handler
self.data_handler,
cash_buffer_percentage=self.cash_buffer_percentage
)
optimiser = FixedWeightPortfolioOptimiser(
data_handler=self.data_handler
Expand Down

0 comments on commit 4af4515

Please sign in to comment.