파이썬으로 이벤트 기반 백테스팅 - 5부

저자:선함, 2019-03-25 15:54:16, 업데이트:

이벤트 주도 백테스팅에 대한 이전 기사에서 우리는 전략 클래스 계층을 구성하는 방법을 고려했습니다. 여기서 정의 된 전략은 포트폴리오 객체가 명령을 보내는지 여부에 대한 결정을 내리기 위해 사용되는 신호를 생성하는 데 사용됩니다. 이전과 마찬가지로 모든 후속 하위 클래스가 계승하는 포트폴리오 추상 기본 클래스 (ABC) 를 만드는 것이 자연적입니다.

이 문서에서는 포트폴리오 내 포지션을 추적하고 신호를 기반으로 고정된 양의 주식을 주문하는 NaivePortfolio 객체를 설명합니다. 후기 포트폴리오 객체는 더 정교한 리스크 관리 도구를 포함하며 후기 기사의 주제가 될 것입니다.

위치 추적 및 주문 관리

포트폴리오 주문 관리 시스템은 아마도 이벤트 기반 백테스터의 가장 복잡한 구성 요소입니다. 그 역할은 모든 현재 시장 위치와 포지션의 시장 가치 (홀딩) 를 추적하는 것입니다. 이것은 단순히 포지션의 청산 가치의 추정이며 부분적으로 백테스터의 데이터 처리 시설에서 파생됩니다.

포트폴리오는 포지션 및 지분 관리 외에도 위험 요소와 포지션 사이즈 기술에 대해서도 알고 있어야 중개업이나 다른 형태의 시장 접근에 전송되는 명령을 최적화 할 수 있습니다.

이벤트 클래스 계층에 따라 계속하여 포트폴리오 객체는 SignalEvent 객체를 처리하고 OrderEvent 객체를 생성하고 FillEvent 객체를 해석하여 위치를 업데이트 할 수 있어야합니다. 따라서 포트폴리오 객체가 종종 코드 라인 (LOC) 의 관점에서 이벤트 구동 시스템의 가장 큰 구성 요소라는 것은 놀라운 일이 아닙니다.

시행

새 파일을 만들자portfolio.py그리고 필요한 라이브러리를 가져옵니다. 이것은 다른 추상적인 기본 클래스 구현과 동일합니다. 우리는 정수 값의 순서 크기를 생성하기 위해 수학 라이브러리에서 바닥 함수를 가져야합니다. 포트폴리오가 둘 다 처리하기 때문에 FillEvent 및 OrderEvent 객체도 필요합니다.

# portfolio.py

import datetime
import numpy as np
import pandas as pd
import Queue

abc에서 가져오기 ABCMeta, 추상 방법 수학 수입 바닥에서

이벤트 수입에서 FillEvent, OrderEvent 이전과 마찬가지로 우리는 포트폴리오에 대한 ABC를 만들고 두 가지 순수 가상 메소드 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 클래스입니다. 포지션 크기와 현재 보유를 처리하도록 설계되었지만 예전에 정한 고정 금액 크기와 함께 현금 보유와 관계없이 브로커레이스로 직접 전송함으로써 거래 명령을 "거짓" 방식으로 수행합니다. 이것들은 모두 비현실적인 가정이지만 포트폴리오 주문 관리 시스템 (OMS) 이 이벤트 기반 방식으로 작동하는 방법을 설명하는 데 도움이됩니다.

네이브 포트폴리오에는 초기 자본가치가 필요합니다. 제가 기본으로 설정한 자본가치는 10만 달러입니다. 또한 시작 날짜가 필요합니다.

포트폴리오는 all_positions 및 current_positions 멤버를 포함합니다. 전자는 시장 데이터 이벤트의 시간표시에 기록된 모든 이전 포지션 목록을 저장합니다. 포지션은 단순히 자산의 양입니다. 부정적인 포지션은 자산이 단축되었음을 의미합니다. 후자는 마지막 시장 바 업데이트에 대한 현재 포지션을 포함하는 사전을 저장합니다. 포지션은 포지션에 대한 정보의 일부입니다.

