mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-05 03:54:13 +08:00
API: Add slippage and commission models for futures
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user