t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL

     we will explore more sophisticated[səˈfɪstɪkeɪtɪd] 复杂的 trading strategies employed by leading market participants in the algorithmic trading business. We will build on top of the basic algorithmic strategies and learn about more advanced approaches (such as statistical arbitrage[ˈɑːrbɪtrɑːʒ]套利 and pair correlation) and their advantages and disadvantages. We will learn how to create a trading strategy that adjusts for trading instrument volatility. We will also learn how to create a trading strategy for economic events and understand and implement the basics of statistical arbitrage trading strategies.

This chapter will cover the following topics:

  • Creating a trading strategy that adjusts for trading instrument volatility
  • Creating a trading strategy for economic events
  • Understanding and implementing basic statistical arbitrage trading strategies

Creating a trading strategy that adjusts for trading instrument volatility

     An intuitive way to think about price volatility is investor confidence in the specific instrument, that is, how willing the investors are to invest money into the specific instrument and how long they are willing to hold on to a position in that instrument.

  • As price volatility goes up, because prices make bigger swings at faster paces, investor confidence drops.
  • Conversely, as price volatility goes down, investors are more willing to have bigger positions and hold those positions for longer periods of time.

Volatility in a few asset classes often spills over into other asset classes, thus slowly spreading volatility over to all economic fields, housing costs, consumer costs, and so on. Obviously, sophisticated strategies need to dynamically adjust to changing volatility in trading instruments by following a similar pattern of being more cautious更加谨慎 with respect to the positions they take, how long positions are held, and what the profit/loss expectations are.

     In https://blog.csdn.net/Linli522362242/article/details/121406833 , Deciphering解读 the Markets with Technical Analysis, we saw a lot of trading signals; in https://blog.csdn.net/Linli522362242/article/details/121551663 , Predicting the Markets with Basic Machine Learning, we applied machine learning algorithms to those trading signals; and in https://blog.csdn.net/Linli522362242/article/details/121721868 , Classical Trading Strategies Driven by Human Intuition, we explored basic trading strategies. Most of those approaches did not directly consider volatility changes in the underlying trading instrument, or adjust or account for them. In this section, we will discuss the impact of volatility changes in trading instruments and how to deal with that to improve profitability and reduce risk exposure.

Adjusting for trading instrument volatility in technical indicators

     In https://blog.csdn.net/Linli522362242/article/details/121406833, Deciphering the Markets with Technical Analysis, we looked at generating trading signals with predetermined parameters. What we mean by that is we decided beforehand to use, say, 20 days moving average, or the number of time periods to use, or the smoothing constants to use, and these remained constant throughout the entire period of our analysis. These signals have the benefit of being simple, but suffer from the disadvantage of performing differently as the volatility of the trading instrument changed over the course of time.

     Then we also looked at signals such as Bollinger Bandst5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第1张图片(or K: Standard deviation factor of our choice)  and standard deviation(and ,  Typical values for n and K are 20 days(BOLL_PERIOD = 20) and 2(BOLL_STD_TIMES = 2)), which adjusted for trading instrument volatility, that is,

  • during non-volatile periods, the lower standard deviation in price movements would make the signals more aggressive[əˈɡresɪv] 挑衅的;积极进取 to entering positions and less aggressive when closing positions平仓.
  • Conversely, during volatile periods, the higher standard deviation in price movements makes the signals less aggressive to entering positions. This is because the bands that depend on standard deviation widen out from the moving average, which in itself has become more volatile. Thus, these signals implicitly had some aspects of adjusting for trading instrument volatility baked right into them.
  • 股价涨跌幅度加大时,带状区变宽,涨跌幅度狭小盘整时,带状区则变窄。 

     In general, it is possible to take any of the technical indicators we have seen so far and combine a standard deviation signal with it to have a more sophisticated form of the basic technical indicator that has dynamic values for number of days, or number of time periods or smoothing factors. The parameters become dynamic by depending on the standard deviation as a volatility measure. Thus, moving averages can have

  • a smaller history or number of time periods when volatility is high to capture more observations, and
  • a larger history or number of time periods when volatility is low to capture fewer observations.

Similarly, smoothing factors can be made higher or lower in magnitude depending on volatility. In essence, that controls how much weight is assigned to newer observations as compared to older ones. We won't go into any more detail here, but it is easy to apply these concepts to technical indicators once the basic idea of applying volatility measures to simple indicators to form complex indicators is clear. 

Adjusting for trading instrument volatility in trading strategies

VVVVVVVVVVV

     After the momentum strategy, we will now look at another very popular type of strategy, the mean reversion strategy均值回归策略. The underlying precept[ˈpriːsept]规则;格言;训诫 is that prices revert回归 toward the mean. Extreme events are followed by more normal events. We will find a time where a value such as the price or the return is very different from the past values. Once established, we will place an order by forecasting that this value will come back to the mean.

     Reversion strategy uses the belief that the trend of quantity will eventually reverse. This is the opposite of the previous strategy. If a stock return increases too fast, it will eventually return to its average. Reversion strategies assume that any trend will go back to the average value, either an upward or downward trend (divergence or trend trading).

  • Advantages of the reversion strategy:
    • This class of strategy is easy to understand.
  • Disadvantages of the reversion strategy:
    • This class of strategy doesn't take into account noise or special events. It has a tendency to smooth out prior events.

     Momentum, also referred to as MOM, is an important measure of speed and magnitude of price moves. This is often a key indicator of trend/breakout-based trading algorithms.

     In its simplest form, momentum is simply the difference between the current price and price of some fixed time periods in the past. Consecutive periods of positive momentum values indicate an uptrend; conversely, if momentum is consecutively negative, that indicates a downtrend. Often, we use simple/exponential moving averages of the MOM indicator, as shown here, to detect sustained trends:

https://blog.csdn.net/Linli522362242/article/details/121406833

 Here, the following applies:
: Price at time t
: Price n time periods before time t ( or price at time t-n)

^^^^^^^^^^^^^^^^

     We can apply the same concepts of adjusting for volatility measures to trading strategies. Momentum or trend-following strategies can use 不断变化的changing volatility to

  • dynamically change the time period parameters used in the moving averages,
  • or change the thresholds for how many up/down days to count as an entry signal.

Another area of improvement would be using changing volatility to

  • dynamically adjust thresholds on when to enter a position when a trend is detected, and
  • dynamically adjust thresholds on when to exit a position when trend reversal is detected

Volatility adjusted mean reversion trading strategies

     We explored mean reversion trading strategies in great detail in https://blog.csdn.net/Linli522362242/article/details/121721868, Classical Trading Strategies Driven by Human Intuition. For the purposes of this chapter, we will first create a very simple variant of a mean reversion strategy and then show how one would apply volatility adjustment to the strategy to optimize and stabilize its risk-adjusted returns

Mean reversion strategy(+APO) using the absolute price oscillator trading signal

VVVVVVVVVVVVVVVV 

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第2张图片

     The absolute price oscillator, which we will refer to as APO, is a class of indicators that builds on top of moving averages of prices to capture specific short-term deviations in prices.  The absolute price oscillator is computed by finding the difference between a fast exponential moving average and a slow exponential moving average.The faster EMA is more reactive to new price observations, and the slower EMA is less reactive to new price observations and decays slower.

  • The APO values are positive when prices are breaking out to the upside( the trading instrument is overbought and we should expect a bounce back down), and the magnitude of the APO values captures the magnitude of the breakout.
  • The APO values are negative when prices are breaking out to the downside(the trading instrument is oversold and, we should expect a bounce back up), and the magnitude of the APO values captures the magnitude of the breakout.
  • APO values that have higher positive and negative values when the prices are moving away from long-term EMA(here, num_periods_slow=40) very quickly (breaking out), which can have a trend-starting interpretation or an overbought/sold interpretation.

^^^^^^^^^^^^^^^^^^^^^^^ 

     Let's explain and implement a mean reversion strategy that relies on the Absolute Price Oscillator[ˈɑːsɪleɪtər] (APO) trading signal indicator we explored in https://blog.csdn.net/Linli522362242/article/details/121406833, Deciphering the Markets with Technical Analysis. It will use a static constant of 10 days for the Fast EMA and a static constant of 40 days for the Slow EMA.

  • It will perform buy trades when the APO signal value drops below -10(oversold,expect a bounce back up) and
  • perform sell trades when the APO signal value goes above +10(overbought, expect a bounce back down).
  • In addition, it will check that new trades are made at prices that are different from the last trade price to prevent overtrading.

Positions are closed when the APO signal value changes sign, that is,

  • close short positions when APO goes negative and
  • close long positions when APO goes positive.

     In addition, positions are also closed if currently open positions are profitable above a certain amount, regardless of APO values. This is used to algorithmically lock profits and initiate more positions instead of relying only on the trading signal value. Now, let's look at the implementation in the next few sections: 

1. We will fetch data the same way we have done in the past. Let's fetch 4 years of GOOG data. This code will use the DataReader function from the pandas_datareader package. This function will fetch the GOOG prices from Yahoo Finance between 2014-01-2014 and 2018-01-01 . If the . pkl file used to store the data on the disk is not present, the GOOG_data.pkl file will be created.
By doing that, we ensure that we will use the file to fetch the GOOG data for future use:

import pandas as pd
import pandas_datareader.data as pdr

def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
    if len(output_file) == 0:
        output_file = stock_symbol+'_data_large.pkl'   
        
    try:
        df = pd.read_pickle( output_file )
        print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
    except FileNotFoundError:
        print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
        df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
        df.to_pickle( output_file )
    return df 

goog_data = load_financial_data( stock_symbol='GOOG',
                                 start_date='2014-01-01', 
                                 end_date='2018-01-01',
                                 output_file='goog_data.pkl'
                               )
goog_data.head()

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第3张图片

2. Now we will define some constants and variables we will need to perform Fast and Slow EMA calculations and APO trading signal:

Exponential moving average 

     The weighting depends on the selected time period of the EMA;

  • the shorter the time period(==>===>it places more weight on the most recent price observation and less weight on the older price observations), the more reactive越强烈 the EMA is to new price observations; in other words, the EMA converges to new price observations faster and forgets older observations faster, also referred to as Fast EMA.
  • The longer the time period, the less reactive the EMA is to new price observations; that is, EMA converges to new price observations slower and forgets older observations slower, also referred to as Slow EMA.

OR 

 Alternatively, we have the following: 

Here, the following applies:
P : Current price of the instrument
: EMA value prior to the current price observation
(OR K): Smoothing constant, most commonly set to 
n : Number of time periods (similar to what we used in the simple moving average)

# Variables/constants for EMA Calculation:
NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
ema_fast = 0 # initial ema
ema_fast_values = [] # we will hold fast EMA values for visualization purpose

NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
ema_slow = 0 # initial ema
ema_slow_values = [] # we will hold slow EMA values for visualization purpose

apo_values = [] # track computed absolute price oscillator values
清仓、就是卖光手中的股票,
平仓:卖出股票称平仓;股民将自己手中所持有的股票按照当前交易价格全部卖出,从而退出股票交易市场,又称为清仓
     至于股票平仓方法有四个,分别是定点了结法,这是必不可少的股票平仓方法;分批了结法,就是不一次卖光手中股票,分批酌量卖出。止损了结法,这种股票平仓方法是将所持股票在股价回落到一定点时了结,以停止损失。卖利了结法,是指把本金和利润区别对待

加仓、在已经持有的股票上加码买进,持有更多的该股票,
建仓、就是开始买入股票。
半仓、就是持有的股票金额占自己资产的一半。资金的一半买入
减仓、就是降低仓位。卖出一部分
满仓、就是所有的钱都买了股票。
持仓、就是持有暂时不卖出。持有股票
守仓、就是持有不卖出。
底仓、就是某股票即使进行卖出操作,也留有一定得仓位
空仓、就是不持有股票。
斩仓、就是割肉,亏损卖出股票。
补仓,在下跌过程中买入,被套后在原有仓位的基础上,再买入该股,以摊薄成本。
倒仓,日内T+0交易。买入手中已持有的股票,再在当天上涨后卖出原来的股数,以获得收益,摊低成本。

3. We will also need variables that define/control strategy trading behavior and position and PnL management:

# Variables for Trading Strategy trade, position & pnl management:

# Container for tracking buy/sell order, 
# +1 for buy order, -1 for sell order, 0 for no-action
orders = []

# Container for tracking positions, 
# positive for long positions, negative for short positions, 0 for flat/no position
positions = []

# Container for tracking total_pnls, this is the sum of 
# closed_pnl i.e. pnls already locked in 
# and open_pnl i.e. pnls for open-position marked to market price
pnls = []


last_buy_price = 0  # used to prevent over-trading at/around the same price
last_sell_price = 0 # used to prevent over-trading at/around the same price
position = 0 # Current position of the trading strategy

# Summation of products of 
# buy_trade_price and buy_trade_qty for every buy Trade made
# since last time being flat
buy_sum_price_qty = 0
# Summation of buy_trade_qty for every buy Trade made since last time being flat
buy_sum_qty = 0

# Summation of products of 
# sell_trade_price and sell_trade_qty for every sell Trade made 
# since last time being flat
sell_sum_price_qty = 0
# Summation of sell_trade_qty for every sell Trade made since last time being flat
sell_sum_qty = 0

open_pnl = 0   # Open/Unrealized PnL marked to market
closed_pnl = 0 # Closed/Realized PnL so far

4. Finally, we clearly define

  • the entry thresholds
  • the minimum price change since last trade,
  • the minimum profit to expect per trade, and
  • the number of shares to trade per trade:
# Constants that define strategy behavior/thresholds

# APO trading signal value below which(-10) to enter buy-orders/long-position
APO_VALUE_FOR_BUY_ENTRY = -10 # (oversold, expect a bounce back up)
# APO trading signal value above which to enter sell-orders/short-position
APO_VALUE_FOR_SELL_ENTRY = 10 # (overbought, expect a bounce back down)

# Minimum price change since last trade before considering trading again,
MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
NUM_SHARES_PER_TRADE = 10

# positions are closed if currently open positions are profitable above a certain amount, 
# regardless of APO values. 
# This is used to algorithmically lock profits and initiate more positions 
# instead of relying only on the trading signal value. 

# Minimum Open/Unrealized profit at which to close positions and lock profits
MIN_PROFIT_TO_CLOSE = 10*NUM_SHARES_PER_TRADE

5. Now, let's look at the main section of the trading strategy, which has logic for the following:

  • Computation/updates to Fast and Slow EMA and the APO trading signal
  • Reacting to trading signals to enter long or short positions
  • Reacting to trading signals, open positions, open PnLs, and market prices to close long or short positions对交易信号、未平仓头寸、未平仓盈亏和市场价格 做出反应 以关闭多头或空头头寸:

     The code will check for trading signals against根据 trading parameters/thresholds and positions, to trade.