포트폴리오는 포지션 구성원 외에도 보유한 지분을 저장하고 있으며, 이는 보유한 포지션의 현재 시장 가치를 설명합니다. 이 경우 "현재 시장 가치"는 현재 시장 바에서 얻은 종료 가격을 의미합니다. 이것은 분명히 근사이지만 당분간 충분히 합리적입니다. all_holdings는 모든 상징 보유의 역사적 목록을 저장하고, current_holdings는 모든 상징 보유의 가장 최신 사전을 저장합니다.

# 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, 단순히 각 기호에 대한 사전을 만들고, 각각의 값을 0으로 설정하고, 날짜 시간 키를 추가하여 최종적으로 목록에 추가합니다. 그것은 목록 이해와 비슷한 정신의 사전 이해 방식을 사용합니다.

# 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]

이 방법 은 위와 비슷하지만 현금, 수수료 및 총액에 대한 추가 키를 추가 합니다. 이 키들은 각각 매입 후 계좌에 있는 비출 현금, 누적 된 수수료 및 현금 및 모든 오픈 포지션을 포함한 총 계좌 자본을 나타냅니다. 단위 포지션은 부정적으로 취급됩니다. 시작 현금과 총 계좌 자본은 모두 초기 자본에 설정됩니다.

# 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

하트박동, 즉 새로운 시장 데이터가 DataHandler 객체에서 요청될 때마다, 포트폴리오는 보유된 모든 포지션의 현재 시장 가치를 업데이트해야합니다. 라이브 거래 시나리오에서이 정보는 브로커리그에서 직접 다운로드하여 분석 할 수 있지만 백테스팅 구현을 위해 이러한 값을 수동으로 계산해야합니다.

불행히도, 입찰/수요 스프레드와 유동성 문제로 인해 현재 시장 가치는 존재하지 않습니다. 따라서 보유 자산의 양을 가격으로 곱하여 추정해야합니다. 여기서 제가 취한 접근법은 받은 마지막 바의 폐쇄 가격을 사용하는 것입니다. 내일 전략에서는 상대적으로 현실적입니다. 매일 전략에서는 개시 가격이 종료 가격과 크게 다를 수 있기 때문에 덜 현실적입니다.

메소드 update_timeindex는 새로운 지점 추적을 처리합니다. 먼저 시장 데이터 처리기에서 최신 가격을 얻고 현재 지점을 나타내는 새로운 기호 사전을 생성하여 new 지점을 current 지점과 동일하게 설정합니다. FillEvent가 얻었을 때만 변경됩니다. 포트폴리오에서 나중에 처리됩니다. 메소드는 이 세트의 현재 지점을 all_positions 목록에 첨부합니다. 다음으로 지점은 비슷한 방식으로 업데이트됩니다. 시장 가치가 최신 바 (self.current_positions[s] * bars[s][05]]) 의 종료 가격으로 현재 지점을 곱하여 재 계산됩니다. 마지막으로 새로운 지점은 all_holdings에 첨부됩니다:

# 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가 구매 또는 판매인지 결정하고, 다음에는 올바른 주식 수를 더하거나 빼면서 current_positions 사전을 그에 따라 업데이트합니다.

# 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)

포트폴리오 ABC의 순수 가상 업데이트_필 메소드는 여기에 구현됩니다. 그것은 단순히 앞서 논의 된 두 가지 방법, 업데이트_포지션_포트폴리오_필과 업데이트_홀딩_포트폴리오_필을 실행합니다.

# 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)

네이브 포트폴리오의 최종 방법은 주식 곡선을 생성하는 것입니다. 이것은 단순히 성과 계산에 유용한 수익 스트림을 생성하고 주식 곡선을 비율로 정상화합니다. 따라서 계정의 초기 크기는 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

포트폴리오 객체는 전체 이벤트 주도 백테스트 시스템의 가장 복잡한 측면입니다. 여기서 구현은 복잡하지만 포지션 처리에서 상대적으로 기본적입니다. 후속 버전은 위험 관리와 포지션 사이징을 고려하여 전략 성과에 대한 훨씬 현실적인 아이디어를 얻을 것입니다.

다음 기사에서는 이벤트 구동 백테스터의 마지막 조각, 즉 OrderEvent 객체를 받아서 FillEvent 객체를 생성하는 데 사용되는 ExecutionHandler 객체를 고려할 것입니다.


더 많은