From 48486c981404f1b5182aa1710a28c7192fa985e1 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin Date: Thu, 8 Aug 2013 15:52:59 -0400 Subject: [PATCH 1/2] ENH: New order methods. --- zipline/algorithm.py | 69 ++++++++++++++++++++++++++++++++++++++ zipline/finance/blotter.py | 28 +++++++++++++++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/zipline/algorithm.py b/zipline/algorithm.py index 6112fabe..492f8ad4 100644 --- a/zipline/algorithm.py +++ b/zipline/algorithm.py @@ -366,6 +366,12 @@ class TradingAlgorithm(object): def order(self, sid, amount, limit_price=None, stop_price=None): return self.blotter.order(sid, amount, limit_price, stop_price) + def order_value(self, sid, value, limit_price=None, stop_price=None): + last_price = self.trading_client.current_data[sid].price + return self.blotter.order_value(sid, value, last_price, + limit_price=limit_price, + stop_price=stop_price) + @property def recorded_vars(self): return copy(self._recorded_vars) @@ -430,3 +436,66 @@ class TradingAlgorithm(object): assert data_frequency in ('daily', 'minute') self.data_frequency = data_frequency self.annualizer = ANNUALIZER[self.data_frequency] + + def order_percent(self, sid, percent, limit_price=None, stop_price=None): + """ + Place an order in the specified security corresponding to the given + percent of the current portfolio value. + + Note that percent must expressed as a decimal (0.50 means 50\%). + """ + value = self.portfolio.portfolio_value * percent + return self.order_value(sid, value, limit_price, stop_price) + + def target(self, sid, target, limit_price=None, stop_price=None): + """ + Place an order to adjust a position to a target number of shares. If + the position doesn't already exist, this is equivalent to placing a new + order. If the position does exist, this is equivalent to placing an + order for the difference between the target number of shares and the + current number of shares. + """ + if sid in self.portfolio.positions: + current_position = self.portfolio.positions[sid].amount + req_shares = target - current_position + return self.order(sid, req_shares, limit_price, stop_price) + else: + return self.order(sid, target, limit_price, stop_price) + + def target_value(self, sid, target, limit_price=None, stop_price=None): + """ + Place an order to adjust a position to a target value. If + the position doesn't already exist, this is equivalent to placing a new + order. If the position does exist, this is equivalent to placing an + order for the difference between the target value and the + current value. + """ + if sid in self.portfolio.positions: + current_position = self.portfolio.positions[sid].amount + current_price = self.portfolio.positions[sid].last_sale_price + current_value = current_position * current_price + req_value = target - current_value + return self.order_value(sid, req_value, limit_price, stop_price) + else: + return self.order_value(sid, target, limit_price, stop_price) + + def target_percent(self, sid, target, limit_price=None, stop_price=None): + """ + Place an order to adjust a position to a target percent of the + current portfolio value. If the position doesn't already exist, this is + equivalent to placing a new order. If the position does exist, this is + equivalent to placing an order for the difference between the target + percent and the current percent. + + Note that target must expressed as a decimal (0.50 means 50\%). + """ + if sid in self.portfolio.positions: + current_position = self.portfolio.positions[sid].amount + current_price = self.portfolio.positions[sid].last_sale_price + current_value = current_position * current_price + else: + current_value = 0 + target_value = self.portfolio.portfolio_value * target + + req_value = target_value - current_value + return self.order_value(sid, req_value, limit_price, stop_price) diff --git a/zipline/finance/blotter.py b/zipline/finance/blotter.py index 8a32592a..7406a22f 100644 --- a/zipline/finance/blotter.py +++ b/zipline/finance/blotter.py @@ -18,6 +18,7 @@ import uuid from copy import copy from logbook import Logger from collections import defaultdict +import numpy as np import zipline.errors import zipline.protocol as zp @@ -93,7 +94,7 @@ class Blotter(object): # just validates amount and passes rest on to TransactionSimulator # Tell the user if they try to buy 0 shares of something. - if amount == 0: + if int(amount) == 0: zero_message = "Requested to trade zero shares of {psid}".format( psid=sid ) @@ -124,6 +125,31 @@ class Blotter(object): return order.id + def order_value(self, sid, value, last_price, + limit_price=None, stop_price=None): + """ + Place an order by desired value rather than desired number of shares. + If the requested sid is found in the universe, the requested value is + divided by its price to imply the number of shares to transact. + + value > 0 :: Buy/Cover + value < 0 :: Sell/Short + Market order: order(sid, value) + Limit order: order(sid, value, limit_price) + Stop order: order(sid, value, None, stop_price) + StopLimit order: order(sid, value, limit_price, stop_price) + """ + if np.allclose(last_price, 0): + zero_message = "Price of 0 for {psid}; can't infer value".format( + psid=sid + ) + log.debug(zero_message) + # Don't place any order + return + else: + amount = value / last_price + return self.order(sid, amount, limit_price, stop_price) + def cancel(self, order_id): if order_id not in self.orders: return From b1fdebfb7c5af8677571410805a0d346cc296abb Mon Sep 17 00:00:00 2001 From: Thomas Wiecki Date: Thu, 8 Aug 2013 15:53:09 -0400 Subject: [PATCH 2/2] TST: Added tests for new order methods. --- tests/test_algorithm.py | 28 +++++++++++++++++++++------- tests/test_batchtransform.py | 2 +- zipline/utils/factory.py | 2 +- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/test_algorithm.py b/tests/test_algorithm.py index 63d0cf6e..ea76a2f5 100644 --- a/tests/test_algorithm.py +++ b/tests/test_algorithm.py @@ -21,7 +21,13 @@ from zipline.utils.test_utils import setup_logger import zipline.utils.factory as factory from zipline.test_algorithms import (TestRegisterTransformAlgorithm, RecordAlgorithm, - TestOrderAlgorithm) + TestOrderAlgorithm, + TestOrderValueAlgorithm, + TestTargetAlgorithm, + TestOrderPercentAlgorithm, + TestTargetPercentAlgorithm, + TestTargetValueAlgorithm) + from zipline.sources import (SpecificEquityTrades, DataFrameSource, DataPanelSource) @@ -166,9 +172,17 @@ class TestTransformAlgorithm(TestCase): self.assertEqual(algo.data_frequency, 'minute') self.assertEqual(algo.annualizer, 10) - def test_orders_executed(self): - algo = TestOrderAlgorithm( - sim_params=self.sim_params, - data_frequency='daily' - ) - algo.run(self.df) + def test_order_methods(self): + AlgoClasses = [TestOrderAlgorithm, + TestOrderValueAlgorithm, + TestTargetAlgorithm, + TestOrderPercentAlgorithm, + TestTargetPercentAlgorithm, + TestTargetValueAlgorithm] + + for AlgoClass in AlgoClasses: + algo = AlgoClass( + sim_params=self.sim_params, + data_frequency='daily' + ) + algo.run(self.df) diff --git a/tests/test_batchtransform.py b/tests/test_batchtransform.py index b2a2c9c8..a0e19fb7 100644 --- a/tests/test_batchtransform.py +++ b/tests/test_batchtransform.py @@ -231,7 +231,7 @@ class TestBatchTransform(TestCase): # consecutive (of window length) numbers up till the end. for i in range(algo.window_length, len(test_history)): np.testing.assert_array_equal( - range(i - algo.window_length + 1, i + 1), + range(i - algo.window_length + 2, i + 2d), test_history[i].values.flatten() ) diff --git a/zipline/utils/factory.py b/zipline/utils/factory.py index a0899ae3..065b4d41 100644 --- a/zipline/utils/factory.py +++ b/zipline/utils/factory.py @@ -326,7 +326,7 @@ def create_test_df_source(sim_params=None, bars='daily'): if i >= market_open and i <= market_close: new_index.append(i) index = new_index - x = np.arange(0, len(index)) + x = np.arange(1, len(index) + 1) df = pd.DataFrame(x, index=index, columns=[0])