6. We will perform a sell trade at close_price if the following conditions are met: 

  • The APO trading signal value(positive) is above the Sell-Entry threshold
    and the difference between the last trade price and current price is different enough(>Minimum price change).
  • We are long (positive position)
    and either the APO trading signal value is at or above 0 or current position is profitable enough to lock profit(>MIN_PROFIT_TO_CLOSE):

     Volume-weighted average price (VWAP)成交量加权平均价格 is a lagging volume indicator. The VWAP is a weighted moving average that uses the volume as the weighting factor so that higher volume days have more weight. It is a non-cumulative moving average, so only data within the time period is used in the calculation. 

The formula for calculating VWAP is as follows:
OR

Here, n is the time period and has to be defined by the user.

  • is Volume Weighted Average Price;
  • is price of trade j;
  •  is quantity of trade j;
  • j is each individual trade that takes place over the defined period of time, excluding cross trades and basket cross trades

     The VWAP can be used similar to moving averages, where prices above the VWAP reflect a bullish看涨 sentiment and prices below the VWAP reflect a bearish看跌 sentiment. Traders may

  • initiate short positions as a stock price moves below VWAP for a given time period
  • or initiate long position as the price moves above VWAP

7. We will perform a buy trade at close_price if the following conditions are met:

  • the APO trading signal value is below the Buy-Entry threshold
    and the difference between the last trade price and current price is different enough(>Minimum price change).
  • We are short (negative position)
    and either the APO trading signal value is at or below 0 or current position is profitable enough to lock profit(>MIN_PROFIT_TO_CLOSE):

8. The code of the trading strategy contains logic for position/PnL management. It needs to update positions and compute open and closed PnLs when market prices change and/or trades are made causing a change in positions :

#########################

ILLUSTRATING TOTAL P&L CALCULATIONS

Assume the initial fill download includes the following fills (all prices in points):

  • Buy 12 @ 100
  • Buy 17 @ 99
  • Sell 9 @ 101
  • Sell 4 @ 105
  • Buy 3 @ 103

INITIAL P&L CALCULATIONS

From these fills, TT FIX Adapter calculates the following base values:

  • Total Buy Quantity = 12 + 17 + 3 = 32
  • Average Buy Price = ((12 * 100) + (17 * 99) + (3 * 103)) / 32 = 99.75 (points)
  • Total Sell Quantity = 9 + 4 = 13
  • Average Sell Price = ((9 * 101) + (4 * 105)) / 13 = 102.230769 (points)

To determine the realized P&L, TT FIX Adapter matches thirteen Buys with thirteen Sells using the Averaging technique, as follows:

  • P&LRealized (points) = (Sell Price - Buy Price) * Qty = (102.230769 - 99.75) * 13 = 32.249997
  • P&LRealized (contract currency) = P&LRealized (points) * Contract Point Value

which results in the following starting state after the initial fill download:

  • Position = +19
  • Average Open Price = 99.75

With the Average Open Price for the initial fills, the FIX client can calculate the unrealized P&L for the initial position. To do so, you must use some Theoretical Exit Price to calculate the unrealized P&L. This example, and all of the scenarios, assumes a Theoretical Exit Price of 99 (points), which results in the following calculations:

  • P&LUnrealized (points) = (Theoretical Exit Price - Average Open Price) * Position = (99 - 99.75) * (+19) = -14.25
  • P&LUnrealized (contract currency) = P&LUnrealized (points) * Contract Point Value

With these values, you can calculate the total P&L after the initial fill download as follows:

  • P<otal (points) = P&LRealized (points) + P&LUnrealized (points) = 32.249997 + (-14.25) = 17.999997
  • P<otal (contract currency) = P<otal (points) * Contract Point Value

 SCENARIO 1: RECEIVING NEW FILLS THAT INCREASE YOUR POSITION

     After calculating the initial position and P&L, suppose you receive a fill that increases your position (adding Buys to a positive position or Sells to a negative one). In this example, you receive a fill for a Buy order, as follows:
t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第4张图片

In this scenario, the fills do not affect the realized P&L, but they do affect the unrealized and total P&L, as follows:

  • Position = 19 + 10 = +29
  • Average Open Price = ( (19 * 99.75) + (10 * 100) ) / (19 + 10) = 99.83620

