赞
踩
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:
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.
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.
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,
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
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.
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).
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
Another area of improvement would be using changing volatility to
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.
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.
^^^^^^^^^^^^^^^^^^^^^^^
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.
Positions are closed when the APO signal value changes sign, that is,
In addition, positions are also closed if currently open positions are profitable above a certain amount, regardless of APO values. This is used to algorithmically lock profits and initiate more positions instead of relying only on the trading signal value. Now, let's look at the implementation in the next few sections:
1. We will fetch data the same way we have done in the past. Let's fetch 4 years of GOOG data. This code will use the DataReader function from the pandas_datareader package. This function will fetch the GOOG prices from Yahoo Finance between 2014-01-2014 and 2018-01-01 . If the . pkl file used to store the data on the disk is not present, the GOOG_data.pkl file will be created.
By doing that, we ensure that we will use the file to fetch the GOOG data for future use:
- import pandas as pd
- import pandas_datareader.data as pdr
-
- def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
- if len(output_file) == 0:
- output_file = stock_symbol+'_data_large.pkl'
-
- try:
- df = pd.read_pickle( output_file )
- print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
- except FileNotFoundError:
- print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
- df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
- df.to_pickle( output_file )
- return df
-
- goog_data = load_financial_data( stock_symbol='GOOG',
- start_date='2014-01-01',
- end_date='2018-01-01',
- output_file='goog_data.pkl'
- )
- goog_data.head()
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;
OR
Alternatively, we have the following:
Here, the following applies:
P : Current price of the instrument
: EMA value prior to the current price observation
(OR K): Smoothing constant, most commonly set to
n : Number of time periods (similar to what we used in the simple moving average)
- # Variables/constants for EMA Calculation:
- NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
- K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
- ema_fast = 0 # initial ema
- ema_fast_values = [] # we will hold fast EMA values for visualization purpose
-
- NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
- K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
- ema_slow = 0 # initial ema
- ema_slow_values = [] # we will hold slow EMA values for visualization purpose
-
- apo_values = [] # track computed absolute price oscillator values
清仓、就是卖光手中的股票, 平仓:卖出股票称平仓;股民将自己手中所持有的股票按照当前交易价格全部卖出,从而退出股票交易市场,又称为清仓 至于股票平仓方法有四个,分别是定点了结法,这是必不可少的股票平仓方法;分批了结法,就是不一次卖光手中股票,分批酌量卖出。止损了结法,这种股票平仓方法是将所持股票在股价回落到一定点时了结,以停止损失。卖利了结法,是指把本金和利润区别对待 加仓、在已经持有的股票上加码买进,持有更多的该股票, 建仓、就是开始买入股票。 半仓、就是持有的股票金额占自己资产的一半。资金的一半买入 减仓、就是降低仓位。卖出一部分 满仓、就是所有的钱都买了股票。 持仓、就是持有暂时不卖出。持有股票 守仓、就是持有不卖出。 底仓、就是某股票即使进行卖出操作,也留有一定得仓位 空仓、就是不持有股票。 斩仓、就是割肉,亏损卖出股票。 补仓,在下跌过程中买入,被套后在原有仓位的基础上,再买入该股,以摊薄成本。 倒仓,日内T+0交易。买入手中已持有的股票,再在当天上涨后卖出原来的股数,以获得收益,摊低成本。
3. We will also need variables that define/control strategy trading behavior and position and PnL management:
- # Variables for Trading Strategy trade, position & pnl management:
-
- # Container for tracking buy/sell order,
- # +1 for buy order, -1 for sell order, 0 for no-action
- orders = []
-
- # Container for tracking positions,
- # positive for long positions, negative for short positions, 0 for flat/no position
- positions = []
-
- # Container for tracking total_pnls, this is the sum of
- # closed_pnl i.e. pnls already locked in
- # and open_pnl i.e. pnls for open-position marked to market price
- pnls = []
-
-
- last_buy_price = 0 # used to prevent over-trading at/around the same price
- last_sell_price = 0 # used to prevent over-trading at/around the same price
- position = 0 # Current position of the trading strategy
-
- # Summation of products of
- # buy_trade_price and buy_trade_qty for every buy Trade made
- # since last time being flat
- buy_sum_price_qty = 0
- # Summation of buy_trade_qty for every buy Trade made since last time being flat
- buy_sum_qty = 0
-
- # Summation of products of
- # sell_trade_price and sell_trade_qty for every sell Trade made
- # since last time being flat
- sell_sum_price_qty = 0
- # Summation of sell_trade_qty for every sell Trade made since last time being flat
- sell_sum_qty = 0
-
- open_pnl = 0 # Open/Unrealized PnL marked to market
- closed_pnl = 0 # Closed/Realized PnL so far
4. Finally, we clearly define
- # Constants that define strategy behavior/thresholds
-
- # APO trading signal value below which(-10) to enter buy-orders/long-position
- APO_VALUE_FOR_BUY_ENTRY = -10 # (oversold, expect a bounce back up)
- # APO trading signal value above which to enter sell-orders/short-position
- APO_VALUE_FOR_SELL_ENTRY = 10 # (overbought, expect a bounce back down)
-
- # Minimum price change since last trade before considering trading again,
- MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
- NUM_SHARES_PER_TRADE = 10
-
- # positions are closed if currently open positions are profitable above a certain amount,
- # regardless of APO values.
- # This is used to algorithmically lock profits and initiate more positions
- # instead of relying only on the trading signal value.
-
- # Minimum Open/Unrealized profit at which to close positions and lock profits
- MIN_PROFIT_TO_CLOSE = 10*NUM_SHARES_PER_TRADE
5. Now, let's look at the main section of the trading strategy, which has logic for the following:
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:
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.
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
7. We will perform a buy trade at close_price if the following conditions are met:
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 :
#########################
Assume the initial fill download includes the following fills (all prices in points):
INITIAL P&L CALCULATIONS
From these fills, TT FIX Adapter calculates the following base values:
To determine the realized P&L, TT FIX Adapter matches thirteen Buys with thirteen Sells using the Averaging technique, as follows:
which results in the following starting state after the initial fill download:
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:
With these values, you can calculate the total P&L after the initial fill download as follows:
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:
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):
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):
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):
#########################
- close = goog_data['Close']
- for close_price in close:
- # This section updates fast and slow EMA and computes APO trading signal
- if (ema_fast == 0): # first observation
- ema_fast = close_price
- ema_slow = close_price
- else:
- ema_fast = (close_price - ema_fast)*K_FAST + ema_fast # K_FAST = 2/(NUM_PERIODS_FAST_10 + 1)
- ema_slow = (close_price - ema_slow)*K_SLOW + ema_slow # K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1)
-
- ema_fast_values.append(ema_fast)
- ema_slow_values.append(ema_slow)
-
- apo = ema_fast-ema_slow
- apo_values.append(apo)
-
- # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
-
- # We will perform a sell trade at close_price if the following conditions are met:
- # 1. The APO trading signal value(positive) > Sell-Entry threshold (overbought, expect a bounce back down, sell for profit)
- # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
- # 2. We are long( +ve position ) and
- # either APO trading signal value >= 0 or current position is profitable enough to lock profit.
- if ( ( apo > APO_VALUE_FOR_SELL_ENTRY and \
- abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
- )
- or
- ( position>0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
- ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
- orders.append(-1) # mark the sell trade
- last_sell_price = close_price
- position -= NUM_SHARES_PER_TRADE
- sell_sum_qty += NUM_SHARES_PER_TRADE
- sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
- print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
-
- # 7. We will perform a buy trade at close_price if the following conditions are met:
- # 1. The APO trading signal value(negative) < below Buy-Entry threshold (oversold, expect a bounce back up, buy for future profit)
- # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
- # 2. We are short( -ve position ) and
- # either APO trading signal value is <= 0 or current position is profitable enough to lock profit.
- elif ( ( apo < APO_VALUE_FOR_BUY_ENTRY and \
- abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
- )
- or
- ( position<0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
- ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
- orders.append(+1) # mark the buy trade
- last_buy_price = close_price
- position += NUM_SHARES_PER_TRADE
- buy_sum_qty += NUM_SHARES_PER_TRADE
- buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
- print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
- else:
- # No trade since none of the conditions were met to buy or sell
- orders.append( 0 )
-
- positions.append( position )
-
- # 8. The code of the trading strategy contains logic for position/PnL management.
- # It needs to update positions and compute open and closed PnLs when market prices change
- # and/or trades are made causing a change in positions
-
- # This section updates Open/Unrealized & Closed/Realized positions
- open_pnl = 0
- if position > 0:
- # long position and some sell trades have been made against it,
- # close that amount based on how much was sold against this long position
- # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
- if sell_sum_qty > 0: # vwap for sell # vwap for buy
- open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
- # mark the remaining position to market
- # i.e. pnl would be what it would be if we closed at current price
- # sell
- # position -= NUM_SHARES_PER_TRADE
- # sell_sum_qty += NUM_SHARES_PER_TRADE
- # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
- # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
- open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
- elif position < 0:
- # short position and some buy trades have been made against it,
- # close that amount based on how much was bought against this short position
- # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
- if buy_sum_qty > 0: # vwap for sell # vwap for buy
- open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
- # mark the remaining position to market
- # i.e. pnl would be what it would be if we closed at current price
- # buy
- # position += NUM_SHARE_PER_TRADE
- # buy_sum_qty += NUM_SHARE_PER_TRADE
- # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
- # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) *
- open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
- else:
- # flat, so update closed_pnl and reset tracking variables for positions & pnls
- closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
-
- buy_sum_price_qty = 0
- buy_sum_qty = 0
- last_buy_price = 0
-
- sell_sum_price_qty = 0
- sell_sum_qty = 0
- last_sell_price = 0
-
- print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
- pnls.append(closed_pnl + open_pnl)
... ...
9. Now we look at some Python/Matplotlib code to see how to gather the relevant results of the trading strategy such as market prices, Fast and Slow EMA values, APO values, Buy and Sell trades, Positions and PnLs achieved by the strategy over its lifetime and then plot them in a manner that gives us insight into the strategy's behavior:
- data = goog_data.copy()
- # This section prepares the dataframe from the trading strategy results and visualizes the results
- data = data.assign( ClosePrice = pd.Series(close, index=data.index) )
- data = data.assign( Fast10DayEMA = pd.Series(ema_fast_values, index=data.index) )
- data = data.assign( Slow40DayEMA = pd.Series(ema_slow_values, index=data.index) )
- data = data.assign( APO = pd.Series(apo_values, index=data.index) )
- data = data.assign( Trades = pd.Series(orders, index=data.index) )
- data = data.assign( Position = pd.Series(positions, index=data.index) )
- data = data.assign( Pnl = pd.Series(pnls, index=data.index) )
data.head(20)
10. Now we will add columns to the data frame with different series that we computed in the previous sections, first the Market Price and then the fast and slow EMA values. We will also have another plot for the APO trading signal value. In both plots, we will overlay buy and sell trades so we can understand when the strategy enters and exits positions:
- import matplotlib.pyplot as plt
-
- fig = plt.figure( figsize=(20,10))
-
- data['ClosePrice'].plot(color='k', lw=3., legend=True)
- data['Fast10DayEMA'].plot(color='y', lw=1., legend=True)
- data['Slow40DayEMA'].plot(color='m', lw=1., legend=True)
- plt.plot( data.loc[ data.Trades == 1 ].index, data.ClosePrice[data.Trades == 1 ],
- color='y', lw=0, marker='^', markersize=7, label='buy'
- )
- plt.plot( data.loc[ data.Trades == -1 ].index, data.ClosePrice[data.Trades == -1 ],
- color='b', lw=0, marker='v', markersize=7, label='sell'
- )
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
Let's take a look at what our trading behavior looks like, paying attention to the EMA and APO values when the trades are made. When we look at the positions and PnL plots, this will become completely clear:
In the plot, we can see where the buy and sell trades were made as the price of the Google stock change over the last 4 years, but now, let's look at what the APO trading signal values where the buy trades were made and sell trades were made. According to the design of these trading strategies, we expect sell trades when APO values are positive and expect buy trades when APO values are negative:
- fig = plt.figure( figsize=(20,10) )
-
- data['APO'].plot(color='k', lw=3., legend=True)
- plt.plot( data.loc[ data.Trades == 1 ].index, data.APO[data.Trades == 1 ],
- color='y', lw=0, marker='^', markersize=7, label='buy'
- )
- plt.plot( data.loc[ data.Trades == -1 ].index, data.APO[data.Trades == -1 ],
- color='b', lw=0, marker='v', markersize=7, label='sell'
- )
- plt.axhline(y=0, lw=0.5, color='k')
- for i in range( APO_VALUE_FOR_BUY_ENTRY, APO_VALUE_FOR_BUY_ENTRY*5, APO_VALUE_FOR_BUY_ENTRY ):
- plt.axhline(y=i, lw=0.5, color='r')
- for i in range( APO_VALUE_FOR_SELL_ENTRY, APO_VALUE_FOR_SELL_ENTRY*5, APO_VALUE_FOR_SELL_ENTRY ):
- plt.axhline(y=i, lw=0.5, color='g')
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
-
- plt.show()
In the plot, we can see that a lot of sell trades are executed when APO trading signal values are positive and a lot of buy trades are executed when APO trading signal values are negative. We also observe that some buy trades are executed when APO trading signal values are positive and some sell trades are executed when APO trading signal values are negative. How do we explain that?
11. As we will see in the following code, those trades are the ones executed to close profits. Let's observe the position and PnL evolution over the lifetime of this strategy:
- fig = plt.figure( figsize=(20,10))
-
- data['Position'].plot(color='k', lw=1., legend=True)
- plt.plot( data.loc[ data.Position == 0 ].index, data.Position[ data.Position == 0 ],
- color='r', lw=0, marker='.', label='flat'
- )
- plt.plot( data.loc[ data.Position > 0 ].index, data.Position[ data.Position > 0 ],
- color='y', lw=0, marker='+', label='long'
- )
- plt.plot( data.loc[ data.Position < 0 ].index, data.Position[ data.Position < 0 ],
- color='b', lw=0, marker='_', label='short'
- )
- plt.axhline(y=0, lw=0.5, color='k')
- for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE*25, NUM_SHARES_PER_TRADE*5 ):
- plt.axhline(y=i, lw=0.5, color='r')
- for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE*25, -NUM_SHARES_PER_TRADE*5 ):
- plt.axhline(y=i, lw=0.5, color='g')
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
From the position plot, we can see some large short positions around 2016-01, then again in 2017-07, and finally again in 2018-01. If we go back to the APO trading signal values, that is when APO values went through large patches of positive values.
Finally, let's look at how the PnL evolves for this trading strategy over the course of the stock's life cycle:
- fig = plt.figure( figsize=(20,10))
-
- data['Pnl'].plot(color='k', lw=1., legend=True)
- plt.plot( data.loc[ data.Pnl > 0 ].index, data.Pnl[ data.Pnl > 0 ],
- color='b', lw=0, marker='.',
- label='Pnl'
- )
- plt.plot( data.loc[ data.Pnl < 0 ].index, data.Pnl[ data.Pnl < 0 ],
- color='y', lw=0, marker='.',
- label='Pnl'
- )
- plt.axhline(y=15000, ls='--', alpha=0.5)
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
-
- data.to_csv("basic_mean_reversion.csv", sep=",")
The basic mean reversion strategy makes money pretty consistently over the course of time, with some volatility in returns during 2016-01 and 2017-07, where the strategy has large positions, but finally ending around $15K, which is close to its maximum achieved PnL.
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
The way in which we incorporate STDEV into our strategy is through the following changes:
- import pandas as pd
- import pandas_datareader.data as pdr
-
- def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
- if len(output_file) == 0:
- output_file = stock_symbol+'_data_large.pkl'
-
- try:
- df = pd.read_pickle( output_file )
- print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
- except FileNotFoundError:
- print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
- df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
- df.to_pickle( output_file )
- return df
-
- goog_data = load_financial_data( stock_symbol='GOOG',
- start_date='2014-01-01',
- end_date='2018-01-01',
- output_file='goog_data.pkl'
- )
- goog_data.head()
- # Variables/constants for EMA Calculation:
- NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
- K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
- ema_fast = 0 # initial ema
- ema_fast_values = [] # we will hold fast EMA values for visualization purpose
-
- NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
- K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
- ema_slow = 0 # initial ema
- ema_slow_values = [] # we will hold slow EMA values for visualization purpose
-
- apo_values = [] # track computed absolute price oscillator values
-
- # Variables for Trading Strategy trade, position & pnl management:
-
- # Container for tracking buy/sell order,
- # +1 for buy order, -1 for sell order, 0 for no-action
- orders = []
-
- # Container for tracking positions,
- # positive for long positions, negative for short positions, 0 for flat/no position
- positions = []
-
- # Container for tracking total_pnls, this is the sum of
- # closed_pnl i.e. pnls already locked in
- # and open_pnl i.e. pnls for open-position marked to market price
- pnls = []
-
-
- last_buy_price = 0 # used to prevent over-trading at/around the same price
- last_sell_price = 0 # used to prevent over-trading at/around the same price
- position = 0 # Current position of the trading strategy
-
- # Summation of products of
- # buy_trade_price and buy_trade_qty for every buy Trade made
- # since last time being flat
- buy_sum_price_qty = 0
- # Summation of buy_trade_qty for every buy Trade made since last time being flat
- buy_sum_qty = 0
-
- # Summation of products of
- # sell_trade_price and sell_trade_qty for every sell Trade made
- # since last time being flat
- sell_sum_price_qty = 0
- # Summation of sell_trade_qty for every sell Trade made since last time being flat
- sell_sum_qty = 0
-
- open_pnl = 0 # Open/Unrealized PnL marked to market
- closed_pnl = 0 # Closed/Realized PnL so far
-
- # Constants that define strategy behavior/thresholds
-
- # APO trading signal value below which(-10) to enter buy-orders/long-position
- APO_VALUE_FOR_BUY_ENTRY = -10 # (oversold, expect a bounce back up)
- # APO trading signal value above which to enter sell-orders/short-position
- APO_VALUE_FOR_SELL_ENTRY = 10 # (overbought, expect a bounce back down)
-
- # Minimum price change since last trade before considering trading again,
- MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
- NUM_SHARES_PER_TRADE = 10
-
- # positions are closed if currently open positions are profitable above a certain amount,
- # regardless of APO values.
- # This is used to algorithmically lock profits and initiate more positions
- # instead of relying only on the trading signal value.
-
- # Minimum Open/Unrealized profit at which to close positions and lock profits
- MIN_PROFIT_TO_CLOSE = 10*NUM_SHARES_PER_TRADE
Let's look at the modifications needed to the basic mean reversion strategy to achieve this. First, we need some code to track and update the volatility measure (STDEV):
- import statistics as stats
- import math as math
-
- data2 = goog_data.copy()
- close = data2['Close']
-
- # Constants/variables that are used to compute standard deviation as a volatility measure
- SMA_NUM_PERIODS_20 = 20 # look back period
- price_history = [] # history of prices
-
- for close_price in close:
- price_history.append( close_price )
- if len( price_history) > SMA_NUM_PERIODS_20 : # we track at most 'time_period' number of prices
- del ( price_history[0] )
-
- # calculate vairance during the SMA_NUM_PERIODS_20 periods
- sma = stats.mean( price_history )
- variance = 0 # variance is square of standard deviation
- for hist_price in price_history:
- variance = variance + ( (hist_price-sma)**2 )
-
- stddev = math.sqrt( variance/len(price_history) )
-
- # a volatility factor that ranges from 0 to 1
- stddev_factor = stddev/15 # 15 since since the population stddev.mean() = 15.45
- # closer to 0 indicate very low volatility,
- # around 1 indicate normal volatility
- # > 1 indicate above-normal volatility
- if stddev_factor == 0:
- stddev_factor = 1
-
- # This section updates fast and slow EMA and computes APO trading signal
- if (ema_fast==0): # first observation
- ema_fast = close_price # initial ema_fast or ema_slow
- ema_slow = close_price
- else:
- # ema fomula
- # K_FAST*stddev_factor or K_SLOW*stddev_factor
- # more reactive to newest observations during periods of higher than normal volatility
- ema_fast = (close_price-ema_fast) * K_FAST*stddev_factor + ema_fast
- ema_slow = (close_price-ema_slow) * K_SLOW*stddev_factor + ema_slow
-
- ema_fast_values.append( ema_fast )
- ema_slow_values.append( ema_slow )
-
- apo = ema_fast - ema_slow
- apo_values.append( apo )
-
- # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
-
- # We will perform a sell trade at close_price if the following conditions are met:
- # 1. The APO trading signal value(positive) > Sell-Entry threshold (overbought, expect a bounce back down, sell for profit)
- # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
- # 2. We are long( +ve position ) and
- # either APO trading signal value >= 0 or current position is profitable enough to lock profit.
- # APO_VALUE_FOR_SELL_ENTRY * stdev_factor:
- # by increasing the threshold for entry by a factor of volatility,
- # makes us less aggressive in entering positions(here is sell) during periods of higher volatility,
- # dynamic MIN_PROFIT_TO_CLOSE / stddev_factor:
- # to decrease the the expected profit threshold during periods of increased volatility
- # to be more aggressive in exciting positions
- # it is riskier to hold on to positions for longer periods of time.
- if ( ( apo > APO_VALUE_FOR_SELL_ENTRY*stddev_factor and \
- abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE*stddev_factor
- )
- or
- ( position>0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE/stddev_factor ) )
- ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
- orders.append(-1) # mark the sell trade
- last_sell_price = close_price
- position -= NUM_SHARES_PER_TRADE
- sell_sum_qty += NUM_SHARES_PER_TRADE
- sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
- print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
-
- # 7. We will perform a buy trade at close_price if the following conditions are met:
- # 1. The APO trading signal value(negative) < below Buy-Entry threshold (oversold, expect a bounce back up, buy for future profit)
- # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
- # 2. We are short( -ve position ) and
- # either APO trading signal value is <= 0 or current position is profitable enough to lock profit.
- # APO_VALUE_FOR_BUY_ENTRY * stdev_factor:
- # by increasing the threshold for entry by a factor of volatility,
- # makes us less aggressive in entering positions(here is sell) during periods of higher volatility,
- # dynamic MIN_PROFIT_TO_CLOSE / stddev_factor:
- # to decrease the the expected profit threshold during periods of increased volatility
- # to be more aggressive in exciting positions
- # it is riskier to hold on to positions for longer periods of time.
- elif ( ( apo < APO_VALUE_FOR_BUY_ENTRY*stddev_factor and \
- abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE*stddev_factor
- )
- or
- ( position<0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE/stddev_factor ) )
- ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
- orders.append(+1) # mark the buy trade
- last_buy_price = close_price
- position += NUM_SHARES_PER_TRADE
- buy_sum_qty += NUM_SHARES_PER_TRADE
- buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
- print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
- else:
- # No trade since none of the conditions were met to buy or sell
- orders.append( 0 )
-
- positions.append( position )
-
- # 8. The code of the trading strategy contains logic for position/PnL management.
- # It needs to update positions and compute open and closed PnLs when market prices change
- # and/or trades are made causing a change in positions
-
- # This section updates Open/Unrealized & Closed/Realized positions
- open_pnl = 0
- if position > 0:
- # long position and some sell trades have been made against it,
- # close that amount based on how much was sold against this long position
- # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
- if sell_sum_qty > 0: # vwap for sell # vwap for buy
- open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
- # mark the remaining position to market
- # i.e. pnl would be what it would be if we closed at current price
- # sell
- # position -= NUM_SHARES_PER_TRADE
- # sell_sum_qty += NUM_SHARES_PER_TRADE
- # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
- # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
- open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
- # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
- elif position < 0:
- # short position and some buy trades have been made against it,
- # close that amount based on how much was bought against this short position
- # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
- if buy_sum_qty > 0: # vwap for sell # vwap for buy
- open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
- # mark the remaining position to market
- # i.e. pnl would be what it would be if we closed at current price
- # buy
- # position += NUM_SHARE_PER_TRADE
- # buy_sum_qty += NUM_SHARE_PER_TRADE
- # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
- # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) *
- open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
- # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
- else:
- # flat, so update closed_pnl and reset tracking variables for positions & pnls
- closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
-
- buy_sum_price_qty = 0
- buy_sum_qty = 0
- last_buy_price = 0
-
- sell_sum_price_qty = 0
- sell_sum_qty = 0
- last_sell_price = 0
-
- print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
- pnls.append(closed_pnl + open_pnl)
- # This section prepares the dataframe from the trading strategy results and visualizes the results
- data2 = data2.assign( ClosePrice = pd.Series(close, index=data2.index) )
- data2 = data2.assign( Fast10DayEMA = pd.Series(ema_fast_values, index=data2.index) )
- data2 = data2.assign( Slow40DayEMA = pd.Series(ema_slow_values, index=data2.index) )
- data2 = data2.assign( APO = pd.Series(apo_values, index=data2.index) )
- data2 = data2.assign( Trades = pd.Series(orders, index=data2.index) )
- data2 = data2.assign( Position = pd.Series(positions, index=data2.index) )
- data2 = data2.assign( Pnl = pd.Series(pnls, index=data2.index) )
- import matplotlib.pyplot as plt
-
- fig = plt.figure( figsize=(20,10) )
-
- data2['ClosePrice'].plot(color='k', lw=3., legend=True)
- data2['Fast10DayEMA'].plot(color='y', lw=1., legend=True)
- data2['Slow40DayEMA'].plot(color='m', lw=1., legend=True)
- plt.plot( data2.loc[ data2.Trades == 1 ].index, data2.ClosePrice[data2.Trades == 1 ],
- color='y', lw=0, marker='^', markersize=7, label='buy'
- )
- plt.plot( data2.loc[ data2.Trades == -1 ].index, data2.ClosePrice[data2.Trades == -1 ],
- color='b', lw=0, marker='v', markersize=7, label='sell'
- )
- plt.autoscale(enable=True, axis='x', tight=True)
-
- plt.legend()
- plt.show()
more aggressive in exciting positions during periods of increased volatility(for example, VS), because as we discussed before, during periods of higher than normal volatility, it is riskier to hold on to positions for longer periods of time.
- fig = plt.figure( figsize=(20,10) )
-
- data2['APO'].plot(color='k', lw=3., legend=True)
- plt.plot( data2.loc[ data2.Trades == 1 ].index, data2.APO[data2.Trades == 1 ],
- color='y', lw=0, marker='^', markersize=7, label='buy'
- )
- plt.plot( data2.loc[ data2.Trades == -1 ].index, data2.APO[data2.Trades == -1 ],
- color='b', lw=0, marker='v', markersize=7, label='sell'
- )
- plt.axhline(y=0, lw=0.5, color='k')
- for i in range( APO_VALUE_FOR_BUY_ENTRY, APO_VALUE_FOR_BUY_ENTRY*5, APO_VALUE_FOR_BUY_ENTRY ):
- plt.axhline(y=i, lw=0.5, color='r')
- for i in range( APO_VALUE_FOR_SELL_ENTRY, APO_VALUE_FOR_SELL_ENTRY*5, APO_VALUE_FOR_SELL_ENTRY ):
- plt.axhline(y=i, lw=0.5, color='g')
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
-
- plt.show()
- fig = plt.figure( figsize=(20,10))
-
- data2['Position'].plot(color='k', lw=1., legend=True)
- plt.plot( data2.loc[ data2.Position == 0 ].index, data2.Position[ data2.Position == 0 ],
- color='r', lw=0, marker='.', label='flat'
- )
- plt.plot( data2.loc[ data2.Position > 0 ].index, data2.Position[ data2.Position > 0 ],
- color='y', lw=0, marker='+', label='long'
- )
- plt.plot( data2.loc[ data2.Position < 0 ].index, data2.Position[ data2.Position < 0 ],
- color='b', lw=0, marker='_', label='short'
- )
- plt.axhline(y=0, lw=0.5, color='k')
- for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE*25, NUM_SHARES_PER_TRADE*5 ):
- plt.axhline(y=i, lw=0.5, color='r')
- for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE*25, -NUM_SHARES_PER_TRADE*5 ):
- plt.axhline(y=i, lw=0.5, color='g')
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
- fig = plt.figure( figsize=(20,10))
-
- plt.plot( data.index, data['Pnl'], color='g', lw=1.,
- label='BasicMeanReversionPnL'
- )#########################
- plt.plot( data.loc[ data.Pnl > 0 ].index, data.Pnl[ data.Pnl > 0 ],
- color='y', lw=0, marker='.',
- #label='Pnl'
- )
- plt.plot( data.loc[ data.Pnl < 0 ].index, data.Pnl[ data.Pnl < 0 ],
- color='r', lw=0, marker='.',
- #label='Pnl'
- )
-
- plt.plot( data2.index, data2['Pnl'], color='b', lw=1.,
- label='VolatilityAdjustedMeanReversionPnL'
- )#########################
- plt.plot( data2.loc[ data.Pnl > 0 ].index, data2.Pnl[ data.Pnl > 0 ],
- color='y', lw=0, marker='.',
- #label='Pnl'
- )
- plt.plot( data2.loc[ data.Pnl < 0 ].index, data2.Pnl[ data.Pnl < 0 ],
- color='r', lw=0, marker='.',
- #label='Pnl'
- )
- plt.axhline(y=15000, ls='--', alpha=0.5)
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
In this case, adjusting the trading strategy for volatility increases the strategy performance by 200%!
- import pandas as pd
- import pandas_datareader.data as pdr
-
- def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
- if len(output_file) == 0:
- output_file = stock_symbol+'_data_large.pkl'
-
- try:
- df = pd.read_pickle( output_file )
- print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
- except FileNotFoundError:
- print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
- df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
- df.to_pickle( output_file )
- return df
-
- goog_data = load_financial_data( stock_symbol='GOOG',
- start_date='2014-01-01',
- end_date='2018-01-01',
- output_file='goog_data.pkl'
- )
- goog_data.head()
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
In effect, this is the exact opposite trading strategy with some differences in position management. One might expect this trading strategy to be exactly opposite in performance but, as we will see, that is not the case, that is, both trend-following and mean reversion strategies can be profitable in the same market conditions:
- # Variables/constants for EMA Calculation:
- NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
- K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
- ema_fast = 0 # initial ema
- ema_fast_values = [] # we will hold fast EMA values for visualization purpose
-
- NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
- K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
- ema_slow = 0 # initial ema
- ema_slow_values = [] # we will hold slow EMA values for visualization purpose
-
- apo_values = [] # track computed absolute price oscillator values
-
- # Variables for Trading Strategy trade, position & pnl management:
-
- # Container for tracking buy/sell order,
- # +1 for buy order, -1 for sell order, 0 for no-action
- orders = []
-
- # Container for tracking positions,
- # positive for long positions, negative for short positions, 0 for flat/no position
- positions = []
-
- # Container for tracking total_pnls, this is the sum of
- # closed_pnl i.e. pnls already locked in
- # and open_pnl i.e. pnls for open-position marked to market price
- pnls = []
-
- last_buy_price = 0 # used to prevent over-trading at/around the same price
- last_sell_price = 0 # used to prevent over-trading at/around the same price
- position = 0 # Current position of the trading strategy
-
- # Summation of products of
- # buy_trade_price and buy_trade_qty for every buy Trade made
- # since last time being flat
- buy_sum_price_qty = 0
- # Summation of buy_trade_qty for every buy Trade made since last time being flat
- buy_sum_qty = 0
-
- # Summation of products of
- # sell_trade_price and sell_trade_qty for every sell Trade made
- # since last time being flat
- sell_sum_price_qty = 0
- # Summation of sell_trade_qty for every sell Trade made since last time being flat
- sell_sum_qty = 0
-
- open_pnl = 0 # Open/Unrealized PnL marked to market
- closed_pnl = 0 # Closed/Realized PnL so far
-
- # Constants that define strategy behavior/thresholds
-
- ################################
- # APO trading signal value below which(-10) to enter buy-orders/long-position
- APO_VALUE_FOR_BUY_ENTRY = 10 # (oversold, expect a bounce back up; >10 continue going up for trending)
- # APO trading signal value above which(-10) to enter sell-orders/short-position
- APO_VALUE_FOR_SELL_ENTRY = -10 # (overbought, expect a bounce back down; <-10 continue going down for trending)
- ################################
-
- # Minimum price change since last trade before considering trading again,
- MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
- NUM_SHARES_PER_TRADE = 10
-
- # positions are closed if currently open positions are profitable above a certain amount,
- # regardless of APO values.
- # This is used to algorithmically lock profits and initiate more positions
- # instead of relying only on the trading signal value.
-
- # Minimum Open/Unrealized profit at which to close positions and lock profits
- MIN_PROFIT_TO_CLOSE = 10*NUM_SHARES_PER_TRADE
- import statistics as stats
- import math as math
-
- data3 = goog_data.copy()
- close = data3['Close']
-
- for close_price in close:
- # This section updates fast and slow EMA and computes APO trading signal
- if (ema_fast == 0): # first observation
- ema_fast = close_price
- ema_slow = close_price
- else:
- ema_fast = (close_price - ema_fast)*K_FAST + ema_fast # K_FAST = 2/(NUM_PERIODS_FAST_10 + 1)
- ema_slow = (close_price - ema_slow)*K_SLOW + ema_slow # K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1)
-
- ema_fast_values.append(ema_fast)
- ema_slow_values.append(ema_slow)
-
- apo = ema_fast-ema_slow
- apo_values.append(apo)
-
- # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
-
- # We will perform a sell trade at close_price if the following conditions are met:
- # 1. The APO trading signal value(negative) < Sell-Entry threshold (expecting price moves to continue going down)
- # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
- # 2. We are long( +ve position ) and
- # either APO trading signal value >= 0 or current position is profitable enough to lock profit.
- ###
- if ( ( apo < APO_VALUE_FOR_SELL_ENTRY and \
- abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
- )
- or ###
- ( position>0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
- ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
- orders.append(-1) # mark the sell trade
- last_sell_price = close_price
- position -= NUM_SHARES_PER_TRADE
- sell_sum_qty += NUM_SHARES_PER_TRADE
- sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
- print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
-
- # 7. We will perform a buy trade at close_price if the following conditions are met:
- # 1. The APO trading signal value(positive) > Buy-Entry threshold (expecting price moves to continue going up)
- # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
- # 2. We are short( -ve position ) and
- # either APO trading signal value is >= 0 or current position is profitable enough to lock profit.
- ###
- elif ( ( apo > APO_VALUE_FOR_BUY_ENTRY and \
- abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
- )
- or ###
- ( position<0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
- ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
- orders.append(+1) # mark the buy trade
- last_buy_price = close_price
- position += NUM_SHARES_PER_TRADE
- buy_sum_qty += NUM_SHARES_PER_TRADE
- buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
- print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
- else:
- # No trade since none of the conditions were met to buy or sell
- orders.append( 0 )
-
- positions.append( position )
-
- # 8. The code of the trading strategy contains logic for position/PnL management.
- # It needs to update positions and compute open and closed PnLs when market prices change
- # and/or trades are made causing a change in positions
-
- # This section updates Open/Unrealized & Closed/Realized positions
- open_pnl = 0
- if position > 0:
- # long position and some sell trades have been made against it,
- # close that amount based on how much was sold against this long position
- # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
- if sell_sum_qty > 0: # vwap for sell # vwap for buy
- open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
- # mark the remaining position to market
- # i.e. pnl would be what it would be if we closed at current price
- # sell
- # position -= NUM_SHARES_PER_TRADE
- # sell_sum_qty += NUM_SHARES_PER_TRADE
- # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
- # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
- open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
- # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
- elif position < 0:
- # short position and some buy trades have been made against it,
- # close that amount based on how much was bought against this short position
- # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
- if buy_sum_qty > 0: # vwap for sell # vwap for buy
- open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
- # mark the remaining position to market
- # i.e. pnl would be what it would be if we closed at current price
- # buy
- # position += NUM_SHARE_PER_TRADE
- # buy_sum_qty += NUM_SHARE_PER_TRADE
- # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
- # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) *
- open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
- # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
- else:
- # flat, so update closed_pnl and reset tracking variables for positions & pnls
- closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
-
- buy_sum_price_qty = 0
- buy_sum_qty = 0
- last_buy_price = 0
-
- sell_sum_price_qty = 0
- sell_sum_qty = 0
- last_sell_price = 0
-
- print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
- pnls.append(closed_pnl + open_pnl)
- # This section prepares the dataframe from the trading strategy results and visualizes the results
- data3 = data3.assign( ClosePrice=pd.Series(close, index=data3.index))
- data3 = data3.assign( Fast10DayEMA=pd.Series(ema_fast_values, index=data3.index))
- data3 = data3.assign( Slow40DayEMA=pd.Series(ema_slow_values, index=data3.index))
- data3 = data3.assign( APO=pd.Series(apo_values, index=data3.index))
- data3 = data3.assign( Trades=pd.Series(orders, index=data3.index))
- data3 = data3.assign( Position=pd.Series(positions, index=data3.index))
- data3 = data3.assign( Pnl=pd.Series(pnls, index=data3.index))
- import matplotlib.pyplot as plt
-
- fig = plt.figure( figsize=(20,10) )
-
- data3['ClosePrice'].plot(color='k', lw=3., legend=True)
- data3['Fast10DayEMA'].plot(color='y', lw=1., legend=True)
- data3['Slow40DayEMA'].plot(color='m', lw=1., legend=True)
- plt.plot( data3.loc[ data3.Trades == 1 ].index, data3.ClosePrice[data3.Trades == 1 ],
- color='y', lw=0, marker='^', markersize=7, label='buy'
- )
- plt.plot( data3.loc[ data3.Trades == -1 ].index, data3.ClosePrice[data3.Trades == -1 ],
- color='b', lw=0, marker='v', markersize=7, label='sell'
- )
- plt.autoscale(enable=True, axis='x', tight=True)
-
- plt.legend()
- plt.show()
The plot shows at what prices the buy and sell trades are made throughout the lifetime of the trading strategy applied to Google stock data. The trading strategy behavior will make more sense when we inspect the APO signal values to go along with the actual trade prices. Let's look at that in the next plot:
- fig = plt.figure( figsize=(20,10) )
-
- data3['APO'].plot(color='k', lw=3., legend=True)
- plt.plot( data3.loc[ data3.Trades == 1 ].index, data3.APO[data3.Trades == 1 ],
- color='y', lw=0, marker='^', markersize=7, label='buy'
- )
- plt.plot( data3.loc[ data3.Trades == -1 ].index, data3.APO[data3.Trades == -1 ],
- color='b', lw=0, marker='v', markersize=7, label='sell'
- )
- plt.axhline(y=0, lw=0.5, color='k')
- for i in range( APO_VALUE_FOR_BUY_ENTRY, APO_VALUE_FOR_BUY_ENTRY*5, APO_VALUE_FOR_BUY_ENTRY ):
- plt.axhline(y=i, lw=0.5, color='r')
- for i in range( APO_VALUE_FOR_SELL_ENTRY, APO_VALUE_FOR_SELL_ENTRY*5, APO_VALUE_FOR_SELL_ENTRY ):
- plt.axhline(y=i, lw=0.5, color='g')
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
-
- plt.show()
By the definition of a trend-following strategy using the APO trading signal values, intuitively we expect buy trades when APO signal values are positive and sell trades when APO signal values are negative. There are also some buy trades when APO signal values are negative and some sell trades when APO signal values are positive, which might seem counterintuitive, but these are trades made to close out profitable positions, similar to the mean reversion strategy. Now, let's look at the evolution of positions through the course of this trading strategy:
- fig = plt.figure( figsize=(20,10))
-
- data3['Position'].plot(color='k', lw=1., legend=True)
- plt.plot( data3.loc[ data3.Position == 0 ].index, data3.Position[ data3.Position == 0 ],
- color='r', lw=0, marker='.', label='flat'
- )
- plt.plot( data3.loc[ data3.Position > 0 ].index, data3.Position[ data3.Position > 0 ],
- color='y', lw=0, marker='+', label='long'
- )
- plt.plot( data3.loc[ data3.Position < 0 ].index, data3.Position[ data3.Position < 0 ],
- color='b', lw=0, marker='_', label='short'
- )
- plt.axhline(y=0, lw=0.5, color='k')
- for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE*25, NUM_SHARES_PER_TRADE*5 ):
- plt.axhline(y=i, lw=0.5, color='r')
- for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE*25, -NUM_SHARES_PER_TRADE*5 ):
- plt.axhline(y=i, lw=0.5, color='g')
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
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:
- fig = plt.figure( figsize=(20,10))
-
- data3['Pnl'].plot(color='k', lw=1., legend=True)
- plt.plot( data3.loc[ data3.Pnl > 0 ].index, data3.Pnl[ data3.Pnl > 0 ],
- color='b', lw=0, marker='.',
- label='Pnl'
- )
- plt.plot( data3.loc[ data3.Pnl < 0 ].index, data3.Pnl[ data3.Pnl < 0 ],
- color='y', lw=0, marker='.',
- label='Pnl'
- )
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
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.
Let's use STDEV as a measure of volatility and adjust the trend-following strategy to adapt to changing market volatility. We will use an identical approach to the one we used when adjusting the mean reversion trading strategy for market volatility.
- import pandas as pd
- import pandas_datareader.data as pdr
-
- def load_financial_data( start_date, end_date, output_file='', stock_symbol='GOOG' ):
- if len(output_file) == 0:
- output_file = stock_symbol+'_data_large.pkl'
-
- try:
- df = pd.read_pickle( output_file )
- print( "File {} data found...reading {} data".format( output_file ,stock_symbol) )
- except FileNotFoundError:
- print( "File {} not found...downloading the {} data".format( output_file, stock_symbol ) )
- df = pdr.DataReader( stock_symbol, "yahoo", start_date, end_date )
- df.to_pickle( output_file )
- return df
-
- goog_data = load_financial_data( stock_symbol='GOOG',
- start_date='2014-01-01',
- end_date='2018-01-01',
- output_file='goog_data.pkl'
- )
- goog_data.head()
- # Variables/constants for EMA Calculation:
- NUM_PERIODS_FAST_10 = 10 # Static time period parameter for the fast EMA
- K_FAST = 2/(NUM_PERIODS_FAST_10 + 1) # Static smoothing factor parameter for fast EMA
- ema_fast = 0 # initial ema
- ema_fast_values = [] # we will hold fast EMA values for visualization purpose
-
- NUM_PERIODS_SLOW_40 = 40 # Static time period parameter for the slow EMA
- K_SLOW = 2/(NUM_PERIODS_SLOW_40 + 1) # Static smoothing factor parameter for slow EMA
- ema_slow = 0 # initial ema
- ema_slow_values = [] # we will hold slow EMA values for visualization purpose
-
- apo_values = [] # track computed absolute price oscillator values
-
- # Variables for Trading Strategy trade, position & pnl management:
-
- # Container for tracking buy/sell order,
- # +1 for buy order, -1 for sell order, 0 for no-action
- orders = []
-
- # Container for tracking positions,
- # positive for long positions, negative for short positions, 0 for flat/no position
- positions = []
-
- # Container for tracking total_pnls, this is the sum of
- # closed_pnl i.e. pnls already locked in
- # and open_pnl i.e. pnls for open-position marked to market price
- pnls = []
-
- last_buy_price = 0 # used to prevent over-trading at/around the same price
- last_sell_price = 0 # used to prevent over-trading at/around the same price
- position = 0 # Current position of the trading strategy
-
- # Summation of products of
- # buy_trade_price and buy_trade_qty for every buy Trade made
- # since last time being flat
- buy_sum_price_qty = 0
- # Summation of buy_trade_qty for every buy Trade made since last time being flat
- buy_sum_qty = 0
-
- # Summation of products of
- # sell_trade_price and sell_trade_qty for every sell Trade made
- # since last time being flat
- sell_sum_price_qty = 0
- # Summation of sell_trade_qty for every sell Trade made since last time being flat
- sell_sum_qty = 0
-
- open_pnl = 0 # Open/Unrealized PnL marked to market
- closed_pnl = 0 # Closed/Realized PnL so far
-
- # Constants that define strategy behavior/thresholds
-
- ################################
- # APO trading signal value below which(-10) to enter buy-orders/long-position
- APO_VALUE_FOR_BUY_ENTRY = 10 # (oversold, expect a bounce back up)
- # APO trading signal value above which to enter sell-orders/short-position
- APO_VALUE_FOR_SELL_ENTRY = -10 # (overbought, expect a bounce back down)
- ################################
-
- # Minimum price change since last trade before considering trading again,
- MIN_PRICE_MOVE_FROM_LAST_TRADE = 10 # this is to prevent over-trading at/around same prices
- NUM_SHARES_PER_TRADE = 10
-
- # positions are closed if currently open positions are profitable above a certain amount,
- # regardless of APO values.
- # This is used to algorithmically lock profits and initiate more positions
- # instead of relying only on the trading signal value.
-
- # Minimum Open/Unrealized profit at which to close positions and lock profits
- MIN_PROFIT_TO_CLOSE = 10*NUM_SHARES_PER_TRADE
- import statistics as stats
- import math as math
-
- data4 = goog_data.copy()
- close = data4['Close']
-
- # Constants/variables that are used to compute standard deviation as a volatility measure
- SMA_NUM_PERIODS_20 = 20 # look back period
- price_history = [] # history of prices
-
- for close_price in close:
- price_history.append( close_price )
- if len( price_history) > SMA_NUM_PERIODS_20 : # we track at most 'time_period' number of prices
- del ( price_history[0] )
-
- # calculate vairance during the SMA_NUM_PERIODS_20 periods
- sma = stats.mean( price_history )
- variance = 0 # variance is square of standard deviation
- for hist_price in price_history:
- variance = variance + ( (hist_price-sma)**2 )
-
- stddev = math.sqrt( variance/len(price_history) )
-
- # a volatility factor that ranges from 0 to 1
- stddev_factor = stddev/15 # 15 since since the population stddev.mean() = 15.45
- # closer to 0 indicate very low volatility,
- # around 1 indicate normal volatility
- # > 1 indicate above-normal volatility
- if stddev_factor == 0:
- stddev_factor = 1
-
- # This section updates fast and slow EMA and computes APO trading signal
- if (ema_fast==0): # first observation
- ema_fast = close_price # initial ema_fast or ema_slow
- ema_slow = close_price
- else:
- # ema fomula
- # K_FAST*stddev_factor or K_SLOW*stddev_factor
- # more reactive to newest observations during periods of higher than normal volatility
- ema_fast = (close_price-ema_fast) * K_FAST*stddev_factor + ema_fast
- ema_slow = (close_price-ema_slow) * K_SLOW*stddev_factor + ema_slow
-
- ema_fast_values.append( ema_fast )
- ema_slow_values.append( ema_slow )
-
- apo = ema_fast - ema_slow
- apo_values.append( apo )
-
- # 6. This section checks trading signal against trading parameters/thresholds and positions, to trade.
-
- # We will perform a sell trade at close_price if the following conditions are met:
- # 1. The APO trading signal value(negative) < Sell-Entry threshold (expecting price moves to continue going down)
- # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
- # 2. We are long( +ve position ) and
- # either APO trading signal value >= 0 or current position is profitable enough to lock profit.
- ###
- if ( ( apo < APO_VALUE_FOR_SELL_ENTRY and \
- abs( close_price-last_sell_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
- )
- or ###
- ( position>0 and (apo <=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
- ): # long from -ve APO and APO has gone positive or position is profitable, sell to close position
- orders.append(-1) # mark the sell trade
- last_sell_price = close_price
- position -= NUM_SHARES_PER_TRADE
- sell_sum_qty += NUM_SHARES_PER_TRADE
- sell_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update vwap sell-price
- print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
-
- # 7. We will perform a buy trade at close_price if the following conditions are met:
- # 1. The APO trading signal value(positive) > Buy-Entry threshold (expecting price moves to continue going up)
- # and the difference between current-price and last trade-price is different enough.(>Minimum price change)
- # 2. We are short( -ve position ) and
- # either APO trading signal value is >= 0 or current position is profitable enough to lock profit.
- ###
- elif ( ( apo > APO_VALUE_FOR_BUY_ENTRY and \
- abs( close_price-last_buy_price ) > MIN_PRICE_MOVE_FROM_LAST_TRADE
- )
- or ###
- ( position<0 and (apo >=0 or open_pnl > MIN_PROFIT_TO_CLOSE ) )
- ): # short from +ve APO and APO has gone negative or position is profitable, buy to close position
- orders.append(+1) # mark the buy trade
- last_buy_price = close_price
- position += NUM_SHARES_PER_TRADE
- buy_sum_qty += NUM_SHARES_PER_TRADE
- buy_sum_price_qty += (close_price * NUM_SHARES_PER_TRADE) # update the vwap buy-price
- print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
- else:
- # No trade since none of the conditions were met to buy or sell
- orders.append( 0 )
-
- positions.append( position )
-
- # 8. The code of the trading strategy contains logic for position/PnL management.
- # It needs to update positions and compute open and closed PnLs when market prices change
- # and/or trades are made causing a change in positions
-
- # This section updates Open/Unrealized & Closed/Realized positions
- open_pnl = 0
- if position > 0:
- # long position and some sell trades have been made against it,
- # close that amount based on how much was sold against this long position
- # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
- if sell_sum_qty > 0: # vwap for sell # vwap for buy
- open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
- # mark the remaining position to market
- # i.e. pnl would be what it would be if we closed at current price
- # sell
- # position -= NUM_SHARES_PER_TRADE
- # sell_sum_qty += NUM_SHARES_PER_TRADE
- # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
- # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
- open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
- # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
- elif position < 0:
- # short position and some buy trades have been made against it,
- # close that amount based on how much was bought against this short position
- # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
- if buy_sum_qty > 0: # vwap for sell # vwap for buy
- open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
- # mark the remaining position to market
- # i.e. pnl would be what it would be if we closed at current price
- # buy
- # position += NUM_SHARE_PER_TRADE
- # buy_sum_qty += NUM_SHARE_PER_TRADE
- # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
- # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) *
- open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
- # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
- else:
- # flat, so update closed_pnl and reset tracking variables for positions & pnls
- closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
-
- buy_sum_price_qty = 0
- buy_sum_qty = 0
- last_buy_price = 0
-
- sell_sum_price_qty = 0
- sell_sum_qty = 0
- last_sell_price = 0
-
- print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
- pnls.append(closed_pnl + open_pnl)
- # This section prepares the dataframe from the trading strategy results and visualizes the results
- data4 = data4.assign( ClosePrice = pd.Series(close, index=data4.index) )
- data4 = data4.assign( Fast10DayEMA = pd.Series(ema_fast_values, index=data4.index) )
- data4 = data4.assign( Slow40DayEMA = pd.Series(ema_slow_values, index=data4.index) )
- data4 = data4.assign( APO = pd.Series(apo_values, index=data4.index) )
- data4 = data4.assign( Trades = pd.Series(orders, index=data4.index) )
- data4 = data4.assign( Position = pd.Series(positions, index=data4.index) )
- data4 = data4.assign( Pnl = pd.Series(pnls, index=data4.index) )
- fig = plt.figure( figsize=(20,10))
-
- plt.plot( data3.index, data3['Pnl'], color='g', lw=1.,
- label='BasicTrendFollowingPnL'
- )#########################
- plt.plot( data3.loc[ data3.Pnl > 0 ].index, data3.Pnl[ data3.Pnl > 0 ],
- color='y', lw=0, marker='.',
- #label='Pnl'
- )
- plt.plot( data3.loc[ data3.Pnl < 0 ].index, data3.Pnl[ data3.Pnl < 0 ],
- color='r', lw=0, marker='.',
- #label='Pnl'
- )
-
- plt.plot( data4.index, data4['Pnl'], color='b', lw=1.,
- label='VolatilityAdjustedTrendFollowingPnL'
- )#########################
- plt.plot( data4.loc[ data4.Pnl > 0 ].index, data4.Pnl[ data4.Pnl > 0 ],
- color='y', lw=0, marker='.',
- #label='Pnl'
- )
- plt.plot( data4.loc[ data4.Pnl < 0 ].index, data4.Pnl[ data4.Pnl < 0 ],
- color='r', lw=0, marker='.',
- #label='Pnl'
- )
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
Finally, let's compare trend-following strategy performance with and without accounting for volatility changes:
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.
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 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:
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/.
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.
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.
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,
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
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,
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.
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.
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
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
So the ideal approach is to construct portfolios and establish relationships between lead and lag instruments differently in different trading sessions.
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.
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.
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++.
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:
And for this implementation of the StatArb trading strategy, we will try to trade CAD/USD using its relationship with the other currency pairs:
1. Let's fetch 4 years' worth of data for these currency pairs and set up our data frames:
- import pandas as pd
- import pandas_datareader.data as pdr
-
- # Fetch daily data for 4 years, for 7 major currency pairs
- TRADING_INSTRUMENT = 'CADUSD=X'
- SYMBOLS = ['AUDUSD=X', 'GBPUSD=X', 'CADUSD=X', 'CHFUSD=X', 'EURUSD=X', 'JPYUSD=X',
- 'NZDUSD=X']
- START_DATE = '2014-01-01'
- END_DATE = '2018-01-01'
-
- # DataSeries for each currency
- symbols_data = {}
- for symbol in SYMBOLS:
- SRC_DATA_FILENAME = symbol + '_data.pkl'
-
- try:
- data = pd.read_pickle( SRC_DATA_FILENAME )
- except FileNotFoundError:
- data = pdr.DataReader( symbol, 'yahoo', START_DATE, END_DATE )
- data.to_pickle( SRC_DATA_FILENAME )
-
- symbols_data[symbol] = data
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:
- # Visualize prices for currency to inspect relationship between them
- import matplotlib.pyplot as plt
- import numpy as np
- from itertools import cycle
-
- cycol = cycle('bgrcmky')
-
- price_data = pd.DataFrame()
-
- fig = plt.figure( figsize=(20,14) )
- for symbol in SYMBOLS:
- multiplier = 1.0
- if symbol == 'JPYUSD=X':
- multiplier = 100.0
- label = symbol + ' ClosePrice'
- price_data = price_data.assign( label=pd.Series( symbols_data[symbol]['Close'] * multiplier,
- index = symbols_data[symbol].index
- ) # datetime index
- )
- if symbol == 'JPYUSD=X' or symbol == 'CHFUSD=X':
- ax = price_data['label'].plot( color=next(cycol), lw=2., label=label, ls='dotted' )
- else:
- ax = price_data['label'].plot( color=next(cycol), lw=2., label=label )
-
- plt.xlabel( 'Date', fontsize=18 )
- plt.ylabel( 'Scaled Price', fontsize=18 )
- plt.legend( prop={'size':18} )
- plt.show()
As one would expect and can observe, these currency pairs' price moves are all similar to each other in varying degrees. CAD/USD, AUD/USD, and NZD/USD seem to be most correlated(bottom), with CHF/USD and JPY/USD being least correlated to CAD/USD(middle). For the purposes of this strategy, we will use all currencies in the trading model because these relationships are obviously not known ahead of time.
- df=pd.DataFrame()
- for symbol in SYMBOLS:
- df[symbol]=symbols_data[symbol]['Close']
- df.head()
- from mlxtend.plotting import heatmap
-
- cm = np.corrcoef( df[SYMBOLS].values.T )
- # df[cols].values.T : (n_instances, n_features) ==> (n_features, n_instances or n_observations)
- hm = heatmap( cm, row_names=SYMBOLS, column_names=SYMBOLS )
-
- plt.show()
==>
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.
Now, let's define and quantify some parameters we will need to define moving averages, price deviation from moving averages, history of price deviations, and variables to compute and track correlations:
- import statistics as stats
-
- # Constants/variables that are used to compute
- # simple moving average and price deviation from simple moving average
- SMA_NUM_PERIODS = 20 # look back period
- price_history = {} # history of prices
-
- PRICE_DEV_NUM_PRICES = 200 # look back period of ClosePrice deviations from SMA
- price_deviation_from_sma = {} # history of ClosePrice deviations from SMA
-
- # We will use this to iterate over all the days of data we have
- # TRADING_INSTRUMENT = 'CADUSD=X' # datetime index
- num_days = len( symbols_data[TRADING_INSTRUMENT].index )
- correlation_history = {} # history of correlations per currency pair
- # history of differences between
- # Projected ClosePrice deviation and actual ClosePrice deviation per currency pair
- delta_projected_actual_history = {}
-
- # history of differences between
- # final Projected ClosePrice deviation for TRADING_INSTRUMENT and actual ClosePrice deviation
- final_delta_projected_history = []
Now, before we get into the main strategy loop, let's define some final variables and thresholds we will need to build our StatArb trading strategy
- ######## Variables for Trading Strategy trade, position & pnl management ########
-
- # Container for tracking buy/sell order,
- # +1 for buy order, -1 for sell order, 0 for no-action
- orders = []
-
- # Container for tracking positions,
- # positive for long positions, negative for short positions, 0 for flat/no position
- positions = []
-
- # Container for tracking total_pnls, this is the sum of
- # closed_pnl i.e. pnls already locked in
- # and open_pnl i.e. pnls for open-position marked to market price
- pnls = []
-
- last_buy_price = 0 # used to prevent over-trading at/around the same price
- last_sell_price = 0 # used to prevent over-trading at/around the same price
- position = 0 # Current position of the trading strategy
-
- # Summation of products of
- # buy_trade_price and buy_trade_qty for every buy Trade made
- # since last time being flat
- buy_sum_price_qty = 0
- # Summation of buy_trade_qty for every buy Trade made since last time being flat
- buy_sum_qty = 0
-
- # Summation of products of
- # sell_trade_price and sell_trade_qty for every sell Trade made
- # since last time being flat
- sell_sum_price_qty = 0
- # Summation of sell_trade_qty for every sell Trade made since last time being flat
- sell_sum_qty = 0
-
- open_pnl = 0 # Open/Unrealized PnL marked to market
- closed_pnl = 0 # Closed/Realized PnL so far
-
- # Constants that define strategy behavior/thresholds
- # StatArb trading signal value above which to enter buy-orders/long-position
- StatArb_VALUE_FOR_BUY_ENTRY = 0.01
- # StatArb trading signal value below which to enter sell-orders/short-position
- StatArb_VALUE_FOR_SELL_ENTRY = -0.01
-
- # Minimum price change since last trade before considering trading again,
- # this is to prevent over-trading at/around same prices
- MIN_PRICE_MOVE_FROM_LAST_TRADE = 0.01
- # Number of currency to buy/sell on every trade
- NUM_SHARES_PER_TRADE = 1000000
- # Minimum Open/Unrealized profit at which to close positions and lock profits
- MIN_PROFIT_TO_CLOSE = 10
Remember that these positions are in dollar notional terms, so a position of 100K(100000) is equivalent to roughly 1 future contract, which we mention to make it clear that a position of 100K does not mean a position of 100K contracts!
1. We will see over available prices a day at a time and see what calculations need to be performed, starting with the computation of SimpleMovingAverages and price deviation from the rolling SMA first:
- for i in range(0, num_days):
- close_prices = {}
-
- # Build ClosePrice series,
- # compute SMA for each symbol and price-deviation from SMA for each symbol
- for symbol in SYMBOLS:
- close_prices[symbol] = symbols_data[symbol]['Close'].iloc[i]
-
- if not symbol in price_history.keys():
- price_history[symbol] = []
- price_deviation_from_sma[symbol] = []
-
- price_history[symbol].append( close_prices[symbol] )
-
- # we track at most SMA_NUM_PERIODS number of prices
- if len( price_history[symbol] ) > SMA_NUM_PERIODS:
- del ( price_history[symbol][0] )
- sma = stats.mean( price_history[symbol] ) # Rolling SimpleMovingAverage
-
- # price deviation from mean
- price_deviation_from_sma[symbol].append( close_prices[symbol] - sma )
- if len( price_deviation_from_sma[symbol] ) > PRICE_DEV_NUM_PRICES:
- del ( price_deviation_from_sma[symbol][0] )
-
- # Now compute covariance and correlation between TRADING_INSTRUMENT and every other lead symbol
- # also compute projected price deviation and find delta between projected and actual price deviations.
- projected_dev_from_sma_using = {}
- for symbol in SYMBOLS:
- # no need to find relationship between trading instrument and itself
- if symbol == TRADING_INSTRUMENT:
- continue
-
- correlation_label = TRADING_INSTRUMENT + '<-' + symbol
- # first entry for this pair in the history dictionary
- if correlation_label not in correlation_history.keys():
- correlation_history[correlation_label] = []
- delta_projected_actual_history[correlation_label] = []
-
- # need at least 2 observations to compute covariance/correlation
- # close_prices[symbol] = symbols_data[symbol]['Close'].iloc[i] and 0<=i<num_days
- # price_deviation_from_sma[symbol].append( close_prices[symbol] - sma )
- if len( price_deviation_from_sma[symbol] ) < 2:
- correlation_history[correlation_label].append(0)
- delta_projected_actual_history[correlation_label].append(0)
- continue
-
- # 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 )
-
- # 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 +ve => signal says TRADING_INSTRUMENT price should have moved up more than what it did
- # delta -ve => signal says TRADING_INSTRUMENT price should have moved down more than what it did
- delta_projected_actual = (projected_dev_from_sma_using[symbol] - price_deviation_from_sma[TRADING_INSTRUMENT][-1])
- delta_projected_actual_history[correlation_label].append(delta_projected_actual)
-
- # weigh predictions from each pair,
- # weight is the correlation between those pairs
- sum_weights = 0 # sum of weights is sum of correlations for each symbol with TRADING_INSTRUMENT
- for symbol in SYMBOLS:
- if symbol == TRADING_INSTRUMENT: # no need to find relationship between trading instrument and itself
- continue
- correlation_label = TRADING_INSTRUMENT + '<-' + symbol
- # the sum of each individual weight (magnitude of correlation)
- sum_weights += abs( correlation_history[correlation_label][-1] )
-
- # will hold final prediction of price deviation in TRADING_INSTRUMENT, weighing projections from all other symbols.
- final_delta_projected = 0
- close_price = close_prices[TRADING_INSTRUMENT]
- for symbol in SYMBOLS:
- if symbol == TRADING_INSTRUMENT:
- continue
- correlation_label = TRADING_INSTRUMENT + '<-' + symbol
-
- # weight projection from a symbol by correlation
- # use the magnitude of the correlation between CAD/USD and the other currency pairs
- # to weigh
- # the delta between projected and actual price deviations in CAD/USD as predicted by the other pairs
- final_delta_projected += abs(correlation_history[correlation_label][-1]) * delta_projected_actual_history[correlation_label][-1]
-
- # normalize by divding by sum of weights for all pairs
- # as our final signal to build our trading strategy around
- if sum_weights !=0:
- final_delta_projected /= sum_weights
- else:
- final_delta_projected = 0
- final_delta_projected_history.append( final_delta_projected )
-
- # StatArb execution logic
- # Now, using the StatArb signal we just computed, we can build a strategy similar to the trend-following strategy
-
- # We will perform a sell trade at close_prices if the following conditions are met:
- # 1. The StatArb trading signal value(-ve) is < Sell-Entry threshold(StatArb_VALUE_FOR_SELL_ENTRY = -0.01)
- # and the difference between last trade-price and current-price is different enough.
- # 2. We are long( +ve position )
- # and current position is profitable enough to lock profit
- if ( ( final_delta_projected < StatArb_VALUE_FOR_SELL_ENTRY and \
- abs(close_price-last_sell_price) > MIN_PRICE_MOVE_FROM_LAST_TRADE
- )
- or
- ( position > 0 and \
- abs( open_pnl > MIN_PROFIT_TO_CLOSE )
- )# long from -ve StatArb and StatArb has gone positive or position is profitable, sell to close position
- ):
- orders.append(-1) # mark the sell trade
- last_sell_price = close_price
- position -= NUM_SHARES_PER_TRADE # reduce position by the size of this trade
- sell_sum_qty += NUM_SHARES_PER_TRADE
- sell_sum_price_qty += (close_price*NUM_SHARES_PER_TRADE) # update vwap sell-price
- print( "Sell ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
- print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
- # We will perform a buy trade at close_prices if the following conditions are met:
- # 1. The StatArb trading signal value(+ve) is above Buy-Entry threshold
- # and the difference between last trade-price and current-price is different enough.
- # 2. We are short( -ve position )
- # and current position is profitable enough to lock profit.
- elif ( ( final_delta_projected > StatArb_VALUE_FOR_BUY_ENTRY and \
- abs(close_price-last_buy_price) > MIN_PRICE_MOVE_FROM_LAST_TRADE
- )
- or
- ( position < 0 and \
- abs( open_pnl > MIN_PROFIT_TO_CLOSE )
- )# short from +ve StatArb and StatArb has gone negative or position is profitable, buy to close position
- ):
- orders.append(1) # mark the buy trade
- last_buy_price = close_price
- position += NUM_SHARES_PER_TRADE # increase position by the size of this trade
- buy_sum_qty += NUM_SHARES_PER_TRADE
- buy_sum_price_qty += (close_price*NUM_SHARES_PER_TRADE) # update the vwap buy-price
- print( "Buy ", NUM_SHARES_PER_TRADE, " @ ", close_price, "Position: ", position )
- print( "OpenPnL: ", open_pnl, " ClosedPnL: ", closed_pnl, " TotalPnL: ", (open_pnl + closed_pnl) )
- else:
- # No trade since none of the conditions were met to buy or sell
- orders.append(0)
-
- positions.append(position)
-
- # This section updates Open/Unrealized & Closed/Realized positions
- open_pnl = 0
- if position > 0:
- # long position and some sell trades have been made against it,
- # close that amount based on how much was sold against this long position
- # PnL_realized = sell_sum_qty * (Average Sell Price - Average Buy Price)
- if sell_sum_qty > 0: # vwap for sell # vwap for buy
- open_pnl = sell_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
- # mark the remaining position to market
- # i.e. pnl would be what it would be if we closed at current price
- # sell
- # position -= NUM_SHARES_PER_TRADE
- # sell_sum_qty += NUM_SHARES_PER_TRADE
- # PnL_unrealized = remaining position * (Exit Price - Average Buy Price)
- # if now, sell sell_sum_qty @ any price, we should use abs(position-sell_sum_qty) *
- open_pnl += abs(position) * ( close_price - buy_sum_price_qty/buy_sum_qty )
- # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
- elif position < 0:
- # short position and some buy trades have been made against it,
- # close that amount based on how much was bought against this short position
- # PnL_realized = buy_sum_qty * (Average Sell Price - Average Buy Price)
- if buy_sum_qty > 0: # vwap for sell # vwap for buy
- open_pnl = buy_sum_qty * (sell_sum_price_qty/sell_sum_qty - buy_sum_price_qty/buy_sum_qty)
- # mark the remaining position to market
- # i.e. pnl would be what it would be if we closed at current price
- # buy
- # position += NUM_SHARE_PER_TRADE
- # buy_sum_qty += NUM_SHARE_PER_TRADE
- # PnL_unrealized = remaining position * (Average Sell Price - Exit Price)
- # if now, buy buy_sum_qty @ any price, we should use abs(position+buy_sum_qty) *
- open_pnl += abs(position) * ( sell_sum_price_qty/sell_sum_qty - close_price )
- # print( position, (buy_sum_qty-sell_sum_qty), open_pnl)
- else:
- # flat, so update closed_pnl and reset tracking variables for positions & pnls
- closed_pnl += (sell_sum_price_qty - buy_sum_price_qty)
-
- buy_sum_price_qty = 0
- buy_sum_qty = 0
- last_buy_price = 0
-
- sell_sum_price_qty = 0
- sell_sum_qty = 0
- last_sell_price = 0
-
- pnls.append(closed_pnl + open_pnl)
Now, let's analyze the StatArb signal using the following steps:
1. Let's visualize a few more details about the signals in this trading strategy, starting with the correlations between CAD/USD and the other currency pairs as it evolves over time:
# https://blog.csdn.net/Linli522362242/article/details/121721868
# Each row of m represents a variable, and each column a single observation(features) of all those variables.
corr = np.corrcoef( price_deviation_from_sma[TRADING_INSTRUMENT],
price_deviation_from_sma[symbol]
) # Return Pearson product-moment correlation coefficients
# the correlation matrix is identical to a covariance matrix computed from standardized features.
cov = np.cov( price_deviation_from_sma[TRADING_INSTRUMENT],
price_deviation_from_sma[symbol]
) # Estimate a covariance matrix(here is 2x2), given data and weights(here is None)
# get the correlation between the 2 series
# here is the correlation between CAD/USD and the other currency pairs
corr_trading_instrument_lead_instrument = corr[0,1]
# get the covariance between the 2 series
cov_trading_instrument_lead_instrument = cov[0,0]/cov[0,1]
correlation_history[correlation_label].append(corr_trading_instrument_lead_instrument )
- # Plot correlations between TRADING_INSTRUMENT and other currency pairs
- fig = plt.figure( figsize=(20,10))
-
- correlation_data = pd.DataFrame()
- for symbol in SYMBOLS:
- if symbol == TRADING_INSTRUMENT:
- continue
-
- correlation_label = TRADING_INSTRUMENT + '<-' + symbol
- correlation_data = correlation_data.assign( label=pd.Series( correlation_history[correlation_label],
- index=symbols_data[symbol].index
- )
- )
- if symbol == 'JPYUSD=X':
- ax = correlation_data['label'].plot( color=next(cycol), lw=2., ls='dotted',
- label='Correlation '+ correlation_label )
- elif symbol == 'AUDUSD=X' or symbol == 'NZDUSD=X':
- ax = correlation_data['label'].plot( color=next(cycol), lw=5.,
- label='Correlation '+ correlation_label )
- else:
- ax = correlation_data['label'].plot( color=next(cycol), lw=2.,
- label='Correlation '+ correlation_label )
-
-
- for i in np.arange(-1,1, 0.25):
- plt.axhline( y=i, lw=0.5, color='k' )
- plt.legend()
- plt.autoscale(enable=True, axis='x', tight=True)
-
- plt.show()
This plot shows the correlation between CADUSD and other currency pairs as it evolves over the course of this trading strategy.
As we suspected, the currency pairs that are most strongly correlated to CAD/USD price deviations are AUD/USD and NZD/USD. JPY/USD is the least correlated to CAD/USD price deviations.
2. inspect the delta between projected (=covariance * price-deviation-in-lead-symbol from sma) and actual price deviations in CAD/USD as projected by each individual currency pair individually:
cov = np.cov( price_deviation_from_sma[TRADING_INSTRUMENT],
price_deviation_from_sma[symbol]
) # Estimate a covariance matrix(here is 2x2), given data and weights(here is None)
# get the covariance between the 2 series
cov_trading_instrument_lead_instrument = cov[0,0]/cov[0,1]
# computes the projected price movement,
# uses that to find the difference between the projected movement and actual movement,
# and saves it in our delta_projected_actual_history list per currency pair
# projected-price-deviation is predicted by the other pairs(lead-symbol) * covariance
##################
# projected-price-deviation-in-TRADING_INSTRUMENT is covariance * price-deviation-in-lead-symbol
projected_dev_from_sma_using[symbol] = price_deviation_from_sma[symbol][-1] * cov_trading_instrument_lead_instrument
# the delta between projected and actual price deviations in CAD/USD as predicted by the other pairs
# delta +value => signal says TRADING_INSTRUMENT price should have moved up more than what it did
# delta -value => signal says TRADING_INSTRUMENT price should have moved down more than what it did
delta_projected_actual = (projected_dev_from_sma_using[symbol] - price_deviation_from_sma[TRADING_INSTRUMENT][-1])
delta_projected_actual_history[correlation_label].append(delta_projected_actual)
- # Plot StatArb signal provided by each currency pair
- from itertools import cycle
-
- cycol = cycle('bgrcmky')
-
- fig = plt.figure( figsize=(15,8))
-
- delta_projected_actual_data = pd.DataFrame()
- for symbol in SYMBOLS:
- if symbol == TRADING_INSTRUMENT:
- continue
-
- projection_label = TRADING_INSTRUMENT + '<-' + symbol
- delta_projected_actual_data = delta_projected_actual_data.assign(
- StatArbTradingSignal = pd.Series( delta_projected_actual_history[projection_label],
- index=symbols_data[TRADING_INSTRUMENT].index
- )
- )
-
- if symbol == 'JPYUSD=X' or symbol == 'CHFUSD=X':
- ax = delta_projected_actual_data['StatArbTradingSignal'].plot( color=next(cycol),
- lw=2., ls='dotted',
- label='StatArbTradingSignal'+projection_label
- )
- else:
- ax = delta_projected_actual_data['StatArbTradingSignal'].plot( color=next(cycol),
- lw=1.,
- label='StatArbTradingSignal'+projection_label
- )
- plt.legend()
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.show()
This is what the StatArb signal values would look like if we used any of the currency pairs alone to project预测 CAD/USD price deviations: Here, the plot seems to suggest that JPYUSD and CHFUSD have very large predictions, but as we saw before those pairs do not have good correlations with CADUSD, so these are likely to be bad predictions due to poor predictive relationships between CADUSD - JPYUSD and CADUSD - CHFUSD . One lesson to take away from this is that StatArb benefits from having multiple leading trading instruments, because when relationships break down between specific pairs, the other strongly correlated pairs can help offset bad predictions, which we discussed earlier.
# weigh predictions from each pair,
# weight is the correlation between those pairs
sum_weights = 0 # sum of weights is sum of correlations for each symbol with TRADING_INSTRUMENT
for symbol in SYMBOLS:
if symbol == TRADING_INSTRUMENT: # no need to find relationship between trading instrument and itself
continue
correlation_label = TRADING_INSTRUMENT + '<-' + symbol
# the sum of each individual weight (magnitude of correlation)
sum_weights += abs( correlation_history[correlation_label][-1] )
# will hold final prediction of price deviation in TRADING_INSTRUMENT, weighing projections from all other symbols.
final_delta_projected = 0
close_price = close_prices[TRADING_INSTRUMENT]
for symbol in SYMBOLS:
if symbol == TRADING_INSTRUMENT:
continue
correlation_label = TRADING_INSTRUMENT + '<-' + symbol
# weight projection from a symbol by correlation
# use the magnitude of the correlation between CAD/USD and the other currency pairs
# to weigh
# the delta between projected and actual price deviations in CAD/USD as predicted by the other pairs
final_delta_projected += abs(correlation_history[correlation_label][-1]) * delta_projected_actual_history[correlation_label][-1]
# normalize by divding by sum of weights for all pairs
# as our final signal to build our trading strategy around
if sum_weights !=0:
final_delta_projected /= sum_weights
else:
final_delta_projected = 0
final_delta_projected_history.append( final_delta_projected )
3. Now, let's set up our data frames to plot the close price, trades, positions, and PnLs we will observe:
- delta_projected_actual_data = delta_projected_actual_data.assign(
- ClosePrice = pd.Series( symbols_data[TRADING_INSTRUMENT]['Close'],
- index=symbols_data[TRADING_INSTRUMENT].index
- )
- )
- delta_projected_actual_data = delta_projected_actual_data.assign(
- FinalStatArbTradingSignal = pd.Series( final_delta_projected_history,
- index=symbols_data[TRADING_INSTRUMENT].index
- )
- )
- delta_projected_actual_data = delta_projected_actual_data.assign(
- Trades = pd.Series( orders,
- index=symbols_data[TRADING_INSTRUMENT].index
- )
- )
- delta_projected_actual_data = delta_projected_actual_data.assign(
- Position = pd.Series( positions,
- index=symbols_data[TRADING_INSTRUMENT].index
- )
- )
- delta_projected_actual_data = delta_projected_actual_data.assign(
- Pnl = pd.Series( pnls,
- index=symbols_data[TRADING_INSTRUMENT].index
- )
- )
- fig = plt.figure( figsize=(15,8))
-
- plt.plot( delta_projected_actual_data.index,
- delta_projected_actual_data.ClosePrice,
- color='k', lw=1., label='ClosePrice')
- plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == 1].index,
- delta_projected_actual_data.ClosePrice[delta_projected_actual_data.Trades == 1],
- color='b', lw=0, marker='^', markersize=7, label='buy')
- plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == -1].index,
- delta_projected_actual_data.ClosePrice[delta_projected_actual_data.Trades == -1],
- color='y', lw=0, marker='v', markersize=7, label='sell'
- )
- plt.legend()
- plt.show()
The following plot tells us at what prices the buy and sell trades are made in CADUSD . We will need to inspect the final trading signal (final_delta_projected)in addition to this plot to fully understand the behavior of this StatArb signal and strategy:
Now, let's look at the actual code to build visualization for the final StatArb trading signal, and overlay buy and sell trades over the lifetime of the signal evolution. This will help us understand for what signal values buy and sell trades are made and if that is in line with our expectations:
- fig = plt.figure( figsize=(15,8))
-
- plt.plot( delta_projected_actual_data.index,
- delta_projected_actual_data.FinalStatArbTradingSignal,
- color='k', lw=1.,
- label='FinalStatArbTradingSignal'
- )
- plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == 1].index,
- delta_projected_actual_data.FinalStatArbTradingSignal[delta_projected_actual_data.Trades == 1],
- color='b', lw=0, marker='^', markersize=7, label='buy'
- )
- plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Trades == -1].index,
- delta_projected_actual_data.FinalStatArbTradingSignal[delta_projected_actual_data.Trades == -1],
- color='y', lw=0, marker='v', markersize=7, label='sell'
- )
-
- plt.axhline(y=0, lw=0.5, color='k')
- for i in np.arange( StatArb_VALUE_FOR_BUY_ENTRY, StatArb_VALUE_FOR_BUY_ENTRY * 10, StatArb_VALUE_FOR_BUY_ENTRY * 2 ):
- plt.axhline(y=i, lw=0.5, color='r')
- for i in np.arange( StatArb_VALUE_FOR_SELL_ENTRY, StatArb_VALUE_FOR_SELL_ENTRY * 10, StatArb_VALUE_FOR_SELL_ENTRY * 2 ):
- plt.axhline(y=i, lw=0.5, color='g')
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
Since we adopted the trend-following approach in our StatArb trading strategy, we
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:
- fig = plt.figure( figsize=(15,8))
-
- plt.plot( delta_projected_actual_data.index,
- delta_projected_actual_data.Position,
- color='k', lw=1.,
- label='Position'
- )
- plt.plot( delta_projected_actual_data.loc[ delta_projected_actual_data.Position == 0 ].index,
- delta_projected_actual_data.Position[delta_projected_actual_data.Position == 0],
- color='r', lw=0, marker='.',
- label='flat'
- )
- plt.plot( delta_projected_actual_data.loc[ delta_projected_actual_data.Position > 0 ].index,
- delta_projected_actual_data.Position[delta_projected_actual_data.Position > 0],
- color='b', lw=0, marker='+',
- label='long'
- )
- plt.plot( delta_projected_actual_data.loc[ delta_projected_actual_data.Position < 0 ].index,
- delta_projected_actual_data.Position[delta_projected_actual_data.Position < 0],
- color='y', lw=0, marker='x',
- label='short'
- )
-
- plt.axhline(y=0, lw=0.5, color='k')
-
- for i in range( NUM_SHARES_PER_TRADE, NUM_SHARES_PER_TRADE * 5, NUM_SHARES_PER_TRADE ):
- plt.axhline(y=i, lw=0.5, color='b')
- for i in range( -NUM_SHARES_PER_TRADE, -NUM_SHARES_PER_TRADE * 5, -NUM_SHARES_PER_TRADE ):
- plt.axhline(y=i, lw=0.5, color='g')
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
The position plot shows the evolution of the StatArb trading strategy's position over the course of its lifetime. Remember that these positions are in dollar notional terms, so a position of 100K is equivalent to roughly 1 future contract, which we mention to make it clear that a position of 100K does not mean a position of 100K contracts!
5. Finally, let's have a look at the code for the PnL plot, identical to what we've been using before:
- fig = plt.figure( figsize=(15,8))
-
- plt.plot( delta_projected_actual_data.index,
- delta_projected_actual_data.Pnl,
- color='k', lw=1.,
- label='PnL'
- )
- plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Pnl > 0].index,
- delta_projected_actual_data.Pnl[delta_projected_actual_data.Pnl > 0],
- color='b', lw=0, marker='.',
- label='+PnL'
- )
- plt.plot( delta_projected_actual_data.loc[delta_projected_actual_data.Pnl < 0].index,
- delta_projected_actual_data.Pnl[delta_projected_actual_data.Pnl < 0],
- color='y', lw=0, marker='.',
- label='-PnL'
- )
-
- plt.autoscale(enable=True, axis='x', tight=True)
- plt.legend()
- plt.show()
We expect to see better performance here than in our previously built trading strategies because it relies on a fundamental relationship between different currency pairs and should be able to perform better during different market conditions because of its use of multiple currency pairs as lead trading instruments:
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!
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.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。