mirror of
https://github.com/wassname/catalyst.git
synced 2026-09-09 11:19:23 +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):
|
||||
|
||||
Reference in New Issue
Block a user