SCENARIO 2: RECEIVING NEW FILLS THAT PARTIALLY DECREASE YOUR POSITION

     Instead of increasing a position, suppose the new fill partially decreases the position, still leaving an open position. In this example, you receive a fill for a Sell order, as follows: t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第5张图片

     In this scenario, the reduced open position does affect the realized P&L, as well as the unrealized and total P&L. The following calculations show how TT FIX Adapter derives the new realized P&L and how you can calculate the updated unrealized and total P&L (with Theoretical Exit Price = 99):

  • Previously, P&LRealized (points) = (Average Sell Price - Average Buy Price) * Sell_Qty = (102.230769 - 99.75) * 13 = 32.249997

    P&LRealized (points) = P&LRealized + ( (Sell Price – Average Buy Price) * Qty ) = 32.249997 + ((101 – 99.75) * 12) = 47.249997

    P&LRealized (points) = ( ( Average Sell Price* Sell_Qty + Sell Price* Qty )/(Sell_Qty+Qty) - Average Buy Price) * (Sell_Qty+Qty)
    P&LRealized (points) = (sell_sum_qty/sell_sum_qty - Average Buy Price) * sell_sum_qty
                                         = (sell_sum_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
                                            * sell_sum_qty
  • Position = 19 – 12 = 7
  • Average Open Price = 99.75 (does not change)
  • P&LUnrealized (points) = (Theoretical Exit Price – Average Open Price) * Position = (99 – 99.75) * 7 = -5.25
  • P&LTotal (points) = (P&LRealized (points)) + (P&LUnrealized (points)) = 47.249997 + (-5.25) = 41.999997

SCENARIO 3: RECEIVING FILLS THAT FLATTEN YOUR POSITION

     When a fill flattens a position, the average price becomes unavailable, as does the unrealized P&L. In this example, you receive a fill for a Sell order that matches the current position, as follows:
t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第6张图片

      In this scenario, flattening a position causes the realized and total P&L to derive the same value. The following calculations show how TT FIX Adapter derives the new realized P&L and how you can calculate the updated unrealized and total P&L (with Theoretical Exit Price = 99):

  • Previously, P&LRealized (points) = (Average Sell Price - Average Buy Price) * Sell_Qty = (102.230769 - 99.75) * 13 = 32.249997

    P&LRealized (points) = P&LRealized + ((Sell Price – Buy Price) * Qty) = 32.249997 + ((101 – 99.75) * 19) = 55.999997
  • Position = 19 – 19 = 0
  • Average Open Price = none (as position = 0)
  • P&LUnrealized (points) = (Theoretical Exit Price – Average Open Price) * Position = none (as position = 0)
  • P&LTotal (points) = (P&LRealized (points)) + (P&LUnrealized (points)) = 55.999997 + 0 = 55.999997

SCENARIO 4: RECEIVING FILLS THAT REVERSE YOUR POSITION

Finally, you can also receive fills that reverse a position. In this example, you receive a fill for a Sell order than exceeds the current position, as follows:t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第7张图片

     In this scenario, reversing the position affects the realized P&L, as well as the unrealized and total P&L. The following calculations show how TT FIX Adapter derives the new realized P&L and how you can calculate the updated unrealized and total P&L (with Theoretical Exit Price = 99):

  • Previously, P&LRealized (points) = (Average Sell Price - Average Buy Price) * Sell_Qty = (102.230769 - 99.75) * 13 = 32.249997

    P&LRealized (points) = P&LRealized + ((Sell Price – Buy Price) * Qty) = 32.249997 + ((101 – 99.75) * 19) = 55.999997
  • Position = 19 – 22 = -3
  • Average Open Price = 101
  • P&LUnrealized (points) = (Theoretical Exit Price – Average Open Price) * Position = (99 – 101) * (-3) = 6
  • P&LTotal (points) = (P&LRealized (points)) + (P&LUnrealized (points)) = 59.749997 + 6 = 61.999997

#########################

close  = goog_data['Close']
for close_price in close:
    # This section updates fast and slow EMA and computes APO trading signal
    if (ema_fast == 0): # first observation
        ema_fast = close_price
        ema_slow = close_price
    else:
        ema_fast = (close_price - ema_fast)*K_FAST + ema_fast # K_FAST = 2/(NUM_PERIODS_FAST_10 + 1)
        ema_slow = (close_price - ema_slow)*K_SLOW + ema_slow # K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1)
    
    ema_fast_values.append(ema_fast)
    ema_slow_values.append(ema_slow)
    
    apo = ema_fast-ema_slow
    apo_values.append(apo)
    
    # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
    
    # We will perform a sell trade at close_price if the following conditions are met:
    # 1. The APO trading signal value(positive) > Sell-Entry threshold (overbought, expect a bounce back down, sell for profit)
    #     and the difference between current-price and last trade-price is different enough.(>Minimum price change)
    # 2. We are long( +ve position ) and 
    #    either APO trading signal value >= 0 or current position is profitable enough to lock profit.
    if ( ( apo > APO_VALUE_FOR_SELL_ENTRY and \
           abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
         )
         or
         ( position>0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
       ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
        orders.append(-1) # mark the sell trade
        last_sell_price = close_price
        position -= NUM_SHARES_PER_TRADE
        sell_sum_qty += NUM_SHARES_PER_TRADE
        sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
        print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
        
    # 7. We will perform a buy trade at close_price if the following conditions are met:
    # 1. The APO trading signal value(negative) < below Buy-Entry threshold (oversold, expect a bounce back up, buy for future profit)
    #    and the difference between current-price and last trade-price is different enough.(>Minimum price change)
    # 2. We are short( -ve position ) and
    #    either APO trading signal value is <= 0 or current position is profitable enough to lock profit.
    elif ( ( apo < APO_VALUE_FOR_BUY_ENTRY and \
             abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
           )
           or
           ( position<0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
         ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
        orders.append(+1) # mark the buy trade
        last_buy_price = close_price
        position += NUM_SHARES_PER_TRADE
        buy_sum_qty += NUM_SHARES_PER_TRADE
        buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
        print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
    else:
        # No trade since none of the conditions were met to buy or sell
        orders.append( 0 )
        
    positions.append( position )
    
    # 8. The code of the trading strategy contains logic for position/PnL management. 
    #    It needs to update positions and compute open and closed PnLs when market prices change 
    #    and/or trades are made causing a change in positions
    
    # This section updates Open/Unrealized & Closed/Realized positions
    open_pnl = 0
    if position > 0:
        # long position and some sell trades have been made against it, 
        # close that amount based on how much was sold against this long position
        # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
        if sell_sum_qty > 0:           # vwap for sell                   # vwap  for buy
            open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
        # mark the remaining position to market 
        # i.e. pnl would be what it would be if we closed at current price
        # sell
        # position -= NUM_SHARES_PER_TRADE
        # sell_sum_qty += NUM_SHARES_PER_TRADE
        # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
        # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) * 
        open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
    elif position < 0:
        # short position and some buy trades have been made against it, 
        # close that amount based on how much was bought against this short position
        # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
        if buy_sum_qty > 0:           # vwap for sell                   # vwap  for buy
            open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
        # mark the remaining position to market
        # i.e. pnl would be what it would be if we closed at current price
        # buy
        # position += NUM_SHARE_PER_TRADE
        # buy_sum_qty += NUM_SHARE_PER_TRADE
        # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
        # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) * 
        open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
    else:
        # flat, so update closed_pnl and reset tracking variables for positions & pnls
        closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
        
        buy_sum_price_qty = 0
        buy_sum_qty = 0
        last_buy_price = 0
        
        sell_sum_price_qty = 0
        sell_sum_qty = 0
        last_sell_price = 0
        
    print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
    pnls.append(closed_pnl + open_pnl)  

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第8张图片
... ...

9. Now we look at some Python/Matplotlib code to see how to gather the relevant results of the trading strategy such as market prices, Fast and Slow EMA values, APO values, Buy and Sell trades, Positions and PnLs achieved by the strategy over its lifetime and then plot them in a manner that gives us insight into the strategy's behavior: 

data = goog_data.copy()
# This section prepares the dataframe from the trading strategy results and visualizes the results
data = data.assign( ClosePrice = pd.Series(close, index=data.index) )
data = data.assign( Fast10DayEMA = pd.Series(ema_fast_values, index=data.index) )
data = data.assign( Slow40DayEMA = pd.Series(ema_slow_values, index=data.index) )
data = data.assign( APO = pd.Series(apo_values, index=data.index) )
data = data.assign( Trades = pd.Series(orders, index=data.index) )
data = data.assign( Position = pd.Series(positions, index=data.index) )
data = data.assign( Pnl = pd.Series(pnls, index=data.index) )
data.head(20)

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第9张图片

10. Now we will add columns to the data frame with different series that we computed in the previous sections, first the Market Price and then the fast and slow EMA values. We will also have another plot for the APO trading signal value. In both plots, we will overlay buy and sell trades so we can understand when the strategy enters and exits positions: 

import matplotlib.pyplot as plt

fig = plt.figure( figsize=(20,10))

data['ClosePrice'].plot(color='k', lw=3., legend=True)
data['Fast10DayEMA'].plot(color='y', lw=1., legend=True)
data['Slow40DayEMA'].plot(color='m', lw=1., legend=True)
plt.plot( data.loc[ data.Trades == 1 ].index, data.ClosePrice[data.Trades == 1 ], 
          color='y', lw=0, marker='^', markersize=7, label='buy'
        )
plt.plot( data.loc[ data.Trades == -1 ].index, data.ClosePrice[data.Trades == -1 ], 
          color='b', lw=0, marker='v', markersize=7, label='sell'
        )
plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()
plt.show()

     Let's take a look at what our trading behavior looks like, paying attention to the EMA and APO values when the trades are made. When we look at the positions and PnL plots, this will become completely clear: 

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第10张图片

     In the plot, we can see where the buy and sell trades were made as the price of the Google stock change over the last 4 years, but now, let's look at what the APO trading signal values where the buy trades were made and sell trades were made. According to the design of these trading strategies, we expect sell trades when APO values are positive and expect buy trades when APO values are negative:

fig = plt.figure( figsize=(20,10) )

data['APO'].plot(color='k', lw=3., legend=True)
plt.plot( data.loc[ data.Trades == 1 ].index, data.APO[data.Trades == 1 ],
          color='y', lw=0, marker='^', markersize=7, label='buy'
        )
plt.plot( data.loc[ data.Trades == -1 ].index, data.APO[data.Trades == -1 ],
          color='b', lw=0, marker='v', markersize=7, label='sell'
        )
plt.axhline(y=0, lw=0.5, color='k')
for i in range( APO_VALUE_FOR_BUY_ENTRY, APO_VALUE_FOR_BUY_ENTRY*5, APO_VALUE_FOR_BUY_ENTRY ):
  plt.axhline(y=i, lw=0.5, color='r')
for i in range( APO_VALUE_FOR_SELL_ENTRY, APO_VALUE_FOR_SELL_ENTRY*5, APO_VALUE_FOR_SELL_ENTRY ):
  plt.axhline(y=i, lw=0.5, color='g')

plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()

plt.show()

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第11张图片
     In the plot, we can see that a lot of sell trades are executed when APO trading signal values are positive and a lot of buy trades are executed when APO trading signal values are negative. We also observe that some buy trades are executed when APO trading signal values are positive and some sell trades are executed when APO trading signal values are negative. How do we explain that?

11. As we will see in the following code, those trades are the ones executed to close profits. Let's observe the position and PnL evolution over the lifetime of this strategy:

fig = plt.figure( figsize=(20,10))

data['Position'].plot(color='k', lw=1., legend=True)
plt.plot( data.loc[ data.Position == 0 ].index, data.Position[ data.Position == 0 ], 
          color='r', lw=0, marker='.', label='flat'
        )
plt.plot( data.loc[ data.Position > 0 ].index, data.Position[ data.Position > 0 ],
          color='y', lw=0, marker='+', label='long'
        )
plt.plot( data.loc[ data.Position < 0 ].index, data.Position[ data.Position < 0 ],
          color='b', lw=0, marker='_', label='short'
        )
plt.axhline(y=0, lw=0.5, color='k')
for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE*25, NUM_SHARES_PER_TRADE*5 ):
  plt.axhline(y=i, lw=0.5, color='r')
for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE*25, -NUM_SHARES_PER_TRADE*5 ):
  plt.axhline(y=i, lw=0.5, color='g')

plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()
plt.show()

     From the position plot, we can see some large short positions around 2016-01, then again in 2017-07, and finally again in 2018-01. If we go back to the APO trading signal values, that is when APO values went through large patches of positive values.t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第12张图片

 Finally, let's look at how the PnL evolves for this trading strategy over the course of the stock's life cycle:

fig = plt.figure( figsize=(20,10))

data['Pnl'].plot(color='k', lw=1., legend=True)
plt.plot( data.loc[ data.Pnl > 0 ].index, data.Pnl[ data.Pnl > 0 ], 
          color='b', lw=0, marker='.',
          label='Pnl'
        )
plt.plot( data.loc[ data.Pnl < 0 ].index, data.Pnl[ data.Pnl < 0 ],
          color='y', lw=0, marker='.',
          label='Pnl'
        )
plt.axhline(y=15000, ls='--', alpha=0.5)
plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()
plt.show()

data.to_csv("basic_mean_reversion.csv", sep=",")

      The basic mean reversion strategy makes money pretty consistently over the course of time, with some volatility in returns during 2016-01 and 2017-07, where the strategy has large positions, but finally ending around $15K, which is close to its maximum achieved PnL.t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第13张图片

Mean reversion strategy(+APO+StdDev) that dynamically adjusts for changing volatility

     Now, let's apply the previously introduced concepts of using a volatility measure to adjust the number of days used in Fast and Slow EMA and using a volatility-adjusted APO entry signal. We will use the standard deviation (STDDEV to increase the strategy performance(PnL)
by 200%)
and indicator we explored in https://blog.csdn.net/Linli522362242/article/details/121406833, Deciphering the Markets with Technical Analysis, as a measure of volatility. Let's observe the output of that indicator quickly to recap the Google dataset: 
t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第14张图片
     From the output, it seems like volatility measure(Standard deviation (StdDev)) ranges from somewhere between $8 over 20 days to $40 over 20 days, with $15 over 20 days being the average. So we will use a volatility factor that ranges from 0 to 1, by designing it to be(15 since since the population stddev.mean() = 15.45), where

  • values closer to 0 indicate very low volatility,
  • values around 1 indicate normal volatility, and
  • values above 1 indicate above-normal volatility.

The way in which we incorporate STDEV into our strategy is through the following changes:

  • Instead of having static K_FAST and K_SLOW smoothing factors for the fast and slow EMA, we will instead make them additionally a function of volatility and use K_FAST * stdev_factor and K_SLOW * stdev_factor, to make them more reactive to newest observations during periods of higher than normal volatility, which makes intuitive sense. 
  • Instead of using static APO_VALUE_FOR_BUY_ENTRY and APO_VALUE_FOR_SELL_ENTRY thresholds for entering positions based on the primary trading signal APO, we will also incorporate volatility to have dynamic thresholds APO_VALUE_FOR_BUY_ENTRY * stdev_factor and APO_VALUE_FOR_SELL_ENTRY * stdev_factor. This makes us less aggressive不那么积极地 in entering positions during periods of higher volatility, by increasing the threshold for entry by a factor of volatility, which also makes intuitive sense based on what we discussed in the previous section.
  • Finally, we will incorporate volatility in one last threshold and that is by having a dynamic expected profit threshold to lock in profit in a position. In this case, instead of using the static MIN_PROFIT_TO_CLOSE threshold, we will use a dynamic MIN_PROFIT_TO_CLOSE / stddev_factor . Here, the idea is to be more aggressive in exciting positions during periods of increased volatility, because as we discussed before, during periods of higher than normal volatility, it is riskier to hold on to positions for longer periods of time.
import pandas as pd
import pandas_datareader.data as pdr

def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
    if len(output_file) == 0:
        output_file = stock_symbol+'_data_large.pkl'   
        
    try:
        df = pd.read_pickle( output_file )
        print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
    except FileNotFoundError:
        print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
        df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
        df.to_pickle( output_file )
    return df 

goog_data = load_financial_data( stock_symbol='GOOG',
                                 start_date='2014-01-01', 
                                 end_date='2018-01-01',
                                 output_file='goog_data.pkl'
                               )
goog_data.head()

 t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第15张图片

# Variables/constants for EMA Calculation:
NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
ema_fast = 0 # initial ema
ema_fast_values = [] # we will hold fast EMA values for visualization purpose

NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
ema_slow = 0 # initial ema
ema_slow_values = [] # we will hold slow EMA values for visualization purpose

apo_values = [] # track computed absolute price oscillator values

# Variables for Trading Strategy trade, position & pnl management:

# Container for tracking buy/sell order, 
# +1 for buy order, -1 for sell order, 0 for no-action
orders = []

# Container for tracking positions, 
# positive for long positions, negative for short positions, 0 for flat/no position
positions = []

# Container for tracking total_pnls, this is the sum of 
# closed_pnl i.e. pnls already locked in 
# and open_pnl i.e. pnls for open-position marked to market price
pnls = []


last_buy_price = 0  # used to prevent over-trading at/around the same price
last_sell_price = 0 # used to prevent over-trading at/around the same price
position = 0 # Current position of the trading strategy

# Summation of products of 
# buy_trade_price and buy_trade_qty for every buy Trade made
# since last time being flat
buy_sum_price_qty = 0
# Summation of buy_trade_qty for every buy Trade made since last time being flat
buy_sum_qty = 0

# Summation of products of 
# sell_trade_price and sell_trade_qty for every sell Trade made 
# since last time being flat
sell_sum_price_qty = 0
# Summation of sell_trade_qty for every sell Trade made since last time being flat
sell_sum_qty = 0

open_pnl = 0   # Open/Unrealized PnL marked to market
closed_pnl = 0 # Closed/Realized PnL so far

# Constants that define strategy behavior/thresholds

# APO trading signal value below which(-10) to enter buy-orders/long-position
APO_VALUE_FOR_BUY_ENTRY = -10 # (oversold, expect a bounce back up)
# APO trading signal value above which to enter sell-orders/short-position
APO_VALUE_FOR_SELL_ENTRY = 10 # (overbought, expect a bounce back down)

# Minimum price change since last trade before considering trading again,
MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
NUM_SHARES_PER_TRADE = 10

# positions are closed if currently open positions are profitable above a certain amount, 
# regardless of APO values. 
# This is used to algorithmically lock profits and initiate more positions 
# instead of relying only on the trading signal value. 

# Minimum Open/Unrealized profit at which to close positions and lock profits
MIN_PROFIT_TO_CLOSE = 10*NUM_SHARES_PER_TRADE

     Let's look at the modifications needed to the basic mean reversion strategy to achieve this. First, we need some code to track and update the volatility measure (STDEV):

import statistics as stats
import math as math

data2 = goog_data.copy()
close = data2['Close']

# Constants/variables that are used to compute standard deviation as a volatility measure
SMA_NUM_PERIODS_20 = 20 # look back period
price_history = []      # history of prices

for close_price in close:
    price_history.append( close_price )
    if len( price_history) > SMA_NUM_PERIODS_20 : # we track at most 'time_period' number of prices
        del ( price_history[0] )
    
    # calculate vairance during the SMA_NUM_PERIODS_20 periods
    sma = stats.mean( price_history )
    variance = 0 # variance is square of standard deviation
    for hist_price in price_history:
        variance = variance + ( (hist_price-sma)**2 )
    
    stddev = math.sqrt( variance/len(price_history) )
    
    # a volatility factor that ranges from 0 to 1
    stddev_factor = stddev/15 # 15 since since the population stddev.mean() = 15.45
    # closer to 0 indicate very low volatility,
    # around 1 indicate normal volatility
    # > 1 indicate above-normal volatility
    if stddev_factor == 0:
        stddev_factor = 1
        
    # This section updates fast and slow EMA and computes APO trading signal
    if (ema_fast==0): # first observation
        ema_fast = close_price # initial ema_fast or ema_slow
        ema_slow = close_price
    else:
        # ema fomula 
        # K_FAST*stddev_factor or K_SLOW*stddev_factor 
        # more reactive to newest observations during periods of higher than normal volatility
        ema_fast = (close_price-ema_fast) * K_FAST*stddev_factor + ema_fast
        ema_slow = (close_price-ema_slow) * K_SLOW*stddev_factor + ema_slow
    
    ema_fast_values.append( ema_fast )
    ema_slow_values.append( ema_slow )
    
    apo = ema_fast - ema_slow
    apo_values.append( apo )
    
    # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
    
    # We will perform a sell trade at close_price if the following conditions are met:
    # 1. The APO trading signal value(positive) > Sell-Entry threshold (overbought, expect a bounce back down, sell for profit)
    #     and the difference between current-price and last trade-price is different enough.(>Minimum price change)
    # 2. We are long( +ve position ) and 
    #    either APO trading signal value >= 0 or current position is profitable enough to lock profit.
    # APO_VALUE_FOR_SELL_ENTRY * stdev_factor:
    #              by increasing the threshold for entry by a factor of volatility,
    #              makes us less aggressive in entering positions(here is sell) during periods of higher volatility,            
    # dynamic MIN_PROFIT_TO_CLOSE / stddev_factor: 
    #              to decrease the the expected profit threshold during periods of increased volatility
    #              to be more aggressive in exciting positions
    #              it is riskier to hold on to positions for longer periods of time.
    if ( ( apo > APO_VALUE_FOR_SELL_ENTRY*stddev_factor and \
           abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE*stddev_factor
         )
         or
         ( position>0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE/stddev_factor ) )
       ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
        orders.append(-1) # mark the sell trade
        last_sell_price = close_price
        position -= NUM_SHARES_PER_TRADE
        sell_sum_qty += NUM_SHARES_PER_TRADE
        sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
        print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
        
    # 7. We will perform a buy trade at close_price if the following conditions are met:
    # 1. The APO trading signal value(negative) < below Buy-Entry threshold (oversold, expect a bounce back up, buy for future profit)
    #    and the difference between current-price and last trade-price is different enough.(>Minimum price change)
    # 2. We are short( -ve position ) and
    #    either APO trading signal value is <= 0 or current position is profitable enough to lock profit.
    # APO_VALUE_FOR_BUY_ENTRY * stdev_factor:
    #              by increasing the threshold for entry by a factor of volatility,
    #              makes us less aggressive in entering positions(here is sell) during periods of higher volatility,            
    # dynamic MIN_PROFIT_TO_CLOSE / stddev_factor: 
    #              to decrease the the expected profit threshold during periods of increased volatility
    #              to be more aggressive in exciting positions
    #              it is riskier to hold on to positions for longer periods of time.    
    elif ( ( apo < APO_VALUE_FOR_BUY_ENTRY*stddev_factor and \
             abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE*stddev_factor
           )
           or
           ( position<0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE/stddev_factor ) )
         ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
        orders.append(+1) # mark the buy trade
        last_buy_price = close_price
        position += NUM_SHARES_PER_TRADE
        buy_sum_qty += NUM_SHARES_PER_TRADE
        buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
        print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
    else:
        # No trade since none of the conditions were met to buy or sell
        orders.append( 0 )
        
    positions.append( position )
    
    # 8. The code of the trading strategy contains logic for position/PnL management. 
    #    It needs to update positions and compute open and closed PnLs when market prices change 
    #    and/or trades are made causing a change in positions
    
    # This section updates Open/Unrealized & Closed/Realized positions
    open_pnl = 0
    if position > 0:
        # long position and some sell trades have been made against it, 
        # close that amount based on how much was sold against this long position
        # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
        if sell_sum_qty > 0:           # vwap for sell                   # vwap  for buy
            open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
        # mark the remaining position to market 
        # i.e. pnl would be what it would be if we closed at current price
        # sell
        # position -= NUM_SHARES_PER_TRADE
        # sell_sum_qty += NUM_SHARES_PER_TRADE
        # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
        # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
        open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
        # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
    elif position < 0:
        # short position and some buy trades have been made against it, 
        # close that amount based on how much was bought against this short position
        # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
        if buy_sum_qty > 0:           # vwap for sell                   # vwap  for buy
            open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
        # mark the remaining position to market
        # i.e. pnl would be what it would be if we closed at current price
        # buy
        # position += NUM_SHARE_PER_TRADE
        # buy_sum_qty += NUM_SHARE_PER_TRADE
        # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
        # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) * 
        open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
        # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
    else:
        # flat, so update closed_pnl and reset tracking variables for positions & pnls
        closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
        
        buy_sum_price_qty = 0
        buy_sum_qty = 0
        last_buy_price = 0
        
        sell_sum_price_qty = 0
        sell_sum_qty = 0
        last_sell_price = 0
        
    print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
    pnls.append(closed_pnl + open_pnl)

# This section prepares the dataframe from the trading strategy results and visualizes the results
data2 = data2.assign( ClosePrice = pd.Series(close, index=data2.index) )
data2 = data2.assign( Fast10DayEMA = pd.Series(ema_fast_values, index=data2.index) )
data2 = data2.assign( Slow40DayEMA = pd.Series(ema_slow_values, index=data2.index) )
data2 = data2.assign( APO = pd.Series(apo_values, index=data2.index) )
data2 = data2.assign( Trades = pd.Series(orders, index=data2.index) )
data2 = data2.assign( Position = pd.Series(positions, index=data2.index) )
data2 = data2.assign( Pnl = pd.Series(pnls, index=data2.index) )
import matplotlib.pyplot as plt

fig = plt.figure( figsize=(20,10) )

data2['ClosePrice'].plot(color='k', lw=3., legend=True)
data2['Fast10DayEMA'].plot(color='y', lw=1., legend=True)
data2['Slow40DayEMA'].plot(color='m', lw=1., legend=True)
plt.plot( data2.loc[ data2.Trades == 1 ].index, data2.ClosePrice[data2.Trades == 1 ],
          color='y', lw=0, marker='^', markersize=7, label='buy'
        )
plt.plot( data2.loc[ data2.Trades == -1 ].index, data2.ClosePrice[data2.Trades == -1 ],
          color='b', lw=0, marker='v', markersize=7, label='sell'
        )
plt.autoscale(enable=True, axis='x', tight=True)

plt.legend()
plt.show()

more aggressive in exciting positions during periods of increased volatility(for example, VS), because as we discussed before, during periods of higher than normal volatility, it is riskier to hold on to positions for longer periods of time

fig = plt.figure( figsize=(20,10) )

data2['APO'].plot(color='k', lw=3., legend=True)
plt.plot( data2.loc[ data2.Trades == 1 ].index, data2.APO[data2.Trades == 1 ],
          color='y', lw=0, marker='^', markersize=7, label='buy'
        )
plt.plot( data2.loc[ data2.Trades == -1 ].index, data2.APO[data2.Trades == -1 ],
          color='b', lw=0, marker='v', markersize=7, label='sell'
        )
plt.axhline(y=0, lw=0.5, color='k')
for i in range( APO_VALUE_FOR_BUY_ENTRY, APO_VALUE_FOR_BUY_ENTRY*5, APO_VALUE_FOR_BUY_ENTRY ):
  plt.axhline(y=i, lw=0.5, color='r')
for i in range( APO_VALUE_FOR_SELL_ENTRY, APO_VALUE_FOR_SELL_ENTRY*5, APO_VALUE_FOR_SELL_ENTRY ):
  plt.axhline(y=i, lw=0.5, color='g')

plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()

plt.show()

fig = plt.figure( figsize=(20,10))

data2['Position'].plot(color='k', lw=1., legend=True)
plt.plot( data2.loc[ data2.Position == 0 ].index, data2.Position[ data2.Position == 0 ], 
          color='r', lw=0, marker='.', label='flat'
        )
plt.plot( data2.loc[ data2.Position > 0 ].index, data2.Position[ data2.Position > 0 ],
          color='y', lw=0, marker='+', label='long'
        )
plt.plot( data2.loc[ data2.Position < 0 ].index, data2.Position[ data2.Position < 0 ],
          color='b', lw=0, marker='_', label='short'
        )
plt.axhline(y=0, lw=0.5, color='k')
for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE*25, NUM_SHARES_PER_TRADE*5 ):
  plt.axhline(y=i, lw=0.5, color='r')
for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE*25, -NUM_SHARES_PER_TRADE*5 ):
  plt.axhline(y=i, lw=0.5, color='g')

plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()
plt.show()

fig = plt.figure( figsize=(20,10))

plt.plot( data.index, data['Pnl'], color='g', lw=1.,
          label='BasicMeanReversionPnL'
        )#########################
plt.plot( data.loc[ data.Pnl > 0 ].index, data.Pnl[ data.Pnl > 0 ], 
          color='y', lw=0, marker='.',
          #label='Pnl'
        )
plt.plot( data.loc[ data.Pnl < 0 ].index, data.Pnl[ data.Pnl < 0 ],
          color='r', lw=0, marker='.',
          #label='Pnl'
        )

plt.plot( data2.index, data2['Pnl'], color='b', lw=1.,
          label='VolatilityAdjustedMeanReversionPnL'
        )#########################
plt.plot( data2.loc[ data.Pnl > 0 ].index, data2.Pnl[ data.Pnl > 0 ], 
          color='y', lw=0, marker='.',
          #label='Pnl'
        )
plt.plot( data2.loc[ data.Pnl < 0 ].index, data2.Pnl[ data.Pnl < 0 ],
          color='r', lw=0, marker='.',
          #label='Pnl'
        )
plt.axhline(y=15000, ls='--', alpha=0.5)
plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()
plt.show()


     In this case, adjusting the trading strategy for volatility increases the strategy performance by 200%!

Trend-following strategy(+APO) using absolute price oscillator trading signal

import pandas as pd
import pandas_datareader.data as pdr

def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
    if len(output_file) == 0:
        output_file = stock_symbol+'_data_large.pkl'   
        
    try:
        df = pd.read_pickle( output_file )
        print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
    except FileNotFoundError:
        print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
        df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
        df.to_pickle( output_file )
    return df 

goog_data = load_financial_data( stock_symbol='GOOG',
                                 start_date='2014-01-01', 
                                 end_date='2018-01-01',
                                 output_file='goog_data.pkl'
                               )
goog_data.head()

 t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第16张图片

     Similar to the mean reversion strategy(prices revert toward the mean) we explored, we can build a trend-following strategy that uses the APO trading signal. The only difference here is that

  • we enter long positions when the APO is above a certain value, expecting price moves to continue in that direction, and
  • we enter short positions when the APO is below a certain value, expecting price moves to continue going down.

     In effect, this is the exact opposite trading strategy with some differences in position management. One might expect this trading strategy to be exactly opposite in performance but, as we will see, that is not the case, that is, both trend-following and mean reversion strategies can be profitable in the same market conditions

# Variables/constants for EMA Calculation:
NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
ema_fast = 0 # initial ema
ema_fast_values = [] # we will hold fast EMA values for visualization purpose

NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
ema_slow = 0 # initial ema
ema_slow_values = [] # we will hold slow EMA values for visualization purpose

apo_values = [] # track computed absolute price oscillator values

# Variables for Trading Strategy trade, position & pnl management:

# Container for tracking buy/sell order, 
# +1 for buy order, -1 for sell order, 0 for no-action
orders = []

# Container for tracking positions, 
# positive for long positions, negative for short positions, 0 for flat/no position
positions = []

# Container for tracking total_pnls, this is the sum of 
# closed_pnl i.e. pnls already locked in 
# and open_pnl i.e. pnls for open-position marked to market price
pnls = []

last_buy_price = 0  # used to prevent over-trading at/around the same price
last_sell_price = 0 # used to prevent over-trading at/around the same price
position = 0 # Current position of the trading strategy

# Summation of products of 
# buy_trade_price and buy_trade_qty for every buy Trade made
# since last time being flat
buy_sum_price_qty = 0
# Summation of buy_trade_qty for every buy Trade made since last time being flat
buy_sum_qty = 0

# Summation of products of 
# sell_trade_price and sell_trade_qty for every sell Trade made 
# since last time being flat
sell_sum_price_qty = 0
# Summation of sell_trade_qty for every sell Trade made since last time being flat
sell_sum_qty = 0

open_pnl = 0   # Open/Unrealized PnL marked to market
closed_pnl = 0 # Closed/Realized PnL so far

# Constants that define strategy behavior/thresholds

################################
# APO trading signal value below which(-10) to enter buy-orders/long-position
APO_VALUE_FOR_BUY_ENTRY = 10   # (oversold, expect a bounce back up; >10 continue going up for trending)
# APO trading signal value above which(-10) to enter sell-orders/short-position
APO_VALUE_FOR_SELL_ENTRY = -10 # (overbought, expect a bounce back down; <-10 continue going down for trending)
################################

# Minimum price change since last trade before considering trading again,
MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
NUM_SHARES_PER_TRADE = 10

# positions are closed if currently open positions are profitable above a certain amount, 
# regardless of APO values. 
# This is used to algorithmically lock profits and initiate more positions 
# instead of relying only on the trading signal value. 

# Minimum Open/Unrealized profit at which to close positions and lock profits
MIN_PROFIT_TO_CLOSE = 10*NUM_SHARES_PER_TRADE
import statistics as stats
import math as math

data3 = goog_data.copy()
close  = data3['Close']

