API: Add slippage and commission models for futures

This commit is contained in:
dmichalowicz
2017-04-11 10:50:04 -04:00
parent 0da8a59f4c
commit dd21346eca
12 changed files with 1390 additions and 139 deletions
+2 -2
View File
@@ -28,7 +28,7 @@ from zipline.finance.execution import (
)
from zipline.finance.order import ORDER_STATUS, Order
from zipline.finance.slippage import (
DEFAULT_VOLUME_SLIPPAGE_BAR_LIMIT,
DEFAULT_EQUITY_VOLUME_SLIPPAGE_BAR_LIMIT,
FixedSlippage,
)
from zipline.gens.sim_engine import BAR, SESSION_END
@@ -292,7 +292,7 @@ class BlotterTestCase(WithCreateBarData,
order_size = 100
expected_filled = int(trade_amt *
DEFAULT_VOLUME_SLIPPAGE_BAR_LIMIT)
DEFAULT_EQUITY_VOLUME_SLIPPAGE_BAR_LIMIT)
expected_open = order_size - expected_filled
expected_status = ORDER_STATUS.OPEN if expected_open else \
ORDER_STATUS.FILLED
+261 -39
View File
@@ -1,8 +1,18 @@
from datetime import timedelta
from textwrap import dedent
from nose_parameterized import parameterized
from pandas import DataFrame
from zipline import TradingAlgorithm
from zipline.finance.commission import PerTrade, PerShare, PerDollar
from zipline.errors import IncompatibleCommissionModel
from zipline.finance.commission import (
PerContract,
PerDollar,
PerFutureTrade,
PerShare,
PerTrade,
)
from zipline.finance.order import Order
from zipline.finance.transaction import Transaction
from zipline.testing import ZiplineTestCase, trades_by_sid_to_dfs
@@ -17,83 +27,199 @@ from zipline.utils import factory
class CommissionUnitTests(WithAssetFinder, ZiplineTestCase):
ASSET_FINDER_EQUITY_SIDS = 1, 2
def generate_order_and_txns(self):
asset1 = self.asset_finder.retrieve_asset(1)
@classmethod
def make_futures_info(cls):
return DataFrame({
'sid': [1000, 1001],
'root_symbol': ['CL', 'FV'],
'symbol': ['CLF07', 'FVF07'],
'start_date': [cls.START_DATE, cls.START_DATE],
'end_date': [cls.END_DATE, cls.END_DATE],
'notice_date': [cls.END_DATE, cls.END_DATE],
'expiration_date': [cls.END_DATE, cls.END_DATE],
'multiplier': [500, 500],
'exchange': ['CME', 'CME'],
})
def generate_order_and_txns(self, sid, order_amount, fill_amounts):
asset1 = self.asset_finder.retrieve_asset(sid)
# one order
order = Order(dt=None, asset=asset1, amount=500)
order = Order(dt=None, asset=asset1, amount=order_amount)
# three fills
txn1 = Transaction(asset=asset1, amount=230, dt=None,
txn1 = Transaction(asset=asset1, amount=fill_amounts[0], dt=None,
price=100, order_id=order.id)
txn2 = Transaction(asset=asset1, amount=170, dt=None,
txn2 = Transaction(asset=asset1, amount=fill_amounts[1], dt=None,
price=101, order_id=order.id)
txn3 = Transaction(asset=asset1, amount=100, dt=None,
txn3 = Transaction(asset=asset1, amount=fill_amounts[2], dt=None,
price=102, order_id=order.id)
return order, [txn1, txn2, txn3]
def test_per_trade(self):
model = PerTrade(cost=10)
def verify_per_trade_commissions(self,
model,
expected_commission,
sid,
order_amount=None,
fill_amounts=None):
fill_amounts = fill_amounts or [230, 170, 100]
order_amount = order_amount or sum(fill_amounts)
order, txns = self.generate_order_and_txns()
order, txns = self.generate_order_and_txns(
sid, order_amount, fill_amounts,
)
self.assertEqual(10, model.calculate(order, txns[0]))
self.assertEqual(expected_commission, model.calculate(order, txns[0]))
order.commission = 10
order.commission = expected_commission
self.assertEqual(0, model.calculate(order, txns[1]))
self.assertEqual(0, model.calculate(order, txns[2]))
def test_per_trade(self):
# Test per trade model for equities.
model = PerTrade(cost=10)
self.verify_per_trade_commissions(model, expected_commission=10, sid=1)
# Test per trade model for futures.
model = PerFutureTrade(cost=10)
self.verify_per_trade_commissions(
model, expected_commission=10, sid=1000,
)
# Test per trade model with custom costs per future symbol.
model = PerFutureTrade(cost={'CL': 5, 'FV': 10})
self.verify_per_trade_commissions(
model, expected_commission=5, sid=1000,
)
self.verify_per_trade_commissions(
model, expected_commission=10, sid=1001,
)
def test_per_share_no_minimum(self):
model = PerShare(cost=0.0075, min_trade_cost=None)
order, txns = self.generate_order_and_txns()
order, txns = self.generate_order_and_txns(
sid=1, order_amount=500, fill_amounts=[230, 170, 100],
)
# make sure each commission is pro-rated
self.assertAlmostEqual(1.725, model.calculate(order, txns[0]))
self.assertAlmostEqual(1.275, model.calculate(order, txns[1]))
self.assertAlmostEqual(0.75, model.calculate(order, txns[2]))
def verify_per_share_commissions(self, model, commission_totals):
order, txns = self.generate_order_and_txns()
def verify_per_unit_commissions(self,
model,
commission_totals,
sid,
order_amount=None,
fill_amounts=None):
fill_amounts = fill_amounts or [230, 170, 100]
order_amount = order_amount or sum(fill_amounts)
order, txns = self.generate_order_and_txns(
sid, order_amount, fill_amounts,
)
for i, commission_total in enumerate(commission_totals):
order.commission += model.calculate(order, txns[i])
self.assertAlmostEqual(commission_total, order.commission)
order.filled += txns[i].amount
def test_per_contract_no_minimum(self):
# Note that the exchange fee is a one-time cost that is only applied to
# the first fill of an order.
#
# The commission on the first fill is (230 * 0.01) + 0.3 = 2.6
# The commission on the second fill is 170 * 0.01 = 1.7
# The total after the second fill is 2.6 + 1.7 = 4.3
# The commission on the third fill is 100 * 0.01 = 1.0
# The total after the third fill is 5.3
model = PerContract(cost=0.01, exchange_fee=0.3, min_trade_cost=None)
self.verify_per_unit_commissions(
model=model,
commission_totals=[2.6, 4.3, 5.3],
sid=1000,
order_amount=500,
fill_amounts=[230, 170, 100],
)
# Test using custom costs and fees.
model = PerContract(
cost={'CL': 0.01, 'FV': 0.0075},
exchange_fee={'CL': 0.3, 'FV': 0.5},
min_trade_cost=None,
)
self.verify_per_unit_commissions(model, [2.6, 4.3, 5.3], sid=1000)
self.verify_per_unit_commissions(model, [2.225, 3.5, 4.25], sid=1001)
def test_per_share_with_minimum(self):
# minimum is met by the first trade
self.verify_per_share_commissions(
self.verify_per_unit_commissions(
PerShare(cost=0.0075, min_trade_cost=1),
[1.725, 3, 3.75]
commission_totals=[1.725, 3, 3.75],
sid=1,
)
# minimum is met by the second trade
self.verify_per_share_commissions(
self.verify_per_unit_commissions(
PerShare(cost=0.0075, min_trade_cost=2.5),
[2.5, 3, 3.75]
commission_totals=[2.5, 3, 3.75],
sid=1,
)
# minimum is met by the third trade
self.verify_per_share_commissions(
self.verify_per_unit_commissions(
PerShare(cost=0.0075, min_trade_cost=3.5),
[3.5, 3.5, 3.75]
commission_totals=[3.5, 3.5, 3.75],
sid=1,
)
# minimum is not met by any of the trades
self.verify_per_share_commissions(
self.verify_per_unit_commissions(
PerShare(cost=0.0075, min_trade_cost=5.5),
[5.5, 5.5, 5.5]
commission_totals=[5.5, 5.5, 5.5],
sid=1,
)
def test_per_contract_with_minimum(self):
# Minimum is met by the first trade.
self.verify_per_unit_commissions(
PerContract(cost=.01, exchange_fee=0.3, min_trade_cost=1),
commission_totals=[2.6, 4.3, 5.3],
sid=1000,
)
# Minimum is met by the second trade.
self.verify_per_unit_commissions(
PerContract(cost=.01, exchange_fee=0.3, min_trade_cost=3),
commission_totals=[3.0, 4.3, 5.3],
sid=1000,
)
# Minimum is met by the third trade.
self.verify_per_unit_commissions(
PerContract(cost=.01, exchange_fee=0.3, min_trade_cost=5),
commission_totals=[5.0, 5.0, 5.3],
sid=1000,
)
# Minimum is not met by any of the trades.
self.verify_per_unit_commissions(
PerContract(cost=.01, exchange_fee=0.3, min_trade_cost=7),
commission_totals=[7.0, 7.0, 7.0],
sid=1000,
)
def test_per_dollar(self):
model = PerDollar(cost=0.0015)
order, txns = self.generate_order_and_txns()
order, txns = self.generate_order_and_txns(
sid=1, order_amount=500, fill_amounts=[230, 170, 100],
)
# make sure each commission is pro-rated
self.assertAlmostEqual(34.5, model.calculate(order, txns[0]))
@@ -116,19 +242,36 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase):
def initialize(context):
# for these tests, let us take out the entire bar with no price
# impact
set_slippage(slippage.VolumeShareSlippage(1.0, 0))
set_slippage(
us_equities=slippage.VolumeShareSlippage(1.0, 0),
us_futures=slippage.VolumeShareSlippage(1.0, 0),
)
{0}
{commission}
context.ordered = False
def handle_data(context, data):
if not context.ordered:
order(sid(133), {1})
order(sid({sid}), {amount})
context.ordered = True
""",
)
@classmethod
def make_futures_info(cls):
return DataFrame({
'sid': [1000, 1001],
'root_symbol': ['CL', 'FV'],
'symbol': ['CLF07', 'FVF07'],
'start_date': [cls.START_DATE, cls.START_DATE],
'end_date': [cls.END_DATE, cls.END_DATE],
'notice_date': [cls.END_DATE, cls.END_DATE],
'expiration_date': [cls.END_DATE, cls.END_DATE],
'multiplier': [500, 500],
'exchange': ['CME', 'CME'],
})
@classmethod
def make_equity_daily_bar_data(cls):
num_days = len(cls.sim_params.sessions)
@@ -158,7 +301,11 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase):
def test_per_trade(self):
results = self.get_results(
self.code.format("set_commission(commission.PerTrade(1))", 300)
self.code.format(
commission="set_commission(commission.PerTrade(1))",
sid=133,
amount=300,
)
)
# should be 3 fills at 100 shares apiece
@@ -169,10 +316,30 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase):
self.verify_capital_used(results, [-1001, -1000, -1000])
def test_futures_per_trade(self):
results = self.get_results(
self.code.format(
commission=(
'set_commission(us_futures=commission.PerFutureTrade(1))'
),
sid=1000,
amount=10,
)
)
# The capital used is only -1.0 (the commission cost) because no
# capital is actually spent to enter into a long position on a futures
# contract.
self.assertEqual(results.orders[1][0]['commission'], 1.0)
self.assertEqual(results.capital_used[1], -1.0)
def test_per_share_no_minimum(self):
results = self.get_results(
self.code.format("set_commission(commission.PerShare(0.05, None))",
300)
self.code.format(
commission="set_commission(commission.PerShare(0.05, None))",
sid=133,
amount=300,
)
)
# should be 3 fills at 100 shares apiece
@@ -186,8 +353,11 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase):
def test_per_share_with_minimum(self):
# minimum hit by first trade
results = self.get_results(
self.code.format("set_commission(commission.PerShare(0.05, 3))",
300)
self.code.format(
commission="set_commission(commission.PerShare(0.05, 3))",
sid=133,
amount=300,
)
)
# commissions should be 5, 10, 15
@@ -198,8 +368,11 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase):
# minimum hit by second trade
results = self.get_results(
self.code.format("set_commission(commission.PerShare(0.05, 8))",
300)
self.code.format(
commission="set_commission(commission.PerShare(0.05, 8))",
sid=133,
amount=300,
)
)
# commissions should be 8, 10, 15
@@ -211,8 +384,11 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase):
# minimum hit by third trade
results = self.get_results(
self.code.format("set_commission(commission.PerShare(0.05, 12))",
300)
self.code.format(
commission="set_commission(commission.PerShare(0.05, 12))",
sid=133,
amount=300,
)
)
# commissions should be 12, 12, 15
@@ -224,8 +400,11 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase):
# minimum never hit
results = self.get_results(
self.code.format("set_commission(commission.PerShare(0.05, 18))",
300)
self.code.format(
commission="set_commission(commission.PerShare(0.05, 18))",
sid=133,
amount=300,
)
)
# commissions should be 18, 18, 18
@@ -235,9 +414,40 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase):
self.verify_capital_used(results, [-1018, -1000, -1000])
@parameterized.expand([
# The commission is (10 * 0.05) + 1.3 = 1.8, and the capital used is
# the same as the commission cost because no capital is actually spent
# to enter into a long position on a futures contract.
(None, 1.8),
# Minimum hit by first trade.
(1, 1.8),
# Minimum not hit by first trade, so use the minimum.
(3, 3.0),
])
def test_per_contract(self, min_trade_cost, expected_commission):
results = self.get_results(
self.code.format(
commission=(
'set_commission(us_futures=commission.PerContract('
'cost=0.05, exchange_fee=1.3, min_trade_cost={}))'
).format(min_trade_cost),
sid=1000,
amount=10,
),
)
self.assertEqual(
results.orders[1][0]['commission'], expected_commission,
)
self.assertEqual(results.capital_used[1], -expected_commission)
def test_per_dollar(self):
results = self.get_results(
self.code.format("set_commission(commission.PerDollar(0.01))", 300)
self.code.format(
commission="set_commission(commission.PerDollar(0.01))",
sid=133,
amount=300,
)
)
# should be 3 fills at 100 shares apiece, each fill is worth $1k, so
@@ -249,6 +459,18 @@ class CommissionAlgorithmTests(WithDataPortal, WithSimParams, ZiplineTestCase):
self.verify_capital_used(results, [-1010, -1010, -1010])
def test_incorrectly_set_futures_model(self):
with self.assertRaises(IncompatibleCommissionModel):
# Passing a futures commission model as the first argument, which
# is for setting equity models, should fail.
self.get_results(
self.code.format(
commission='set_commission(commission.PerContract(0, 0))',
sid=1000,
amount=10,
)
)
def verify_capital_used(self, results, values):
self.assertEqual(values[0], results.capital_used[1])
self.assertEqual(values[1], results.capital_used[2])
+275 -12
View File
@@ -16,24 +16,32 @@
'''
Unit tests for finance.slippage
'''
import datetime
from collections import namedtuple
import pytz
import datetime
from math import sqrt
from nose_parameterized import parameterized
import pandas as pd
from pandas.tslib import normalize_date
import numpy as np
import pandas as pd
import pytz
from zipline.finance.slippage import VolumeShareSlippage, \
fill_price_worse_than_limit_price
from zipline.protocol import DATASOURCE_TYPE, BarData
from zipline.finance.blotter import Order
from zipline.finance.asset_restrictions import NoRestrictions
from zipline.assets import Equity
from zipline.data.data_portal import DataPortal
from zipline.testing import tmp_bcolz_equity_minute_bar_reader
from zipline.finance.asset_restrictions import NoRestrictions
from zipline.finance.order import Order
from zipline.finance.slippage import (
fill_price_worse_than_limit_price,
MarketImpactBase,
NO_DATA_VOLATILITY_SLIPPAGE_IMPACT,
VolatilityVolumeShare,
VolumeShareSlippage,
)
from zipline.protocol import DATASOURCE_TYPE, BarData
from zipline.testing import (
create_minute_bar_data,
tmp_bcolz_equity_minute_bar_reader,
)
from zipline.testing.fixtures import (
WithCreateBarData,
WithDataPortal,
@@ -560,10 +568,36 @@ class VolumeShareSlippageTestCase(WithCreateBarData,
index=[cls.minutes[0]],
)
@classmethod
def make_futures_info(cls):
return pd.DataFrame({
'sid': [1000],
'root_symbol': ['CL'],
'symbol': ['CLF06'],
'start_date': [cls.ASSET_FINDER_EQUITY_START_DATE],
'end_date': [cls.ASSET_FINDER_EQUITY_END_DATE],
'multiplier': [500],
'exchange': ['CME'],
})
@classmethod
def make_future_minute_bar_data(cls):
yield 1000, pd.DataFrame(
{
'open': [5.00],
'high': [5.15],
'low': [4.85],
'close': [5.00],
'volume': [100],
},
index=[cls.minutes[0]],
)
@classmethod
def init_class_fixtures(cls):
super(VolumeShareSlippageTestCase, cls).init_class_fixtures()
cls.ASSET133 = cls.env.asset_finder.retrieve_asset(133)
cls.ASSET1000 = cls.env.asset_finder.retrieve_asset(1000)
def test_volume_share_slippage(self):
@@ -631,6 +665,235 @@ class VolumeShareSlippageTestCase(WithCreateBarData,
self.assertEquals(len(orders_txns), 0)
def test_volume_share_slippage_with_future(self):
slippage_model = VolumeShareSlippage(volume_limit=1, price_impact=0.3)
open_orders = [
Order(
dt=datetime.datetime(2006, 1, 5, 14, 30, tzinfo=pytz.utc),
amount=10,
filled=0,
asset=self.ASSET1000,
),
]
bar_data = self.create_bardata(
simulation_dt_func=lambda: self.minutes[0],
)
orders_txns = list(
slippage_model.simulate(bar_data, self.ASSET1000, open_orders)
)
self.assertEquals(len(orders_txns), 1)
_, txn = orders_txns[0]
# We expect to fill the order for all 10 contracts. The volume for the
# futures contract in this bar is 100, so our volume share is:
# 10.0 / 100 = 0.1
# The current price is 5.0 and the price impact is 0.3, so the expected
# impacted price is:
# 5.0 + (5.0 * (0.1 ** 2) * 0.3) = 5.015
expected_txn = {
'price': 5.015,
'dt': datetime.datetime(2006, 1, 5, 14, 31, tzinfo=pytz.utc),
'amount': 10,
'asset': self.ASSET1000,
'commission': None,
'type': DATASOURCE_TYPE.TRANSACTION,
'order_id': open_orders[0].id,
}
self.assertIsNotNone(txn)
self.assertEquals(expected_txn, txn.__dict__)
class VolatilityVolumeShareTestCase(WithCreateBarData,
WithSimParams,
WithDataPortal,
ZiplineTestCase):
ASSET_START_DATE = pd.Timestamp('2006-02-10')
TRADING_CALENDAR_STRS = ('NYSE', 'us_futures')
TRADING_CALENDAR_PRIMARY_CAL = 'us_futures'
@classmethod
def init_class_fixtures(cls):
super(VolatilityVolumeShareTestCase, cls).init_class_fixtures()
cls.ASSET = cls.asset_finder.retrieve_asset(1000)
@classmethod
def make_futures_info(cls):
return pd.DataFrame({
'sid': [1000],
'root_symbol': ['CL'],
'symbol': ['CLF07'],
'start_date': [cls.ASSET_START_DATE],
'end_date': [cls.END_DATE],
'multiplier': [500],
'exchange': ['CME'],
})
@classmethod
def make_future_minute_bar_data(cls):
data = list(
super(
VolatilityVolumeShareTestCase, cls,
).make_future_minute_bar_data()
)
# Make the first month's worth of data NaN to simulate cases where a
# futures contract does not exist yet.
data[0][1].loc[:cls.ASSET_START_DATE] = np.NaN
return data
def test_calculate_impact_buy(self):
answer_key = [
# We ordered 10 contracts, but are capped at 100 * 0.05 = 5
(91485.500085168125, 5),
(91486.500085169057, 5),
(None, None),
]
order = Order(
dt=pd.Timestamp.now(tz='utc').round('min'),
asset=self.ASSET,
amount=10,
)
self._calculate_impact(order, answer_key)
def test_calculate_impact_sell(self):
answer_key = [
# We ordered -10 contracts, but are capped at -(100 * 0.05) = -5
(91485.499914831875, -5),
(91486.499914830943, -5),
(None, None),
]
order = Order(
dt=pd.Timestamp.now(tz='utc').round('min'),
asset=self.ASSET,
amount=-10,
)
self._calculate_impact(order, answer_key)
def _calculate_impact(self, test_order, answer_key):
model = VolatilityVolumeShare(volume_limit=0.05)
first_minute = pd.Timestamp('2006-03-31 11:35AM', tz='UTC')
next_3_minutes = self.trading_calendar.minutes_window(first_minute, 3)
remaining_shares = test_order.open_amount
for i, minute in enumerate(next_3_minutes):
data = self.create_bardata(simulation_dt_func=lambda: minute)
new_order = Order(
dt=data.current_dt, asset=self.ASSET, amount=remaining_shares,
)
price, amount = model.process_order(data, new_order)
self.assertEqual(price, answer_key[i][0])
self.assertEqual(amount, answer_key[i][1])
amount = amount or 0
if remaining_shares < 0:
remaining_shares = min(0, remaining_shares - amount)
else:
remaining_shares = max(0, remaining_shares - amount)
def test_calculate_impact_without_history(self):
model = VolatilityVolumeShare(volume_limit=1)
minutes = [
# Start day of the futures contract; no history yet.
pd.Timestamp('2006-02-10 11:35AM', tz='UTC'),
# Only a week's worth of history data.
pd.Timestamp('2006-02-17 11:35AM', tz='UTC'),
]
for minute in minutes:
data = self.create_bardata(simulation_dt_func=lambda: minute)
order = Order(dt=data.current_dt, asset=self.ASSET, amount=10)
price, amount = model.process_order(data, order)
avg_price = (
data.current(self.ASSET, 'high') +
data.current(self.ASSET, 'low')
) / 2
expected_price = \
avg_price + (avg_price * NO_DATA_VOLATILITY_SLIPPAGE_IMPACT)
self.assertEqual(price, expected_price)
self.assertEqual(amount, 10)
def test_impacted_price_worse_than_limit(self):
model = VolatilityVolumeShare(volume_limit=0.05)
# Use all the same numbers from the 'calculate_impact' tests. Since the
# impacted price is 59805.5, which is worse than the limit price of
# 59800, the model should return None.
minute = pd.Timestamp('2006-03-01 11:35AM', tz='UTC')
data = self.create_bardata(simulation_dt_func=lambda: minute)
order = Order(
dt=data.current_dt, asset=self.ASSET, amount=10, limit=59800,
)
price, amount = model.process_order(data, order)
self.assertIsNone(price)
self.assertIsNone(amount)
class MarketImpactTestCase(WithCreateBarData, ZiplineTestCase):
ASSET_FINDER_EQUITY_SIDS = (1,)
@classmethod
def make_equity_minute_bar_data(cls):
trading_calendar = cls.trading_calendars[Equity]
return create_minute_bar_data(
trading_calendar.minutes_for_sessions_in_range(
cls.equity_minute_bar_days[0],
cls.equity_minute_bar_days[-1],
),
cls.asset_finder.equities_sids,
)
def test_window_data(self):
session = pd.Timestamp('2006-03-01')
minute = self.trading_calendar.minutes_for_session(session)[1]
data = self.create_bardata(simulation_dt_func=lambda: minute)
asset = self.asset_finder.retrieve_asset(1)
mean_volume, volatility = MarketImpactBase()._get_window_data(
data, asset, window_length=20,
)
# close volume
# 2006-01-31 00:00:00+00:00 29.0 119.0
# 2006-02-01 00:00:00+00:00 30.0 120.0
# 2006-02-02 00:00:00+00:00 31.0 121.0
# 2006-02-03 00:00:00+00:00 32.0 122.0
# 2006-02-06 00:00:00+00:00 33.0 123.0
# 2006-02-07 00:00:00+00:00 34.0 124.0
# 2006-02-08 00:00:00+00:00 35.0 125.0
# 2006-02-09 00:00:00+00:00 36.0 126.0
# 2006-02-10 00:00:00+00:00 37.0 127.0
# 2006-02-13 00:00:00+00:00 38.0 128.0
# 2006-02-14 00:00:00+00:00 39.0 129.0
# 2006-02-15 00:00:00+00:00 40.0 130.0
# 2006-02-16 00:00:00+00:00 41.0 131.0
# 2006-02-17 00:00:00+00:00 42.0 132.0
# 2006-02-21 00:00:00+00:00 43.0 133.0
# 2006-02-22 00:00:00+00:00 44.0 134.0
# 2006-02-23 00:00:00+00:00 45.0 135.0
# 2006-02-24 00:00:00+00:00 46.0 136.0
# 2006-02-27 00:00:00+00:00 47.0 137.0
# 2006-02-28 00:00:00+00:00 48.0 138.0
# Mean volume is (119 + 138) / 2 = 128.5
self.assertEqual(mean_volume, 128.5)
# Volatility is closes.pct_change().std() * sqrt(252)
reference_vol = pd.Series(range(29, 49)).pct_change().std() * sqrt(252)
self.assertEqual(volatility, reference_vol)
class OrdersStopTestCase(WithSimParams,
WithTradingEnvironment,
+115
View File
@@ -56,6 +56,7 @@ from zipline.data.us_equity_pricing import (
from zipline.errors import (
AccountControlViolation,
CannotOrderDelistedAsset,
IncompatibleSlippageModel,
OrderDuringInitialize,
OrderInBeforeTradingStart,
RegisterTradingControlPostInit,
@@ -1738,6 +1739,27 @@ def handle_data(context, data):
finally:
tempdir.cleanup()
def test_incorrectly_set_futures_slippage_model(self):
code = dedent(
"""
from zipline.api import set_slippage, slippage
class MySlippage(slippage.FutureSlippageModel):
def process_order(self, data, order):
return data.current(order.asset, 'price'), order.amount
def initialize(context):
set_slippage(MySlippage())
"""
)
test_algo = TradingAlgorithm(
script=code, sim_params=self.sim_params, env=self.env,
)
with self.assertRaises(IncompatibleSlippageModel):
# Passing a futures slippage model as the first argument, which is
# for setting equity models, should fail.
test_algo.run(self.data_portal)
def test_algo_record_vars(self):
test_algo = TradingAlgorithm(
script=record_variables,
@@ -3655,6 +3677,99 @@ class TestFuturesAlgo(WithDataPortal, WithSimParams, ZiplineTestCase):
algo.history_values[1].values, list(map(float, range(3636, 3641))),
)
@staticmethod
def algo_with_slippage(slippage_model):
return dedent(
"""
from zipline.api import (
commission,
order,
set_commission,
set_slippage,
sid,
slippage,
get_datetime,
)
def initialize(context):
commission_model = commission.PerFutureTrade(0)
set_commission(us_futures=commission_model)
slippage_model = slippage.{model}
set_slippage(us_futures=slippage_model)
context.ordered = False
def handle_data(context, data):
if not context.ordered:
order(sid(1), 10)
context.ordered = True
context.order_price = data.current(sid(1), 'price')
"""
).format(model=slippage_model)
def test_fixed_future_slippage(self):
algo_code = self.algo_with_slippage('FixedSlippage(spread=0.10)')
algo = TradingAlgorithm(
script=algo_code,
sim_params=self.sim_params,
env=self.env,
trading_calendar=get_calendar('us_futures'),
)
results = algo.run(self.data_portal)
# Flatten the list of transactions.
all_txns = [
val for sublist in results['transactions'].tolist()
for val in sublist
]
self.assertEqual(len(all_txns), 1)
txn = all_txns[0]
# Add 1 to the expected price because the order does not fill until the
# bar after the price is recorded.
expected_spread = 0.05
expected_price = (algo.order_price + 1) + expected_spread
# Capital used should be 0 because there is no commission, and the cost
# to enter into a long position on a futures contract is 0.
self.assertEqual(txn['price'], expected_price)
self.assertEqual(results['orders'][0][0]['commission'], 0.0)
self.assertEqual(results.capital_used[0], 0.0)
def test_volume_contract_slippage(self):
algo_code = self.algo_with_slippage(
'VolumeShareSlippage(volume_limit=0.05, price_impact=0.1)',
)
algo = TradingAlgorithm(
script=algo_code,
sim_params=self.sim_params,
env=self.env,
trading_calendar=get_calendar('us_futures'),
)
results = algo.run(self.data_portal)
# There should be no commissions.
self.assertEqual(results['orders'][0][0]['commission'], 0.0)
# Flatten the list of transactions.
all_txns = [
val for sublist in results['transactions'].tolist()
for val in sublist
]
# With a volume limit of 0.05, and a total volume of 100 contracts
# traded per minute, we should require 2 transactions to order 10
# contracts.
self.assertEqual(len(all_txns), 2)
for i, txn in enumerate(all_txns):
# Add 1 to the order price because the order does not fill until
# the bar after the price is recorded.
order_price = algo.order_price + i + 1
expected_impact = order_price * 0.1 * (0.05 ** 2)
expected_price = order_price + expected_impact
self.assertEqual(txn['price'], expected_price)
class TestTradingAlgorithm(ZiplineTestCase):
def test_analyze_called(self):
+49 -23
View File
@@ -49,6 +49,8 @@ from zipline.errors import (
AttachPipelineAfterInitialize,
CannotOrderDelistedAsset,
HistoryInInitialize,
IncompatibleCommissionModel,
IncompatibleSlippageModel,
NoSuchPipeline,
OrderDuringInitialize,
OrderInBeforeTradingStart,
@@ -61,14 +63,11 @@ from zipline.errors import (
SetCommissionPostInit,
SetSlippagePostInit,
UnsupportedCancelPolicy,
UnsupportedCommissionModel,
UnsupportedDatetimeFormat,
UnsupportedOrderParameters,
UnsupportedSlippageModel,
)
from zipline.finance.trading import TradingEnvironment
from zipline.finance.blotter import Blotter
from zipline.finance.commission import CommissionModel
from zipline.finance.controls import (
LongOnly,
MaxOrderCount,
@@ -85,7 +84,6 @@ from zipline.finance.execution import (
)
from zipline.finance.performance import PerformanceTracker
from zipline.finance.asset_restrictions import Restrictions
from zipline.finance.slippage import SlippageModel
from zipline.finance.cancel_policy import NeverCancel, CancelPolicy
from zipline.finance.asset_restrictions import (
NoRestrictions,
@@ -1656,34 +1654,51 @@ class TradingAlgorithm(object):
return dt
@api_method
def set_slippage(self, slippage):
"""Set the slippage model for the simulation.
def set_slippage(self, us_equities=None, us_futures=None):
"""Set the slippage models for the simulation.
Parameters
----------
slippage : SlippageModel
The slippage model to use.
us_equities : EquitySlippageModel
The slippage model to use for trading US equities.
us_futures : FutureSlippageModel
The slippage model to use for trading US futures.
See Also
--------
:class:`zipline.finance.slippage.SlippageModel`
"""
if not isinstance(slippage, SlippageModel):
raise UnsupportedSlippageModel()
if self.initialized:
raise SetSlippagePostInit()
# TODO: Create separate API methods for setting Equity and Future
# slippage models.
self.blotter.slippage_models[Equity] = slippage
if us_equities is not None:
if Equity not in us_equities.allowed_asset_types:
raise IncompatibleSlippageModel(
asset_type='equities',
given_model=us_equities,
supported_asset_types=us_equities.allowed_asset_types,
)
self.blotter.slippage_models[Equity] = us_equities
if us_futures is not None:
if Future not in us_futures.allowed_asset_types:
raise IncompatibleSlippageModel(
asset_type='futures',
given_model=us_futures,
supported_asset_types=us_futures.allowed_asset_types,
)
self.blotter.slippage_models[Future] = us_futures
@api_method
def set_commission(self, commission):
"""Sets the commission model for the simulation.
def set_commission(self, us_equities=None, us_futures=None):
"""Sets the commission models for the simulation.
Parameters
----------
commission : CommissionModel
The commission model to use.
us_equities : EquityCommissionModel
The commission model to use for trading US equities.
us_futures : FutureCommissionModel
The commission model to use for trading US futures.
See Also
--------
@@ -1691,15 +1706,26 @@ class TradingAlgorithm(object):
:class:`zipline.finance.commission.PerTrade`
:class:`zipline.finance.commission.PerDollar`
"""
if not isinstance(commission, CommissionModel):
raise UnsupportedCommissionModel()
if self.initialized:
raise SetCommissionPostInit()
# TODO: Create separate API methods for setting Equity and Future
# commission models.
self.blotter.commission_models[Equity] = commission
if us_equities is not None:
if Equity not in us_equities.allowed_asset_types:
raise IncompatibleCommissionModel(
asset_type='equities',
given_model=us_equities,
supported_asset_types=us_equities.allowed_asset_types,
)
self.blotter.commission_models[Equity] = us_equities
if us_futures is not None:
if Future not in us_futures.allowed_asset_types:
raise IncompatibleCommissionModel(
asset_type='futures',
given_model=us_futures,
supported_asset_types=us_futures.allowed_asset_types,
)
self.blotter.commission_models[Future] = us_futures
@api_method
def set_cancel_policy(self, cancel_policy):
+22
View File
@@ -82,6 +82,17 @@ Please use VolumeShareSlippage or FixedSlippage.
""".strip()
class IncompatibleSlippageModel(ZiplineError):
"""
Raised if a user tries to set a futures slippage model for equities or vice
versa.
"""
msg = """
You attempted to set an incompatible slippage model for {asset_type}. \
The slippage model '{given_model}' only supports {supported_asset_types}.
""".strip()
class SetSlippagePostInit(ZiplineError):
# Raised if a users script calls set_slippage magic
# after the initialize method has returned.
@@ -130,6 +141,17 @@ Please use PerShare or PerTrade.
""".strip()
class IncompatibleCommissionModel(ZiplineError):
"""
Raised if a user tries to set a futures commission model for equities or
vice versa.
"""
msg = """
You attempted to set an incompatible commission model for {asset_type}. \
The commission model '{given_model}' only supports {supported_asset_types}.
""".strip()
class UnsupportedCancelPolicy(ZiplineError):
"""
Raised if a user script calls set_cancel_policy with an object that isn't
+14 -6
View File
@@ -20,11 +20,16 @@ from six import iteritems
from zipline.assets import Equity, Future, Asset
from zipline.finance.order import Order
from zipline.finance.slippage import VolumeShareSlippage
from zipline.finance.slippage import (
DEFAULT_FUTURE_VOLUME_SLIPPAGE_BAR_LIMIT,
VolatilityVolumeShare,
VolumeShareSlippage,
)
from zipline.finance.commission import (
DEFAULT_FUTURE_COST_PER_TRADE,
DEFAULT_PER_CONTRACT_COST,
FUTURE_EXCHANGE_FEES_BY_SYMBOL,
PerContract,
PerShare,
PerTrade,
)
from zipline.finance.cancel_policy import NeverCancel
from zipline.utils.input_validation import expect_types
@@ -51,12 +56,15 @@ class Blotter(object):
self.slippage_models = {
Equity: equity_slippage or VolumeShareSlippage(),
Future: future_slippage or VolumeShareSlippage(),
Future: future_slippage or VolatilityVolumeShare(
volume_limit=DEFAULT_FUTURE_VOLUME_SLIPPAGE_BAR_LIMIT,
),
}
self.commission_models = {
Equity: equity_commission or PerShare(),
Future: future_commission or PerTrade(
cost=DEFAULT_FUTURE_COST_PER_TRADE,
Future: future_commission or PerContract(
cost=DEFAULT_PER_CONTRACT_COST,
exchange_fee=FUTURE_EXCHANGE_FEES_BY_SYMBOL,
),
}
+224 -47
View File
@@ -13,13 +13,21 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
from abc import abstractmethod, abstractproperty
from collections import defaultdict
from abc import abstractmethod
from six import with_metaclass
from toolz import merge
DEFAULT_PER_SHARE_COST = 0.0075 # 0.75 cents per share
DEFAULT_MINIMUM_COST_PER_TRADE = 1.0 # $1 per trade
DEFAULT_FUTURE_COST_PER_TRADE = 2.35
from zipline.assets import Equity, Future
from zipline.finance.constants import FUTURE_EXCHANGE_FEES_BY_SYMBOL
from zipline.utils.dummy import DummyMapping
DEFAULT_PER_SHARE_COST = 0.0075 # 0.75 cents per share
DEFAULT_PER_CONTRACT_COST = 0.85 # $0.85 per future contract
DEFAULT_PER_DOLLAR_COST = 0.0015 # 0.15 cents per dollar
DEFAULT_MINIMUM_COST_PER_EQUITY_TRADE = 1.0 # $1 per trade
DEFAULT_MINIMUM_COST_PER_FUTURE_TRADE = 1.0 # $1 per trade
class CommissionModel(with_metaclass(abc.ABCMeta)):
@@ -31,6 +39,13 @@ class CommissionModel(with_metaclass(abc.ABCMeta)):
on each transaction.
"""
@abstractproperty
def allowed_asset_types(self):
"""
Return a tuple of asset types that are compatible with the given model.
"""
raise NotImplementedError('allowed_asset_types')
@abstractmethod
def calculate(self, order, transaction):
"""
@@ -59,7 +74,60 @@ class CommissionModel(with_metaclass(abc.ABCMeta)):
raise NotImplementedError('calculate')
class PerShare(CommissionModel):
class EquityCommissionModel(CommissionModel):
"""
Base class for commission models which only support equities.
"""
allowed_asset_types = (Equity,)
class FutureCommissionModel(CommissionModel):
"""
Base class for commission models which only support futures.
"""
allowed_asset_types = (Future,)
def calculate_per_unit_commission(order,
transaction,
cost_per_unit,
initial_commission,
min_trade_cost):
"""
If there is a minimum commission:
If the order hasn't had a commission paid yet, pay the minimum
commission.
If the order has paid a commission, start paying additional
commission once the minimum commission has been reached.
If there is no minimum commission:
Pay commission based on number of units in the transaction.
"""
additional_commission = abs(transaction.amount * cost_per_unit)
if order.commission == 0:
# no commission paid yet, pay at least the minimum plus a one-time
# exchange fee.
return max(min_trade_cost, additional_commission + initial_commission)
else:
# we've already paid some commission, so figure out how much we
# would be paying if we only counted per unit.
per_unit_total = \
(order.filled * cost_per_unit) + \
additional_commission + \
initial_commission
if per_unit_total < min_trade_cost:
# if we haven't hit the minimum threshold yet, don't pay
# additional commission
return 0
else:
# we've exceeded the threshold, so pay more commission.
return per_unit_total - order.commission
class PerShare(EquityCommissionModel):
"""
Calculates a commission for a transaction based on a per share cost with
an optional minimum cost per trade.
@@ -68,70 +136,138 @@ class PerShare(CommissionModel):
----------
cost : float, optional
The amount of commissions paid per share traded.
min_trade_cost : optional
min_trade_cost : float, optional
The minimum amount of commissions paid per trade.
"""
def __init__(self,
cost=DEFAULT_PER_SHARE_COST,
min_trade_cost=DEFAULT_MINIMUM_COST_PER_TRADE):
min_trade_cost=DEFAULT_MINIMUM_COST_PER_EQUITY_TRADE):
self.cost_per_share = float(cost)
self.min_trade_cost = min_trade_cost
self.min_trade_cost = min_trade_cost or 0
def __repr__(self):
return "{class_name}(cost_per_share={cost_per_share}, " \
"min_trade_cost={min_trade_cost})" \
.format(class_name=self.__class__.__name__,
cost_per_share=self.cost_per_share,
min_trade_cost=self.min_trade_cost)
return (
'{class_name}(cost_per_share={cost_per_share}, '
'min_trade_cost={min_trade_cost})'
.format(
class_name=self.__class__.__name__,
cost_per_share=self.cost_per_share,
min_trade_cost=self.min_trade_cost,
)
)
def calculate(self, order, transaction):
"""
If there is a minimum commission:
If the order hasn't had a commission paid yet, pay the minimum
commission.
return calculate_per_unit_commission(
order=order,
transaction=transaction,
cost_per_unit=self.cost_per_share,
initial_commission=0,
min_trade_cost=self.min_trade_cost,
)
If the order has paid a commission, start paying additional
commission once the minimum commission has been reached.
If there is no minimum commission:
Pay commission based on number of shares in the transaction.
"""
additional_commission = abs(transaction.amount * self.cost_per_share)
class PerContract(FutureCommissionModel):
"""
Calculates a commission for a transaction based on a per contract cost with
an optional minimum cost per trade.
if self.min_trade_cost is None:
# no min trade cost, so just return the cost for this transaction
return additional_commission
Parameters
----------
cost : float or dict
The amount of commissions paid per contract traded. If given a float,
the commission for all futures contracts is the same. If given a
dictionary, it must map root symbols to the commission cost for
contracts of that symbol.
exchange_fee : float or dict
A flat-rate fee charged by the exchange per trade. This value is a
constant, one-time charge no matter how many contracts are being
traded. If given a float, the fee for all contracts is the same. If
given a dictionary, it must map root symbols to the fee for contracts
of that symbol.
min_trade_cost : float, optional
The minimum amount of commissions paid per trade.
"""
if order.commission == 0:
# no commission paid yet, pay at least the minimum
return max(self.min_trade_cost, additional_commission)
def __init__(self,
cost,
exchange_fee,
min_trade_cost=DEFAULT_MINIMUM_COST_PER_FUTURE_TRADE):
# If 'cost' or 'exchange fee' are constants, use a dummy mapping to
# treat them as a dictionary that always returns the same value.
# NOTE: These dictionary does not handle unknown root symbols, so it
# may be worth revisiting this behavior.
if isinstance(cost, (int, float)):
self._cost_per_contract = DummyMapping(float(cost))
else:
# we've already paid some commission, so figure out how much we
# would be paying if we only counted per share.
per_share_total = \
(order.filled * self.cost_per_share) + additional_commission
# Cost per contract is a dictionary. If the user's dictionary does
# not provide a commission cost for a certain contract, fall back
# on the pre-defined cost values per root symbol.
self._cost_per_contract = defaultdict(
lambda: DEFAULT_PER_CONTRACT_COST, **cost
)
if per_share_total < self.min_trade_cost:
# if we haven't hit the minimum threshold yet, don't pay
# additional commission
return 0
else:
# we've exceeded the threshold, so pay more commission.
return per_share_total - order.commission
if isinstance(exchange_fee, (int, float)):
self._exchange_fee = DummyMapping(float(exchange_fee))
else:
# Exchange fee is a dictionary. If the user's dictionary does not
# provide an exchange fee for a certain contract, fall back on the
# pre-defined exchange fees per root symbol.
self._exchange_fee = merge(
FUTURE_EXCHANGE_FEES_BY_SYMBOL, exchange_fee,
)
self.min_trade_cost = min_trade_cost or 0
def __repr__(self):
if isinstance(self._cost_per_contract, DummyMapping):
# Cost per contract is a constant, so extract it.
cost_per_contract = self._cost_per_contract['dummy key']
else:
cost_per_contract = '<varies>'
if isinstance(self._exchange_fee, DummyMapping):
# Exchange fee is a constant, so extract it.
exchange_fee = self._exchange_fee['dummy key']
else:
exchange_fee = '<varies>'
return (
'{class_name}(cost_per_contract={cost_per_contract}, '
'exchange_fee={exchange_fee}, min_trade_cost={min_trade_cost})'
.format(
class_name=self.__class__.__name__,
cost_per_contract=cost_per_contract,
exchange_fee=exchange_fee,
min_trade_cost=self.min_trade_cost,
)
)
def calculate(self, order, transaction):
root_symbol = order.asset.root_symbol
cost_per_contract = self._cost_per_contract[root_symbol]
exchange_fee = self._exchange_fee[root_symbol]
return calculate_per_unit_commission(
order=order,
transaction=transaction,
cost_per_unit=cost_per_contract,
initial_commission=exchange_fee,
min_trade_cost=self.min_trade_cost,
)
class PerTrade(CommissionModel):
class PerEquityTrade(EquityCommissionModel):
"""
Calculates a commission for a transaction based on a per trade cost.
Parameters
----------
cost : float, optional
The flat amount of commissions paid per trade.
The flat amount of commissions paid per equity trade.
"""
def __init__(self, cost=DEFAULT_MINIMUM_COST_PER_TRADE):
def __init__(self, cost=DEFAULT_MINIMUM_COST_PER_EQUITY_TRADE):
"""
Cost parameter is the cost of a trade, regardless of share count.
$5.00 per trade is fairly typical of discount brokers.
@@ -140,6 +276,11 @@ class PerTrade(CommissionModel):
# logic does not floor to an integer.
self.cost = float(cost)
def __repr__(self):
return '{class_name}(cost_per_trade={cost})'.format(
class_name=self.__class__.__name__, cost=self.cost,
)
def calculate(self, order, transaction):
"""
If the order hasn't had a commission paid yet, pay the fixed
@@ -155,17 +296,49 @@ class PerTrade(CommissionModel):
return 0.0
class PerDollar(CommissionModel):
class PerFutureTrade(PerContract):
"""
Calculates a commission for a transaction based on a per trade cost.
Parameters
----------
cost : float
The flat amount of commissions paid per trade.
cost : float or dict
The flat amount of commissions paid per trade, regardless of the number
of contracts being traded. If given a float, the commission for all
futures contracts is the same. If given a dictionary, it must map root
symbols to the commission cost for trading contracts of that symbol.
"""
def __init__(self, cost=0.0015):
def __init__(self, cost=DEFAULT_MINIMUM_COST_PER_FUTURE_TRADE):
# The per-trade cost can be represented as the exchange fee in a
# per-contract model because the exchange fee is just a one time cost
# incurred on the first fill.
super(PerFutureTrade, self).__init__(
cost=0, exchange_fee=cost, min_trade_cost=0,
)
self._cost_per_trade = self._exchange_fee
def __repr__(self):
if isinstance(self._cost_per_trade, DummyMapping):
# Cost per trade is a constant, so extract it.
cost_per_trade = self._cost_per_trade['dummy key']
else:
cost_per_trade = '<varies>'
return '{class_name}(cost_per_trade={cost_per_trade})'.format(
class_name=self.__class__.__name__, cost_per_trade=cost_per_trade,
)
class PerDollar(EquityCommissionModel):
"""
Calculates a commission for a transaction based on a per dollar cost.
Parameters
----------
cost : float
The flat amount of commissions paid per dollar of equities traded.
"""
def __init__(self, cost=DEFAULT_PER_DOLLAR_COST):
"""
Cost parameter is the cost of a trade per-dollar. 0.0015
on $1 million means $1,500 commission (=1M * 0.0015)
@@ -183,3 +356,7 @@ class PerDollar(CommissionModel):
"""
cost_per_share = transaction.price * self.cost_per_dollar
return abs(transaction.amount) * cost_per_share
# Alias PerTrade for backwards compatibility.
PerTrade = PerEquityTrade
+156
View File
@@ -21,3 +21,159 @@ ANNUALIZER = {'daily': TRADING_DAYS_IN_YEAR,
'hourly': TRADING_DAYS_IN_YEAR * TRADING_HOURS_IN_DAY,
'minute': TRADING_DAYS_IN_YEAR * TRADING_HOURS_IN_DAY *
MINUTES_IN_HOUR}
# NOTE: It may be worth revisiting how the keys for this dictionary are
# specified, for instance making them ContinuousFuture objects instead of
# static strings.
FUTURE_EXCHANGE_FEES_BY_SYMBOL = {
'AD': 1.60, # AUD
'AI': 0.96, # Bloomberg Commodity Index
'BD': 1.50, # Big Dow
'BO': 1.95, # Soybean Oil
'BP': 1.60, # GBP
'CD': 1.60, # CAD
'CL': 1.50, # Crude Oil
'CM': 1.03, # Corn e-mini
'CN': 1.95, # Corn
'DJ': 1.50, # Dow Jones
'EC': 1.60, # Euro FX
'ED': 1.25, # Eurodollar
'EE': 1.50, # Euro FX e-mini
'EI': 1.50, # MSCI Emerging Markets mini
'EL': 1.50, # Eurodollar NYSE LIFFE
'ER': 0.65, # Russell2000 e-mini
'ES': 1.18, # SP500 e-mini
'ET': 1.50, # Ethanol
'EU': 1.50, # Eurodollar e-micro
'FC': 2.03, # Feeder Cattle
'FF': 0.96, # 3-Day Federal Funds
'FI': 0.56, # Deliverable Interest Rate Swap 5y
'FS': 1.50, # Interest Rate Swap 5y
'FV': 0.65, # US 5y
'GC': 1.50, # Gold
'HG': 1.50, # Copper
'HO': 1.50, # Heating Oil
'HU': 1.50, # Unleaded Gasoline
'JE': 0.16, # JPY e-mini
'JY': 1.60, # JPY
'LB': 2.03, # Lumber
'LC': 2.03, # Live Cattle
'LH': 2.03, # Lean Hogs
'MB': 1.50, # Municipal Bonds
'MD': 1.50, # SP400 Midcap
'ME': 1.60, # MXN
'MG': 1.50, # MSCI EAFE mini
'MI': 1.18, # SP400 Midcap e-mini
'MS': 1.03, # Soybean e-mini
'MW': 1.03, # Wheat e-mini
'ND': 1.50, # Nasdaq100
'NG': 1.50, # Natural Gas
'NK': 2.15, # Nikkei225
'NQ': 1.18, # Nasdaq100 e-mini
'NZ': 1.60, # NZD
'OA': 1.95, # Oats
'PA': 1.50, # Palladium
'PB': 1.50, # Pork Bellies
'PL': 1.50, # Platinum
'QG': 0.50, # Natural Gas e-mini
'QM': 1.20, # Crude Oil e-mini
'RM': 1.50, # Russell1000 e-mini
'RR': 1.95, # Rough Rice
'SB': 2.10, # Sugar
'SF': 1.60, # CHF
'SM': 1.95, # Soybean Meal
'SP': 2.40, # SP500
'SV': 1.50, # Silver
'SY': 1.95, # Soybean
'TB': 1.50, # Treasury Bills
'TN': 0.56, # Deliverable Interest Rate Swap 10y
'TS': 1.50, # Interest Rate Swap 10y
'TU': 1.50, # US 2y
'TY': 0.75, # US 10y
'UB': 0.85, # Ultra Tbond
'US': 0.80, # US 30y
'VX': 1.50, # VIX
'WC': 1.95, # Wheat
'XB': 1.50, # RBOB Gasoline
'XG': 0.75, # Gold e-mini
'YM': 1.50, # Dow Jones e-mini
'YS': 0.75, # Silver e-mini
}
# See `zipline.finance.slippage.VolatilityVolumeShare` for more information on
# how these constants are used.
DEFAULT_ETA = 0.049018143225019836
ROOT_SYMBOL_TO_ETA = {
'AD': DEFAULT_ETA, # AUD
'AI': DEFAULT_ETA, # Bloomberg Commodity Index
'BD': 0.050346811117733474, # Big Dow
'BO': 0.054930995070046298, # Soybean Oil
'BP': 0.047841544238716338, # GBP
'CD': 0.051124420640250717, # CAD
'CL': 0.04852544628414196, # Crude Oil
'CM': 0.052683478163348625, # Corn e-mini
'CN': 0.053499718390037809, # Corn
'DJ': 0.02313009072076987, # Dow Jones
'EC': 0.04885131067661861, # Euro FX
'ED': 0.094184297090245755, # Eurodollar
'EE': 0.048713151357687556, # Euro FX e-mini
'EI': 0.031712708439692663, # MSCI Emerging Markets mini
'EL': 0.044207422018209361, # Eurodollar NYSE LIFFE
'ER': 0.045930567737711307, # Russell2000 e-mini
'ES': 0.047304418321993502, # SP500 e-mini
'ET': DEFAULT_ETA, # Ethanol
'EU': 0.049750396084029064, # Eurodollar e-micro
'FC': 0.058728734202178494, # Feeder Cattle
'FF': 0.048970591527624042, # 3-Day Federal Funds
'FI': 0.033477176738170772, # Deliverable Interest Rate Swap 5y
'FS': 0.034557788010453824, # Interest Rate Swap 5y
'FV': 0.046544427716056963, # US 5y
'GC': 0.048933313546125207, # Gold
'HG': 0.052238417524987799, # Copper
'HO': 0.045061318412156062, # Heating Oil
'HU': 0.017154313062463938, # Unleaded Gasoline
'JE': 0.013948949613401812, # JPY e-mini
'JY': DEFAULT_ETA, # JPY
'LB': 0.06146586386903994, # Lumber
'LC': 0.055853801862858619, # Live Cattle
'LH': 0.057557004630219781, # Lean Hogs
'MB': DEFAULT_ETA, # Municipal Bonds
'MD': DEFAULT_ETA, # SP400 Midcap
'ME': 0.030383767727818548, # MXN
'MG': 0.029579261656151684, # MSCI EAFE mini
'MI': 0.041026288873007355, # SP400 Midcap e-mini
'MS': DEFAULT_ETA, # Soybean e-mini
'MW': 0.052579919663880245, # Wheat e-mini
'ND': DEFAULT_ETA, # Nasdaq100
'NG': 0.047897809233755716, # Natural Gas
'NK': 0.044555435054791433, # Nikkei225
'NQ': 0.044772425085977945, # Nasdaq100 e-mini
'NZ': 0.049170418073872041, # NZD
'OA': 0.056973267232775522, # Oats
'PA': DEFAULT_ETA, # Palladium
'PB': DEFAULT_ETA, # Pork Bellies
'PL': 0.054579379665647493, # Platinum
'QG': DEFAULT_ETA, # Natural Gas e-mini
'QM': DEFAULT_ETA, # Crude Oil e-mini
'RM': 0.037425041244579654, # Russell1000 e-mini
'RR': DEFAULT_ETA, # Rough Rice
'SB': 0.057388160345668134, # Sugar
'SF': 0.047784825569615726, # CHF
'SM': 0.048552860559844223, # Soybean Meal
'SP': DEFAULT_ETA, # SP500
'SV': 0.052691435039931109, # Silver
'SY': 0.052041703657281613, # Soybean
'TB': DEFAULT_ETA, # Treasury Bills
'TN': 0.033363465365262503, # Deliverable Interest Rate Swap 10y
'TS': 0.032908878455069152, # Interest Rate Swap 10y
'TU': 0.063867646063840794, # US 2y
'TY': 0.050586988554700826, # US 10y
'UB': DEFAULT_ETA, # Ultra Tbond
'US': 0.047984179873590722, # US 30y
'VX': DEFAULT_ETA, # VIX
'WC': 0.052636542119329242, # Wheat
'XB': 0.044444916388854484, # RBOB Gasoline
'XG': DEFAULT_ETA, # Gold e-mini
'YM': DEFAULT_ETA, # Dow Jones e-mini
'YS': DEFAULT_ETA, # Silver e-mini
}
+259 -10
View File
@@ -14,27 +14,36 @@
# limitations under the License.
from __future__ import division
import abc
from abc import ABCMeta, abstractmethod, abstractproperty
import math
from six import with_metaclass, iteritems
from toolz import merge
import numpy as np
from pandas import isnull
from zipline.assets import Equity, Future
from zipline.finance.constants import ROOT_SYMBOL_TO_ETA
from zipline.finance.transaction import create_transaction
from zipline.utils.cache import ExpiringCache
from zipline.utils.dummy import DummyMapping
SELL = 1 << 0
BUY = 1 << 1
STOP = 1 << 2
LIMIT = 1 << 3
SQRT_252 = math.sqrt(252)
DEFAULT_EQUITY_VOLUME_SLIPPAGE_BAR_LIMIT = 0.025
DEFAULT_FUTURE_VOLUME_SLIPPAGE_BAR_LIMIT = 0.025
NO_DATA_VOLATILITY_SLIPPAGE_IMPACT = 10.0 / 10000
class LiquidityExceeded(Exception):
pass
DEFAULT_VOLUME_SLIPPAGE_BAR_LIMIT = 0.025
def fill_price_worse_than_limit_price(fill_price, order):
"""
Checks whether the fill price is worse than the order's limit price.
@@ -68,7 +77,7 @@ def fill_price_worse_than_limit_price(fill_price, order):
return False
class SlippageModel(with_metaclass(abc.ABCMeta)):
class SlippageModel(with_metaclass(ABCMeta)):
"""Abstract interface for defining a slippage model.
"""
def __init__(self):
@@ -78,7 +87,14 @@ class SlippageModel(with_metaclass(abc.ABCMeta)):
def volume_for_bar(self):
return self._volume_for_bar
@abc.abstractproperty
@abstractproperty
def allowed_asset_types(self):
"""
Return a tuple of asset types that are compatible with the given model.
"""
raise NotImplementedError('allowed_asset_types')
@abstractproperty
def process_order(self, data, order):
"""Process how orders get filled.
@@ -162,11 +178,27 @@ class SlippageModel(with_metaclass(abc.ABCMeta)):
return self.__dict__
class VolumeShareSlippage(SlippageModel):
"""Model slippage as a function of the volume of shares traded.
class EquitySlippageModel(SlippageModel):
"""
Base class for slippage models which only support equities.
"""
allowed_asset_types = (Equity,)
def __init__(self, volume_limit=DEFAULT_VOLUME_SLIPPAGE_BAR_LIMIT,
class FutureSlippageModel(SlippageModel):
"""
Base class for slippage models which only support futures.
"""
allowed_asset_types = (Future,)
class VolumeShareSlippage(SlippageModel):
"""
Model slippage as a function of the volume of contracts traded.
"""
allowed_asset_types = (Equity, Future)
def __init__(self, volume_limit=DEFAULT_EQUITY_VOLUME_SLIPPAGE_BAR_LIMIT,
price_impact=0.1):
self.volume_limit = volume_limit
@@ -234,17 +266,24 @@ class VolumeShareSlippage(SlippageModel):
class FixedSlippage(SlippageModel):
"""Model slippage as a fixed spread.
"""
Model slippage as a fixed spread.
Parameters
----------
spread : float, optional
spread / 2 will be added to buys and subtracted from sells.
"""
allowed_asset_types = (Equity, Future)
def __init__(self, spread=0.0):
self.spread = spread
def __repr__(self):
return '{class_name}(spread={spread})'.format(
class_name=self.__class__.__name__, spread=self.spread,
)
def process_order(self, data, order):
price = data.current(order.asset, "close")
@@ -252,3 +291,213 @@ class FixedSlippage(SlippageModel):
price + (self.spread / 2.0 * order.direction),
order.amount
)
class MarketImpactBase(object):
"""
Base class for slippage models which compute a simulated price impact
according to a history lookback.
"""
def __init__(self):
super(MarketImpactBase, self).__init__()
self._window_data_cache = ExpiringCache()
@abstractmethod
def get_txn_volume(self, data, order):
"""
Return the number of shares we would like to order in this minute.
Parameters
----------
data : BarData
order : Order
Return
------
int : the number of shares
"""
raise NotImplementedError('get_txn_volume')
@abstractmethod
def simulated_impact(self,
order,
current_price,
current_volume,
txn_volume,
mean_volume,
volatility):
"""
Calculate simulated price impact.
Parameters
----------
order : The order being processed.
current_price : Current price of the asset being ordered.
current_volume : Volume of the asset being ordered for the current bar.
txn_volume : Number of shares/contracts being ordered.
mean_volume : Trailing ADV of the asset.
volatility : Annualized daily volatility of volume.
Return
------
int : impact on the current price.
"""
raise NotImplementedError('simulated_impact')
def process_order(self, data, order):
if order.open_amount == 0:
return None, None
minute_data = data.current(order.asset, ['volume', 'high', 'low'])
mean_volume, volatility = self._get_window_data(data, order.asset, 20)
# Price to use is the average of the minute bar's open and close.
price = np.mean([minute_data['high'], minute_data['low']])
volume = minute_data['volume']
if not volume:
return None, None
txn_volume = int(
min(self.get_txn_volume(data, order), abs(order.open_amount))
)
if mean_volume == 0 or np.isnan(volatility):
# If this is the first day the contract exists or there is no
# volume history, default to a conservative estimate of impact.
simulated_impact = price * NO_DATA_VOLATILITY_SLIPPAGE_IMPACT
else:
simulated_impact = self.get_simulated_impact(
order=order,
current_price=price,
current_volume=volume,
txn_volume=txn_volume,
mean_volume=mean_volume,
volatility=volatility,
)
impacted_price = \
price + math.copysign(simulated_impact, order.direction)
if fill_price_worse_than_limit_price(impacted_price, order):
return None, None
return impacted_price, math.copysign(txn_volume, order.direction)
def _get_window_data(self, data, asset, window_length):
"""
Internal utility method to return the trailing mean volume over the
past 'window_length' days, and volatility of close prices for a
specific asset.
Parameters
----------
data : The BarData from which to fetch the daily windows.
asset : The Asset whose data we are fetching.
window_length : Number of days of history used to calculate the mean
volume and close price volatility.
Returns
-------
(mean volume, volatility)
"""
try:
values = self._window_data_cache.get(asset, data.current_session)
except KeyError:
# Add a day because we want 'window_length' complete days,
# excluding the current day.
volume_history = data.history(
asset, 'volume', window_length + 1, '1d',
)
close_history = data.history(
asset, 'close', window_length + 1, '1d',
)
# Exclude the first value of the percent change array because it is
# always just NaN.
close_volatility = close_history[:-1].pct_change()[1:].std(
skipna=False,
)
values = {
'volume': volume_history[:-1].mean(),
'close': close_volatility * SQRT_252,
}
self._window_data_cache.set(asset, values, data.current_session)
return values['volume'], values['close']
class VolatilityVolumeShare(MarketImpactBase, FutureSlippageModel):
"""
Model slippage for futures contracts according to the following formula:
new_price = price + (price * MI / 10000),
where 'MI' is market impact, which is defined as:
MI = eta * sigma * sqrt(psi)
Eta is a constant which varies by root symbol.
Sigma is 20-day annualized volatility.
Psi is the volume traded in the given bar divided by 20-day ADV.
Parameters
----------
volume_limit : float
Maximum percentage (as a decimal) of a bar's total volume that can be
traded.
eta : float or dict
Constant used in the market impact formula. If given a float, the eta
for all futures contracts is the same. If given a dictionary, it must
map root symbols to the eta for contracts of that symbol.
"""
allowed_asset_types = (Future,)
def __init__(self, volume_limit, eta=ROOT_SYMBOL_TO_ETA):
super(VolatilityVolumeShare, self).__init__()
self.volume_limit = volume_limit
# If 'eta' is a constant, use a dummy mapping to treat it as a
# dictionary that always returns the same value.
# NOTE: This dictionary does not handle unknown root symbols, so it may
# be worth revisiting this behavior.
if isinstance(eta, (int, float)):
self._eta = DummyMapping(float(eta))
else:
# Eta is a dictionary. If the user's dictionary does not provide a
# value for a certain contract, fall back on the pre-defined eta
# values per root symbol.
self._eta = merge(ROOT_SYMBOL_TO_ETA, eta)
def __repr__(self):
if isinstance(self._eta, DummyMapping):
# Eta is a constant, so extract it.
eta = self._eta['dummy key']
else:
eta = '<varies>'
return '{class_name}(volume_limit={volume_limit}, eta={eta})'.format(
class_name=self.__class__.__name__,
volume_limit=self.volume_limit,
eta=eta,
)
def get_simulated_impact(self,
order,
current_price,
current_volume,
txn_volume,
mean_volume,
volatility):
eta = self._eta[order.asset.root_symbol]
psi = txn_volume / mean_volume
market_impact = eta * volatility * math.sqrt(psi)
# We divide by 10,000 because this model computes to basis points.
# To convert from bps to % we need to divide by 100, then again to
# convert from % to fraction.
return (current_price * market_impact) / 10000
def get_txn_volume(self, data, order):
volume = data.current(order.asset, 'volume')
return volume * self.volume_limit
+3
View File
@@ -1,3 +1,4 @@
from zipline.assets import Equity
from zipline.finance.slippage import SlippageModel
from zipline.utils.sentinel import sentinel
@@ -19,6 +20,8 @@ class TestingSlippage(SlippageModel):
"""
ALL = sentinel('ALL')
allowed_asset_types = (Equity,)
def __init__(self, filled_per_tick):
self.filled_per_tick = filled_per_tick
+10
View File
@@ -0,0 +1,10 @@
class DummyMapping(object):
"""
Dummy object used to provide a mapping interface for singular values.
"""
def __init__(self, value):
self._value = value
def __getitem__(self, key):
return self._value