当前位置:   article > 正文

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

fasterema

     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 Bands(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 

     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:

  1. import pandas as pd
  2. import pandas_datareader.data as pdr
  3. def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
  4. if len(output_file) == 0:
  5. output_file = stock_symbol+'_data_large.pkl'
  6. try:
  7. df = pd.read_pickle( output_file )
  8. print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
  9. except FileNotFoundError:
  10. print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
  11. df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
  12. df.to_pickle( output_file )
  13. return df
  14. goog_data = load_financial_data( stock_symbol='GOOG',
  15. start_date='2014-01-01',
  16. end_date='2018-01-01',
  17. output_file='goog_data.pkl'
  18. )
  19. goog_data.head()

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)

  1. # Variables/constants for EMA Calculation:
  2. NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
  3. K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
  4. ema_fast = 0 # initial ema
  5. ema_fast_values = [] # we will hold fast EMA values for visualization purpose
  6. NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
  7. K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
  8. ema_slow = 0 # initial ema
  9. ema_slow_values = [] # we will hold slow EMA values for visualization purpose
  10. 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:

  1. # Variables for Trading Strategy trade, position & pnl management:
  2. # Container for tracking buy/sell order,
  3. # +1 for buy order, -1 for sell order, 0 for no-action
  4. orders = []
  5. # Container for tracking positions,
  6. # positive for long positions, negative for short positions, 0 for flat/no position
  7. positions = []
  8. # Container for tracking total_pnls, this is the sum of
  9. # closed_pnl i.e. pnls already locked in
  10. # and open_pnl i.e. pnls for open-position marked to market price
  11. pnls = []
  12. last_buy_price = 0 # used to prevent over-trading at/around the same price
  13. last_sell_price = 0 # used to prevent over-trading at/around the same price
  14. position = 0 # Current position of the trading strategy
  15. # Summation of products of
  16. # buy_trade_price and buy_trade_qty for every buy Trade made
  17. # since last time being flat
  18. buy_sum_price_qty = 0
  19. # Summation of buy_trade_qty for every buy Trade made since last time being flat
  20. buy_sum_qty = 0
  21. # Summation of products of
  22. # sell_trade_price and sell_trade_qty for every sell Trade made
  23. # since last time being flat
  24. sell_sum_price_qty = 0
  25. # Summation of sell_trade_qty for every sell Trade made since last time being flat
  26. sell_sum_qty = 0
  27. open_pnl = 0 # Open/Unrealized PnL marked to market
  28. 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:
  1. # Constants that define strategy behavior/thresholds
  2. # APO trading signal value below which(-10) to enter buy-orders/long-position
  3. APO_VALUE_FOR_BUY_ENTRY = -10 # (oversold, expect a bounce back up)
  4. # APO trading signal value above which to enter sell-orders/short-position
  5. APO_VALUE_FOR_SELL_ENTRY = 10 # (overbought, expect a bounce back down)
  6. # Minimum price change since last trade before considering trading again,
  7. MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
  8. NUM_SHARES_PER_TRADE = 10
  9. # positions are closed if currently open positions are profitable above a certain amount,
  10. # regardless of APO values.
  11. # This is used to algorithmically lock profits and initiate more positions
  12. # instead of relying only on the trading signal value.
  13. # Minimum Open/Unrealized profit at which to close positions and lock profits
  14. 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&LTotal (points) = P&LRealized (points) + P&LUnrealized (points) = 32.249997 + (-14.25) = 17.999997
  • P&LTotal (contract currency) = P&LTotal (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:

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: 

     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:

      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:

     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

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

  1. close = goog_data['Close']
  2. for close_price in close:
  3. # This section updates fast and slow EMA and computes APO trading signal
  4. if (ema_fast == 0): # first observation
  5. ema_fast = close_price
  6. ema_slow = close_price
  7. else:
  8. ema_fast = (close_price - ema_fast)*K_FAST + ema_fast # K_FAST = 2/(NUM_PERIODS_FAST_10 + 1)
  9. ema_slow = (close_price - ema_slow)*K_SLOW + ema_slow # K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1)
  10. ema_fast_values.append(ema_fast)
  11. ema_slow_values.append(ema_slow)
  12. apo = ema_fast-ema_slow
  13. apo_values.append(apo)
  14. # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
  15. # We will perform a sell trade at close_price if the following conditions are met:
  16. # 1. The APO trading signal value(positive) > Sell-Entry threshold (overbought, expect a bounce back down, sell for profit)
  17. # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
  18. # 2. We are long( +ve position ) and
  19. # either APO trading signal value >= 0 or current position is profitable enough to lock profit.
  20. if ( ( apo > APO_VALUE_FOR_SELL_ENTRY and \
  21. abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
  22. )
  23. or
  24. ( position>0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
  25. ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
  26. orders.append(-1) # mark the sell trade
  27. last_sell_price = close_price
  28. position -= NUM_SHARES_PER_TRADE
  29. sell_sum_qty += NUM_SHARES_PER_TRADE
  30. sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
  31. print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
  32. # 7. We will perform a buy trade at close_price if the following conditions are met:
  33. # 1. The APO trading signal value(negative) < below Buy-Entry threshold (oversold, expect a bounce back up, buy for future profit)
  34. # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
  35. # 2. We are short( -ve position ) and
  36. # either APO trading signal value is <= 0 or current position is profitable enough to lock profit.
  37. elif ( ( apo < APO_VALUE_FOR_BUY_ENTRY and \
  38. abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
  39. )
  40. or
  41. ( position<0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
  42. ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
  43. orders.append(+1) # mark the buy trade
  44. last_buy_price = close_price
  45. position += NUM_SHARES_PER_TRADE
  46. buy_sum_qty += NUM_SHARES_PER_TRADE
  47. buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
  48. print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
  49. else:
  50. # No trade since none of the conditions were met to buy or sell
  51. orders.append( 0 )
  52. positions.append( position )
  53. # 8. The code of the trading strategy contains logic for position/PnL management.
  54. # It needs to update positions and compute open and closed PnLs when market prices change
  55. # and/or trades are made causing a change in positions
  56. # This section updates Open/Unrealized & Closed/Realized positions
  57. open_pnl = 0
  58. if position > 0:
  59. # long position and some sell trades have been made against it,
  60. # close that amount based on how much was sold against this long position
  61. # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
  62. if sell_sum_qty > 0: # vwap for sell # vwap for buy
  63. open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
  64. # mark the remaining position to market
  65. # i.e. pnl would be what it would be if we closed at current price
  66. # sell
  67. # position -= NUM_SHARES_PER_TRADE
  68. # sell_sum_qty += NUM_SHARES_PER_TRADE
  69. # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
  70. # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
  71. open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
  72. elif position < 0:
  73. # short position and some buy trades have been made against it,
  74. # close that amount based on how much was bought against this short position
  75. # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
  76. if buy_sum_qty > 0: # vwap for sell # vwap for buy
  77. open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
  78. # mark the remaining position to market
  79. # i.e. pnl would be what it would be if we closed at current price
  80. # buy
  81. # position += NUM_SHARE_PER_TRADE
  82. # buy_sum_qty += NUM_SHARE_PER_TRADE
  83. # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
  84. # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) *
  85. open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
  86. else:
  87. # flat, so update closed_pnl and reset tracking variables for positions & pnls
  88. closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
  89. buy_sum_price_qty = 0
  90. buy_sum_qty = 0
  91. last_buy_price = 0
  92. sell_sum_price_qty = 0
  93. sell_sum_qty = 0
  94. last_sell_price = 0
  95. print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
  96. pnls.append(closed_pnl + open_pnl)


... ...

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: 

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

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: 

  1. import matplotlib.pyplot as plt
  2. fig = plt.figure( figsize=(20,10))
  3. data['ClosePrice'].plot(color='k', lw=3., legend=True)
  4. data['Fast10DayEMA'].plot(color='y', lw=1., legend=True)
  5. data['Slow40DayEMA'].plot(color='m', lw=1., legend=True)
  6. plt.plot( data.loc[ data.Trades == 1 ].index, data.ClosePrice[data.Trades == 1 ],
  7. color='y', lw=0, marker='^', markersize=7, label='buy'
  8. )
  9. plt.plot( data.loc[ data.Trades == -1 ].index, data.ClosePrice[data.Trades == -1 ],
  10. color='b', lw=0, marker='v', markersize=7, label='sell'
  11. )
  12. plt.autoscale(enable=True, axis='x', tight=True)
  13. plt.legend()
  14. 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: 

     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:

  1. fig = plt.figure( figsize=(20,10) )
  2. data['APO'].plot(color='k', lw=3., legend=True)
  3. plt.plot( data.loc[ data.Trades == 1 ].index, data.APO[data.Trades == 1 ],
  4. color='y', lw=0, marker='^', markersize=7, label='buy'
  5. )
  6. plt.plot( data.loc[ data.Trades == -1 ].index, data.APO[data.Trades == -1 ],
  7. color='b', lw=0, marker='v', markersize=7, label='sell'
  8. )
  9. plt.axhline(y=0, lw=0.5, color='k')
  10. for i in range( APO_VALUE_FOR_BUY_ENTRY, APO_VALUE_FOR_BUY_ENTRY*5, APO_VALUE_FOR_BUY_ENTRY ):
  11. plt.axhline(y=i, lw=0.5, color='r')
  12. for i in range( APO_VALUE_FOR_SELL_ENTRY, APO_VALUE_FOR_SELL_ENTRY*5, APO_VALUE_FOR_SELL_ENTRY ):
  13. plt.axhline(y=i, lw=0.5, color='g')
  14. plt.autoscale(enable=True, axis='x', tight=True)
  15. plt.legend()
  16. plt.show()


     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:

  1. fig = plt.figure( figsize=(20,10))
  2. data['Position'].plot(color='k', lw=1., legend=True)
  3. plt.plot( data.loc[ data.Position == 0 ].index, data.Position[ data.Position == 0 ],
  4. color='r', lw=0, marker='.', label='flat'
  5. )
  6. plt.plot( data.loc[ data.Position > 0 ].index, data.Position[ data.Position > 0 ],
  7. color='y', lw=0, marker='+', label='long'
  8. )
  9. plt.plot( data.loc[ data.Position < 0 ].index, data.Position[ data.Position < 0 ],
  10. color='b', lw=0, marker='_', label='short'
  11. )
  12. plt.axhline(y=0, lw=0.5, color='k')
  13. for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE*25, NUM_SHARES_PER_TRADE*5 ):
  14. plt.axhline(y=i, lw=0.5, color='r')
  15. for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE*25, -NUM_SHARES_PER_TRADE*5 ):
  16. plt.axhline(y=i, lw=0.5, color='g')
  17. plt.autoscale(enable=True, axis='x', tight=True)
  18. plt.legend()
  19. 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.

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

  1. fig = plt.figure( figsize=(20,10))
  2. data['Pnl'].plot(color='k', lw=1., legend=True)
  3. plt.plot( data.loc[ data.Pnl > 0 ].index, data.Pnl[ data.Pnl > 0 ],
  4. color='b', lw=0, marker='.',
  5. label='Pnl'
  6. )
  7. plt.plot( data.loc[ data.Pnl < 0 ].index, data.Pnl[ data.Pnl < 0 ],
  8. color='y', lw=0, marker='.',
  9. label='Pnl'
  10. )
  11. plt.axhline(y=15000, ls='--', alpha=0.5)
  12. plt.autoscale(enable=True, axis='x', tight=True)
  13. plt.legend()
  14. plt.show()
  15. 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.

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: 

     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.
  1. import pandas as pd
  2. import pandas_datareader.data as pdr
  3. def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
  4. if len(output_file) == 0:
  5. output_file = stock_symbol+'_data_large.pkl'
  6. try:
  7. df = pd.read_pickle( output_file )
  8. print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
  9. except FileNotFoundError:
  10. print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
  11. df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
  12. df.to_pickle( output_file )
  13. return df
  14. goog_data = load_financial_data( stock_symbol='GOOG',
  15. start_date='2014-01-01',
  16. end_date='2018-01-01',
  17. output_file='goog_data.pkl'
  18. )
  19. goog_data.head()

 

  1. # Variables/constants for EMA Calculation:
  2. NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
  3. K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
  4. ema_fast = 0 # initial ema
  5. ema_fast_values = [] # we will hold fast EMA values for visualization purpose
  6. NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
  7. K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
  8. ema_slow = 0 # initial ema
  9. ema_slow_values = [] # we will hold slow EMA values for visualization purpose
  10. apo_values = [] # track computed absolute price oscillator values
  11. # Variables for Trading Strategy trade, position & pnl management:
  12. # Container for tracking buy/sell order,
  13. # +1 for buy order, -1 for sell order, 0 for no-action
  14. orders = []
  15. # Container for tracking positions,
  16. # positive for long positions, negative for short positions, 0 for flat/no position
  17. positions = []
  18. # Container for tracking total_pnls, this is the sum of
  19. # closed_pnl i.e. pnls already locked in
  20. # and open_pnl i.e. pnls for open-position marked to market price
  21. pnls = []
  22. last_buy_price = 0 # used to prevent over-trading at/around the same price
  23. last_sell_price = 0 # used to prevent over-trading at/around the same price
  24. position = 0 # Current position of the trading strategy
  25. # Summation of products of
  26. # buy_trade_price and buy_trade_qty for every buy Trade made
  27. # since last time being flat
  28. buy_sum_price_qty = 0
  29. # Summation of buy_trade_qty for every buy Trade made since last time being flat
  30. buy_sum_qty = 0
  31. # Summation of products of
  32. # sell_trade_price and sell_trade_qty for every sell Trade made
  33. # since last time being flat
  34. sell_sum_price_qty = 0
  35. # Summation of sell_trade_qty for every sell Trade made since last time being flat
  36. sell_sum_qty = 0
  37. open_pnl = 0 # Open/Unrealized PnL marked to market
  38. closed_pnl = 0 # Closed/Realized PnL so far
  39. # Constants that define strategy behavior/thresholds
  40. # APO trading signal value below which(-10) to enter buy-orders/long-position
  41. APO_VALUE_FOR_BUY_ENTRY = -10 # (oversold, expect a bounce back up)
  42. # APO trading signal value above which to enter sell-orders/short-position
  43. APO_VALUE_FOR_SELL_ENTRY = 10 # (overbought, expect a bounce back down)
  44. # Minimum price change since last trade before considering trading again,
  45. MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
  46. NUM_SHARES_PER_TRADE = 10
  47. # positions are closed if currently open positions are profitable above a certain amount,
  48. # regardless of APO values.
  49. # This is used to algorithmically lock profits and initiate more positions
  50. # instead of relying only on the trading signal value.
  51. # Minimum Open/Unrealized profit at which to close positions and lock profits
  52. 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):

  1. import statistics as stats
  2. import math as math
  3. data2 = goog_data.copy()
  4. close = data2['Close']
  5. # Constants/variables that are used to compute standard deviation as a volatility measure
  6. SMA_NUM_PERIODS_20 = 20 # look back period
  7. price_history = [] # history of prices
  8. for close_price in close:
  9. price_history.append( close_price )
  10. if len( price_history) > SMA_NUM_PERIODS_20 : # we track at most 'time_period' number of prices
  11. del ( price_history[0] )
  12. # calculate vairance during the SMA_NUM_PERIODS_20 periods
  13. sma = stats.mean( price_history )
  14. variance = 0 # variance is square of standard deviation
  15. for hist_price in price_history:
  16. variance = variance + ( (hist_price-sma)**2 )
  17. stddev = math.sqrt( variance/len(price_history) )
  18. # a volatility factor that ranges from 0 to 1
  19. stddev_factor = stddev/15 # 15 since since the population stddev.mean() = 15.45
  20. # closer to 0 indicate very low volatility,
  21. # around 1 indicate normal volatility
  22. # > 1 indicate above-normal volatility
  23. if stddev_factor == 0:
  24. stddev_factor = 1
  25. # This section updates fast and slow EMA and computes APO trading signal
  26. if (ema_fast==0): # first observation
  27. ema_fast = close_price # initial ema_fast or ema_slow
  28. ema_slow = close_price
  29. else:
  30. # ema fomula
  31. # K_FAST*stddev_factor or K_SLOW*stddev_factor
  32. # more reactive to newest observations during periods of higher than normal volatility
  33. ema_fast = (close_price-ema_fast) * K_FAST*stddev_factor + ema_fast
  34. ema_slow = (close_price-ema_slow) * K_SLOW*stddev_factor + ema_slow
  35. ema_fast_values.append( ema_fast )
  36. ema_slow_values.append( ema_slow )
  37. apo = ema_fast - ema_slow
  38. apo_values.append( apo )
  39. # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
  40. # We will perform a sell trade at close_price if the following conditions are met:
  41. # 1. The APO trading signal value(positive) > Sell-Entry threshold (overbought, expect a bounce back down, sell for profit)
  42. # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
  43. # 2. We are long( +ve position ) and
  44. # either APO trading signal value >= 0 or current position is profitable enough to lock profit.
  45. # APO_VALUE_FOR_SELL_ENTRY * stdev_factor:
  46. # by increasing the threshold for entry by a factor of volatility,
  47. # makes us less aggressive in entering positions(here is sell) during periods of higher volatility,
  48. # dynamic MIN_PROFIT_TO_CLOSE / stddev_factor:
  49. # to decrease the the expected profit threshold during periods of increased volatility
  50. # to be more aggressive in exciting positions
  51. # it is riskier to hold on to positions for longer periods of time.
  52. if ( ( apo > APO_VALUE_FOR_SELL_ENTRY*stddev_factor and \
  53. abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE*stddev_factor
  54. )
  55. or
  56. ( position>0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE/stddev_factor ) )
  57. ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
  58. orders.append(-1) # mark the sell trade
  59. last_sell_price = close_price
  60. position -= NUM_SHARES_PER_TRADE
  61. sell_sum_qty += NUM_SHARES_PER_TRADE
  62. sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
  63. print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
  64. # 7. We will perform a buy trade at close_price if the following conditions are met:
  65. # 1. The APO trading signal value(negative) < below Buy-Entry threshold (oversold, expect a bounce back up, buy for future profit)
  66. # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
  67. # 2. We are short( -ve position ) and
  68. # either APO trading signal value is <= 0 or current position is profitable enough to lock profit.
  69. # APO_VALUE_FOR_BUY_ENTRY * stdev_factor:
  70. # by increasing the threshold for entry by a factor of volatility,
  71. # makes us less aggressive in entering positions(here is sell) during periods of higher volatility,
  72. # dynamic MIN_PROFIT_TO_CLOSE / stddev_factor:
  73. # to decrease the the expected profit threshold during periods of increased volatility
  74. # to be more aggressive in exciting positions
  75. # it is riskier to hold on to positions for longer periods of time.
  76. elif ( ( apo < APO_VALUE_FOR_BUY_ENTRY*stddev_factor and \
  77. abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE*stddev_factor
  78. )
  79. or
  80. ( position<0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE/stddev_factor ) )
  81. ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
  82. orders.append(+1) # mark the buy trade
  83. last_buy_price = close_price
  84. position += NUM_SHARES_PER_TRADE
  85. buy_sum_qty += NUM_SHARES_PER_TRADE
  86. buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
  87. print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
  88. else:
  89. # No trade since none of the conditions were met to buy or sell
  90. orders.append( 0 )
  91. positions.append( position )
  92. # 8. The code of the trading strategy contains logic for position/PnL management.
  93. # It needs to update positions and compute open and closed PnLs when market prices change
  94. # and/or trades are made causing a change in positions
  95. # This section updates Open/Unrealized & Closed/Realized positions
  96. open_pnl = 0
  97. if position > 0:
  98. # long position and some sell trades have been made against it,
  99. # close that amount based on how much was sold against this long position
  100. # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
  101. if sell_sum_qty > 0: # vwap for sell # vwap for buy
  102. open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
  103. # mark the remaining position to market
  104. # i.e. pnl would be what it would be if we closed at current price
  105. # sell
  106. # position -= NUM_SHARES_PER_TRADE
  107. # sell_sum_qty += NUM_SHARES_PER_TRADE
  108. # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
  109. # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
  110. open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
  111. # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
  112. elif position < 0:
  113. # short position and some buy trades have been made against it,
  114. # close that amount based on how much was bought against this short position
  115. # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
  116. if buy_sum_qty > 0: # vwap for sell # vwap for buy
  117. open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
  118. # mark the remaining position to market
  119. # i.e. pnl would be what it would be if we closed at current price
  120. # buy
  121. # position += NUM_SHARE_PER_TRADE
  122. # buy_sum_qty += NUM_SHARE_PER_TRADE
  123. # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
  124. # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) *
  125. open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
  126. # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
  127. else:
  128. # flat, so update closed_pnl and reset tracking variables for positions & pnls
  129. closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
  130. buy_sum_price_qty = 0
  131. buy_sum_qty = 0
  132. last_buy_price = 0
  133. sell_sum_price_qty = 0
  134. sell_sum_qty = 0
  135. last_sell_price = 0
  136. print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
  137. pnls.append(closed_pnl + open_pnl)

  1. # This section prepares the dataframe from the trading strategy results and visualizes the results
  2. data2 = data2.assign( ClosePrice = pd.Series(close, index=data2.index) )
  3. data2 = data2.assign( Fast10DayEMA = pd.Series(ema_fast_values, index=data2.index) )
  4. data2 = data2.assign( Slow40DayEMA = pd.Series(ema_slow_values, index=data2.index) )
  5. data2 = data2.assign( APO = pd.Series(apo_values, index=data2.index) )
  6. data2 = data2.assign( Trades = pd.Series(orders, index=data2.index) )
  7. data2 = data2.assign( Position = pd.Series(positions, index=data2.index) )
  8. data2 = data2.assign( Pnl = pd.Series(pnls, index=data2.index) )
  1. import matplotlib.pyplot as plt
  2. fig = plt.figure( figsize=(20,10) )
  3. data2['ClosePrice'].plot(color='k', lw=3., legend=True)
  4. data2['Fast10DayEMA'].plot(color='y', lw=1., legend=True)
  5. data2['Slow40DayEMA'].plot(color='m', lw=1., legend=True)
  6. plt.plot( data2.loc[ data2.Trades == 1 ].index, data2.ClosePrice[data2.Trades == 1 ],
  7. color='y', lw=0, marker='^', markersize=7, label='buy'
  8. )
  9. plt.plot( data2.loc[ data2.Trades == -1 ].index, data2.ClosePrice[data2.Trades == -1 ],
  10. color='b', lw=0, marker='v', markersize=7, label='sell'
  11. )
  12. plt.autoscale(enable=True, axis='x', tight=True)
  13. plt.legend()
  14. 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

  1. fig = plt.figure( figsize=(20,10) )
  2. data2['APO'].plot(color='k', lw=3., legend=True)
  3. plt.plot( data2.loc[ data2.Trades == 1 ].index, data2.APO[data2.Trades == 1 ],
  4. color='y', lw=0, marker='^', markersize=7, label='buy'
  5. )
  6. plt.plot( data2.loc[ data2.Trades == -1 ].index, data2.APO[data2.Trades == -1 ],
  7. color='b', lw=0, marker='v', markersize=7, label='sell'
  8. )
  9. plt.axhline(y=0, lw=0.5, color='k')
  10. for i in range( APO_VALUE_FOR_BUY_ENTRY, APO_VALUE_FOR_BUY_ENTRY*5, APO_VALUE_FOR_BUY_ENTRY ):
  11. plt.axhline(y=i, lw=0.5, color='r')
  12. for i in range( APO_VALUE_FOR_SELL_ENTRY, APO_VALUE_FOR_SELL_ENTRY*5, APO_VALUE_FOR_SELL_ENTRY ):
  13. plt.axhline(y=i, lw=0.5, color='g')
  14. plt.autoscale(enable=True, axis='x', tight=True)
  15. plt.legend()
  16. plt.show()

  1. fig = plt.figure( figsize=(20,10))
  2. data2['Position'].plot(color='k', lw=1., legend=True)
  3. plt.plot( data2.loc[ data2.Position == 0 ].index, data2.Position[ data2.Position == 0 ],
  4. color='r', lw=0, marker='.', label='flat'
  5. )
  6. plt.plot( data2.loc[ data2.Position > 0 ].index, data2.Position[ data2.Position > 0 ],
  7. color='y', lw=0, marker='+', label='long'
  8. )
  9. plt.plot( data2.loc[ data2.Position < 0 ].index, data2.Position[ data2.Position < 0 ],
  10. color='b', lw=0, marker='_', label='short'
  11. )
  12. plt.axhline(y=0, lw=0.5, color='k')
  13. for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE*25, NUM_SHARES_PER_TRADE*5 ):
  14. plt.axhline(y=i, lw=0.5, color='r')
  15. for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE*25, -NUM_SHARES_PER_TRADE*5 ):
  16. plt.axhline(y=i, lw=0.5, color='g')
  17. plt.autoscale(enable=True, axis='x', tight=True)
  18. plt.legend()
  19. plt.show()

  1. fig = plt.figure( figsize=(20,10))
  2. plt.plot( data.index, data['Pnl'], color='g', lw=1.,
  3. label='BasicMeanReversionPnL'
  4. )#########################
  5. plt.plot( data.loc[ data.Pnl > 0 ].index, data.Pnl[ data.Pnl > 0 ],
  6. color='y', lw=0, marker='.',
  7. #label='Pnl'
  8. )
  9. plt.plot( data.loc[ data.Pnl < 0 ].index, data.Pnl[ data.Pnl < 0 ],
  10. color='r', lw=0, marker='.',
  11. #label='Pnl'
  12. )
  13. plt.plot( data2.index, data2['Pnl'], color='b', lw=1.,
  14. label='VolatilityAdjustedMeanReversionPnL'
  15. )#########################
  16. plt.plot( data2.loc[ data.Pnl > 0 ].index, data2.Pnl[ data.Pnl > 0 ],
  17. color='y', lw=0, marker='.',
  18. #label='Pnl'
  19. )
  20. plt.plot( data2.loc[ data.Pnl < 0 ].index, data2.Pnl[ data.Pnl < 0 ],
  21. color='r', lw=0, marker='.',
  22. #label='Pnl'
  23. )
  24. plt.axhline(y=15000, ls='--', alpha=0.5)
  25. plt.autoscale(enable=True, axis='x', tight=True)
  26. plt.legend()
  27. 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

  1. import pandas as pd
  2. import pandas_datareader.data as pdr
  3. def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
  4. if len(output_file) == 0:
  5. output_file = stock_symbol+'_data_large.pkl'
  6. try:
  7. df = pd.read_pickle( output_file )
  8. print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
  9. except FileNotFoundError:
  10. print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
  11. df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
  12. df.to_pickle( output_file )
  13. return df
  14. goog_data = load_financial_data( stock_symbol='GOOG',
  15. start_date='2014-01-01',
  16. end_date='2018-01-01',
  17. output_file='goog_data.pkl'
  18. )
  19. goog_data.head()

 

     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

  1. # Variables/constants for EMA Calculation:
  2. NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
  3. K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
  4. ema_fast = 0 # initial ema
  5. ema_fast_values = [] # we will hold fast EMA values for visualization purpose
  6. NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
  7. K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
  8. ema_slow = 0 # initial ema
  9. ema_slow_values = [] # we will hold slow EMA values for visualization purpose
  10. apo_values = [] # track computed absolute price oscillator values
  11. # Variables for Trading Strategy trade, position & pnl management:
  12. # Container for tracking buy/sell order,
  13. # +1 for buy order, -1 for sell order, 0 for no-action
  14. orders = []
  15. # Container for tracking positions,
  16. # positive for long positions, negative for short positions, 0 for flat/no position
  17. positions = []
  18. # Container for tracking total_pnls, this is the sum of
  19. # closed_pnl i.e. pnls already locked in
  20. # and open_pnl i.e. pnls for open-position marked to market price
  21. pnls = []
  22. last_buy_price = 0 # used to prevent over-trading at/around the same price
  23. last_sell_price = 0 # used to prevent over-trading at/around the same price
  24. position = 0 # Current position of the trading strategy
  25. # Summation of products of
  26. # buy_trade_price and buy_trade_qty for every buy Trade made
  27. # since last time being flat
  28. buy_sum_price_qty = 0
  29. # Summation of buy_trade_qty for every buy Trade made since last time being flat
  30. buy_sum_qty = 0
  31. # Summation of products of
  32. # sell_trade_price and sell_trade_qty for every sell Trade made
  33. # since last time being flat
  34. sell_sum_price_qty = 0
  35. # Summation of sell_trade_qty for every sell Trade made since last time being flat
  36. sell_sum_qty = 0
  37. open_pnl = 0 # Open/Unrealized PnL marked to market
  38. closed_pnl = 0 # Closed/Realized PnL so far
  39. # Constants that define strategy behavior/thresholds
  40. ################################
  41. # APO trading signal value below which(-10) to enter buy-orders/long-position
  42. APO_VALUE_FOR_BUY_ENTRY = 10 # (oversold, expect a bounce back up; >10 continue going up for trending)
  43. # APO trading signal value above which(-10) to enter sell-orders/short-position
  44. APO_VALUE_FOR_SELL_ENTRY = -10 # (overbought, expect a bounce back down; <-10 continue going down for trending)
  45. ################################
  46. # Minimum price change since last trade before considering trading again,
  47. MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
  48. NUM_SHARES_PER_TRADE = 10
  49. # positions are closed if currently open positions are profitable above a certain amount,
  50. # regardless of APO values.
  51. # This is used to algorithmically lock profits and initiate more positions
  52. # instead of relying only on the trading signal value.
  53. # Minimum Open/Unrealized profit at which to close positions and lock profits
  54. MIN_PROFIT_TO_CLOSE = 10*NUM_SHARES_PER_TRADE
  1. import statistics as stats
  2. import math as math
  3. data3 = goog_data.copy()
  4. close = data3['Close']
  5. for close_price in close:
  6. # This section updates fast and slow EMA and computes APO trading signal
  7. if (ema_fast == 0): # first observation
  8. ema_fast = close_price
  9. ema_slow = close_price
  10. else:
  11. ema_fast = (close_price - ema_fast)*K_FAST + ema_fast # K_FAST = 2/(NUM_PERIODS_FAST_10 + 1)
  12. ema_slow = (close_price - ema_slow)*K_SLOW + ema_slow # K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1)
  13. ema_fast_values.append(ema_fast)
  14. ema_slow_values.append(ema_slow)
  15. apo = ema_fast-ema_slow
  16. apo_values.append(apo)
  17. # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
  18. # We will perform a sell trade at close_price if the following conditions are met:
  19. # 1. The APO trading signal value(negative) < Sell-Entry threshold (expecting price moves to continue going down)
  20. # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
  21. # 2. We are long( +ve position ) and
  22. # either APO trading signal value >= 0 or current position is profitable enough to lock profit.
  23. ###
  24. if ( ( apo < APO_VALUE_FOR_SELL_ENTRY and \
  25. abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
  26. )
  27. or ###
  28. ( position>0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
  29. ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
  30. orders.append(-1) # mark the sell trade
  31. last_sell_price = close_price
  32. position -= NUM_SHARES_PER_TRADE
  33. sell_sum_qty += NUM_SHARES_PER_TRADE
  34. sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
  35. print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
  36. # 7. We will perform a buy trade at close_price if the following conditions are met:
  37. # 1. The APO trading signal value(positive) > Buy-Entry threshold (expecting price moves to continue going up)
  38. # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
  39. # 2. We are short( -ve position ) and
  40. # either APO trading signal value is >= 0 or current position is profitable enough to lock profit.
  41. ###
  42. elif ( ( apo > APO_VALUE_FOR_BUY_ENTRY and \
  43. abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
  44. )
  45. or ###
  46. ( position<0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
  47. ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
  48. orders.append(+1) # mark the buy trade
  49. last_buy_price = close_price
  50. position += NUM_SHARES_PER_TRADE
  51. buy_sum_qty += NUM_SHARES_PER_TRADE
  52. buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
  53. print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
  54. else:
  55. # No trade since none of the conditions were met to buy or sell
  56. orders.append( 0 )
  57. positions.append( position )
  58. # 8. The code of the trading strategy contains logic for position/PnL management.
  59. # It needs to update positions and compute open and closed PnLs when market prices change
  60. # and/or trades are made causing a change in positions
  61. # This section updates Open/Unrealized & Closed/Realized positions
  62. open_pnl = 0
  63. if position > 0:
  64. # long position and some sell trades have been made against it,
  65. # close that amount based on how much was sold against this long position
  66. # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
  67. if sell_sum_qty > 0: # vwap for sell # vwap for buy
  68. open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
  69. # mark the remaining position to market
  70. # i.e. pnl would be what it would be if we closed at current price
  71. # sell
  72. # position -= NUM_SHARES_PER_TRADE
  73. # sell_sum_qty += NUM_SHARES_PER_TRADE
  74. # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
  75. # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
  76. open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
  77. # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
  78. elif position < 0:
  79. # short position and some buy trades have been made against it,
  80. # close that amount based on how much was bought against this short position
  81. # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
  82. if buy_sum_qty > 0: # vwap for sell # vwap for buy
  83. open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
  84. # mark the remaining position to market
  85. # i.e. pnl would be what it would be if we closed at current price
  86. # buy
  87. # position += NUM_SHARE_PER_TRADE
  88. # buy_sum_qty += NUM_SHARE_PER_TRADE
  89. # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
  90. # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) *
  91. open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
  92. # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
  93. else:
  94. # flat, so update closed_pnl and reset tracking variables for positions & pnls
  95. closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
  96. buy_sum_price_qty = 0
  97. buy_sum_qty = 0
  98. last_buy_price = 0
  99. sell_sum_price_qty = 0
  100. sell_sum_qty = 0
  101. last_sell_price = 0
  102. print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
  103. pnls.append(closed_pnl + open_pnl)

  1. # This section prepares the dataframe from the trading strategy results and visualizes the results
  2. data3 = data3.assign( ClosePrice=pd.Series(close, index=data3.index))
  3. data3 = data3.assign( Fast10DayEMA=pd.Series(ema_fast_values, index=data3.index))
  4. data3 = data3.assign( Slow40DayEMA=pd.Series(ema_slow_values, index=data3.index))
  5. data3 = data3.assign( APO=pd.Series(apo_values, index=data3.index))
  6. data3 = data3.assign( Trades=pd.Series(orders, index=data3.index))
  7. data3 = data3.assign( Position=pd.Series(positions, index=data3.index))
  8. data3 = data3.assign( Pnl=pd.Series(pnls, index=data3.index))
  1. import matplotlib.pyplot as plt
  2. fig = plt.figure( figsize=(20,10) )
  3. data3['ClosePrice'].plot(color='k', lw=3., legend=True)
  4. data3['Fast10DayEMA'].plot(color='y', lw=1., legend=True)
  5. data3['Slow40DayEMA'].plot(color='m', lw=1., legend=True)
  6. plt.plot( data3.loc[ data3.Trades == 1 ].index, data3.ClosePrice[data3.Trades == 1 ],
  7. color='y', lw=0, marker='^', markersize=7, label='buy'
  8. )
  9. plt.plot( data3.loc[ data3.Trades == -1 ].index, data3.ClosePrice[data3.Trades == -1 ],
  10. color='b', lw=0, marker='v', markersize=7, label='sell'
  11. )
  12. plt.autoscale(enable=True, axis='x', tight=True)
  13. plt.legend()
  14. plt.show()

     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:

  1. fig = plt.figure( figsize=(20,10) )
  2. data3['APO'].plot(color='k', lw=3., legend=True)
  3. plt.plot( data3.loc[ data3.Trades == 1 ].index, data3.APO[data3.Trades == 1 ],
  4. color='y', lw=0, marker='^', markersize=7, label='buy'
  5. )
  6. plt.plot( data3.loc[ data3.Trades == -1 ].index, data3.APO[data3.Trades == -1 ],
  7. color='b', lw=0, marker='v', markersize=7, label='sell'
  8. )
  9. plt.axhline(y=0, lw=0.5, color='k')
  10. for i in range( APO_VALUE_FOR_BUY_ENTRY, APO_VALUE_FOR_BUY_ENTRY*5, APO_VALUE_FOR_BUY_ENTRY ):
  11. plt.axhline(y=i, lw=0.5, color='r')
  12. for i in range( APO_VALUE_FOR_SELL_ENTRY, APO_VALUE_FOR_SELL_ENTRY*5, APO_VALUE_FOR_SELL_ENTRY ):
  13. plt.axhline(y=i, lw=0.5, color='g')
  14. plt.autoscale(enable=True, axis='x', tight=True)
  15. plt.legend()
  16. plt.show()

     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:

  1. fig = plt.figure( figsize=(20,10))
  2. data3['Position'].plot(color='k', lw=1., legend=True)
  3. plt.plot( data3.loc[ data3.Position == 0 ].index, data3.Position[ data3.Position == 0 ],
  4. color='r', lw=0, marker='.', label='flat'
  5. )
  6. plt.plot( data3.loc[ data3.Position > 0 ].index, data3.Position[ data3.Position > 0 ],
  7. color='y', lw=0, marker='+', label='long'
  8. )
  9. plt.plot( data3.loc[ data3.Position < 0 ].index, data3.Position[ data3.Position < 0 ],
  10. color='b', lw=0, marker='_', label='short'
  11. )
  12. plt.axhline(y=0, lw=0.5, color='k')
  13. for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE*25, NUM_SHARES_PER_TRADE*5 ):
  14. plt.axhline(y=i, lw=0.5, color='r')
  15. for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE*25, -NUM_SHARES_PER_TRADE*5 ):
  16. plt.axhline(y=i, lw=0.5, color='g')
  17. plt.autoscale(enable=True, axis='x', tight=True)
  18. plt.legend()
  19. plt.show()

     Here, compared to the mean reversion trading strategy, 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:

  1. fig = plt.figure( figsize=(20,10))
  2. data3['Pnl'].plot(color='k', lw=1., legend=True)
  3. plt.plot( data3.loc[ data3.Pnl > 0 ].index, data3.Pnl[ data3.Pnl > 0 ],
  4. color='b', lw=0, marker='.',
  5. label='Pnl'
  6. )
  7. plt.plot( data3.loc[ data3.Pnl < 0 ].index, data3.Pnl[ data3.Pnl < 0 ],
  8. color='y', lw=0, marker='.',
  9. label='Pnl'
  10. )
  11. plt.autoscale(enable=True, axis='x', tight=True)
  12. plt.legend()
  13. plt.show()

     So, for this case, the trend-following strategy makes a third of the money that the mean reversion strategy makes; 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.

  1. import pandas as pd
  2. import pandas_datareader.data as pdr
  3. def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
  4. if len(output_file) == 0:
  5. output_file = stock_symbol+'_data_large.pkl'
  6. try:
  7. df = pd.read_pickle( output_file )
  8. print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
  9. except FileNotFoundError:
  10. print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
  11. df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
  12. df.to_pickle( output_file )
  13. return df
  14. goog_data = load_financial_data( stock_symbol='GOOG',
  15. start_date='2014-01-01',
  16. end_date='2018-01-01',
  17. output_file='goog_data.pkl'
  18. )
  19. goog_data.head()

  1. # Variables/constants for EMA Calculation:
  2. NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
  3. K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
  4. ema_fast = 0 # initial ema
  5. ema_fast_values = [] # we will hold fast EMA values for visualization purpose
  6. NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
  7. K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
  8. ema_slow = 0 # initial ema
  9. ema_slow_values = [] # we will hold slow EMA values for visualization purpose
  10. apo_values = [] # track computed absolute price oscillator values
  11. # Variables for Trading Strategy trade, position & pnl management:
  12. # Container for tracking buy/sell order,
  13. # +1 for buy order, -1 for sell order, 0 for no-action
  14. orders = []
  15. # Container for tracking positions,
  16. # positive for long positions, negative for short positions, 0 for flat/no position
  17. positions = []
  18. # Container for tracking total_pnls, this is the sum of
  19. # closed_pnl i.e. pnls already locked in
  20. # and open_pnl i.e. pnls for open-position marked to market price
  21. pnls = []
  22. last_buy_price = 0 # used to prevent over-trading at/around the same price
  23. last_sell_price = 0 # used to prevent over-trading at/around the same price
  24. position = 0 # Current position of the trading strategy
  25. # Summation of products of
  26. # buy_trade_price and buy_trade_qty for every buy Trade made
  27. # since last time being flat
  28. buy_sum_price_qty = 0
  29. # Summation of buy_trade_qty for every buy Trade made since last time being flat
  30. buy_sum_qty = 0
  31. # Summation of products of
  32. # sell_trade_price and sell_trade_qty for every sell Trade made
  33. # since last time being flat
  34. sell_sum_price_qty = 0
  35. # Summation of sell_trade_qty for every sell Trade made since last time being flat
  36. sell_sum_qty = 0
  37. open_pnl = 0 # Open/Unrealized PnL marked to market
  38. closed_pnl = 0 # Closed/Realized PnL so far
  39. # Constants that define strategy behavior/thresholds
  40. ################################
  41. # APO trading signal value below which(-10) to enter buy-orders/long-position
  42. APO_VALUE_FOR_BUY_ENTRY = 10 # (oversold, expect a bounce back up)
  43. # APO trading signal value above which to enter sell-orders/short-position
  44. APO_VALUE_FOR_SELL_ENTRY = -10 # (overbought, expect a bounce back down)
  45. ################################
  46. # Minimum price change since last trade before considering trading again,
  47. MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
  48. NUM_SHARES_PER_TRADE = 10
  49. # positions are closed if currently open positions are profitable above a certain amount,
  50. # regardless of APO values.
  51. # This is used to algorithmically lock profits and initiate more positions
  52. # instead of relying only on the trading signal value.
  53. # Minimum Open/Unrealized profit at which to close positions and lock profits
  54. MIN_PROFIT_TO_CLOSE = 10*NUM_SHARES_PER_TRADE
  1. import statistics as stats
  2. import math as math
  3. data4 = goog_data.copy()
  4. close = data4['Close']
  5. # Constants/variables that are used to compute standard deviation as a volatility measure
  6. SMA_NUM_PERIODS_20 = 20 # look back period
  7. price_history = [] # history of prices
  8. for close_price in close:
  9. price_history.append( close_price )
  10. if len( price_history) > SMA_NUM_PERIODS_20 : # we track at most 'time_period' number of prices
  11. del ( price_history[0] )
  12. # calculate vairance during the SMA_NUM_PERIODS_20 periods
  13. sma = stats.mean( price_history )
  14. variance = 0 # variance is square of standard deviation
  15. for hist_price in price_history:
  16. variance = variance + ( (hist_price-sma)**2 )
  17. stddev = math.sqrt( variance/len(price_history) )
  18. # a volatility factor that ranges from 0 to 1
  19. stddev_factor = stddev/15 # 15 since since the population stddev.mean() = 15.45
  20. # closer to 0 indicate very low volatility,
  21. # around 1 indicate normal volatility
  22. # > 1 indicate above-normal volatility
  23. if stddev_factor == 0:
  24. stddev_factor = 1
  25. # This section updates fast and slow EMA and computes APO trading signal
  26. if (ema_fast==0): # first observation
  27. ema_fast = close_price # initial ema_fast or ema_slow
  28. ema_slow = close_price
  29. else:
  30. # ema fomula
  31. # K_FAST*stddev_factor or K_SLOW*stddev_factor
  32. # more reactive to newest observations during periods of higher than normal volatility
  33. ema_fast = (close_price-ema_fast) * K_FAST*stddev_factor + ema_fast
  34. ema_slow = (close_price-ema_slow) * K_SLOW*stddev_factor + ema_slow
  35. ema_fast_values.append( ema_fast )
  36. ema_slow_values.append( ema_slow )
  37. apo = ema_fast - ema_slow
  38. apo_values.append( apo )
  39. # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
  40. # We will perform a sell trade at close_price if the following conditions are met:
  41. # 1. The APO trading signal value(negative) < Sell-Entry threshold (expecting price moves to continue going down)
  42. # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
  43. # 2. We are long( +ve position ) and
  44. # either APO trading signal value >= 0 or current position is profitable enough to lock profit.
  45. ###
  46. if ( ( apo < APO_VALUE_FOR_SELL_ENTRY and \
  47. abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
  48. )
  49. or ###
  50. ( position>0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
  51. ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
  52. orders.append(-1) # mark the sell trade
  53. last_sell_price = close_price
  54. position -= NUM_SHARES_PER_TRADE
  55. sell_sum_qty += NUM_SHARES_PER_TRADE
  56. sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
  57. print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
  58. # 7. We will perform a buy trade at close_price if the following conditions are met:
  59. # 1. The APO trading signal value(positive) > Buy-Entry threshold (expecting price moves to continue going up)
  60. # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
  61. # 2. We are short( -ve position ) and
  62. # either APO trading signal value is >= 0 or current position is profitable enough to lock profit.
  63. ###
  64. elif ( ( apo > APO_VALUE_FOR_BUY_ENTRY and \
  65. abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
  66. )
  67. or ###
  68. ( position<0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
  69. ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
  70. orders.append(+1) # mark the buy trade
  71. last_buy_price = close_price
  72. position += NUM_SHARES_PER_TRADE
  73. buy_sum_qty += NUM_SHARES_PER_TRADE
  74. buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
  75. print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
  76. else:
  77. # No trade since none of the conditions were met to buy or sell
  78. orders.append( 0 )
  79. positions.append( position )
  80. # 8. The code of the trading strategy contains logic for position/PnL management.
  81. # It needs to update positions and compute open and closed PnLs when market prices change
  82. # and/or trades are made causing a change in positions
  83. # This section updates Open/Unrealized & Closed/Realized positions
  84. open_pnl = 0
  85. if position > 0:
  86. # long position and some sell trades have been made against it,
  87. # close that amount based on how much was sold against this long position
  88. # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
  89. if sell_sum_qty > 0: # vwap for sell # vwap for buy
  90. open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
  91. # mark the remaining position to market
  92. # i.e. pnl would be what it would be if we closed at current price
  93. # sell
  94. # position -= NUM_SHARES_PER_TRADE
  95. # sell_sum_qty += NUM_SHARES_PER_TRADE
  96. # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
  97. # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
  98. open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
  99. # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
  100. elif position < 0:
  101. # short position and some buy trades have been made against it,
  102. # close that amount based on how much was bought against this short position
  103. # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
  104. if buy_sum_qty > 0: # vwap for sell # vwap for buy
  105. open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
  106. # mark the remaining position to market
  107. # i.e. pnl would be what it would be if we closed at current price
  108. # buy
  109. # position += NUM_SHARE_PER_TRADE
  110. # buy_sum_qty += NUM_SHARE_PER_TRADE
  111. # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
  112. # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) *
  113. open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
  114. # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
  115. else:
  116. # flat, so update closed_pnl and reset tracking variables for positions & pnls
  117. closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
  118. buy_sum_price_qty = 0
  119. buy_sum_qty = 0
  120. last_buy_price = 0
  121. sell_sum_price_qty = 0
  122. sell_sum_qty = 0
  123. last_sell_price = 0
  124. print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
  125. pnls.append(closed_pnl + open_pnl)

  1. # This section prepares the dataframe from the trading strategy results and visualizes the results
  2. data4 = data4.assign( ClosePrice = pd.Series(close, index=data4.index) )
  3. data4 = data4.assign( Fast10DayEMA = pd.Series(ema_fast_values, index=data4.index) )
  4. data4 = data4.assign( Slow40DayEMA = pd.Series(ema_slow_values, index=data4.index) )
  5. data4 = data4.assign( APO = pd.Series(apo_values, index=data4.index) )
  6. data4 = data4.assign( Trades = pd.Series(orders, index=data4.index) )
  7. data4 = data4.assign( Position = pd.Series(positions, index=data4.index) )
  8. data4 = data4.assign( Pnl = pd.Series(pnls, index=data4.index) )
  1. fig = plt.figure( figsize=(20,10))
  2. plt.plot( data3.index, data3['Pnl'], color='g', lw=1.,
  3. label='BasicTrendFollowingPnL'
  4. )#########################
  5. plt.plot( data3.loc[ data3.Pnl > 0 ].index, data3.Pnl[ data3.Pnl > 0 ],
  6. color='y', lw=0, marker='.',
  7. #label='Pnl'
  8. )
  9. plt.plot( data3.loc[ data3.Pnl < 0 ].index, data3.Pnl[ data3.Pnl < 0 ],
  10. color='r', lw=0, marker='.',
  11. #label='Pnl'
  12. )
  13. plt.plot( data4.index, data4['Pnl'], color='b', lw=1.,
  14. label='VolatilityAdjustedTrendFollowingPnL'
  15. )#########################
  16. plt.plot( data4.loc[ data4.Pnl > 0 ].index, data4.Pnl[ data4.Pnl > 0 ],
  17. color='y', lw=0, marker='.',
  18. #label='Pnl'
  19. )
  20. plt.plot( data4.loc[ data4.Pnl < 0 ].index, data4.Pnl[ data4.Pnl < 0 ],
  21. color='r', lw=0, marker='.',
  22. #label='Pnl'
  23. )
  24. plt.autoscale(enable=True, axis='x', tight=True)
  25. plt.legend()
  26. plt.show()

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

     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:

     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: 

     Let's quickly put together a scatter plot to easily visualize how price moves correspond to misses in economic indicator releases:

     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:

  1. import pandas as pd
  2. import pandas_datareader.data as pdr
  3. # Fetch daily data for 4 years, for 7 major currency pairs
  4. TRADING_INSTRUMENT = 'CADUSD=X'
  5. SYMBOLS = ['AUDUSD=X', 'GBPUSD=X', 'CADUSD=X', 'CHFUSD=X', 'EURUSD=X', 'JPYUSD=X',
  6. 'NZDUSD=X']
  7. START_DATE = '2014-01-01'
  8. END_DATE = '2018-01-01'
  9. # DataSeries for each currency
  10. symbols_data = {}
  11. for symbol in SYMBOLS:
  12. SRC_DATA_FILENAME = symbol + '_data.pkl'
  13. try:
  14. data = pd.read_pickle( SRC_DATA_FILENAME )
  15. except FileNotFoundError:
  16. data = pdr.DataReader( symbol, 'yahoo', START_DATE, END_DATE )
  17. data.to_pickle( SRC_DATA_FILENAME )
  18. symbols_data[symbol] = data

symbols_data


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:

  1. # Visualize prices for currency to inspect relationship between them
  2. import matplotlib.pyplot as plt
  3. import numpy as np
  4. from itertools import cycle
  5. cycol = cycle('bgrcmky')
  6. price_data = pd.DataFrame()
  7. fig = plt.figure( figsize=(20,14) )
  8. for symbol in SYMBOLS:
  9. multiplier = 1.0
  10. if symbol == 'JPYUSD=X':
  11. multiplier = 100.0
  12. label = symbol + ' ClosePrice'
  13. price_data = price_data.assign( label=pd.Series( symbols_data[symbol]['Close'] * multiplier,
  14. index = symbols_data[symbol].index
  15. ) # datetime index
  16. )
  17. if symbol == 'JPYUSD=X' or symbol == 'CHFUSD=X':
  18. ax = price_data['label'].plot( color=next(cycol), lw=2., label=label, ls='dotted' )
  19. else:
  20. ax = price_data['label'].plot( color=next(cycol), lw=2., label=label )
  21. plt.xlabel( 'Date', fontsize=18 )
  22. plt.ylabel( 'Scaled Price', fontsize=18 )
  23. plt.legend( prop={'size':18} )
  24. 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. 

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

  1. from mlxtend.plotting import heatmap
  2. cm = np.corrcoef( df[SYMBOLS].values.T )
  3. # df[cols].values.T : (n_instances, n_features) ==> (n_features, n_instances or n_observations)
  4. hm = heatmap( cm, row_names=SYMBOLS, column_names=SYMBOLS )
  5. plt.show()

==> 

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:

  1. import statistics as stats
  2. # Constants/variables that are used to compute
  3. # simple moving average and price deviation from simple moving average
  4. SMA_NUM_PERIODS = 20 # look back period
  5. price_history = {} # history of prices
  6. PRICE_DEV_NUM_PRICES = 200 # look back period of ClosePrice deviations from SMA
  7. price_deviation_from_sma = {} # history of ClosePrice deviations from SMA
  8. # We will use this to iterate over all the days of data we have
  9. # TRADING_INSTRUMENT = 'CADUSD=X' # datetime index
  10. num_days = len( symbols_data[TRADING_INSTRUMENT].index )
  11. correlation_history = {} # history of correlations per currency pair
  12. # history of differences between
  13. # Projected ClosePrice deviation and actual ClosePrice deviation per currency pair
  14. delta_projected_actual_history = {}
  15. # history of differences between
  16. # final Projected ClosePrice deviation for TRADING_INSTRUMENT and actual ClosePrice deviation
  17. 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

  1. ######## Variables for Trading Strategy trade, position & pnl management ########
  2. # Container for tracking buy/sell order,
  3. # +1 for buy order, -1 for sell order, 0 for no-action
  4. orders = []
  5. # Container for tracking positions,
  6. # positive for long positions, negative for short positions, 0 for flat/no position
  7. positions = []
  8. # Container for tracking total_pnls, this is the sum of
  9. # closed_pnl i.e. pnls already locked in
  10. # and open_pnl i.e. pnls for open-position marked to market price
  11. pnls = []
  12. last_buy_price = 0 # used to prevent over-trading at/around the same price
  13. last_sell_price = 0 # used to prevent over-trading at/around the same price
  14. position = 0 # Current position of the trading strategy
  15. # Summation of products of
  16. # buy_trade_price and buy_trade_qty for every buy Trade made
  17. # since last time being flat
  18. buy_sum_price_qty = 0
  19. # Summation of buy_trade_qty for every buy Trade made since last time being flat
  20. buy_sum_qty = 0
  21. # Summation of products of
  22. # sell_trade_price and sell_trade_qty for every sell Trade made
  23. # since last time being flat
  24. sell_sum_price_qty = 0
  25. # Summation of sell_trade_qty for every sell Trade made since last time being flat
  26. sell_sum_qty = 0
  27. open_pnl = 0 # Open/Unrealized PnL marked to market
  28. closed_pnl = 0 # Closed/Realized PnL so far
  29. # Constants that define strategy behavior/thresholds
  30. # StatArb trading signal value above which to enter buy-orders/long-position
  31. StatArb_VALUE_FOR_BUY_ENTRY = 0.01
  32. # StatArb trading signal value below which to enter sell-orders/short-position
  33. StatArb_VALUE_FOR_SELL_ENTRY = -0.01
  34. # Minimum price change since last trade before considering trading again,
  35. # this is to prevent over-trading at/around same prices
  36. MIN_PRICE_MOVE_FROM_LAST_TRADE = 0.01
  37. # Number of currency to buy/sell on every trade
  38. NUM_SHARES_PER_TRADE = 1000000
  39. # Minimum Open/Unrealized profit at which to close positions and lock profits
  40. 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:

  1. for i in range(0, num_days):
  2. close_prices = {}
  3. # Build ClosePrice series,
  4. # compute SMA for each symbol and price-deviation from SMA for each symbol
  5. for symbol in SYMBOLS:
  6. close_prices[symbol] = symbols_data[symbol]['Close'].iloc[i]
  7. if not symbol in price_history.keys():
  8. price_history[symbol] = []
  9. price_deviation_from_sma[symbol] = []
  10. price_history[symbol].append( close_prices[symbol] )
  11. # we track at most SMA_NUM_PERIODS number of prices
  12. if len( price_history[symbol] ) > SMA_NUM_PERIODS:
  13. del ( price_history[symbol][0] )
  14. sma = stats.mean( price_history[symbol] ) # Rolling SimpleMovingAverage
  15. # price deviation from mean
  16. price_deviation_from_sma[symbol].append( close_prices[symbol] - sma )
  17. if len( price_deviation_from_sma[symbol] ) > PRICE_DEV_NUM_PRICES:
  18. del ( price_deviation_from_sma[symbol][0] )
  19. # Now compute covariance and correlation between TRADING_INSTRUMENT and every other lead symbol
  20. # also compute projected price deviation and find delta between projected and actual price deviations.
  21. projected_dev_from_sma_using = {}
  22. for symbol in SYMBOLS:
  23. # no need to find relationship between trading instrument and itself
  24. if symbol == TRADING_INSTRUMENT:
  25. continue
  26. correlation_label = TRADING_INSTRUMENT + '<-' + symbol
  27. # first entry for this pair in the history dictionary
  28. if correlation_label not in correlation_history.keys():
  29. correlation_history[correlation_label] = []
  30. delta_projected_actual_history[correlation_label] = []
  31. # need at least 2 observations to compute covariance/correlation
  32. # close_prices[symbol] = symbols_data[symbol]['Close'].iloc[i] and 0<=i<num_days
  33. # price_deviation_from_sma[symbol].append( close_prices[symbol] - sma )
  34. if len( price_deviation_from_sma[symbol] ) < 2:
  35. correlation_history[correlation_label].append(0)
  36. delta_projected_actual_history[correlation_label].append(0)
  37. continue
  38. # https://blog.csdn.net/Linli522362242/article/details/121721868
  39. # Each row of m represents a variable, and each column a single observation(features) of all those variables.
  40. corr = np.corrcoef( price_deviation_from_sma[TRADING_INSTRUMENT],
  41. price_deviation_from_sma[symbol]
  42. ) # Return Pearson product-moment correlation coefficients
  43. # the correlation matrix is identical to a covariance matrix computed from standardized features.
  44. cov = np.cov( price_deviation_from_sma[TRADING_INSTRUMENT],
  45. price_deviation_from_sma[symbol]
  46. ) # Estimate a covariance matrix(here is 2x2), given data and weights(here is None)
  47. # get the correlation between the 2 series
  48. # here is the correlation between CAD/USD and the other currency pairs
  49. corr_trading_instrument_lead_instrument = corr[0,1]
  50. # get the covariance between the 2 series
  51. cov_trading_instrument_lead_instrument = cov[0,0]/cov[0,1]
  52. correlation_history[correlation_label].append( corr_trading_instrument_lead_instrument )
  53. # computes the projected price movement,
  54. # uses that to find the difference between the projected movement and actual movement,
  55. # and saves it in our delta_projected_actual_history list per currency pair
  56. # projected-price-deviation is predicted by the other pairs(lead-symbol) * covariance
  57. ##################
  58. # projected-price-deviation-in-TRADING_INSTRUMENT is covariance * price-deviation-in-lead-symbol
  59. projected_dev_from_sma_using[symbol] = price_deviation_from_sma[symbol][-1] * cov_trading_instrument_lead_instrument
  60. # the delta between projected and actual price deviations in CAD/USD as predicted by the other pairs
  61. # delta +ve => signal says TRADING_INSTRUMENT price should have moved up more than what it did
  62. # delta -ve => signal says TRADING_INSTRUMENT price should have moved down more than what it did
  63. delta_projected_actual = (projected_dev_from_sma_using[symbol] - price_deviation_from_sma[TRADING_INSTRUMENT][-1])
  64. delta_projected_actual_history[correlation_label].append(delta_projected_actual)
  65. # weigh predictions from each pair,
  66. # weight is the correlation between those pairs
  67. sum_weights = 0 # sum of weights is sum of correlations for each symbol with TRADING_INSTRUMENT
  68. for symbol in SYMBOLS:
  69. if symbol == TRADING_INSTRUMENT: # no need to find relationship between trading instrument and itself
  70. continue
  71. correlation_label = TRADING_INSTRUMENT + '<-' + symbol
  72. # the sum of each individual weight (magnitude of correlation)
  73. sum_weights += abs( correlation_history[correlation_label][-1] )
  74. # will hold final prediction of price deviation in TRADING_INSTRUMENT, weighing projections from all other symbols.
  75. final_delta_projected = 0
  76. close_price = close_prices[TRADING_INSTRUMENT]
  77. for symbol in SYMBOLS:
  78. if symbol == TRADING_INSTRUMENT:
  79. continue
  80. correlation_label = TRADING_INSTRUMENT + '<-' + symbol
  81. # weight projection from a symbol by correlation
  82. # use the magnitude of the correlation between CAD/USD and the other currency pairs
  83. # to weigh
  84. # the delta between projected and actual price deviations in CAD/USD as predicted by the other pairs
  85. final_delta_projected += abs(correlation_history[correlation_label][-1]) * delta_projected_actual_history[correlation_label][-1]
  86. # normalize by divding by sum of weights for all pairs
  87. # as our final signal to build our trading strategy around
  88. if sum_weights !=0:
  89. final_delta_projected /= sum_weights
  90. else:
  91. final_delta_projected = 0
  92. final_delta_projected_history.append( final_delta_projected )
  93. # StatArb execution logic
  94. # Now, using the StatArb signal we just computed, we can build a strategy similar to the trend-following strategy
  95. # We will perform a sell trade at close_prices if the following conditions are met:
  96. # 1. The StatArb trading signal value(-ve) is < Sell-Entry threshold(StatArb_VALUE_FOR_SELL_ENTRY = -0.01)
  97. # and the difference between last trade-price and current-price is different enough.
  98. # 2. We are long( +ve position )
  99. # and current position is profitable enough to lock profit
  100. if ( ( final_delta_projected < StatArb_VALUE_FOR_SELL_ENTRY and \
  101. abs(close_price-last_sell_price) > MIN_PRICE_MOVE_FROM_LAST_TRADE
  102. )
  103. or
  104. ( position > 0 and \
  105. abs( open_pnl > MIN_PROFIT_TO_CLOSE )
  106. )# long from -ve StatArb and StatArb has gone positive or position is profitable, sell to close position
  107. ):
  108. orders.append(-1) # mark the sell trade
  109. last_sell_price = close_price
  110. position -= NUM_SHARES_PER_TRADE # reduce position by the size of this trade
  111. sell_sum_qty += NUM_SHARES_PER_TRADE
  112. sell_sum_price_qty += (close_price*NUM_SHARES_PER_TRADE) # update vwap sell-price
  113. print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
  114. print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
  115. # We will perform a buy trade at close_prices if the following conditions are met:
  116. # 1. The StatArb trading signal value(+ve) is above Buy-Entry threshold
  117. # and the difference between last trade-price and current-price is different enough.
  118. # 2. We are short( -ve position )
  119. # and current position is profitable enough to lock profit.
  120. elif ( ( final_delta_projected > StatArb_VALUE_FOR_BUY_ENTRY and \
  121. abs(close_price-last_buy_price) > MIN_PRICE_MOVE_FROM_LAST_TRADE
  122. )
  123. or
  124. ( position < 0 and \
  125. abs( open_pnl > MIN_PROFIT_TO_CLOSE )
  126. )# short from +ve StatArb and StatArb has gone negative or position is profitable, buy to close position
  127. ):
  128. orders.append(1) # mark the buy trade
  129. last_buy_price = close_price
  130. position += NUM_SHARES_PER_TRADE # increase position by the size of this trade
  131. buy_sum_qty += NUM_SHARES_PER_TRADE
  132. buy_sum_price_qty += (close_price*NUM_SHARES_PER_TRADE) # update the vwap buy-price
  133. print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
  134. print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
  135. else:
  136. # No trade since none of the conditions were met to buy or sell
  137. orders.append(0)
  138. positions.append(position)
  139. # This section updates Open/Unrealized & Closed/Realized positions
  140. open_pnl = 0
  141. if position > 0:
  142. # long position and some sell trades have been made against it,
  143. # close that amount based on how much was sold against this long position
  144. # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
  145. if sell_sum_qty > 0: # vwap for sell # vwap for buy
  146. open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
  147. # mark the remaining position to market
  148. # i.e. pnl would be what it would be if we closed at current price
  149. # sell
  150. # position -= NUM_SHARES_PER_TRADE
  151. # sell_sum_qty += NUM_SHARES_PER_TRADE
  152. # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
  153. # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
  154. open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
  155. # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
  156. elif position < 0:
  157. # short position and some buy trades have been made against it,
  158. # close that amount based on how much was bought against this short position
  159. # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
  160. if buy_sum_qty > 0: # vwap for sell # vwap for buy
  161. open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
  162. # mark the remaining position to market
  163. # i.e. pnl would be what it would be if we closed at current price
  164. # buy
  165. # position += NUM_SHARE_PER_TRADE
  166. # buy_sum_qty += NUM_SHARE_PER_TRADE
  167. # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
  168. # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) *
  169. open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
  170. # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
  171. else:
  172. # flat, so update closed_pnl and reset tracking variables for positions & pnls
  173. closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
  174. buy_sum_price_qty = 0
  175. buy_sum_qty = 0
  176. last_buy_price = 0
  177. sell_sum_price_qty = 0
  178. sell_sum_qty = 0
  179. last_sell_price = 0
  180. pnls.append(closed_pnl + open_pnl)

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 )

  1. # Plot correlations between TRADING_INSTRUMENT and other currency pairs
  2. fig = plt.figure( figsize=(20,10))
  3. correlation_data = pd.DataFrame()
  4. for symbol in SYMBOLS:
  5. if symbol == TRADING_INSTRUMENT:
  6. continue
  7. correlation_label = TRADING_INSTRUMENT + '<-' + symbol
  8. correlation_data = correlation_data.assign( label=pd.Series( correlation_history[correlation_label],
  9. index=symbols_data[symbol].index
  10. )
  11. )
  12. if symbol == 'JPYUSD=X':
  13. ax = correlation_data['label'].plot( color=next(cycol), lw=2., ls='dotted',
  14. label='Correlation '+ correlation_label )
  15. elif symbol == 'AUDUSD=X' or symbol == 'NZDUSD=X':
  16. ax = correlation_data['label'].plot( color=next(cycol), lw=5.,
  17. label='Correlation '+ correlation_label )
  18. else:
  19. ax = correlation_data['label'].plot( color=next(cycol), lw=2.,
  20. label='Correlation '+ correlation_label )
  21. for i in np.arange(-1,1, 0.25):
  22. plt.axhline( y=i, lw=0.5, color='k' )
  23. plt.legend()
  24. plt.autoscale(enable=True, axis='x', tight=True)
  25. 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

     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)

  1. # Plot StatArb signal provided by each currency pair
  2. from itertools import cycle
  3. cycol = cycle('bgrcmky')
  4. fig = plt.figure( figsize=(15,8))
  5. delta_projected_actual_data = pd.DataFrame()
  6. for symbol in SYMBOLS:
  7. if symbol == TRADING_INSTRUMENT:
  8. continue
  9. projection_label = TRADING_INSTRUMENT + '<-' + symbol
  10. delta_projected_actual_data = delta_projected_actual_data.assign(
  11. StatArbTradingSignal = pd.Series( delta_projected_actual_history[projection_label],
  12. index=symbols_data[TRADING_INSTRUMENT].index
  13. )
  14. )
  15. if symbol == 'JPYUSD=X' or symbol == 'CHFUSD=X':
  16. ax = delta_projected_actual_data['StatArbTradingSignal'].plot( color=next(cycol),
  17. lw=2., ls='dotted',
  18. label='StatArbTradingSignal'+projection_label
  19. )
  20. else:
  21. ax = delta_projected_actual_data['StatArbTradingSignal'].plot( color=next(cycol),
  22. lw=1.,
  23. label='StatArbTradingSignal'+projection_label
  24. )
  25. plt.legend()
  26. plt.autoscale(enable=True, axis='x', tight=True)
  27. 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:     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:

  1. delta_projected_actual_data = delta_projected_actual_data.assign(
  2. ClosePrice = pd.Series( symbols_data[TRADING_INSTRUMENT]['Close'],
  3. index=symbols_data[TRADING_INSTRUMENT].index
  4. )
  5. )
  6. delta_projected_actual_data = delta_projected_actual_data.assign(
  7. FinalStatArbTradingSignal = pd.Series( final_delta_projected_history,
  8. index=symbols_data[TRADING_INSTRUMENT].index
  9. )
  10. )
  11. delta_projected_actual_data = delta_projected_actual_data.assign(
  12. Trades = pd.Series( orders,
  13. index=symbols_data[TRADING_INSTRUMENT].index
  14. )
  15. )
  16. delta_projected_actual_data = delta_projected_actual_data.assign(
  17. Position = pd.Series( positions,
  18. index=symbols_data[TRADING_INSTRUMENT].index
  19. )
  20. )
  21. delta_projected_actual_data = delta_projected_actual_data.assign(
  22. Pnl = pd.Series( pnls,
  23. index=symbols_data[TRADING_INSTRUMENT].index
  24. )
  25. )
  26. fig = plt.figure( figsize=(15,8))
  27. plt.plot( delta_projected_actual_data.index,
  28. delta_projected_actual_data.ClosePrice,
  29. color='k', lw=1., label='ClosePrice')
  30. plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == 1].index,
  31. delta_projected_actual_data.ClosePrice[delta_projected_actual_data.Trades == 1],
  32. color='b', lw=0, marker='^', markersize=7, label='buy')
  33. plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == -1].index,
  34. delta_projected_actual_data.ClosePrice[delta_projected_actual_data.Trades == -1],
  35. color='y', lw=0, marker='v', markersize=7, label='sell'
  36. )
  37. plt.legend()
  38. 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:

     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:

  1. fig = plt.figure( figsize=(15,8))
  2. plt.plot( delta_projected_actual_data.index,
  3. delta_projected_actual_data.FinalStatArbTradingSignal,
  4. color='k', lw=1.,
  5. label='FinalStatArbTradingSignal'
  6. )
  7. plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == 1].index,
  8. delta_projected_actual_data.FinalStatArbTradingSignal[delta_projected_actual_data.Trades == 1],
  9. color='b', lw=0, marker='^', markersize=7, label='buy'
  10. )
  11. plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == -1].index,
  12. delta_projected_actual_data.FinalStatArbTradingSignal[delta_projected_actual_data.Trades == -1],
  13. color='y', lw=0, marker='v', markersize=7, label='sell'
  14. )
  15. plt.axhline(y=0, lw=0.5, color='k')
  16. for i in np.arange( StatArb_VALUE_FOR_BUY_ENTRY, StatArb_VALUE_FOR_BUY_ENTRY * 10, StatArb_VALUE_FOR_BUY_ENTRY * 2 ):
  17. plt.axhline(y=i, lw=0.5, color='r')
  18. for i in np.arange( StatArb_VALUE_FOR_SELL_ENTRY, StatArb_VALUE_FOR_SELL_ENTRY * 10, StatArb_VALUE_FOR_SELL_ENTRY * 2 ):
  19. plt.axhline(y=i, lw=0.5, color='g')
  20. plt.autoscale(enable=True, axis='x', tight=True)
  21. plt.legend()
  22. 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: 

      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:

  1. fig = plt.figure( figsize=(15,8))
  2. plt.plot( delta_projected_actual_data.index,
  3. delta_projected_actual_data.Position,
  4. color='k', lw=1.,
  5. label='Position'
  6. )
  7. plt.plot( delta_projected_actual_data.loc[ delta_projected_actual_data.Position == 0 ].index,
  8. delta_projected_actual_data.Position[delta_projected_actual_data.Position == 0],
  9. color='r', lw=0, marker='.',
  10. label='flat'
  11. )
  12. plt.plot( delta_projected_actual_data.loc[ delta_projected_actual_data.Position > 0 ].index,
  13. delta_projected_actual_data.Position[delta_projected_actual_data.Position > 0],
  14. color='b', lw=0, marker='+',
  15. label='long'
  16. )
  17. plt.plot( delta_projected_actual_data.loc[ delta_projected_actual_data.Position < 0 ].index,
  18. delta_projected_actual_data.Position[delta_projected_actual_data.Position < 0],
  19. color='y', lw=0, marker='x',
  20. label='short'
  21. )
  22. plt.axhline(y=0, lw=0.5, color='k')
  23. for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE * 5, NUM_SHARES_PER_TRADE ):
  24. plt.axhline(y=i, lw=0.5, color='b')
  25. for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE * 5, -NUM_SHARES_PER_TRADE ):
  26. plt.axhline(y=i, lw=0.5, color='g')
  27. plt.autoscale(enable=True, axis='x', tight=True)
  28. plt.legend()
  29. plt.show()

     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:

  1. fig = plt.figure( figsize=(15,8))
  2. plt.plot( delta_projected_actual_data.index,
  3. delta_projected_actual_data.Pnl,
  4. color='k', lw=1.,
  5. label='PnL'
  6. )
  7. plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Pnl > 0].index,
  8. delta_projected_actual_data.Pnl[delta_projected_actual_data.Pnl > 0],
  9. color='b', lw=0, marker='.',
  10. label='+PnL'
  11. )
  12. plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Pnl < 0].index,
  13. delta_projected_actual_data.Pnl[delta_projected_actual_data.Pnl < 0],
  14. color='y', lw=0, marker='.',
  15. label='-PnL'
  16. )
  17. plt.autoscale(enable=True, axis='x', tight=True)
  18. plt.legend()
  19. 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

     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.

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/250998
推荐阅读
相关标签
  

闽ICP备14008679号