for close_price in close:
    # This section updates fast and slow EMA and computes APO trading signal
    if (ema_fast == 0): # first observation
        ema_fast = close_price
        ema_slow = close_price
    else:
        ema_fast = (close_price - ema_fast)*K_FAST + ema_fast # K_FAST = 2/(NUM_PERIODS_FAST_10 + 1)
        ema_slow = (close_price - ema_slow)*K_SLOW + ema_slow # K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1)
    
    ema_fast_values.append(ema_fast)
    ema_slow_values.append(ema_slow)
    
    apo = ema_fast-ema_slow
    apo_values.append(apo)
    
    # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
    
    # We will perform a sell trade at close_price if the following conditions are met:
    # 1. The APO trading signal value(negative) < Sell-Entry threshold (expecting price moves to continue going down)
    #     and the difference between current-price and last trade-price is different enough.(>Minimum price change)
    # 2. We are long( +ve position ) and 
    #    either APO trading signal value >= 0 or current position is profitable enough to lock profit.
              ###
    if ( ( apo < APO_VALUE_FOR_SELL_ENTRY and \
           abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
         )
         or                   ###
         ( position>0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
       ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
        orders.append(-1) # mark the sell trade
        last_sell_price = close_price
        position -= NUM_SHARES_PER_TRADE
        sell_sum_qty += NUM_SHARES_PER_TRADE
        sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
        print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
        
    # 7. We will perform a buy trade at close_price if the following conditions are met:
    # 1. The APO trading signal value(positive) > Buy-Entry threshold (expecting price moves to continue going up)
    #    and the difference between current-price and last trade-price is different enough.(>Minimum price change)
    # 2. We are short( -ve position ) and
    #    either APO trading signal value is >= 0 or current position is profitable enough to lock profit.
                ###
    elif ( ( apo > APO_VALUE_FOR_BUY_ENTRY and \
             abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
           )
           or                   ###
           ( position<0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
         ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
        orders.append(+1) # mark the buy trade
        last_buy_price = close_price
        position += NUM_SHARES_PER_TRADE
        buy_sum_qty += NUM_SHARES_PER_TRADE
        buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
        print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
    else:
        # No trade since none of the conditions were met to buy or sell
        orders.append( 0 )
        
    positions.append( position )
    
    # 8. The code of the trading strategy contains logic for position/PnL management. 
    #    It needs to update positions and compute open and closed PnLs when market prices change 
    #    and/or trades are made causing a change in positions
    
    # This section updates Open/Unrealized & Closed/Realized positions
    open_pnl = 0
    if position > 0:
        # long position and some sell trades have been made against it, 
        # close that amount based on how much was sold against this long position
        # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
        if sell_sum_qty > 0:           # vwap for sell                   # vwap  for buy
            open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
        # mark the remaining position to market 
        # i.e. pnl would be what it would be if we closed at current price
        # sell
        # position -= NUM_SHARES_PER_TRADE
        # sell_sum_qty += NUM_SHARES_PER_TRADE
        # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
        # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
        open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
        # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
    elif position < 0:
        # short position and some buy trades have been made against it, 
        # close that amount based on how much was bought against this short position
        # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
        if buy_sum_qty > 0:           # vwap for sell                   # vwap  for buy
            open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
        # mark the remaining position to market
        # i.e. pnl would be what it would be if we closed at current price
        # buy
        # position += NUM_SHARE_PER_TRADE
        # buy_sum_qty += NUM_SHARE_PER_TRADE
        # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
        # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) * 
        open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
        # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
    else:
        # flat, so update closed_pnl and reset tracking variables for positions & pnls
        closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
        
        buy_sum_price_qty = 0
        buy_sum_qty = 0
        last_buy_price = 0
        
        sell_sum_price_qty = 0
        sell_sum_qty = 0
        last_sell_price = 0
        
    print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
    pnls.append(closed_pnl + open_pnl)

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第17张图片

# This section prepares the dataframe from the trading strategy results and visualizes the results
data3 = data3.assign( ClosePrice=pd.Series(close, index=data3.index))
data3 = data3.assign( Fast10DayEMA=pd.Series(ema_fast_values, index=data3.index))
data3 = data3.assign( Slow40DayEMA=pd.Series(ema_slow_values, index=data3.index))
data3 = data3.assign( APO=pd.Series(apo_values, index=data3.index))
data3 = data3.assign( Trades=pd.Series(orders, index=data3.index))
data3 = data3.assign( Position=pd.Series(positions, index=data3.index))
data3 = data3.assign( Pnl=pd.Series(pnls, index=data3.index))
import matplotlib.pyplot as plt

fig = plt.figure( figsize=(20,10) )

data3['ClosePrice'].plot(color='k', lw=3., legend=True)
data3['Fast10DayEMA'].plot(color='y', lw=1., legend=True)
data3['Slow40DayEMA'].plot(color='m', lw=1., legend=True)
plt.plot( data3.loc[ data3.Trades == 1 ].index, data3.ClosePrice[data3.Trades == 1 ],
          color='y', lw=0, marker='^', markersize=7, label='buy'
        )
plt.plot( data3.loc[ data3.Trades == -1 ].index, data3.ClosePrice[data3.Trades == -1 ],
          color='b', lw=0, marker='v', markersize=7, label='sell'
        )
plt.autoscale(enable=True, axis='x', tight=True)

plt.legend()
plt.show()

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第18张图片

     The plot shows at what prices the buy and sell trades are made throughout the lifetime of the trading strategy applied to Google stock data. The trading strategy behavior will make more sense when we inspect the APO signal values to go along with the actual trade prices. Let's look at that in the next plot:

fig = plt.figure( figsize=(20,10) )

data3['APO'].plot(color='k', lw=3., legend=True)
plt.plot( data3.loc[ data3.Trades == 1 ].index, data3.APO[data3.Trades == 1 ],
          color='y', lw=0, marker='^', markersize=7, label='buy'
        )
plt.plot( data3.loc[ data3.Trades == -1 ].index, data3.APO[data3.Trades == -1 ],
          color='b', lw=0, marker='v', markersize=7, label='sell'
        )
plt.axhline(y=0, lw=0.5, color='k')
for i in range( APO_VALUE_FOR_BUY_ENTRY, APO_VALUE_FOR_BUY_ENTRY*5, APO_VALUE_FOR_BUY_ENTRY ):
  plt.axhline(y=i, lw=0.5, color='r')
for i in range( APO_VALUE_FOR_SELL_ENTRY, APO_VALUE_FOR_SELL_ENTRY*5, APO_VALUE_FOR_SELL_ENTRY ):
  plt.axhline(y=i, lw=0.5, color='g')

plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()

plt.show()

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第19张图片

     By the definition of a trend-following strategy using the APO trading signal values, intuitively we expect buy trades when APO signal values are positive and sell trades when APO signal values are negative. There are also some buy trades when APO signal values are negative and some sell trades when APO signal values are positive, which might seem counterintuitive, but these are trades made to close out profitable positions, similar to the mean reversion strategy. Now, let's look at the evolution of positions through the course of this trading strategy:

fig = plt.figure( figsize=(20,10))

data3['Position'].plot(color='k', lw=1., legend=True)
plt.plot( data3.loc[ data3.Position == 0 ].index, data3.Position[ data3.Position == 0 ], 
          color='r', lw=0, marker='.', label='flat'
        )
plt.plot( data3.loc[ data3.Position > 0 ].index, data3.Position[ data3.Position > 0 ],
          color='y', lw=0, marker='+', label='long'
        )
plt.plot( data3.loc[ data3.Position < 0 ].index, data3.Position[ data3.Position < 0 ],
          color='b', lw=0, marker='_', label='short'
        )
plt.axhline(y=0, lw=0.5, color='k')
for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE*25, NUM_SHARES_PER_TRADE*5 ):
  plt.axhline(y=i, lw=0.5, color='r')
for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE*25, -NUM_SHARES_PER_TRADE*5 ):
  plt.axhline(y=i, lw=0.5, color='g')

plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()
plt.show()

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第20张图片

     Here, compared to the mean reversion trading strategyt5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第21张图片, there are more long positions than short positions, and the positions are usually small and closed quickly and a new position (likely long) is initiated shortly after. This observation is consistent with the fact that this is a trend-following strategy applied to a strongly upward-trending trading instrument such as the Google stock. Since Google stocks have been steadily trending upward over the course of this trading strategy, it makes sense that most of the positions are long and also makes sense that most of the long positions end up being profitable and are flattened shortly after being initiated. Finally, let's observe the evolution of PnL for this trading strategy:

fig = plt.figure( figsize=(20,10))

data3['Pnl'].plot(color='k', lw=1., legend=True)
plt.plot( data3.loc[ data3.Pnl > 0 ].index, data3.Pnl[ data3.Pnl > 0 ], 
          color='b', lw=0, marker='.',
          label='Pnl'
        )
plt.plot( data3.loc[ data3.Pnl < 0 ].index, data3.Pnl[ data3.Pnl < 0 ],
          color='y', lw=0, marker='.',
          label='Pnl'
        )

plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()
plt.show()

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第22张图片

     So, for this case, the trend-following strategy makes a third of the money that the mean reversion strategy makest5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第23张图片; however, the trend-following strategy also makes money for the same market conditions by entering and exiting positions at different price points.

Trend-following strategy(+APO+StdDev) that dynamically adjusts for changing volatility

     Let's use STDEV as a measure of volatility and adjust the trend-following strategy to adapt to changing market volatility. We will use an identical approach to the one we used when adjusting the mean reversion trading strategy for market volatility.

import pandas as pd
import pandas_datareader.data as pdr

def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
    if len(output_file) == 0:
        output_file = stock_symbol+'_data_large.pkl'   
        
    try:
        df = pd.read_pickle( output_file )
        print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
    except FileNotFoundError:
        print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
        df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
        df.to_pickle( output_file )
    return df 

goog_data = load_financial_data( stock_symbol='GOOG',
                                 start_date='2014-01-01', 
                                 end_date='2018-01-01',
                                 output_file='goog_data.pkl'
                               )
goog_data.head()

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第24张图片

# Variables/constants for EMA Calculation:
NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
ema_fast = 0 # initial ema
ema_fast_values = [] # we will hold fast EMA values for visualization purpose

NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
ema_slow = 0 # initial ema
ema_slow_values = [] # we will hold slow EMA values for visualization purpose

apo_values = [] # track computed absolute price oscillator values

# Variables for Trading Strategy trade, position & pnl management:

# Container for tracking buy/sell order, 
# +1 for buy order, -1 for sell order, 0 for no-action
orders = []

# Container for tracking positions, 
# positive for long positions, negative for short positions, 0 for flat/no position
positions = []

# Container for tracking total_pnls, this is the sum of 
# closed_pnl i.e. pnls already locked in 
# and open_pnl i.e. pnls for open-position marked to market price
pnls = []

last_buy_price = 0  # used to prevent over-trading at/around the same price
last_sell_price = 0 # used to prevent over-trading at/around the same price
position = 0 # Current position of the trading strategy

# Summation of products of 
# buy_trade_price and buy_trade_qty for every buy Trade made
# since last time being flat
buy_sum_price_qty = 0
# Summation of buy_trade_qty for every buy Trade made since last time being flat
buy_sum_qty = 0

# Summation of products of 
# sell_trade_price and sell_trade_qty for every sell Trade made 
# since last time being flat
sell_sum_price_qty = 0
# Summation of sell_trade_qty for every sell Trade made since last time being flat
sell_sum_qty = 0

open_pnl = 0   # Open/Unrealized PnL marked to market
closed_pnl = 0 # Closed/Realized PnL so far

# Constants that define strategy behavior/thresholds

################################
# APO trading signal value below which(-10) to enter buy-orders/long-position
APO_VALUE_FOR_BUY_ENTRY = 10   # (oversold, expect a bounce back up)
# APO trading signal value above which to enter sell-orders/short-position
APO_VALUE_FOR_SELL_ENTRY = -10 # (overbought, expect a bounce back down)
################################

# Minimum price change since last trade before considering trading again,
MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
NUM_SHARES_PER_TRADE = 10

# positions are closed if currently open positions are profitable above a certain amount, 
# regardless of APO values. 
# This is used to algorithmically lock profits and initiate more positions 
# instead of relying only on the trading signal value. 

# Minimum Open/Unrealized profit at which to close positions and lock profits
MIN_PROFIT_TO_CLOSE = 10*NUM_SHARES_PER_TRADE
import statistics as stats
import math as math

data4 = goog_data.copy()
close = data4['Close']

# Constants/variables that are used to compute standard deviation as a volatility measure
SMA_NUM_PERIODS_20 = 20 # look back period
price_history = []      # history of prices

for close_price in close:
    price_history.append( close_price )
    if len( price_history) > SMA_NUM_PERIODS_20 : # we track at most 'time_period' number of prices
        del ( price_history[0] )
    
    # calculate vairance during the SMA_NUM_PERIODS_20 periods
    sma = stats.mean( price_history )
    variance = 0 # variance is square of standard deviation
    for hist_price in price_history:
        variance = variance + ( (hist_price-sma)**2 )
    
    stddev = math.sqrt( variance/len(price_history) )
    
    # a volatility factor that ranges from 0 to 1
    stddev_factor = stddev/15 # 15 since since the population stddev.mean() = 15.45
    # closer to 0 indicate very low volatility,
    # around 1 indicate normal volatility
    # > 1 indicate above-normal volatility
    if stddev_factor == 0:
        stddev_factor = 1
        
    # This section updates fast and slow EMA and computes APO trading signal
    if (ema_fast==0): # first observation
        ema_fast = close_price # initial ema_fast or ema_slow
        ema_slow = close_price
    else:
        # ema fomula 
        # K_FAST*stddev_factor or K_SLOW*stddev_factor 
        # more reactive to newest observations during periods of higher than normal volatility
        ema_fast = (close_price-ema_fast) * K_FAST*stddev_factor + ema_fast
        ema_slow = (close_price-ema_slow) * K_SLOW*stddev_factor + ema_slow
    
    ema_fast_values.append( ema_fast )
    ema_slow_values.append( ema_slow )
    
    apo = ema_fast - ema_slow
    apo_values.append( apo )
    
    # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
    
    # We will perform a sell trade at close_price if the following conditions are met:
    # 1. The APO trading signal value(negative) < Sell-Entry threshold (expecting price moves to continue going down)
    #     and the difference between current-price and last trade-price is different enough.(>Minimum price change)
    # 2. We are long( +ve position ) and 
    #    either APO trading signal value >= 0 or current position is profitable enough to lock profit.
              ###
    if ( ( apo < APO_VALUE_FOR_SELL_ENTRY and \
           abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
         )
         or                   ###
         ( position>0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
       ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
        orders.append(-1) # mark the sell trade
        last_sell_price = close_price
        position -= NUM_SHARES_PER_TRADE
        sell_sum_qty += NUM_SHARES_PER_TRADE
        sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
        print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
        
    # 7. We will perform a buy trade at close_price if the following conditions are met:
    # 1. The APO trading signal value(positive) > Buy-Entry threshold (expecting price moves to continue going up)
    #    and the difference between current-price and last trade-price is different enough.(>Minimum price change)
    # 2. We are short( -ve position ) and
    #    either APO trading signal value is >= 0 or current position is profitable enough to lock profit.
                ###
    elif ( ( apo > APO_VALUE_FOR_BUY_ENTRY and \
             abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
           )
           or                   ###
           ( position<0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
         ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
        orders.append(+1) # mark the buy trade
        last_buy_price = close_price
        position += NUM_SHARES_PER_TRADE
        buy_sum_qty += NUM_SHARES_PER_TRADE
        buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
        print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
    else:
        # No trade since none of the conditions were met to buy or sell
        orders.append( 0 )
        
    positions.append( position )
    
    # 8. The code of the trading strategy contains logic for position/PnL management. 
    #    It needs to update positions and compute open and closed PnLs when market prices change 
    #    and/or trades are made causing a change in positions
    
    # This section updates Open/Unrealized & Closed/Realized positions
    open_pnl = 0
    if position > 0:
        # long position and some sell trades have been made against it, 
        # close that amount based on how much was sold against this long position
        # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
        if sell_sum_qty > 0:           # vwap for sell                   # vwap  for buy
            open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
        # mark the remaining position to market 
        # i.e. pnl would be what it would be if we closed at current price
        # sell
        # position -= NUM_SHARES_PER_TRADE
        # sell_sum_qty += NUM_SHARES_PER_TRADE
        # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
        # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
        open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
        # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
    elif position < 0:
        # short position and some buy trades have been made against it, 
        # close that amount based on how much was bought against this short position
        # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
        if buy_sum_qty > 0:           # vwap for sell                   # vwap  for buy
            open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
        # mark the remaining position to market
        # i.e. pnl would be what it would be if we closed at current price
        # buy
        # position += NUM_SHARE_PER_TRADE
        # buy_sum_qty += NUM_SHARE_PER_TRADE
        # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
        # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) * 
        open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
        # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
    else:
        # flat, so update closed_pnl and reset tracking variables for positions & pnls
        closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
        
        buy_sum_price_qty = 0
        buy_sum_qty = 0
        last_buy_price = 0
        
        sell_sum_price_qty = 0
        sell_sum_qty = 0
        last_sell_price = 0
        
    print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
    pnls.append(closed_pnl + open_pnl) 

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第25张图片

# This section prepares the dataframe from the trading strategy results and visualizes the results
data4 = data4.assign( ClosePrice = pd.Series(close, index=data4.index) )
data4 = data4.assign( Fast10DayEMA = pd.Series(ema_fast_values, index=data4.index) )
data4 = data4.assign( Slow40DayEMA = pd.Series(ema_slow_values, index=data4.index) )
data4 = data4.assign( APO = pd.Series(apo_values, index=data4.index) )
data4 = data4.assign( Trades = pd.Series(orders, index=data4.index) )
data4 = data4.assign( Position = pd.Series(positions, index=data4.index) )
data4 = data4.assign( Pnl = pd.Series(pnls, index=data4.index) )
fig = plt.figure( figsize=(20,10))

plt.plot( data3.index, data3['Pnl'], color='g', lw=1.,
          label='BasicTrendFollowingPnL'
        )#########################
plt.plot( data3.loc[ data3.Pnl > 0 ].index, data3.Pnl[ data3.Pnl > 0 ], 
          color='y', lw=0, marker='.',
          #label='Pnl'
        )
plt.plot( data3.loc[ data3.Pnl < 0 ].index, data3.Pnl[ data3.Pnl < 0 ],
          color='r', lw=0, marker='.',
          #label='Pnl'
        )

plt.plot( data4.index, data4['Pnl'], color='b', lw=1.,
          label='VolatilityAdjustedTrendFollowingPnL'
        )#########################
plt.plot( data4.loc[ data4.Pnl > 0 ].index, data4.Pnl[ data4.Pnl > 0 ], 
          color='y', lw=0, marker='.',
          #label='Pnl'
        )
plt.plot( data4.loc[ data4.Pnl < 0 ].index, data4.Pnl[ data4.Pnl < 0 ],
          color='r', lw=0, marker='.',
          #label='Pnl'
        )

plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()
plt.show()

     Finally, let's compare trend-following strategy performance with and without accounting for volatility changes

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第26张图片

     So, for trend-following strategies, having dynamic trading thresholds degrades strategy performance. We can explore tweaking the application of the volatility measure to see whether there are variants that actually improve performance compared to static trend-following.

Creating a trading strategy for economic events

     In this section, we will explore a new class of trading strategies that is different from what we've seen before. Instead of using technical indicators, we can research economic releases and use various economic releases to estimate/predict the impact on the trading instruments and trade them accordingly. Let's first take a look at what economic releases are and how instrument pricing is influenced by releases.

Economic releases

     Economic indicators are a measure of economic activity for a certain country or region or asset classes. These indicators are measured, researched, and released by different entities. Some of these entities are government agencies and some are private research firms. Most of these are released on a schedule, known as an economic calendar. In addition, there is plenty of data available for past releases, expected releases, and actual releases. Each economic indicator captures different economic activity measures:

  • some might affect housing prices,
  • some show employment information,
  • some affect grain, corn, and wheat instruments,
  • others affect precious metals and energy commodities.

For example, possibly the most well-known economic indicator, Nonfarm Payrolls非农就业人数 in America, is a monthly indicator released by the US Department of Labor ( https://www.bls.gov/ces/ ) that represents the number of new jobs created in all non-agricultural industries. This economic release has a huge impact on almost all asset classes. Another example is the EIA Crude Oil Stockpiles reportEIA 原油库存报告, which is a weekly indicator released by the Energy Information Administration that measures change in the number of barrels[ˈbærəlz]桶 of crude oil原油 available. This is a high-impact release for energy products, oil, gas, and so on, but does not usually directly affect things such as stocks, and interest rates.

     Now that we have an intuitive idea of what economic indicators are and what economic releases capture and signify[ˈsɪɡnɪfaɪ]内容和含义, let's look at a short list of important US economic releases. We will not be covering the details of these releases here, but we encourage the reader to explore the economic indicators mentioned here as well as others in greater detail:

     ADP Employment, API Crude, Balance of Trade, Baker Hughes Oil Rig Count贝克休斯石油钻井平台数量, Business Optimism, Business Inventories, Case-Shiller, CB Consumer Confidence, CB Leading Index, Challenger Job Cuts挑战者裁员, Chicago PMI芝加哥采购经理人指数, Construction Spending, Consumer Credit, Consumer Inflation Expectations, Durable Goods耐用品, EIA Crude原油, EIA Natural Gas, Empire State Manufacturing Index帝国州制造业指数, Employment Cost Index就业成本指数, Factory Orders, Fed Beige Book美联储褐皮书, Fed Interest Rate Decision, Fed Press Conference美联储新闻发布会, Fed Manufacturing Index, Fed National Activity, FOMC Economic Projections经济预测, FOMC Minutes经济预测经济预测, GDP, Home Sales, Housing Starts, House Price Index, Import Prices, Industrial Production, Inflation Rate, ISM Manufacturing, ISM Non-Manufacturing, ISM New York Index, Jobless Claims失业救济人数, JOLTs, Markit Composite PMI综合采购经理人指数, Markit Manufacturing PMI制造业采购经理人指数, Michigan Consumer Sentiment消费者情绪, Mortgage Applications抵押贷款申请, NAHB Housing Market Index, Nonfarm Payrolls非农就业人数, Nonfarm Productivity非农生产力, PCE, PPI, Personal Spending, Redbook, Retail Sales, Total Vehicle Sales, WASDE & Wholesale Inventories批发投资条目.

More information about these releases is available at https://tradingeconomics.com/​.

Economic release format

     There are plenty of free and paid economic release calendars available, which can be scraped for historical release data or accessed through a proprietary API. Since the focus of this section is utilizing economic release data in trading, we will skip the details of accessing historical data, but it is quite straightforward. Most common economic release calendars look like this:
t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第27张图片
     As we discussed earlier, the date and time of releases are set well in advance. Most calendars also provide the previous year's release, or sometimes the previous month's release. The Consensus[kənˈsensəs]一致看法,共识 estimate is what multiple economists or firms expect the release to be; this is generally treated as the expected value of the release, and any large misses from this expectation will cause large price volatility. A lot of calendars, in addition, provide a Forecast field, which is the calendar provider's expected value for that economic release. At the time of writing, https://tradingeconomics.com/ , https://www.forexfactory.com/​, and https://www.fxstreet.com/​ are some of the many free and paid economic calendar providers.

Electronic economic release services

     One last concept we need to understand before we can look into the analysis of economic releases and price movement is how to deliver these economic releases electronic to trading strategies right to the trading servers. There are a lot of service providers that provide economic releases directly to trading servers electronically via low-latency direct lines. Most providers cover most of the major economic indicators and usually deliver releases to the trading strategies in machine-parsable feeds机器可解析的源. These releases can reach the trading servers anywhere from a few microseconds up to a few milliseconds after the official release. Nowadays, it's quite common for a lot of algorithmic trading market participants to make use of such electronic economic release providers as alternative data providers to improve trading performance.

Economic releases in trading

     Now that we have a good grasp[ɡrɑːsp] of what economic indicators are, how the economic releases are scheduled, and how they can be delivered electronically directly to trading servers, let's dive in and look at some possible edge trading strategies gain from economic indicator releases. There are a couple of different ways to use economic indicator releases in algorithmic trading, but we will explore the most common and most intuitive approach. Given the history of expected economic indicator values and actual releases similar to the format we saw before, it is possible to correlate the difference between expected and actual values with price movement that follows. Generally, there are two approaches. One capitalizes on利用 price moves that are less than expected for a big miss in expected and actual economic indicator release, that is, the price should have moved a certain amount based on historical research, but moved much less. This strategy takes a position with the view采取一个立场与观点 that prices will move further and tries to capture a profit if it does, similar to trend-following trading strategies in some sense.

     The other approach is the opposite one, which tries to detect overreactions过度反应 in price movements and make the opposite bet, that is, prices will go back to previous price levels, similar to a mean reversion strategy in some sense. In practice, this approach is often improved by using classification methods we explored in https://blog.csdn.net/Linli522362242/article/details/121551663, Predicting the Markets with Basic Machine Learning. Classification methods allow us to improve the process of combining multiple economic releases that occur at the same time in addition to having multiple possible value-boundaries for each release, to provide greater granularity and thresholds粒度和阈值. For the purposes of this example, we will not dive into the complexity of applying classification methods to this economic release trading strategy.

     Let's look at a couple of Non Farm Payroll releases非农就业数据的发布 and observe the impact on the S&P futures标准普尔期货. Because this requires tick data, which is not freely available, we will skip the actual analysis code, but it should be easy to conceptualize this analysis and understand how to apply it to different datasets: 
t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第28张图片

     Let's quickly put together a scatter plot to easily visualize how price moves correspond to misses in economic indicator releases:
t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第29张图片

     As you can observe,

  • positive misses (actual indicator value higher than consensus indicator values) cause prices to move higher. Conversely,
  • negative misses (actual indicator value lower than consensus indicator values) cause prices to move lower.

In general, higher NonFarm Payroll job additions较高的非农就业岗位增加 are considered to indicate a healthy economy and thus cause the S&P, which tracks major stocks, to increase in value. Another interesting thing to observe is that the larger the miss, in general, the bigger the price move. So with this simple analysis, we have expected reaction for two unknowns: the direction of a price move due to a miss and the magnitude of the price move as a function of the magnitude of the miss. Now, let's look at how to use this information. 

     As we discussed before, one of the approaches is to use the miss value and the research to use a trend-following approach (This strategy takes a position with the view采取一个立场与观点 that prices will move further and tries to capture a profit if it does) and

  • buy on a large positive miss and sell on a large negative miss, with the expectation that prices will move up or down a certain amount.
  • The strategy then closes the long or short position when the expected price move has materialized实现.
  • This strategy works when the price move and magnitude are in line with the research.
  • Another important consideration is the latency between the release and when the prices begin to move. The strategy needs to be fast enough to initiate a position before the information is available to all other participants and price move has finished.

     The other approach is to use the miss value and the research to detect overreaction in price moves and then take the opposite position (prices will go back to previous price levels, similar to a mean reversion strategy in some sense). In this instance,

  • for a positive miss( (actual indicator value higher than consensus indicator values), cause prices to move higher ), if the price decreases, we can have the view that this move is a mistake or an overreaction and initiate a long position with the expectation that prices will go up as our research indicates it should.
  • The other overreaction is if prices move up due to a positive miss as our research indicated but the magnitude of the move is significantly larger than our research indicates. In that case, the strategy waits till prices have moved significantly outside of expectation and then initiates a short position, expecting the overreaction to die down and prices to revert a bit价格会稍微回落, allowing us to capture a profit.
  • The benefit of the mean reversion trading approach to economic releases over the trend-following approach is that the latter is less sensitive to latency between economic indicator release and time window within which the trading strategy must initiate a position

Understanding and implementing basic statistical arbitrage trading strategies

     Statistical arbitrage[ˈɑːrbɪtrɑːʒ] trading strategies统计套利交易策略 (StatArb) first became popular in the 1980s, delivering many firms double-digit returns两位数的回报. It is a class of strategies that tries to capture relationships between short-term price movements in many correlated products. Then it uses relationships that have been found to be statistically significant in the past research to make predictions in the instrument being traded based on price movements in a large group of correlated products.

Basics of StatArb

     Statistical arbitrage or StatArb is in some way similar to pairs trading that takes offsetting positions采取抵消头寸 in co-linearly related products(就是买一个产品的头寸,卖掉另外一个线性相关产品的头寸,价格上相等) that we explored in https://blog.csdn.net/Linli522362242/article/details/121721868, Classical Trading Strategies Driven by Human Intuition. However, the difference here is that StatArb trading strategies often have baskets or portfolios of hundreds of trading instrument, whether they are futures instruments期货工具, equities, options期权, or even currencies. Also, StatArb strategies have a mixture of mean reversion and trend-following strategies. One possibility is that price deviation in the instrument being traded is less than the expected price deviation based on the expected relationship with the price deviations for the portfolio of instruments. In that case, StatArb strategies resemble a trend-following strategy by positioning themselves on the expectation that the trading instrument's price will catch up to the portfolio.

     The other case is that price deviation in the instrument being traded is more than the expected price deviation based on the expected relationship with the price deviations for the portfolio of instruments. Here, StatArb strategies resemble a mean reversion strategy by positioning themselves on the expectation that the trading instrument's price will revert back to the portfolio. Most widespread applications of StatArb trading strategies lean more toward mean reversion strategies. StatArb strategies can be considered HFT but can also be medium frequency if the strategy positions last longer than a few milliseconds or a few seconds.

Lead-lag超前滞后 in StatArb

     Another important consideration is that this strategy implicitly expects the portfolio to lead and the trading instrument is lagging in terms of reaction by market participants.

     When this is not true, for example, when the trading instrument we are trying to trade is actually the one leading price moves across the portfolio, then this strategy doesn't perform well, because instead of the trading instrument price catching up to the portfolio, now the portfolio prices catch up to the trading instrument. This is the concept of lead-lag in StatArb; to be profitable, we need

  • to find trading instruments that are mostly lagging and
  • build a portfolio of instruments that are mostly leading.

     A lot of the time, the way this manifests[ˈmænɪfest]显示,表明 itself is that during some market hours, some instruments lead others and during other market hours, that relationship is reversed. For example, intuitively one can understand that

  • during Asia market hours, trading instruments traded in Asian electronic exchanges such as Singapore, India, Hong Kong, and Japan lead price moves in global assets.
  • During European market hours, trading instruments traded in Germany, London, and other European countries lead most price moves across global assets.
  • Finally, during American market hours, trading instruments in America lead price moves.

So the ideal approach is to construct portfolios and establish relationships between lead and lag instruments differently in different trading sessions

Adjusting portfolio composition and relationships 

     Another important factor to build StatArb strategies that perform well consistently持续表现良好 is understanding and building systems to adapt to changing portfolio compositions and relationships between different trading instruments. The drawback of having the StatArb trading strategy depend primarily on the short-term relationships between large number of trading instruments is that it is hard to understand and adapt to changing relationships between price moves in all the different instruments that constitute a portfolio. The portfolio weights themselves change over time. Principal component analysis(https://blog.csdn.net/Linli522362242/article/details/120559394), a statistical tool from dimensionality reduction techniques, can be used to construct, adapt, and monitor portfolio weights and significance that change over time

     The other important issue is dealing with relationships between the trading instrument and the leading instruments and also between the trading instrument and the portfolio of leading instruments. Sometimes, localized volatility and country-specific economic events cause the fundamental relationship needed to make StatArb profitable break down. For example, political or economic conditions in Brazil can start affecting the Brazilian real currency price moves to no longer be driven by major currencies around the world. Similarly, during periods of localized economic distress贫困,危难 in Britain, say for Brexit英国退欧, or in America, say due to trade wars against China, these portfolio relationships as well as the lead-lag relationships break down from historical expectations and kill the profitability of StatArb trading strategies. Trying to deal with such conditions can require a lot more statistical edges and sophistication beyond just StatArb techniques.

Infrastructure expenses in StatArb

     The last big consideration in StatArb trading is the fact that to be successful in StatArb trading strategies as a business, it is very important to be connected to a lot of electronic trading exchanges to get market data across different exchanges across different countries/continents/markets. Being co-located共处一地 in so many trading exchanges is extremely expensive from an infrastructure cost perspective. The other problem is that one needs to not only be connected to as many exchanges as possible, but a lot of software development investment needs to make to receive, decode, and store market data and also to send orders, since a lot of these exchanges likely use different market data feed and order gateway communication formats.

     The final big consideration is that since StatArb strategies need to receive market data from all exchanges, now every venue[ˈvenjuː]场所 needs a physical data link from every other venue, which gets exponentially expensive for every exchange added. Then, if one considers using the much more expensive microwave services to deliver data faster to the trading boxes, that makes it even worse. So to summarize, StatArb trading strategies can be significantly more expensive than some of the other trading strategies from an infrastructure perspective when it comes to running an algorithmic trading business.

  • StatArb trading strategy in Python

     Now that we have a good understanding of the principles involved in StatArb trading strategies and some practical considerations in building and operating an algorithmic trading business utilizing StatArb trading strategies, let's look at a realistic trading strategy implementation and understand its behavior and performance. In practice, modern algorithmic trading businesses that operate with high frequency usually use a low-level programming language such as C++.

StatArb data set

     Let's first get the data set we will need to implement a StatArb trading strategy. For this section, we will use the following major currencies across the world:

  • Austrian Dollar versus American Dollar (AUD/USD)
  • British Pound versus American Dollar (GBP/USD)
  • Canadian Dollar versus American Dollar (CAD/USD)
  • Swiss Franc versus American Dollar (CHF/USD)
  • Euro versus American Dollar (EUR/USD)
  • Japanese Yen versus American Dollar (JPY/USD)
  • New Zealand Kiwi versus American Dollar (NZD/USD)

     And for this implementation of the StatArb trading strategy, we will try to trade CAD/USD using its relationship with the other currency pairs:

1. Let's fetch 4 years' worth of data for these currency pairs and set up our data frames:

import pandas as pd
import pandas_datareader.data as pdr

# Fetch daily data for 4 years, for 7 major currency pairs
TRADING_INSTRUMENT = 'CADUSD=X'
SYMBOLS = ['AUDUSD=X', 'GBPUSD=X', 'CADUSD=X', 'CHFUSD=X', 'EURUSD=X', 'JPYUSD=X',
           'NZDUSD=X']
START_DATE = '2014-01-01'
END_DATE = '2018-01-01'

# DataSeries for each currency
symbols_data = {}
for symbol in SYMBOLS:
    SRC_DATA_FILENAME = symbol + '_data.pkl'
    
    try:
        data = pd.read_pickle( SRC_DATA_FILENAME )
    except FileNotFoundError:
        data = pdr.DataReader( symbol, 'yahoo', START_DATE, END_DATE )
        data.to_pickle( SRC_DATA_FILENAME )
        
    symbols_data[symbol] = data

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第30张图片

symbols_data

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第31张图片
2. Let's quickly visualize each currency pair's prices over the period of our data set and see what we observe. We scale the JPY/USD pair by 100.0 purely for visualization scaling purposes:

# Visualize prices for currency to inspect relationship between them
import matplotlib.pyplot as plt
import numpy as np
from itertools import cycle

cycol = cycle('bgrcmky')

price_data = pd.DataFrame()

fig = plt.figure( figsize=(20,14) )
for symbol in SYMBOLS:
    multiplier = 1.0
    if symbol == 'JPYUSD=X':
        multiplier = 100.0
    label = symbol + ' ClosePrice'
    price_data = price_data.assign( label=pd.Series( symbols_data[symbol]['Close'] * multiplier,
                                                     index = symbols_data[symbol].index
                                                   ) # datetime index
                                  )
    if symbol == 'JPYUSD=X' or symbol == 'CHFUSD=X':
        ax = price_data['label'].plot( color=next(cycol), lw=2., label=label, ls='dotted' )
    else:    
        ax = price_data['label'].plot( color=next(cycol), lw=2., label=label )
    
plt.xlabel( 'Date', fontsize=18 )
plt.ylabel( 'Scaled Price', fontsize=18 )
plt.legend( prop={'size':18} )
plt.show()

     As one would expect and can observe, these currency pairs' price moves are all similar to each other in varying degrees. CAD/USD, AUD/USD, and NZD/USD seem to be most correlated(bottom), with CHF/USD and JPY/USD being least correlated to CAD/USD(middle). For the purposes of this strategy, we will use all currencies in the trading model because these relationships are obviously not known ahead of time. 

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第32张图片

df=pd.DataFrame()
for symbol in SYMBOLS:
    df[symbol]=symbols_data[symbol]['Close']
df.head()

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第33张图片

from mlxtend.plotting import heatmap

cm = np.corrcoef( df[SYMBOLS].values.T )
# df[cols].values.T : (n_instances, n_features) ==> (n_features, n_instances or n_observations)
hm = heatmap( cm, row_names=SYMBOLS, column_names=SYMBOLS )

plt.show()

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第34张图片==> t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第35张图片

CAD/USD, AUD/USD(0.96), and NZD/USD(0.92) seem to be most correlated, with CHF/USD(0.82), GBP/USD(0.76) and JPY/USD(0.48) being least correlated to CAD/USD.

Defining StatArb signal parameters

     Now, let's define and quantify some parameters we will need to define moving averages, price deviation from moving averages, history of price deviations, and variables to compute and track correlations:

import statistics as stats

# Constants/variables that are used to compute 
# simple moving average and price deviation from simple moving average
SMA_NUM_PERIODS = 20 # look back period
price_history = {}   # history of prices

PRICE_DEV_NUM_PRICES = 200    # look back period of ClosePrice deviations from SMA
price_deviation_from_sma = {} # history of ClosePrice deviations from SMA

# We will use this to iterate over all the days of data we have
# TRADING_INSTRUMENT = 'CADUSD=X'     # datetime index
num_days = len( symbols_data[TRADING_INSTRUMENT].index )
correlation_history = {} # history of correlations per currency pair
# history of differences between 
# Projected ClosePrice deviation and actual ClosePrice deviation per currency pair
delta_projected_actual_history = {}

# history of differences between 
# final Projected ClosePrice deviation for TRADING_INSTRUMENT and actual ClosePrice deviation
final_delta_projected_history = []

Defining StatArb trading parameters

     Now, before we get into the main strategy loop, let's define some final variables and thresholds we will need to build our StatArb trading strategy

######## Variables for Trading Strategy trade, position & pnl management ########

# Container for tracking buy/sell order, 
# +1 for buy order, -1 for sell order, 0 for no-action
orders = []

# Container for tracking positions, 
# positive for long positions, negative for short positions, 0 for flat/no position
positions = []

# Container for tracking total_pnls, this is the sum of 
# closed_pnl i.e. pnls already locked in 
# and open_pnl i.e. pnls for open-position marked to market price
pnls = []

last_buy_price = 0  # used to prevent over-trading at/around the same price
last_sell_price = 0 # used to prevent over-trading at/around the same price
position = 0 # Current position of the trading strategy

# Summation of products of 
# buy_trade_price and buy_trade_qty for every buy Trade made
# since last time being flat
buy_sum_price_qty = 0
# Summation of buy_trade_qty for every buy Trade made since last time being flat
buy_sum_qty = 0

# Summation of products of 
# sell_trade_price and sell_trade_qty for every sell Trade made 
# since last time being flat
sell_sum_price_qty = 0
# Summation of sell_trade_qty for every sell Trade made since last time being flat
sell_sum_qty = 0

open_pnl = 0   # Open/Unrealized PnL marked to market
closed_pnl = 0 # Closed/Realized PnL so far

# Constants that define strategy behavior/thresholds
# StatArb trading signal value above which to enter buy-orders/long-position
StatArb_VALUE_FOR_BUY_ENTRY = 0.01
# StatArb trading signal value below which to enter sell-orders/short-position
StatArb_VALUE_FOR_SELL_ENTRY = -0.01 

# Minimum price change since last trade before considering trading again,
# this is to prevent over-trading at/around same prices
MIN_PRICE_MOVE_FROM_LAST_TRADE = 0.01
# Number of currency to buy/sell on every trade
NUM_SHARES_PER_TRADE = 1000000
# Minimum Open/Unrealized profit at which to close positions and lock profits
MIN_PROFIT_TO_CLOSE = 10

     Remember that these positions are in dollar notional terms, so a position of 100K(100000) is equivalent to roughly 1 future contract, which we mention to make it clear that a position of 100K does not mean a position of 100K contracts

Quantifying and computing StatArb trading signals

1. We will see over available prices a day at a time and see what calculations need to be performed, starting with the computation of SimpleMovingAverages and price deviation from the rolling SMA first:

for i in range(0, num_days):
    close_prices = {}
    
    # Build ClosePrice series, 
    # compute SMA for each symbol and price-deviation from SMA for each symbol
    for symbol in SYMBOLS:
        close_prices[symbol] = symbols_data[symbol]['Close'].iloc[i]
        
        if not symbol in price_history.keys():
            price_history[symbol] = []
            price_deviation_from_sma[symbol] = []
            
        price_history[symbol].append( close_prices[symbol] )
        
        # we track at most SMA_NUM_PERIODS number of prices
        if len( price_history[symbol] ) > SMA_NUM_PERIODS:
            del ( price_history[symbol][0] )
        sma = stats.mean( price_history[symbol] ) # Rolling SimpleMovingAverage
        
        # price deviation from mean
        price_deviation_from_sma[symbol].append( close_prices[symbol] - sma )
        if len( price_deviation_from_sma[symbol] ) > PRICE_DEV_NUM_PRICES:
            del ( price_deviation_from_sma[symbol][0] )

    # Now compute covariance and correlation between TRADING_INSTRUMENT and every other lead symbol
    # also compute projected price deviation and find delta between projected and actual price deviations.
    projected_dev_from_sma_using = {}
    for symbol in SYMBOLS:
        # no need to find relationship between trading instrument and itself
        if symbol == TRADING_INSTRUMENT:
            continue
            
        correlation_label = TRADING_INSTRUMENT + '<-' + symbol
        # first entry for this pair in the history dictionary
        if correlation_label not in correlation_history.keys():
            correlation_history[correlation_label] = []
            delta_projected_actual_history[correlation_label] = []
            
        # need at least 2 observations to compute covariance/correlation
        # close_prices[symbol] = symbols_data[symbol]['Close'].iloc[i] and 0<=i signal says TRADING_INSTRUMENT price should have moved up more than what it did
        # delta -ve => signal says TRADING_INSTRUMENT price should have moved down more than what it did
        delta_projected_actual = (projected_dev_from_sma_using[symbol] - price_deviation_from_sma[TRADING_INSTRUMENT][-1])
        delta_projected_actual_history[correlation_label].append(delta_projected_actual)
        
    # weigh predictions from each pair, 
    # weight is the correlation between those pairs
    sum_weights = 0 # sum of weights is sum of correlations for each symbol with TRADING_INSTRUMENT
    for symbol in SYMBOLS:
        if symbol == TRADING_INSTRUMENT: # no need to find relationship between trading instrument and itself
            continue
        correlation_label = TRADING_INSTRUMENT + '<-' + symbol
        # the sum of each individual weight (magnitude of correlation) 
        sum_weights += abs( correlation_history[correlation_label][-1] )
    
    # will hold final prediction of price deviation in TRADING_INSTRUMENT, weighing projections from all other symbols.
    final_delta_projected = 0
    close_price = close_prices[TRADING_INSTRUMENT]
    for symbol in SYMBOLS:
        if symbol == TRADING_INSTRUMENT:
            continue
        correlation_label = TRADING_INSTRUMENT + '<-' + symbol
        
        # weight projection from a symbol by correlation
        # use the magnitude of the correlation between CAD/USD and the other currency pairs 
        # to weigh 
        # the delta between projected and actual price deviations in CAD/USD as predicted by the other pairs
        final_delta_projected += abs(correlation_history[correlation_label][-1]) * delta_projected_actual_history[correlation_label][-1]
        
    # normalize by divding by sum of weights for all pairs
    # as our final signal to build our trading strategy around
    if sum_weights !=0:
        final_delta_projected /= sum_weights
    else:
        final_delta_projected = 0
    final_delta_projected_history.append( final_delta_projected )
    
    # StatArb execution logic
    # Now, using the StatArb signal we just computed, we can build a strategy similar to the trend-following strategy
    
    # We will perform a sell trade at close_prices if the following conditions are met:
    # 1. The StatArb trading signal value(-ve) is < Sell-Entry threshold(StatArb_VALUE_FOR_SELL_ENTRY = -0.01) 
    #    and the difference between last trade-price and current-price is different enough.
    # 2. We are long( +ve position ) 
    #    and current position is profitable enough to lock profit
    if ( ( final_delta_projected < StatArb_VALUE_FOR_SELL_ENTRY and \
           abs(close_price-last_sell_price) > MIN_PRICE_MOVE_FROM_LAST_TRADE
         )
         or
         ( position > 0 and \
           abs( open_pnl > MIN_PROFIT_TO_CLOSE )
         )# long from -ve StatArb and StatArb has gone positive or position is profitable, sell to close position
       ):
        orders.append(-1) # mark the sell trade
        last_sell_price = close_price
        position -= NUM_SHARES_PER_TRADE # reduce position by the size of this trade
        sell_sum_qty += NUM_SHARES_PER_TRADE
        sell_sum_price_qty += (close_price*NUM_SHARES_PER_TRADE) # update vwap sell-price
        print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
        print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
    # We will perform a buy trade at close_prices if the following conditions are met:
    # 1. The StatArb trading signal value(+ve) is above Buy-Entry threshold
    #    and the difference between last trade-price and current-price is different enough.
    # 2. We are short( -ve position )
    #    and current position is profitable enough to lock profit.
    elif ( ( final_delta_projected > StatArb_VALUE_FOR_BUY_ENTRY and \
             abs(close_price-last_buy_price) > MIN_PRICE_MOVE_FROM_LAST_TRADE
           )
           or
           ( position < 0 and \
             abs( open_pnl > MIN_PROFIT_TO_CLOSE )
           )# short from +ve StatArb and StatArb has gone negative or position is profitable, buy to close position
         ):
        orders.append(1) # mark the buy trade
        last_buy_price = close_price
        position += NUM_SHARES_PER_TRADE # increase position by the size of this trade
        buy_sum_qty += NUM_SHARES_PER_TRADE
        buy_sum_price_qty += (close_price*NUM_SHARES_PER_TRADE) # update the vwap buy-price
        print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
        print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
    else:
        # No trade since none of the conditions were met to buy or sell
        orders.append(0)
        
    positions.append(position)
    
    # This section updates Open/Unrealized & Closed/Realized positions
    open_pnl = 0
    if position > 0:
        # long position and some sell trades have been made against it, 
        # close that amount based on how much was sold against this long position
        # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
        if sell_sum_qty > 0:           # vwap for sell                   # vwap  for buy
            open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
        # mark the remaining position to market 
        # i.e. pnl would be what it would be if we closed at current price
        # sell
        # position -= NUM_SHARES_PER_TRADE
        # sell_sum_qty += NUM_SHARES_PER_TRADE
        # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
        # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
        open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
        # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
    elif position < 0:
        # short position and some buy trades have been made against it, 
        # close that amount based on how much was bought against this short position
        # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
        if buy_sum_qty > 0:           # vwap for sell                   # vwap  for buy
            open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
        # mark the remaining position to market
        # i.e. pnl would be what it would be if we closed at current price
        # buy
        # position += NUM_SHARE_PER_TRADE
        # buy_sum_qty += NUM_SHARE_PER_TRADE
        # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
        # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) * 
        open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
        # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
    else:
        # flat, so update closed_pnl and reset tracking variables for positions & pnls
        closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
        
        buy_sum_price_qty = 0
        buy_sum_qty = 0
        last_buy_price = 0
        
        sell_sum_price_qty = 0
        sell_sum_qty = 0
        last_sell_price = 0
        
    pnls.append(closed_pnl + open_pnl)

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第36张图片

StatArb signal and strategy performance analysis

Now, let's analyze the StatArb signal using the following steps:

1. Let's visualize a few more details about the signals in this trading strategy, starting with the correlations between CAD/USD and the other currency pairs as it evolves over time:

        # https://blog.csdn.net/Linli522362242/article/details/121721868
        # Each row of m represents a variable, and each column a single observation(features) of all those variables.
        corr = np.corrcoef( price_deviation_from_sma[TRADING_INSTRUMENT],
                           
price_deviation_from_sma[symbol]
                          ) # Return Pearson product-moment correlation coefficients

        # the correlation matrix is identical to a covariance matrix computed from standardized features.
        
        cov = np.cov( price_deviation_from_sma[TRADING_INSTRUMENT],
                      price_deviation_from_sma[symbol]
                    ) # Estimate a covariance matrix(here is 2x2), given data and weights(here is None)

 
        # get the correlation between the 2 series
        # here is the correlation between CAD/USD and the other currency pairs
        corr_trading_instrument_lead_instrument = corr[0,1]
        # get the covariance between the 2 series
        cov_trading_instrument_lead_instrument = cov[0,0]/cov[0,1]
            
        correlation_history[correlation_label].append(corr_trading_instrument_lead_instrument )

# Plot correlations between TRADING_INSTRUMENT and other currency pairs
fig = plt.figure( figsize=(20,10))

correlation_data = pd.DataFrame()
for symbol in SYMBOLS:
    if symbol == TRADING_INSTRUMENT:
        continue
    
    correlation_label = TRADING_INSTRUMENT + '<-' + symbol
    correlation_data = correlation_data.assign( label=pd.Series( correlation_history[correlation_label],
                                                                 index=symbols_data[symbol].index
                                                               )
                                              )
    if symbol == 'JPYUSD=X':
        ax = correlation_data['label'].plot( color=next(cycol), lw=2., ls='dotted',
                                             label='Correlation '+ correlation_label )
    elif symbol == 'AUDUSD=X' or symbol == 'NZDUSD=X':
        ax = correlation_data['label'].plot( color=next(cycol), lw=5.,
                                             label='Correlation '+ correlation_label )
    else:
        ax = correlation_data['label'].plot( color=next(cycol), lw=2.,
                                             label='Correlation '+ correlation_label )


for i in np.arange(-1,1, 0.25):
    plt.axhline( y=i, lw=0.5, color='k' )
plt.legend()
plt.autoscale(enable=True, axis='x', tight=True)

plt.show()

     This plot shows the correlation between CADUSD and other currency pairs as it evolves over the course of this trading strategy.

  • Correlations close to -1 or +1 signify strongly correlated pairs, and
  • correlations that hold steady are the stable correlated pairs.
  • Currency pairs where correlations swing around between negative and positive values indicate extremely uncorrelated or unstable currency pairs, which are unlikely to yield good predictions in the long run. However, we do not know how the correlation would evolve ahead of time, so we have no choice but to use all currency pairs available to us in our StatArb trading strategy

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第37张图片     As we suspected, the currency pairs that are most strongly correlated to CAD/USD price deviations are AUD/USD and NZD/USD. JPY/USD is the least correlated to CAD/USD price deviations.

2. inspect the delta between projected (=covariance * price-deviation-in-lead-symbol from sma) and actual price deviations in CAD/USD as projected by each individual currency pair individually:

        cov = np.cov( price_deviation_from_sma[TRADING_INSTRUMENT],
                      price_deviation_from_sma[symbol]
                    ) # Estimate a covariance matrix(here is 2x2), given data and weights(here is None)

        # get the covariance between the 2 series
        cov_trading_instrument_lead_instrument = cov[0,0]/cov[0,1]

        # computes the projected price movement, 
        # uses that to find the difference between the projected movement and actual movement,
        # and saves it in our delta_projected_actual_history list per currency pair
        # projected-price-deviation is predicted by the other pairs(lead-symbol) * covariance
                                       ##################
        # projected-price-deviation-in-TRADING_INSTRUMENT is covariance * price-deviation-in-lead-symbol
        projected_dev_from_sma_using[symbol] = price_deviation_from_sma[symbol][-1] * cov_trading_instrument_lead_instrument
        
        # the delta between projected and actual price deviations in CAD/USD as predicted by the other pairs
        # delta +value => signal says TRADING_INSTRUMENT price should have moved up more than what it did
        # delta -value => signal says TRADING_INSTRUMENT price should have moved down more than what it did
        delta_projected_actual = (projected_dev_from_sma_using[symbol] - price_deviation_from_sma[TRADING_INSTRUMENT][-1])
        delta_projected_actual_history[correlation_label].append(delta_projected_actual)

# Plot StatArb signal provided by each currency pair
from itertools import cycle

cycol = cycle('bgrcmky')

fig = plt.figure( figsize=(15,8))

delta_projected_actual_data = pd.DataFrame()
for symbol in SYMBOLS:
    if symbol == TRADING_INSTRUMENT:
        continue
    
    projection_label = TRADING_INSTRUMENT + '<-' + symbol
    delta_projected_actual_data = delta_projected_actual_data.assign( 
        StatArbTradingSignal = pd.Series( delta_projected_actual_history[projection_label], 
                                          index=symbols_data[TRADING_INSTRUMENT].index
                                        )
    )
    
    if symbol == 'JPYUSD=X' or symbol == 'CHFUSD=X':
        ax = delta_projected_actual_data['StatArbTradingSignal'].plot( color=next(cycol), 
                                                                       lw=2., ls='dotted',
                                                                       label='StatArbTradingSignal'+projection_label
                                                                     )
    else:
        ax = delta_projected_actual_data['StatArbTradingSignal'].plot( color=next(cycol),
                                                                       lw=1., 
                                                                       label='StatArbTradingSignal'+projection_label
                                                                     )
plt.legend()
plt.autoscale(enable=True, axis='x', tight=True)
plt.show()

     This is what the StatArb signal values would look like if we used any of the currency pairs alone to project预测 CAD/USD price deviations:t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第38张图片     Here, the plot seems to suggest that JPYUSD and CHFUSD have very large predictions, but as we saw before those pairs do not have good correlations with CADUSD, so these are likely to be bad predictions due to poor predictive relationships between CADUSD - JPYUSD and CADUSD - CHFUSD . One lesson to take away from this is that StatArb benefits from having multiple leading trading instruments, because when relationships break down between specific pairs, the other strongly correlated pairs can help offset bad predictions, which we discussed earlier.

    # weigh predictions from each pair, 
    # weight is the correlation between those pairs
    sum_weights = 0 # sum of weights is sum of correlations for each symbol with TRADING_INSTRUMENT
    for symbol in SYMBOLS:
        if symbol == TRADING_INSTRUMENT: # no need to find relationship between trading instrument and itself
            continue
        correlation_label = TRADING_INSTRUMENT + '<-' + symbol
        # the sum of each individual weight (magnitude of correlation) 
        sum_weights += abs( correlation_history[correlation_label][-1] )
    
    # will hold final prediction of price deviation in TRADING_INSTRUMENT, weighing projections from all other symbols.
    final_delta_projected = 0

    close_price = close_prices[TRADING_INSTRUMENT]
    for symbol in SYMBOLS:
        if symbol == TRADING_INSTRUMENT:
            continue
        correlation_label = TRADING_INSTRUMENT + '<-' + symbol
        
        # weight projection from a symbol by correlation
        # use the magnitude of the correlation between CAD/USD and the other currency pairs 
        #
to weigh 
        # the delta between projected and actual price deviations in CAD/USD as predicted by the other pairs
        final_delta_projected += abs(correlation_history[correlation_label][-1]) * delta_projected_actual_history[correlation_label][-1]
        
    # normalize by divding by sum of weights for all pairs
    # as our final signal to build our trading strategy around
    if sum_weights !=0:
        final_delta_projected /= sum_weights
    else:
        final_delta_projected = 0
    final_delta_projected_history.append( final_delta_projected )

3. Now, let's set up our data frames to plot the close price, trades, positions, and PnLs we will observe:

delta_projected_actual_data = delta_projected_actual_data.assign(
    ClosePrice = pd.Series( symbols_data[TRADING_INSTRUMENT]['Close'], 
                            index=symbols_data[TRADING_INSTRUMENT].index
                          )
)
delta_projected_actual_data = delta_projected_actual_data.assign(
    FinalStatArbTradingSignal = pd.Series( final_delta_projected_history,
                                           index=symbols_data[TRADING_INSTRUMENT].index
                                         )
)
delta_projected_actual_data = delta_projected_actual_data.assign( 
    Trades = pd.Series( orders,
                        index=symbols_data[TRADING_INSTRUMENT].index
                      )
)
delta_projected_actual_data = delta_projected_actual_data.assign(
    Position = pd.Series( positions,
                          index=symbols_data[TRADING_INSTRUMENT].index
                        )
)
delta_projected_actual_data = delta_projected_actual_data.assign( 
    Pnl = pd.Series( pnls,
                     index=symbols_data[TRADING_INSTRUMENT].index
                   )
)
fig = plt.figure( figsize=(15,8))

plt.plot( delta_projected_actual_data.index, 
          delta_projected_actual_data.ClosePrice,
          color='k', lw=1., label='ClosePrice')
plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == 1].index,
          delta_projected_actual_data.ClosePrice[delta_projected_actual_data.Trades == 1],
          color='b', lw=0, marker='^', markersize=7, label='buy')
plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == -1].index,
          delta_projected_actual_data.ClosePrice[delta_projected_actual_data.Trades == -1], 
          color='y', lw=0, marker='v', markersize=7, label='sell'
        )
plt.legend()
plt.show()

     The following plot tells us at what prices the buy and sell trades are made in CADUSD . We will need to inspect the final trading signal (final_delta_projected)in addition to this plot to fully understand the behavior of this StatArb signal and strategy:

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第39张图片

     Now, let's look at the actual code to build visualization for the final StatArb trading signal, and overlay buy and sell trades over the lifetime of the signal evolution. This will help us understand for what signal values buy and sell trades are made and if that is in line with our expectations:

fig = plt.figure( figsize=(15,8))

plt.plot( delta_projected_actual_data.index, 
          delta_projected_actual_data.FinalStatArbTradingSignal,
          color='k', lw=1.,
          label='FinalStatArbTradingSignal'
        )
plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == 1].index,
          delta_projected_actual_data.FinalStatArbTradingSignal[delta_projected_actual_data.Trades == 1],
          color='b', lw=0, marker='^', markersize=7, label='buy'
        )
plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == -1].index,
          delta_projected_actual_data.FinalStatArbTradingSignal[delta_projected_actual_data.Trades == -1],
          color='y', lw=0, marker='v', markersize=7, label='sell'
        )

