TQuant Lab Price Deviation Ratio Trading Strategy

Highlight

Preface

The Price Deviation Ratio is a common technical indicator that compares the current stock price to the N-day moving average price, reflecting whether the current price is relatively high or low compared to its historical values. Generally, when the stock price consistently exceeds the moving average price, it’s called a ‘positive deviation.’ Conversely, it’s called’ negative deviation’ when it consistently falls below the moving average price.’ Therefore, when positive or negative deviation expands, it is interpreted as a sustained overbought or oversold condition in the market, serving as a basis for entry and exit decisions. However, using only the Price Deviation Ratio can generate too many trading signals. Hence, we include the highest and lowest prices over the past N days as a second filter. The actual strategy is as follows:

Trading Strategy

When the closing price is higher than the highest price over the past N days, and the Price Deviation Ratio is negative, enter a long position at the next day’s opening price.

When the closing price is lower than the lowest price over the past N days, and the Price Deviation Ratio is positive, exit the position and close the trade at the next day’s opening price.

The Editing Environment and Module Required

This article uses MacOS and employs Jupyter as the editor.

import os
import pandas as pd
import numpy as np
import tejapi
import matplotlib.pyplot as plt

Data Import

The back testing time period is between 2005/07/02 to 2023/07/02, and we take TSMC(2330) as an example.

os.environ['TEJAPI_BASE'] = 'https://api.tej.com.tw'
os.environ['TEJAPI_KEY'] = 'your_key'
os.environ['mdate'] = '20050702 20230702'
os.environ['ticker'] = '2330'
!zipline ingest -b tquant

Module Import

from zipline.api import (set_slippage,
set_commission,
set_benchmark,
attach_pipeline,
symbol,
pipeline_output,
record,
order,
order_target
)
from zipline.pipeline.filters import StaticSids
from zipline.finance import slippage, commission
from zipline import run_algorithm
from zipline.pipeline import CustomFactor, Pipeline
from zipline.pipeline.data import EquityPricing
from zipline.pipeline.factors import ExponentialWeightedMovingAverage

Create Pipeline function

Pipeline() enables users to quickly process multiple assets’ trading-related data. In today’s article, we use it to process:

  • EMA of price of the past 7 days
  • The highest price of the past 7 days(custom factor: NdaysMaxHigh)
  • The lowest price of the past7 days(custom factor: NdaysMinLow)
  • Current close price
def make_pipeline():
ema = ExponentialWeightedMovingAverage(inputs = [EquityPricing.close],window_length = 7,decay_rate = 1/7)
high = NdaysMaxHigh(inputs = [EquityPricing.close], window_length = 8) # window_length 設定為 8,因為 factor 會包含當日價格。
low = NdaysMinLow(inputs = [EquityPricing.close], window_length = 8)
close = EquityPricing.close.latest
return Pipeline(
columns = {
'ema':ema,
'highesthigh':high,
'lowestlow':low,
'latest':close
}
)
class NdaysMaxHigh(CustomFactor):
def compute(self, today, assets, out, data):
out[:] = np.nanmax(data[:-2], axis=0)
class NdaysMinLow(CustomFactor):
def compute(self, today, assets, out, data):
out[:] = np.nanmin(data[:-2], axis=0)

Creating Initialize Function

Initialize() enables users to set up the trading environment at the beginning of the back test period. In this article, we set up :

  • Slippage
  • Commission
  • Set the return of buying and holding TSMC as the benchmark.
  • Attach Pipline() function into back testing.
def initialize(context):
set_slippage(slippage.VolumeShareSlippage())
set_commission(commission.PerShare(cost=0.00285))
set_benchmark(symbol('2330'))
attach_pipeline(make_pipeline(), 'mystrategy')

Create Handle_data Function

handle_data() is used to process data and make orders daily.

  • Condition1: When current close price is greater than the highest price of last 7 days and the bias is greater than 0, we regard it as a selling signal.
  • Condition2: When current close price is lower than the lowest price of last 7 days and the bias is lower than 0, we regard it as a buying signal.
def handle_data(context, data):

pipe = pipeline_output('mystrategy')

for i in pipe.index:
ema = pipe.loc[i, 'ema']
highesthigh = pipe.loc[i, 'highesthigh']
lowestlow = pipe.loc[i, 'lowestlow']
close = pipe.loc[i, 'latest']
bias = close - ema
residual_position = context.portfolio.positions[i].amount # 當日該資產的股數
condition1 = (close > highesthigh) and (bias > 0) and (residual_position > 0) # 賣出訊號
condition2 = (close < lowestlow) and (bias < 0) # 買入訊號

