घटना-संचालित बैकटेस्टिंग पर पिछले लेख में हमने विचार किया कि रणनीति वर्ग पदानुक्रम का निर्माण कैसे किया जाए। यहां परिभाषित रणनीतियों का उपयोग संकेत उत्पन्न करने के लिए किया जाता है, जिनका उपयोग पोर्टफोलियो ऑब्जेक्ट द्वारा आदेश भेजने या न भेजने के बारे में निर्णय लेने के लिए किया जाता है। पहले की तरह यह एक पोर्टफोलियो अमूर्त आधार वर्ग (एबीसी) बनाने के लिए स्वाभाविक है जो सभी बाद के उपवर्गों से विरासत में मिलता है।
यह लेख एक NaivePortfolio ऑब्जेक्ट का वर्णन करता है जो पोर्टफोलियो के भीतर पदों का ट्रैक रखता है और संकेतों के आधार पर स्टॉक की एक निश्चित मात्रा के आदेश उत्पन्न करता है। बाद के पोर्टफोलियो ऑब्जेक्ट में अधिक परिष्कृत जोखिम प्रबंधन उपकरण शामिल होंगे और बाद के लेखों का विषय होगा।
पोर्टफोलियो ऑर्डर मैनेजमेंट सिस्टम संभवतः एक इवेंट-ड्राइव बैकटेस्टर का सबसे जटिल घटक है। इसकी भूमिका सभी वर्तमान बाजार स्थितियों के साथ-साथ पदों के बाजार मूल्य (जिसे
पोजीशन और होल्डिंग्स मैनेजमेंट के अलावा पोर्टफोलियो को जोखिम कारकों और पोजीशन साइजिंग तकनीकों के बारे में भी पता होना चाहिए ताकि ब्रोकरेज या बाजार तक पहुंच के अन्य रूपों में भेजे जाने वाले ऑर्डर को अनुकूलित किया जा सके।
घटना वर्ग पदानुक्रम की नस में जारी रखते हुए, पोर्टफोलियो ऑब्जेक्ट को सिग्नल ईवेंट ऑब्जेक्ट्स को संभालने, ऑर्डर ईवेंट ऑब्जेक्ट्स उत्पन्न करने और स्थिति को अपडेट करने के लिए फिल ईवेंट ऑब्जेक्ट्स की व्याख्या करने में सक्षम होना चाहिए। इस प्रकार यह कोई आश्चर्य की बात नहीं है कि पोर्टफोलियो ऑब्जेक्ट्स अक्सर कोड की पंक्तियों (LOC) के संदर्भ में घटना-संचालित प्रणालियों का सबसे बड़ा घटक होते हैं।
हम एक नई फ़ाइल बनाते हैंportfolio.pyऔर आवश्यक पुस्तकालयों को आयात करें. ये अन्य अमूर्त आधार वर्ग कार्यान्वयनों के अधिकांश के समान हैं. हमें पूर्णांक-मूल्यवान आदेश आकार उत्पन्न करने के लिए गणित पुस्तकालय से मंजिल फ़ंक्शन आयात करने की आवश्यकता है. हमें FillEvent और OrderEvent ऑब्जेक्ट की भी आवश्यकता है क्योंकि पोर्टफोलियो दोनों को संभालता है.
# portfolio.py
import datetime
import numpy as np
import pandas as pd
import Queue
abc आयात ABCMeta, सार पद्धति से गणित आयात मंजिल से
घटना आयात से FillEvent, OrderEvent पहले की तरह हम पोर्टफोलियो के लिए एक एबीसी बनाते हैं और दो शुद्ध आभासी विधियों update_signal और update_fill हैं। पूर्व घटनाओं की कतार से पकड़े जा रहे नए ट्रेडिंग संकेतों को संभालता है और उत्तरार्द्ध निष्पादन हैंडलर ऑब्जेक्ट से प्राप्त भराव को संभालता है।
# portfolio.py
class Portfolio(object):
"""
The Portfolio class handles the positions and market
value of all instruments at a resolution of a "bar",
i.e. secondly, minutely, 5-min, 30-min, 60 min or EOD.
"""
__metaclass__ = ABCMeta
@abstractmethod
def update_signal(self, event):
"""
Acts on a SignalEvent to generate new orders
based on the portfolio logic.
"""
raise NotImplementedError("Should implement update_signal()")
@abstractmethod
def update_fill(self, event):
"""
Updates the portfolio current positions and holdings
from a FillEvent.
"""
raise NotImplementedError("Should implement update_fill()")
इस लेख का मुख्य विषय NaivePortfolio वर्ग है। यह स्थिति आकार और वर्तमान होल्डिंग्स को संभालने के लिए डिज़ाइन किया गया है, लेकिन उन्हें सीधे ब्रोकरेज को एक पूर्वनिर्धारित निश्चित मात्रा आकार के साथ, नकद के बावजूद भेजकर एक
NaivePortfolio के लिए एक प्रारंभिक पूंजी मूल्य की आवश्यकता होती है, जिसे मैंने डिफ़ॉल्ट रूप से 100,000 USD पर सेट किया है। इसके लिए एक प्रारंभ तिथि-समय की भी आवश्यकता होती है।
पोर्टफोलियो में all_positions और current_positions सदस्य होते हैं। पूर्व में एक बाजार डेटा घटना के टाइमस्टैम्प पर दर्ज सभी पिछले पदों की सूची संग्रहीत होती है। एक स्थिति बस परिसंपत्ति की मात्रा है। नकारात्मक पदों का मतलब है कि परिसंपत्ति को शॉर्ट किया गया है। उत्तरार्द्ध सदस्य एक शब्दकोश संग्रहीत करता है जिसमें अंतिम बाजार बार अपडेट के लिए वर्तमान पद होते हैं।
स्थिति सदस्यों के अतिरिक्त पोर्टफोलियो में होल्डिंग्स संग्रहीत होती हैं, जो धारण किए गए पदों के वर्तमान बाजार मूल्य का वर्णन करती हैं।
# portfolio.py
class NaivePortfolio(Portfolio):
"""
The NaivePortfolio object is designed to send orders to
a brokerage object with a constant quantity size blindly,
i.e. without any risk management or position sizing. It is
used to test simpler strategies such as BuyAndHoldStrategy.
"""
def __init__(self, bars, events, start_date, initial_capital=100000.0):
"""
Initialises the portfolio with bars and an event queue.
Also includes a starting datetime index and initial capital
(USD unless otherwise stated).
Parameters:
bars - The DataHandler object with current market data.
events - The Event Queue object.
start_date - The start date (bar) of the portfolio.
initial_capital - The starting capital in USD.
"""
self.bars = bars
self.events = events
self.symbol_list = self.bars.symbol_list
self.start_date = start_date
self.initial_capital = initial_capital
self.all_positions = self.construct_all_positions()
self.current_positions = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
self.all_holdings = self.construct_all_holdings()
self.current_holdings = self.construct_current_holdings()
निम्नलिखित विधि, construct_all_positions, बस प्रत्येक प्रतीक के लिए एक शब्दकोश बनाता है, प्रत्येक के लिए मान को शून्य पर सेट करता है और फिर एक दिनांक-समय कुंजी जोड़ता है, अंत में इसे सूची में जोड़ता है। यह एक शब्दकोश समझ का उपयोग करता है, जो सूची समझ के समान हैः
# portfolio.py
def construct_all_positions(self):
"""
Constructs the positions list using the start_date
to determine when the time index will begin.
"""
d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
d['datetime'] = self.start_date
return [d]
construct_all_holdings विधि उपरोक्त के समान है, लेकिन नकदी, कमीशन और कुल के लिए अतिरिक्त कुंजी जोड़ती है, जो क्रमशः किसी भी खरीद के बाद खाते में अतिरिक्त नकदी, संचयी कमीशन और नकदी और किसी भी खुली स्थिति सहित कुल खाता इक्विटी का प्रतिनिधित्व करती है। शॉर्ट पदों को नकारात्मक के रूप में माना जाता है। प्रारंभिक नकदी और कुल खाता इक्विटी दोनों प्रारंभिक पूंजी के लिए सेट किए जाते हैंः
# portfolio.py
def construct_all_holdings(self):
"""
Constructs the holdings list using the start_date
to determine when the time index will begin.
"""
d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
d['datetime'] = self.start_date
d['cash'] = self.initial_capital
d['commission'] = 0.0
d['total'] = self.initial_capital
return [d]
निम्नलिखित विधि, construct_current_holdings उपर्युक्त विधि के लगभग समान है सिवाय इसके कि यह शब्दकोश को सूची में लपेटता नहीं हैः
# portfolio.py
def construct_current_holdings(self):
"""
This constructs the dictionary which will hold the instantaneous
value of the portfolio across all symbols.
"""
d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
d['cash'] = self.initial_capital
d['commission'] = 0.0
d['total'] = self.initial_capital
return d
प्रत्येक
दुर्भाग्य से बोली / मांग स्प्रेड और तरलता के मुद्दों के कारण
विधि update_timeindex नई होल्डिंग्स ट्रैकिंग को संभालती है। यह सबसे पहले बाजार डेटा हैंडलर से नवीनतम कीमतों को प्राप्त करता है और
# portfolio.py
def update_timeindex(self, event):
"""
Adds a new record to the positions matrix for the current
market data bar. This reflects the PREVIOUS bar, i.e. all
current market data at this stage is known (OLHCVI).
Makes use of a MarketEvent from the events queue.
"""
bars = {}
for sym in self.symbol_list:
bars[sym] = self.bars.get_latest_bars(sym, N=1)
# Update positions
dp = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
dp['datetime'] = bars[self.symbol_list[0]][0][1]
for s in self.symbol_list:
dp[s] = self.current_positions[s]
# Append the current positions
self.all_positions.append(dp)
# Update holdings
dh = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
dh['datetime'] = bars[self.symbol_list[0]][0][1]
dh['cash'] = self.current_holdings['cash']
dh['commission'] = self.current_holdings['commission']
dh['total'] = self.current_holdings['cash']
for s in self.symbol_list:
# Approximation to the real value
market_value = self.current_positions[s] * bars[s][0][5]
dh[s] = market_value
dh['total'] += market_value
# Append the current holdings
self.all_holdings.append(dh)
विधि update_positions_from_fill यह निर्धारित करती है कि FillEvent एक Buy या एक Sell है और फिर शेयरों की सही मात्रा जोड़कर/घटाकर वर्तमान_position शब्दकोश को तदनुसार अपडेट करती हैः
# portfolio.py
def update_positions_from_fill(self, fill):
"""
Takes a FilltEvent object and updates the position matrix
to reflect the new position.
Parameters:
fill - The FillEvent object to update the positions with.
"""
# Check whether the fill is a buy or sell
fill_dir = 0
if fill.direction == 'BUY':
fill_dir = 1
if fill.direction == 'SELL':
fill_dir = -1
# Update positions list with new quantities
self.current_positions[fill.symbol] += fill_dir*fill.quantity
संबंधित update_holdings_from_fill उपरोक्त विधि के समान है लेकिन इसके बजाय होल्डिंग मानों को अपडेट करता है। एक भरने की लागत का अनुकरण करने के लिए, निम्नलिखित विधि FillEvent से जुड़ी लागत का उपयोग नहीं करती है। ऐसा क्यों है? सरल शब्दों में, एक बैकटेस्टिंग वातावरण में भरने की लागत वास्तव में अज्ञात है और इसलिए इसका अनुमान लगाया जाना चाहिए। इस प्रकार भरने की लागत
एक बार भरने की लागत ज्ञात हो जाने के बाद, चालू होल्डिंग, नकदी और कुल मूल्यों को सभी अद्यतन किया जा सकता है। संचयी कमीशन भी अद्यतन किया जाता हैः
# portfolio.py
def update_holdings_from_fill(self, fill):
"""
Takes a FillEvent object and updates the holdings matrix
to reflect the holdings value.
Parameters:
fill - The FillEvent object to update the holdings with.
"""
# Check whether the fill is a buy or sell
fill_dir = 0
if fill.direction == 'BUY':
fill_dir = 1
if fill.direction == 'SELL':
fill_dir = -1
# Update holdings list with new quantities
fill_cost = self.bars.get_latest_bars(fill.symbol)[0][5] # Close price
cost = fill_dir * fill_cost * fill.quantity
self.current_holdings[fill.symbol] += cost
self.current_holdings['commission'] += fill.commission
self.current_holdings['cash'] -= (cost + fill.commission)
self.current_holdings['total'] -= (cost + fill.commission)
पोर्टफोलियो एबीसी से शुद्ध आभासी update_fill विधि यहाँ लागू की जाती है. यह केवल दो पूर्ववर्ती विधियों, update_positions_from_fill और update_holdings_from_fill को निष्पादित करता है, जिन पर पहले ही चर्चा की गई हैः
# portfolio.py
def update_fill(self, event):
"""
Updates the portfolio current positions and holdings
from a FillEvent.
"""
if event.type == 'FILL':
self.update_positions_from_fill(event)
self.update_holdings_from_fill(event)
जबकि पोर्टफोलियो ऑब्जेक्ट को FillEvents को संभालना चाहिए, इसे एक या अधिक SignalEvents की प्राप्ति पर OrderEvents उत्पन्न करने का भी ध्यान रखना चाहिए। generate_naive_order विधि बस एक परिसंपत्ति को लंबा या छोटा करने के लिए एक संकेत लेती है और फिर ऐसी परिसंपत्ति के 100 शेयरों के लिए ऐसा करने के लिए एक आदेश भेजती है। स्पष्ट रूप से 100 एक मनमाना मूल्य है। एक यथार्थवादी कार्यान्वयन में यह मूल्य जोखिम प्रबंधन या स्थिति आकार ओवरले द्वारा निर्धारित किया जाएगा। हालांकि, यह एक NaivePortfolio है और इसलिए यह
विधि वर्तमान मात्रा और विशिष्ट प्रतीक के आधार पर, एक स्थिति की लालसा, शॉर्टिंग और बाहर निकलने को संभालती है। संबंधित OrderEvent ऑब्जेक्ट तब उत्पन्न होते हैंः
# portfolio.py
def generate_naive_order(self, signal):
"""
Simply transacts an OrderEvent object as a constant quantity
sizing of the signal object, without risk management or
position sizing considerations.
Parameters:
signal - The SignalEvent signal information.
"""
order = None
symbol = signal.symbol
direction = signal.signal_type
strength = signal.strength
mkt_quantity = floor(100 * strength)
cur_quantity = self.current_positions[symbol]
order_type = 'MKT'
if direction == 'LONG' and cur_quantity == 0:
order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
if direction == 'SHORT' and cur_quantity == 0:
order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')
if direction == 'EXIT' and cur_quantity > 0:
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
if direction == 'EXIT' and cur_quantity < 0:
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
return order
update_signal विधि बस उपरोक्त विधि को कॉल करती है और उत्पन्न क्रम को घटना कतार में जोड़ती हैः
# portfolio.py
def update_signal(self, event):
"""
Acts on a SignalEvent to generate new orders
based on the portfolio logic.
"""
if event.type == 'SIGNAL':
order_event = self.generate_naive_order(event)
self.events.put(order_event)
NaivePortfolio में अंतिम विधि एक इक्विटी वक्र का निर्माण है। यह केवल एक रिटर्न स्ट्रीम बनाता है, जो प्रदर्शन गणना के लिए उपयोगी है और फिर इक्विटी वक्र को प्रतिशत आधारित होने के लिए सामान्य करता है। इस प्रकार खाता प्रारंभिक आकार 1.0 के बराबर हैः
# portfolio.py
def create_equity_curve_dataframe(self):
"""
Creates a pandas DataFrame from the all_holdings
list of dictionaries.
"""
curve = pd.DataFrame(self.all_holdings)
curve.set_index('datetime', inplace=True)
curve['returns'] = curve['total'].pct_change()
curve['equity_curve'] = (1.0+curve['returns']).cumprod()
self.equity_curve = curve
पोर्टफोलियो ऑब्जेक्ट पूरे इवेंट-ड्राइव बैकटेस्ट सिस्टम का सबसे जटिल पहलू है। यहां कार्यान्वयन, जटिल होने के बावजूद, पदों को संभालने में अपेक्षाकृत प्राथमिक है। बाद के संस्करण जोखिम प्रबंधन और स्थिति आकार पर विचार करेंगे, जिससे रणनीति प्रदर्शन का बहुत अधिक यथार्थवादी विचार होगा।
अगले लेख में हम घटना-संचालित बैकटेस्टर के अंतिम टुकड़े पर विचार करेंगे, अर्थात् एक ExecutionHandler ऑब्जेक्ट, जिसका उपयोग OrderEvent ऑब्जेक्ट लेने और उनसे FillEvent ऑब्जेक्ट बनाने के लिए किया जाता है।