plt.axhline(y=0, lw=0.5, color='k')
for i in np.arange( StatArb_VALUE_FOR_BUY_ENTRY, StatArb_VALUE_FOR_BUY_ENTRY * 10, StatArb_VALUE_FOR_BUY_ENTRY * 2 ):
    plt.axhline(y=i, lw=0.5, color='r')
for i in np.arange( StatArb_VALUE_FOR_SELL_ENTRY, StatArb_VALUE_FOR_SELL_ENTRY * 10, StatArb_VALUE_FOR_SELL_ENTRY * 2 ):
    plt.axhline(y=i, lw=0.5, color='g')
    
plt.autoscale(enable=True, axis='x', tight=True)    
plt.legend()
plt.show()

     Since we adopted the trend-following approach in our StatArb trading strategy, we

  • expect to buy when the signal value is positive and
  • sell when the signal value is negative.

Let's see whether that's the case in the plot: 

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第40张图片

      Based on this plot and our understanding of trend-following strategies in addition to the StatArb signal we built, we do indeed see many buy trades when the signal value is positive and sell trades when the signal values are negative. The buy trades made when signal values are negative and sell trades made when signal values are positive can be attributed to the trades that close profitable positions, as we saw in our previous mean reversion and trend-following trading strategies.

4. Let's wrap up our analysis of StatArb trading strategies by visualizing the positions and PnLs:

fig = plt.figure( figsize=(15,8))

plt.plot( delta_projected_actual_data.index, 
          delta_projected_actual_data.Position,
          color='k', lw=1., 
          label='Position'
        )
plt.plot( delta_projected_actual_data.loc[ delta_projected_actual_data.Position == 0 ].index,
          delta_projected_actual_data.Position[delta_projected_actual_data.Position == 0],
          color='r', lw=0, marker='.',
          label='flat'
        )
plt.plot( delta_projected_actual_data.loc[ delta_projected_actual_data.Position > 0 ].index,
          delta_projected_actual_data.Position[delta_projected_actual_data.Position > 0],
          color='b', lw=0, marker='+',
          label='long'
        )
plt.plot( delta_projected_actual_data.loc[ delta_projected_actual_data.Position < 0 ].index,
          delta_projected_actual_data.Position[delta_projected_actual_data.Position < 0],
          color='y', lw=0, marker='x',
          label='short'
        )

plt.axhline(y=0, lw=0.5, color='k')

for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE * 5, NUM_SHARES_PER_TRADE ):
    plt.axhline(y=i, lw=0.5, color='b')
for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE * 5, -NUM_SHARES_PER_TRADE ):
    plt.axhline(y=i, lw=0.5, color='g')

plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()
plt.show()

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第41张图片

     The position plot shows the evolution of the StatArb trading strategy's position over the course of its lifetime. Remember that these positions are in dollar notional terms, so a position of 100K is equivalent to roughly 1 future contract, which we mention to make it clear that a position of 100K does not mean a position of 100K contracts!