record( # 用以紀錄以下資訊至最終產出的 result 表格中
con1 = condition1,
con2 = condition2,
price = close,
ema = ema,
bias = bias,
highesthigh = highesthigh,
lowestlow = lowestlow
)

if condition1:
order_target(i, 0)
elif condition2:
order(i, 10)
else:
pass

Creating Analyze Function

Here, we apply matplotlib.pyplot for the trading signals and the portfolio value visualization.

def analyze(context, perf):
fig = plt.figure()
ax1 = fig.add_subplot(211)
perf.portfolio_value.plot(ax=ax1)
ax1.set_ylabel("Portfolio value (NTD)")
ax2 = fig.add_subplot(212)
ax2.set_ylabel("Price (NTD)")
perf.price.plot(ax=ax2)
ax2.plot( # 繪製買入訊號
perf.index[perf.con2],
perf.loc[perf.con2, 'price'],
'^',
markersize=5,
color='red'
)
ax2.plot( # 繪製賣出訊號
perf.index[perf.con1],
perf.loc[perf.con1, 'price'],
'v',
markersize=5,
color='green'
)
plt.legend(loc=0)
plt.gcf().set_size_inches(18,8)
plt.show()

Run Algorithms

We expliot run_algorithm() to execute our strategy. The backtesting time period is set between 2015–01–06 to 2022–11–25. The data bundle we use is tquant. We assume the initial capital base is 10,000. The output of run_algorithm(), which is results, contains information on daily performance and trading receipts.

results = run_algorithm(start = pd.Timestamp('20150106', tz='UTC'),
end = pd.Timestamp('20221125', tz='UTC'),
initialize=initialize,
bundle='tquant',
analyze=analyze,
capital_base=1e4,
handle_data = handle_data
)
Portfolio value and trading time point
Portfolio value and trading time point
Trading record
Trading record

Performance Analysis

Then, we used Pyfolio module which came with TQuant Lab to analyze strategy`s performance and risk. First, we use extract_rets_pos_txn_from_zipline() to calculate returns, positions, and trading records.

import pyfolio as pf
returns, positions, transactions = pf.utils.extract_rets_pos_txn_from_zipline(results)

Daily Returns

Calculating daily portfolio return.

Daily portfolio return
Daily portfolio return

Holding Positions

  • Equity(0 [2330]): TSMC
  • Cash
Holding position record
Holding position record

Transaction Record

  • sid: index
  • symbol: ticker symbol
  • price: buy/sell price
  • order_id: order number
  • amount: trading amount
  • commission: commission cost
  • dt: trading date
  • txn_dollar: trading dollar volume
Trading record
Trading record

Making Performance Table

With show_perf_stats(), one can easily showcase the performance and risk analysis table.

import pyfolio as pf
pf.plotting.show_perf_stats(
returns,
benchmark_rets,
positions=positions,
transactions=transactions)
Performance Table
Performance Table

Plot Accumulated Return and Benchmark Return

benchmark_rets = results['benchmark_return'] 
pf.plotting.plot_rolling_returns(returns, factor_returns=benchmark_rets)
Figure for strategy returns
Figure for strategy returns

Conclusion

The strategy introduced in this session is one of the mean-reversion trading strategies. When the market is oversold (Bullish Divergence, BDI < 0), and the closing price is higher than the highest price over a certain period, it’s assumed that the stock price will gradually return to the moving average price, so a long position is entered. Conversely, when the stock price is overbought (Bearish Divergence, BDI > 0), and the closing price is lower than the lowest price over a certain period, it’s believed that the stock price has risen too much and has a downward trend. In this case, the long position is exited. However, it’s important to note that this strategy involves frequent trading, which transaction costs and taxes can erode. Therefore, it’s recommended to combine other technical indicators to optimize entry and exit points.

Finally, it’s worth mentioning again that the stocks discussed in this article are for illustrative purposes only and do not constitute recommendations for any financial products. If readers are interested in topics such as building strategies, performance backtesting, and empirical research, you are welcome to purchase solutions from TEJ E-Shop, which provides comprehensive databases to easily perform various tests and analyses.

Source Code

Extended Reading

Related Link

Back
Procesing