def __init__(self, initial_cash, events_queue, price_handler, position_sizer, risk_manager): """ The PortfolioHandler is designed to interact with the backtesting or live trading overall event-driven architecture. It exposes two methods, on_signal and on_fill, which handle how SignalEvent and FillEvent objects are dealt with. Each PortfolioHandler contains a Portfolio object, which stores the actual Position objects. The PortfolioHandler takes a handle to a PositionSizer object which determines a mechanism, based on the current Portfolio, as to how to size a new Order. The PortfolioHandler also takes a handle to the RiskManager, which is used to modify any generated Orders to remain in line with risk parameters. """ self.initial_cash = initial_cash self.events_queue = events_queue self.price_handler = price_handler self.position_sizer = position_sizer self.risk_manager = risk_manager self.portfolio = Portfolio(price_handler, initial_cash)
def setUp(self): """ Set up the Portfolio object that will store the collection of Position objects, supplying it with $500,000.00 USD in initial cash. """ ph = PriceHandlerMock() cash = Decimal("500000.00") self.portfolio = Portfolio(ph, cash)
def __init__( self, initial_cash, events_queue, price_handler, position_sizer, risk_manager ): """ The PortfolioHandler is designed to interact with the backtesting or live trading overall event-driven architecture. It exposes two methods, on_signal and on_fill, which handle how SignalEvent and FillEvent objects are dealt with. Each PortfolioHandler contains a Portfolio object, which stores the actual Position objects. The PortfolioHandler takes a handle to a PositionSizer object which determines a mechanism, based on the current Portfolio, as to how to size a new Order. The PortfolioHandler also takes a handle to the RiskManager, which is used to modify any generated Orders to remain in line with risk parameters. """ self.initial_cash = initial_cash self.events_queue = events_queue self.price_handler = price_handler self.position_sizer = position_sizer self.risk_manager = risk_manager self.portfolio = Portfolio(price_handler, initial_cash)
def test_calculating_statistics(self): """ Purchase/sell multiple lots of AMZN, GOOG at various prices/commissions to ensure the arithmetic in calculating equity, drawdowns and sharpe ratio is correct. """ # Create Statistics object price_handler = PriceHandlerMock() self.portfolio = Portfolio(price_handler, Decimal("500000.00")) portfolio_handler = PortfolioHandlerMock(self.portfolio) statistics=SimpleStatistics(portfolio_handler) # Perform transaction and test statistics at this tick self.portfolio.transact_position( "BOT", "AMZN", 100, Decimal("566.56"), Decimal("1.00") ) t="2000-01-01 00:00:00" statistics.update(t) self.assertEqual(statistics.equity[t], Decimal("499807.00")) self.assertEqual(statistics.drawdowns[t], Decimal("193.00")) self.assertEqual(statistics.equity_returns[t], Decimal("-0.0386")) # Perform transaction and test statistics at this tick self.portfolio.transact_position( "BOT", "AMZN", 200, Decimal ("566.395"), Decimal("1.00") ) t="2000-01-02 00:00:00" statistics.update(t) self.assertEqual(statistics.equity[t], Decimal("499455.00")) self.assertEqual(statistics.drawdowns[t], Decimal("545.00")) self.assertEqual(statistics.equity_returns[t], Decimal("-0.0705")) # Perform transaction and test statistics at this tick self.portfolio.transact_position( "BOT", "GOOG", 200, Decimal("707.50"), Decimal("1.00") ) t="2000-01-03 00:00:00" statistics.update(t) self.assertEqual(statistics.equity[t], Decimal("499046.00")) self.assertEqual(statistics.drawdowns[t], Decimal("954.00")) self.assertEqual(statistics.equity_returns[t], Decimal("-0.0820")) # Perform transaction and test statistics at this tick self.portfolio.transact_position( "SLD", "AMZN", 100, Decimal("565.83"), Decimal("1.00") ) t="2000-01-04 00:00:00" statistics.update(t); self.assertEqual(statistics.equity[t], Decimal("499164.00")) self.assertEqual(statistics.drawdowns[t], Decimal("836.00")) self.assertEqual(statistics.equity_returns[t], Decimal("0.0236")) # Perform transaction and test statistics at this tick self.portfolio.transact_position( "BOT", "GOOG", 200, Decimal("705.545"), Decimal("1.00") ) t="2000-01-05 00:00:00" statistics.update(t) self.assertEqual(statistics.equity[t], Decimal("499146.00")) self.assertEqual(statistics.drawdowns[t], Decimal("854.00")) self.assertEqual(statistics.equity_returns[t], Decimal("-0.0036")) # Perform transaction and test statistics at this tick self.portfolio.transact_position( "SLD", "AMZN", 200, Decimal("565.59"), Decimal("1.00") ) t="2000-01-06 00:00:00" statistics.update(t) self.assertEqual(statistics.equity[t], Decimal("499335.00")) self.assertEqual(statistics.drawdowns[t], Decimal("665.00")) self.assertEqual(statistics.equity_returns[t], Decimal("0.0379")) # Perform transaction and test statistics at this tick self.portfolio.transact_position( "SLD", "GOOG", 100, Decimal("707.92"), Decimal("1.00") ) t="2000-01-07 00:00:00" statistics.update(t) self.assertEqual(statistics.equity[t], Decimal("499580.00")) self.assertEqual(statistics.drawdowns[t], Decimal("420.00")) self.assertEqual(statistics.equity_returns[t], Decimal("0.0490")) # Perform transaction and test statistics at this tick self.portfolio.transact_position( "SLD", "GOOG", 100, Decimal("707.90"), Decimal("0.00") ) t="2000-01-08 00:00:00" statistics.update(t) self.assertEqual(statistics.equity[t], Decimal("499824.00")) self.assertEqual(statistics.drawdowns[t], Decimal("176.00")) self.assertEqual(statistics.equity_returns[t], Decimal("0.0488")) # Perform transaction and test statistics at this tick self.portfolio.transact_position( "SLD", "GOOG", 100, Decimal("707.92"), Decimal("0.50") ) t="2000-01-09 00:00:00" statistics.update(t) self.assertEqual(statistics.equity[t], Decimal("500069.50")) self.assertEqual(statistics.drawdowns[t], Decimal("00.00")) self.assertEqual(statistics.equity_returns[t], Decimal("0.0491")) # Perform transaction and test statistics at this tick self.portfolio.transact_position( "SLD", "GOOG", 100, Decimal("707.78"), Decimal("1.00") ) t="2000-01-10 00:00:00" statistics.update(t) self.assertEqual(statistics.equity[t], Decimal("500300.50")) self.assertEqual(statistics.drawdowns[t], Decimal("00.00")) self.assertEqual(statistics.equity_returns[t], Decimal("0.0462")) # Test that results are calculated correctly. results=statistics.get_results() self.assertEqual(results["max_drawdown"], Decimal("954.00")) self.assertEqual(results["max_drawdown_pct"], Decimal("0.1908")) self.assertEqual(results["sharpe"], Decimal("1.8353"))
class PortfolioHandler(object): def __init__(self, initial_cash, events_queue, price_handler, position_sizer, risk_manager): """ The PortfolioHandler is designed to interact with the backtesting or live trading overall event-driven architecture. It exposes two methods, on_signal and on_fill, which handle how SignalEvent and FillEvent objects are dealt with. Each PortfolioHandler contains a Portfolio object, which stores the actual Position objects. The PortfolioHandler takes a handle to a PositionSizer object which determines a mechanism, based on the current Portfolio, as to how to size a new Order. The PortfolioHandler also takes a handle to the RiskManager, which is used to modify any generated Orders to remain in line with risk parameters. """ self.initial_cash = initial_cash self.events_queue = events_queue self.price_handler = price_handler self.position_sizer = position_sizer self.risk_manager = risk_manager self.portfolio = Portfolio(price_handler, initial_cash) def _create_order_from_signal(self, signal_event): """ Take a SignalEvent object and use it to form a SuggestedOrder object. These are not OrderEvent objects, as they have yet to be sent to the RiskManager object. At this stage they are simply "suggestions" that the RiskManager will either verify, modify or eliminate. """ order = SuggestedOrder(signal_event.ticker, signal_event.action) return order def _place_orders_onto_queue(self, order_list): """ Once the RiskManager has verified, modified or eliminated any order objects, they are placed onto the events queue, to ultimately be executed by the ExecutionHandler. """ for order_event in order_list: self.events_queue.put(order_event) def _convert_fill_to_portfolio_update(self, fill_event): """ Upon receipt of a FillEvent, the PortfolioHandler converts the event into a transaction that gets stored in the Portfolio object. This ensures that the broker and the local portfolio are "in sync". In addition, for backtesting purposes, the portfolio value can be reasonably estimated in a realistic manner, simply by modifying how the ExecutionHandler object handles slippage, transaction costs, liquidity and market impact. """ action = fill_event.action ticker = fill_event.ticker quantity = fill_event.quantity price = fill_event.price commission = fill_event.commission # Create or modify the position from the fill info self.portfolio.transact_position(action, ticker, quantity, price, commission) def on_signal(self, signal_event): """ This is called by the backtester or live trading architecture to form the initial orders from the SignalEvent. These orders are sized by the PositionSizer object and then sent to the RiskManager to verify, modify or eliminate. Once received from the RiskManager they are converted into full OrderEvent objects and sent back to the events queue. """ # Create the initial order list from a signal event initial_order = self._create_order_from_signal(signal_event) # Size the quantity of the initial order sized_order = self.position_sizer.size_order(self.portfolio, initial_order) # Refine or eliminate the order via the risk manager overlay order_events = self.risk_manager.refine_orders(self.portfolio, sized_order) # Place orders onto events queue self._place_orders_onto_queue(order_events) def on_fill(self, fill_event): """ This is called by the backtester or live trading architecture to take a FillEvent and update the Portfolio object with new or modified Positions. In a backtesting environment these FillEvents will be simulated by a model representing the execution, whereas in live trading they will come directly from a brokerage (such as Interactive Brokers). """ self._convert_fill_to_portfolio_update(fill_event) def update_portfolio_value(self): """ Update the portfolio to reflect current market value as based on last bid/ask of each ticker. """ self.portfolio._update_portfolio()
class PriceHandlerMock(object): def __init__(self): pass def get_best_bid_ask(self, ticker): prices = { "GOOG": (Decimal("762.15"), Decimal("762.15")), "AMZN": (Decimal("660.51"), Decimal("660.51")), } return prices[ticker] ph = PriceHandlerMock() cash = Decimal("500000.00") port = Portfolio(ph, cash) port.add_position("BOT", "GOOG", 100, Decimal("761.75"), Decimal("1.00")) port.add_position("BOT", "AMZN", 100, Decimal("660.86"), Decimal("1.00")) port.modify_position("BOT", "AMZN", 100, Decimal("660.14"), Decimal("1.00")) port.modify_position("BOT", "AMZN", 150, Decimal("660.20"), Decimal("1.00")) port.modify_position("SLD", "AMZN", 300, Decimal("659.713"), Decimal("1.50")) print("CASH:", port.cur_cash) print("EQUITY:", port.equity) print("UPnL:", port.unrealised_pnl) print("PnL:", port.realised_pnl) pprint.pprint(port.positions) print("") for p in port.positions:
class TestAmazonGooglePortfolio(unittest.TestCase): """ Test a portfolio consisting of Amazon and Google/Alphabet with various orders to create round-trips for both. These orders were carried out in the Interactive Brokers demo account and checked for cash, equity and PnL equality. """ def setUp(self): """ Set up the Portfolio object that will store the collection of Position objects, supplying it with $500,000.00 USD in initial cash. """ ph = PriceHandlerMock() cash = Decimal("500000.00") self.portfolio = Portfolio(ph, cash) def test_calculate_round_trip(self): """ Purchase/sell multiple lots of AMZN and GOOG at various prices/commissions to check the arithmetic and cost handling. """ # Buy 300 of AMZN over two transactions self.portfolio.transact_position( "BOT", "AMZN", 100, Decimal("566.56"), Decimal("1.00") ) self.portfolio.transact_position( "BOT", "AMZN", 200, Decimal("566.395"), Decimal("1.00") ) # Buy 200 GOOG over one transaction self.portfolio.transact_position( "BOT", "GOOG", 200, Decimal("707.50"), Decimal("1.00") ) # Add to the AMZN position by 100 shares self.portfolio.transact_position( "SLD", "AMZN", 100, Decimal("565.83"), Decimal("1.00") ) # Add to the GOOG position by 200 shares self.portfolio.transact_position( "BOT", "GOOG", 200, Decimal("705.545"), Decimal("1.00") ) # Sell 200 of the AMZN shares self.portfolio.transact_position( "SLD", "AMZN", 200, Decimal("565.59"), Decimal("1.00") ) # Multiple transactions bundled into one (in IB) # Sell 300 GOOG from the portfolio self.portfolio.transact_position( "SLD", "GOOG", 100, Decimal("704.92"), Decimal("1.00") ) self.portfolio.transact_position( "SLD", "GOOG", 100, Decimal("704.90"), Decimal("0.00") ) self.portfolio.transact_position( "SLD", "GOOG", 100, Decimal("704.92"), Decimal("0.50") ) # Finally, sell the remaining GOOG 100 shares self.portfolio.transact_position( "SLD", "GOOG", 100, Decimal("704.78"), Decimal("1.00") ) # The figures below are derived from Interactive Brokers # demo account using the above trades with prices provided # by their demo feed. self.assertEqual(self.portfolio.cur_cash, Decimal("499100.50")) self.assertEqual(self.portfolio.equity, Decimal("499100.50")) self.assertEqual(self.portfolio.unrealised_pnl, Decimal("0.00")) self.assertEqual(self.portfolio.realised_pnl, Decimal("-899.50"))
class PortfolioHandler(object): def __init__( self, initial_cash, events_queue, price_handler, position_sizer, risk_manager ): """ The PortfolioHandler is designed to interact with the backtesting or live trading overall event-driven architecture. It exposes two methods, on_signal and on_fill, which handle how SignalEvent and FillEvent objects are dealt with. Each PortfolioHandler contains a Portfolio object, which stores the actual Position objects. The PortfolioHandler takes a handle to a PositionSizer object which determines a mechanism, based on the current Portfolio, as to how to size a new Order. The PortfolioHandler also takes a handle to the RiskManager, which is used to modify any generated Orders to remain in line with risk parameters. """ self.initial_cash = initial_cash self.events_queue = events_queue self.price_handler = price_handler self.position_sizer = position_sizer self.risk_manager = risk_manager self.portfolio = Portfolio(price_handler, initial_cash) def _create_order_from_signal(self, signal_event): """ Take a SignalEvent object and use it to form a SuggestedOrder object. These are not OrderEvent objects, as they have yet to be sent to the RiskManager object. At this stage they are simply "suggestions" that the RiskManager will either verify, modify or eliminate. """ order = SuggestedOrder( signal_event.ticker, signal_event.action ) return order def _place_orders_onto_queue(self, order_list): """ Once the RiskManager has verified, modified or eliminated any order objects, they are placed onto the events queue, to ultimately be executed by the ExecutionHandler. """ for order_event in order_list: self.events_queue.put(order_event) def _convert_fill_to_portfolio_update(self, fill_event): """ Upon receipt of a FillEvent, the PortfolioHandler converts the event into a transaction that gets stored in the Portfolio object. This ensures that the broker and the local portfolio are "in sync". In addition, for backtesting purposes, the portfolio value can be reasonably estimated in a realistic manner, simply by modifying how the ExecutionHandler object handles slippage, transaction costs, liquidity and market impact. """ action = fill_event.action ticker = fill_event.ticker quantity = fill_event.quantity price = fill_event.price commission = fill_event.commission # Create or modify the position from the fill info self.portfolio.transact_position( action, ticker, quantity, price, commission ) def on_signal(self, signal_event): """ This is called by the backtester or live trading architecture to form the initial orders from the SignalEvent. These orders are sized by the PositionSizer object and then sent to the RiskManager to verify, modify or eliminate. Once received from the RiskManager they are converted into full OrderEvent objects and sent back to the events queue. """ # Create the initial order list from a signal event initial_order = self._create_order_from_signal(signal_event) # Size the quantity of the initial order sized_order = self.position_sizer.size_order( self.portfolio, initial_order ) # Refine or eliminate the order via the risk manager overlay order_events = self.risk_manager.refine_orders( self.portfolio, sized_order ) # Place orders onto events queue self._place_orders_onto_queue(order_events) def on_fill(self, fill_event): """ This is called by the backtester or live trading architecture to take a FillEvent and update the Portfolio object with new or modified Positions. In a backtesting environment these FillEvents will be simulated by a model representing the execution, whereas in live trading they will come directly from a brokerage (such as Interactive Brokers). """ self._convert_fill_to_portfolio_update(fill_event)