5. Finally, let's have a look at the code for the PnL plot, identical to what we've been using before:

fig = plt.figure( figsize=(15,8))

plt.plot( delta_projected_actual_data.index,
          delta_projected_actual_data.Pnl,
          color='k', lw=1., 
          label='PnL'
        )
plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Pnl > 0].index,
          delta_projected_actual_data.Pnl[delta_projected_actual_data.Pnl > 0],
          color='b', lw=0, marker='.',
          label='+PnL'
        )
plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Pnl < 0].index,
          delta_projected_actual_data.Pnl[delta_projected_actual_data.Pnl < 0],
          color='y', lw=0, marker='.',
          label='-PnL'
        )

plt.autoscale(enable=True, axis='x', tight=True)
plt.legend()
plt.show()

      We expect to see better performance here than in our previously built trading strategies because it relies on a fundamental relationship between different currency pairs and should be able to perform better during different market conditions because of its use of multiple currency pairs as lead trading instruments

t5_Sophisticated Algorithmic Strategies(MeanReversion+APO+StdDev_TrendFollowing+APO)_StatArb统计套利_PnL_第42张图片

     And that's it, now you have a working example of a profitable statistical arbitrage strategy and should be able to improve and extend it to other trading instruments!

Summary

     This chapter made use of some of the trading signals we've seen in the previous chapters to build realistic and robust trend-following and mean reversion trading strategies. In addition, we went another step further and made those basic strategies more sophisticated by adding a volatility measure trading signal to make it more dynamic and adaptive to different market conditions. We also looked at a completely new form of trading strategy in the form of trading strategies dealing with economic releases and how to carry out the analysis for that flavor of trading strategies for our sample Non Farm Payroll data. Finally, we looked at our most sophisticated and complex trading strategy so far, which was the statistical arbitrage strategy, and applied it to CAD/USD with the major currency pairs as leading trading signals. We investigated in great detail how to quantify and parameterize the StatArb trading signal and trading strategy and visualized every step of that process and concluded that the trading strategy delivered excellent results for our data set.

     In the next chapter, you will learn how to measure and manage the risk (market risk, operational risk, and software implementation bugs) of algorithmic strategies.

你可能感兴趣的:(大数据)