Last active
June 14, 2023 15:04
-
-
Save cs224/4f8624cc78ddd94f5aa0061600559803 to your computer and use it in GitHub Desktop.
Trading Evolved: Futures Trend Following
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import futures_trading | |
import os | |
from zipline.utils.run_algo import load_extensions | |
load_extensions( | |
default=True, | |
extensions=['/home/xxx/.zipline/extension.py'], | |
strict=True, | |
environ=os.environ, | |
) | |
from IPython.display import display | |
import ipywidgets as widgets | |
out = widgets.HTML() | |
display(out) | |
start_date='2003-01-01' | |
end_date='2020-10-01' | |
file_name = '{}-trendmodel-{}-{}.dill'.format(datetime.datetime.now().strftime('%Y-%m-%d-%H%M'), pd.to_datetime(start_date).strftime('%Y%m%d'), pd.to_datetime(end_date).strftime('%Y%m%d')) | |
print(file_name) | |
#widgets_html_out=out | |
algo = futures_trading.FuturesTrendAlgorithm(enable_commission=True, enable_slippage=True, start_date=start_date, end_date=end_date).execute() | |
print(algo.quick_report_result()) | |
# algo.save_perf(file_name) | |
algo.analyze() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import zipline | |
import zipline.api | |
import datetime | |
import pytz | |
import matplotlib, matplotlib.pyplot as plt | |
import pyfolio as pf | |
import pandas as pd | |
import numpy as np | |
import numba, numba.extending | |
import zipline.finance.commission | |
import zipline.finance.constants | |
import zipline.finance.slippage | |
import trading_calendars | |
import empyrical as em | |
import yfinance as yf | |
import multiprocessing | |
import tqdm | |
import re | |
from IPython.core.display import display, HTML | |
import csi_futures_data.csi_futures_data as cfd | |
cfd.futures_lookup_ | |
import warnings, dill | |
import os, threading | |
import collections | |
from zipline.utils.run_algo import load_extensions | |
load_extensions( | |
default=True, | |
extensions=['/home/xxx/.zipline/extension.py'], | |
strict=True, | |
environ=os.environ, | |
) | |
def create_closure(instance, func): | |
def fn(*args): | |
return func(instance, *args) | |
return fn | |
root_symbol_to_η = collections.defaultdict(lambda: zipline.finance.constants.DEFAULT_ETA) | |
root_symbol_to_η.update(zipline.finance.constants.ROOT_SYMBOL_TO_ETA) | |
bundle_name = 'csi_futures_data' | |
class FuturesAlgorithm(): | |
def __init__(self, starting_portfolio=50000000, bundle_name = bundle_name, enable_commission=False, enable_slippage=False, start_date=None, end_date=None, risk_factor = 0.001): | |
self.bundle_name = bundle_name | |
self.risk_factor = risk_factor | |
self.starting_portfolio = starting_portfolio | |
# Default values for futures constants : FUTURE_EXCHANGE_FEES_BY_SYMBOL and ROOT_SYMBOL_TO_ETA : https://github.com/quantopian/zipline/issues/2340 | |
self.enable_commission = enable_commission | |
self.enable_slippage = enable_slippage | |
self.widgets_html_out = None | |
self.start_date = start_date | |
self.end_date = end_date | |
self.fig_ax = None | |
self.markets = None | |
def save_perf(self, file_name): | |
if not hasattr(self, 'perf') or self.perf is None: | |
raise RuntimeError('You need to first run the algorithm before you can save the state') | |
with open(file_name, 'wb') as file: | |
dill.dump(self.perf, file) | |
def quick_report_result(self): | |
return self.perf.portfolio_value[-1] / self.perf.portfolio_value[0] | |
def report_result(self, context, data,): | |
context.months += 1 | |
today = zipline.api.get_datetime().date() | |
# Calculate annualized return so far | |
ann_ret = np.power(context.portfolio.portfolio_value / self.starting_portfolio, 12 / context.months) - 1 | |
# Update the text | |
if self.widgets_html_out is not None: | |
self.widgets_html_out.value = """{} We have traded <b>{}</b> months and the annualized return is <b>{:.2%}</b>""".format(today, context.months, ann_ret) | |
else: | |
return today, context.months, ann_ret | |
def update_chart(self, context, data): | |
if self.fig_ax is None: | |
return | |
fig, ax, ax2 = None, None, None | |
if len(self.fig_ax) == 2: | |
fig, ax = self.fig_ax | |
else: | |
fig, ax, ax2 = self.fig_ax | |
# This function continuously update the graph during the backtest | |
today = data.current_session.date() | |
pv = context.portfolio.portfolio_value | |
exp = context.portfolio.positions_exposure | |
self.dynamic_results.loc[today, 'PortfolioValue'] = pv | |
if ax.lines: # Update existing line | |
ax.lines[0].set_xdata(self.dynamic_results.index) | |
ax.lines[0].set_ydata(self.dynamic_results.PortfolioValue) | |
else: # Create new line | |
ax.semilogy(self.dynamic_results) | |
# Update scales min/max | |
min = self.dynamic_results.PortfolioValue.min() | |
max = self.dynamic_results.PortfolioValue.max() | |
if min <= 0: | |
min = 0.1 | |
if max <= min: | |
max = min + 1 | |
ax.set_ylim(min,max) | |
min = self.dynamic_results.index.min() | |
max = self.dynamic_results.index.max() | |
if max <= min: | |
max = min + pd.DateOffset(days=1) | |
ax.set_xlim(min, max) | |
if ax2 is not None: | |
drawdown = (pv / self.dynamic_results['PortfolioValue'].max()) - 1 | |
exposure = exp / pv | |
self.dynamic_results.loc[today, 'Drawdown'] = drawdown | |
if ax2.lines: | |
ax2.lines[0].set_xdata(self.dynamic_results.index) | |
ax2.lines[0].set_ydata(self.dynamic_results.Drawdown) | |
else: | |
ax2.plot(self.dynamic_results.Drawdown) | |
min = self.dynamic_results.Drawdown.min() | |
max = self.dynamic_results.Drawdown.max() | |
if min <= 0: | |
min = 0.1 | |
if max <= min: | |
max = min + 1 | |
ax2.set_ylim(min, max) | |
min = self.dynamic_results.index.min() | |
max = self.dynamic_results.index.max() | |
if max <= min: | |
max = min + pd.DateOffset(days=1) | |
ax2.set_xlim(min, max) | |
# Redraw the graph | |
fig.canvas.draw() | |
def analyze(self, perf=None): | |
if perf is None: | |
perf = self.perf | |
returns, positions, transactions = pf.utils.extract_rets_pos_txn_from_zipline(perf) | |
with warnings.catch_warnings(): | |
warnings.simplefilter("ignore") | |
pf.create_returns_tear_sheet(returns, benchmark_rets=None) | |
def set_up_commission_and_slippage(self, context): | |
""" | |
Cost Settings | |
""" | |
context.enable_commission = self.enable_commission | |
context.enable_slippage = self.enable_slippage | |
if self.enable_commission: | |
comm_model = zipline.finance.commission.PerContract(cost=0.85, exchange_fee=1.5) | |
else: | |
comm_model = zipline.finance.commission.PerTrade(cost=0.0) | |
self.comm_model = comm_model | |
zipline.api.set_commission(us_futures=comm_model) | |
if self.enable_slippage: | |
slippage_model = zipline.finance.slippage.VolatilityVolumeShare(volume_limit=0.2) # 0.3 | |
else: | |
slippage_model = zipline.finance.slippage.FixedSlippage(spread=0.0) | |
self.slippage_model = slippage_model | |
zipline.api.set_slippage(us_futures=slippage_model) | |
def convertContractName(self, contract): | |
root_symbol = contract.symbol[0:2] | |
month_letter = contract.symbol[2:3] | |
year_digits = contract.symbol[3:5] | |
lexically_sortable_name = root_symbol + year_digits + month_letter | |
return lexically_sortable_name | |
def order_target(self, asset, amount): | |
open_orders = zipline.api.get_open_orders(asset) | |
for order in open_orders: | |
# if order.sid.symbol == 'PAH19': | |
# print(order) | |
zipline.api.cancel_order(order) | |
return zipline.api.order_target(asset, amount) | |
def roll_futures(self, context, data): | |
today = data.current_session.date() | |
open_orders = zipline.api.get_open_orders() | |
for held_contract in context.portfolio.positions: | |
# don't roll positions that are set to change by core logic | |
if held_contract in open_orders: | |
continue | |
# Save some time by only checking rolls for | |
# contracts stopping trading in the next days | |
days_to_auto_close = (held_contract.auto_close_date.date() - today).days | |
if days_to_auto_close > 20: | |
continue | |
# Make a continuation | |
continuation = zipline.api.continuous_future( | |
held_contract.root_symbol, | |
offset=0, | |
roll='volume', | |
adjustment='mul' | |
) | |
# Get the current contract of the continuation | |
continuation_contract = data.current(continuation, 'contract') | |
if days_to_auto_close <= 10: | |
current_chain = data.current_chain(continuation) | |
if len(current_chain) <= 1: | |
print("{}: The current_chain for continuation: {} is empty??".format(today, held_contract.root_symbol)) | |
else: | |
try: | |
next_contract = current_chain[1] | |
continuation_contract = next_contract | |
except: | |
print("{}: Ran into exception, even after length check: {}, The current_chain for continuation: {} is empty??".format(today, len(current_chain), held_contract.root_symbol)) | |
# if held_contract.symbol in ['CLK20'] and days_to_auto_close <= 10: | |
# current_chain = data.current_chain(continuation) | |
# next_contract = current_chain[1] | |
# print('CLK20, {}, {}'.format(data.current_session.date(), next_contract)) | |
# continuation_contract = next_contract | |
if self.convertContractName(continuation_contract) > self.convertContractName(held_contract): | |
# Check how many contracts we hold | |
pos_size = context.portfolio.positions[held_contract].amount | |
# Close current position | |
self.order_target(held_contract, 0) | |
# Open new position | |
self.order_target(continuation_contract, pos_size) | |
def position_size(self, portfolio_value, std, point_value, avg_volume=None): | |
target_variation = portfolio_value * self.risk_factor | |
contract_variation = std * point_value | |
contracts = target_variation / contract_variation | |
return int(np.nan_to_num(contracts)) | |
def initialize(self, context): | |
# Schedule daily roll check | |
zipline.api.schedule_function(create_closure(self, self.roll_futures.__func__), zipline.api.date_rules.every_day(), zipline.api.time_rules.market_close()) | |
context.pbar_month_count = 0 | |
zipline.api.schedule_function(create_closure(self, self.update_pbar.__func__), zipline.api.date_rules.month_start(), zipline.api.time_rules.market_close()) | |
# Schedule charting | |
if self.fig_ax is not None: | |
self.dynamic_results = pd.DataFrame() | |
zipline.api.schedule_function(create_closure(self, self.update_chart.__func__), zipline.api.date_rules.month_start(), zipline.api.time_rules.market_close()) | |
# Schedule monthly report output | |
if self.widgets_html_out is not None: | |
context.months = 0 | |
zipline.api.schedule_function(func=create_closure(self, self.report_result.__func__), date_rule=zipline.api.date_rules.month_start(), time_rule=zipline.api.time_rules.market_open()) | |
self.set_up_commission_and_slippage(context) | |
# Make a list of all continuations | |
context.universe = [zipline.api.continuous_future(market, offset=0, roll='volume', adjustment='mul') for market in self.markets] | |
def daily_trade(self, context, data): | |
pass | |
def configure(self, start_date, end_date): | |
self.start_date = start_date | |
self.end_date = end_date | |
return self | |
def update_pbar(self, context, data): | |
context.pbar_month_count += 1 | |
self.pbar.update(1) | |
def execute(self, start_date=None, end_date=None, widgets_html_out=None, fig_ax=None): | |
self.widgets_html_out = widgets_html_out | |
self.fig_ax = fig_ax | |
if start_date is not None: | |
self.start_date = start_date | |
if end_date is not None: | |
self.end_date = end_date | |
# https://pvlib-python.readthedocs.io/en/stable/timetimezones.html | |
start = pd.Timestamp(self.start_date, tz=pytz.UTC) | |
end = pd.Timestamp(self.end_date, tz=pytz.UTC) | |
self.month_delta = (end.year - start.year)*12 + end.month - start.month | |
with warnings.catch_warnings(): | |
warnings.filterwarnings("ignore", module='empyrical') | |
with tqdm.tqdm(total=self.month_delta) as pbar: | |
self.pbar = pbar | |
self.perf = zipline.run_algorithm( | |
start=start, end=end, | |
initialize=create_closure(self, self.initialize.__func__), | |
capital_base=self.starting_portfolio, | |
data_frequency='daily', | |
# benchmark_returns=zipline.utils.run_algo.BenchmarkSpec._zero_benchmark_returns(start_date=start,end_date=end), | |
bundle=self.bundle_name) | |
return self | |
class FuturesTrendAlgorithm(FuturesAlgorithm): | |
def __init__(self, starting_portfolio=50000000, bundle_name = bundle_name, enable_commission=False, enable_slippage=False, start_date=None, end_date=None, risk_factor = 0.0015): | |
super().__init__(starting_portfolio=starting_portfolio, bundle_name = bundle_name, enable_commission=enable_commission, enable_slippage=enable_slippage, start_date=start_date, end_date=end_date, risk_factor = risk_factor) | |
self.stop_distance = 3 | |
self.breakout_window = 50 | |
self.vola_window = 40 | |
self.slow_ma = 80 | |
self.fast_ma = 40 | |
self.markets = cfd.get_bundle_market_symbols('trend_following_markets') | |
def initialize(self, context): | |
super().initialize(context) | |
# We'll use these to keep track of best position reading | |
# Used to calculate stop points. | |
context.highest_in_position = {market: 0 for market in self.markets} | |
context.lowest_in_position = {market: 0 for market in self.markets} | |
# Schedule the daily trading | |
zipline.api.schedule_function(create_closure(self, self.daily_trade.__func__), zipline.api.date_rules.every_day(), zipline.api.time_rules.market_close()) | |
def exit_position(self, context, data, open_pos, continuation): | |
# if data.current(continuation, 'contract').symbol == 'CTH19': | |
# #print(data.current(continuation, 'contract').symbol) | |
# pass | |
root = continuation.root_symbol | |
contract = open_pos[root] | |
self.order_target(contract, 0) | |
context.highest_in_position[root] = 0 | |
context.lowest_in_position[root] = 0 | |
def daily_trade(self, context, data): | |
# Get continuation data | |
hist = data.history( | |
context.universe, | |
fields=['close', 'volume'], | |
frequency='1d', | |
bar_count=250, | |
) | |
# Calculate trend | |
hist['trend'] = hist['close'].ewm(span=self.fast_ma).mean() > hist['close'].ewm(span=self.slow_ma).mean() | |
# Make dictionary of open positions | |
open_pos = { | |
pos.root_symbol: pos for pos in context.portfolio.positions | |
} | |
# Iterate markets, check for trades | |
for continuation in context.universe: | |
if data.current(continuation, 'contract') is None: | |
continue | |
# if data.current(continuation, 'contract').symbol == 'CTH19': | |
# #print(data.current(continuation, 'contract').symbol) | |
# pass | |
# Get root symbol of continuation | |
root = continuation.root_symbol | |
# Slice off history for just this market | |
h = hist.xs(continuation, 2) | |
# Get standard deviation | |
std = h.close.diff()[-self.vola_window:].std() | |
if root in open_pos: # Position is open | |
# Get position | |
p = context.portfolio.positions[open_pos[root]] | |
if p.amount > 0: # Position is long | |
if context.highest_in_position[root] == 0: # First day holding the position | |
context.highest_in_position[root] = p.last_sale_price | |
else: | |
context.highest_in_position[root] = max( | |
h['close'].iloc[-1], context.highest_in_position[root] | |
) | |
# Calculate stop point | |
stop = context.highest_in_position[root] - (std * self.stop_distance) | |
# Check if stop is hit | |
if h.iloc[-1]['close'] < stop: | |
self.exit_position(context, data, open_pos, continuation) | |
# contract = open_pos[root] | |
# self.order_target(contract, 0) | |
# context.highest_in_position[root] = 0 | |
# Check if trend has flipped | |
elif h['trend'].iloc[-1] == False: | |
self.exit_position(context, data, open_pos, continuation) | |
# contract = open_pos[root] | |
# self.order_target(contract, 0) | |
# context.highest_in_position[root] = 0 | |
else: # Position is short | |
#print(context.lowest_in_position[root]) | |
if context.lowest_in_position[root] == 0: # First day holding the position | |
context.lowest_in_position[root] = p.last_sale_price | |
else: | |
context.lowest_in_position[root] = min( | |
h['close'].iloc[-1], context.lowest_in_position[root] | |
) | |
# Calculate stop point | |
stop = context.lowest_in_position[root] + (std * self.stop_distance) | |
# Check if stop is hit | |
if h.iloc[-1]['close'] > stop: | |
self.exit_position(context, data, open_pos, continuation) | |
# contract = open_pos[root] | |
# self.order_target(contract, 0) | |
# context.lowest_in_position[root] = 0 | |
# Check if trend has flipped | |
elif h['trend'].iloc[-1] == True: | |
self.exit_position(context, data, open_pos, continuation) | |
# contract = open_pos[root] | |
# self.order_target(contract, 0) | |
# context.lowest_in_position[root] = 0 | |
else: # No position on | |
if h['trend'].iloc[-1]: # Bull trend | |
# Check if we just made a new high | |
if h['close'][-1] == h[-self.breakout_window:]['close'].max(): | |
contract = data.current(continuation, 'contract') | |
contracts_to_trade = self.position_size(context.portfolio.portfolio_value, std, contract.price_multiplier) #, h['volume'][-20:].mean() | |
# Limit size to 20% of avg. daily volume | |
contracts_cap = int(h['volume'][-20:].mean() * 0.2) | |
contracts_to_trade = min(contracts_to_trade, contracts_cap) | |
# Place the order | |
self.order_target(contract, contracts_to_trade) | |
else: # Bear trend | |
# Check if we just made a new low | |
if h['close'][-1] == h[-self.breakout_window:]['close'].min(): | |
contract = data.current(continuation, 'contract') | |
contracts_to_trade = self.position_size(context.portfolio.portfolio_value,std, contract.price_multiplier) # , h['volume'][-20:].mean() | |
# Limit size to 20% of avg. daily volume | |
contracts_cap = int(h['volume'][-20:].mean() * 0.2) | |
contracts_to_trade = min(contracts_to_trade, contracts_cap) | |
# Place the order | |
self.order_target(contract, -1 * contracts_to_trade) | |
class FuturesTimeReturnAlgorithm(FuturesAlgorithm): | |
def __init__(self, starting_portfolio=10000000, bundle_name = bundle_name, enable_commission=False, enable_slippage=False, start_date=None, end_date=None, risk_factor = 0.001): | |
super().__init__(starting_portfolio=starting_portfolio, bundle_name = bundle_name, enable_commission=enable_commission, enable_slippage=enable_slippage, start_date=start_date, end_date=end_date, risk_factor = risk_factor) | |
self.vola_window = 60 | |
self.short_trend_window = 125 | |
self.long_trend_window = 250 | |
self.markets = cfd.get_bundle_market_symbols('time_return_markets') | |
def initialize(self, context): | |
super().initialize(context) | |
# Schedule daily trading | |
zipline.api.schedule_function(create_closure(self, self.rebalance.__func__), zipline.api.date_rules.month_start(), zipline.api.time_rules.market_close()) | |
def rebalance(self, context, data): | |
# Get the history | |
hist = data.history( | |
context.universe, | |
fields=['close', 'volume'], | |
frequency='1d', | |
bar_count=self.long_trend_window, | |
) | |
# Make a dictionary of open positions | |
open_pos = {pos.root_symbol: pos for pos in context.portfolio.positions} | |
# Loop all markets | |
for continuation in context.universe: | |
# Slice off history for this market | |
h = hist.xs(continuation, 2) | |
root = continuation.root_symbol | |
# Calculate volatility | |
std = h.close.diff()[-self.vola_window:].std() | |
if root in open_pos: # Position is already open | |
p = context.portfolio.positions[open_pos[root]] | |
if p.amount > 0: # Long position | |
if h.close[-1] < h.close[-self.long_trend_window]: | |
# Lost slow trend, close position | |
self.order_target(open_pos[root], 0) | |
elif h.close[-1] < h.close[-self.short_trend_window]: | |
# Lost fast trend, close position | |
self.order_target(open_pos[root], 0) | |
else: # Short position | |
if h.close[-1] > h.close[-self.long_trend_window]: | |
# Lost slow trend, close position | |
self.order_target(open_pos[root], 0) | |
elif h.close[-1] > h.close[-self.short_trend_window]: | |
# Lost fast trend, close position | |
self.order_target(open_pos[root], 0) | |
else: # No position open yet. | |
if (h.close[-1] > h.close[-self.long_trend_window]) and (h.close[-1] > h.close[-self.short_trend_window]): | |
# Buy new position | |
contract = data.current(continuation, 'contract') | |
contracts_to_trade = self.position_size(context.portfolio.portfolio_value, std, contract.price_multiplier) # , h['volume'][-20:].mean() | |
self.order_target(contract, contracts_to_trade) | |
elif (h.close[-1] < h.close[-self.long_trend_window]) \ | |
and \ | |
(h.close[-1] < h.close[-self.short_trend_window]): | |
# New short position | |
contract = data.current(continuation, 'contract') | |
contracts_to_trade = self.position_size(context.portfolio.portfolio_value, std, contract.price_multiplier) # , h['volume'][-20:].mean() | |
self.order_target(contract, contracts_to_trade * -1) | |
class FuturesCounterTrendTradingAlgorithm(FuturesAlgorithm): | |
def __init__(self, starting_portfolio=20000000, bundle_name = bundle_name, enable_commission=False, enable_slippage=False, start_date=None, end_date=None, risk_factor = 0.0015): | |
super().__init__(starting_portfolio=starting_portfolio, bundle_name = bundle_name, enable_commission=enable_commission, enable_slippage=enable_slippage, start_date=start_date, end_date=end_date, risk_factor = risk_factor) | |
self.vola_window = 40 | |
self.slow_ma = 80 | |
self.fast_ma = 40 | |
self.high_window = 20 | |
self.days_to_hold = 20 | |
self.dip_buy = -3 | |
self.markets = cfd.get_bundle_market_symbols('counter_trend_markets') | |
def initialize(self, context): | |
super().initialize(context) | |
# Dictionary used for keeping track of how many days a position has been open. | |
context.bars_held = {market.root_symbol: 0 for market in context.universe} | |
zipline.api.schedule_function(create_closure(self, self.daily_trade.__func__), zipline.api.date_rules.every_day(), zipline.api.time_rules.market_close()) | |
def daily_trade(self, context, data): | |
open_pos = {pos.root_symbol: pos for pos in context.portfolio.positions} | |
hist = data.history( | |
context.universe, | |
fields=['close', 'volume'], | |
frequency='1d', | |
bar_count=250, | |
) | |
# Calculate the trend | |
hist['trend'] = hist['close'].ewm(span=self.fast_ma).mean() > hist['close'].ewm(span=self.slow_ma).mean() | |
for continuation in context.universe: | |
root = continuation.root_symbol | |
# Slice off history for this market | |
h = hist.xs(continuation, 2) | |
# Calculate volatility | |
std = h.close.diff()[-self.vola_window:].std() | |
if root in open_pos: # Check open positions first. | |
context.bars_held[root] += 1 # One more day held | |
if context.bars_held[root] >= 20: | |
# Held for a month, exit | |
contract = open_pos[root] | |
self.order_target(contract, 0) | |
elif h['trend'].iloc[-1] == False: | |
# Trend changed, exit. | |
contract = open_pos[root] | |
self.order_target(contract, 0) | |
else: # Check for new entries | |
if h['trend'].iloc[-1]: | |
# Calculate the pullback | |
pullback = ( | |
h['close'].values[-1] - np.max(h['close'].values[-self.high_window:]) | |
) / std | |
if pullback < self.dip_buy: | |
# Get the current contract | |
contract = data.current(continuation, 'contract') | |
# Calculate size | |
contracts_to_trade = self.position_size(context.portfolio.portfolio_value, std, contract.price_multiplier) | |
# Trade | |
self.order_target(contract, contracts_to_trade) | |
# Reset bar count to zero | |
context.bars_held[root] = 0 | |
class FuturesCurveTradingAlgorithm(FuturesAlgorithm): | |
def __init__(self, starting_portfolio=1000000, bundle_name = bundle_name, enable_commission=False, enable_slippage=False, start_date=None, end_date=None, risk_factor = 0.0015): | |
super().__init__(starting_portfolio=starting_portfolio, bundle_name = bundle_name, enable_commission=enable_commission, enable_slippage=enable_slippage, start_date=start_date, end_date=end_date, risk_factor = risk_factor) | |
self.spread_months = 12 | |
self.pos_per_side = 5 | |
self.target_exposure_per_side = 1.5 | |
self.volume_order_cap = 0.25 | |
self.markets = cfd.get_bundle_market_symbols('curve_trading_markets') | |
def initialize(self, context): | |
super().initialize(context) | |
zipline.api.schedule_function(create_closure(self, self.weekly_trade.__func__), zipline.api.date_rules.week_start(), zipline.api.time_rules.market_close()) | |
def weekly_trade(self, context, data): | |
# Empty DataFrame to be filled in later. | |
carry_df = pd.DataFrame(index=context.universe) | |
today = data.current_session.date() | |
for continuation in context.universe: | |
# Get the chain | |
chain = data.current_chain(continuation) | |
# Transform the chain into dataframe | |
df = pd.DataFrame(index=chain) | |
for contract in chain: | |
df.loc[contract, 'future'] = contract | |
df.loc[contract, 'expiration_date'] = contract.expiration_date | |
# Locate the contract closest to the target date. | |
# X months out from the front contract. | |
closest_expiration_date = df.iloc[0].expiration_date | |
target_expiration_date = closest_expiration_date + pd.DateOffset(months=self.spread_months) | |
df['days_to_target'] = abs(df.expiration_date - target_expiration_date) | |
target_contract = df.loc[df.days_to_target == df.days_to_target.min()] | |
# Get prices for front contract and target contract | |
prices = data.current( | |
[ | |
df.index[0], | |
target_contract.index[0] | |
], | |
'close' | |
) | |
# Check the exact day difference between the contracts | |
days_to_front = int( | |
(target_contract.expiration_date - closest_expiration_date)[0].days | |
) | |
# Calculate the annualized carry | |
try: | |
annualized_carry = (np.power( | |
(prices[0] / prices[1]), (365 / days_to_front)) | |
) - 1 | |
except: | |
print('{}: {} / {}: Problem in calculating annualized_carry: target_contract_price: {}, days_to_front: {}'.format(today, df.iloc[0].future, target_contract.index[0], prices[1], days_to_front)) | |
annualized_carry = 0.0 | |
carry_df.loc[continuation, 'front'] = df.iloc[0].future | |
carry_df.loc[continuation, 'next'] = target_contract.index[0] | |
carry_df.loc[continuation, 'carry'] = annualized_carry | |
# Sort on carry | |
carry_df.sort_values('carry', inplace=True, ascending=False) | |
carry_df.dropna(inplace=True) | |
new_portfolio = [] | |
new_longs = [] | |
new_shorts = [] | |
# Contract Selection | |
for i in np.arange(0, self.pos_per_side): | |
j = -(i + 1) | |
# Buy top, short bottom | |
long_contract = carry_df.iloc[i].next | |
short_contract = carry_df.iloc[j].next | |
new_longs.append(long_contract) | |
new_shorts.append(short_contract) | |
# Get data for the new portfolio | |
new_portfolio = new_longs + new_shorts | |
hist = data.history(new_portfolio, fields=['close', 'volume'], frequency='1d', bar_count=10,) | |
# Simple Equal Weighted | |
target_weight = ( | |
self.target_exposure_per_side * context.portfolio.portfolio_value | |
) / self.pos_per_side | |
# Trading | |
for contract in new_portfolio: | |
# Slice history for contract | |
h = hist.xs(contract, 2) | |
# Equal weighted, with volume based cap. | |
contracts_to_trade = target_weight / contract.price_multiplier / h.close[-1] | |
# Position size cap | |
contracts_cap = int(h['volume'].mean() * self.volume_order_cap) | |
# Limit trade size to position size cap. | |
contracts_to_trade = min(contracts_to_trade, contracts_cap) | |
# Negative position for shorts | |
if contract in new_shorts: | |
contracts_to_trade *= -1 | |
# Execute | |
self.order_target(contract, contracts_to_trade) | |
# Close any other open position | |
for pos in context.portfolio.positions: | |
if pos not in new_portfolio: | |
self.order_target(pos, 0.0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment