{"id":2178,"date":"2022-12-26T10:03:00","date_gmt":"2022-12-26T10:03:00","guid":{"rendered":"https:\/\/www.tejwin.com\/en\/insights\/price-deviation-ratio-trading-strategy\/"},"modified":"2022-12-26T10:03:00","modified_gmt":"2022-12-26T10:03:00","slug":"price-deviation-ratio-trading-strategy","status":"publish","type":"insight","link":"https:\/\/www.tejwin.com\/en\/insights\/price-deviation-ratio-trading-strategy\/","title":{"rendered":"Price Deviation Ratio Trading Strategy"},"content":{"rendered":"<p class=\"wp-block-paragraph\" id=\"50a2\">Create a price deviation ratio trading strategy using python and perform historical backtesting.<\/p>\n<figure class=\"wp-block-image aligncenter\"><img loading=\"lazy\" decoding=\"async\" alt=\"Price Deviation Ratio Trading Strategy\" class=\"wp-image-17879\" height=\"649\" src=\"https:\/\/www.tejwin.com\/en\/wp-content\/uploads\/2026\/08\/image-273.png\" width=\"865\"\/><figcaption class=\"wp-element-caption\">Photo by\u00a0<a href=\"https:\/\/unsplash.com\/photos\/dBI_My696Rk\" rel=\"noreferrer noopener\" target=\"_blank\">Chris Liverani<\/a>\u00a0on\u00a0<a href=\"https:\/\/unsplash.com\/photos\/dBI_My696Rk\" rel=\"noreferrer noopener\" target=\"_blank\">Unsplash<\/a><\/figcaption><\/figure>\n<h2 class=\"wp-block-heading\">Summary of Key Points in This Article<\/h2>\n<p class=\"wp-block-paragraph\">Article Difficulty: \u2605\u2606\u2606\u2606\u2606<\/p>\n<p class=\"wp-block-paragraph\">Calculate the N-day Price Deviation Ratio Indicator using unadjusted closing prices of individual stocks and use the N-day previous low and high prices as entry and exit signals.<\/p>\n<p class=\"wp-block-paragraph\">Reading Recommendation: This article primarily uses the Price Deviation Ratio and historical high and low points as entry and exit criteria. It provides a framework for readers to reference when conducting backtesting using programming.<\/p>\n<h2 class=\"wp-block-heading\">Introduction<\/h2>\n<p class=\"wp-block-paragraph\">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\u2019s called a \u2018positive deviation.\u2019 Conversely, it\u2019s called\u2019 negative deviation\u2019 when it consistently falls below the moving average price.\u2019 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:<\/p>\n<p class=\"wp-block-paragraph\">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\u2019s opening price.<\/p>\n<p class=\"wp-block-paragraph\">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\u2019s opening price.<\/p>\n<h2 class=\"wp-block-heading\">Editorial Environment and Module Requirements<\/h2>\n<p class=\"wp-block-paragraph\">This article uses Windows OS and employs Jupyter as the editor.<\/p>\n<pre class=\"wp-block-preformatted\">import tejapi as tej<br\/>import pandas as pd<br\/>import numpy as np<br\/>import datetime<br\/>import matplotlib.pyplot as plt<br\/>import ffn<br\/><br\/>tej.ApiConfig.api_key = 'Your Key'<br\/><br\/>plt.rcParams['font.family'] = 'Noto Sans TC'<br\/><br\/>%matplotlib inline<\/pre>\n<h2 class=\"wp-block-heading\" id=\"db4c\">Database <\/h2>\n<p class=\"wp-block-paragraph\" id=\"232a\"><a class=\"ek-link\" href=\"https:\/\/api.tej.com.tw\/columndoc.html?subId=42\" rel=\"noreferrer noopener\" target=\"_blank\">\u4e0a\u5e02(\u6ac3)\u672a\u8abf\u6574\u80a1\u50f9(\u65e5)(TWN\/APRCD)<\/a><\/p>\n<h2 class=\"wp-block-heading\">Data Import<\/h2>\n<p class=\"wp-block-paragraph\">The data period is from 2015\/01\/05 to 2022\/11\/25, and for this example, we will retrieve unadjusted open, high, low, and close prices for TSMC (Taiwan Semiconductor Manufacturing Company).<\/p>\n<pre class=\"wp-block-preformatted\">#\u4e0a\u5e02\u6ac3\u516c\u53f8\u4ee3\u865f<br\/><br\/>price = tej.get('TWN\/APRCD', <br\/>                coid='2330',<br\/>                mdate={'gt': '2015-01-01', 'lt':'2022-11-28'}, <br\/>                opts={'columns': ['coid', 'mdate', 'close_d', 'high_d', 'low_d', 'open_d']},<br\/>                chinese_column_name=True, <br\/>                paginate=True)<br\/><br\/><br\/>price['\u5e74\u6708\u65e5'] = price['\u5e74\u6708\u65e5'].dt.date<br\/>price = price.reset_index(drop=True)<\/pre>\n<p class=\"wp-block-paragraph\" id=\"6fde\">After obtaining the stock prices, the first step is to calculate the Price Deviation Ratio (PD ratio) indicator. This is done by dividing the daily closing price by the moving average price over the past N days. It\u2019s a good practice to write this calculation as a function for ease of parameter optimization in the subsequent steps.<\/p>\n<pre class=\"wp-block-preformatted\">def bias_cal(tmp ,n):<br\/>    <br\/>    df = tmp.copy()<br\/><br\/>    df['BIAS'] = ((df['\u6536\u76e4\u50f9(\u5143)'] - df['\u6536\u76e4\u50f9(\u5143)'].rolling(n).mean()) \/ df['\u6536\u76e4\u50f9(\u5143)'].rolling(n).mean()).round(4)<br\/><br\/>    return df<br\/><br\/>bias_cal(price, 20).tail(10)<\/pre>\n<figure class=\"wp-block-image aligncenter caption-align-center\"><img loading=\"lazy\" decoding=\"async\" alt=\"\" class=\"wp-image-17867\" height=\"386\" src=\"https:\/\/www.tejwin.com\/en\/wp-content\/uploads\/2026\/08\/image-268.png\" width=\"771\"\/><figcaption class=\"wp-element-caption\">Price Deviation Ratio (PD ratio) indicator<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"8d10\">After calculating the price deviation ratio (PD ratio) for each day, historical backtesting is performed with the following transaction costs: buying commission of 0.1425%, and selling commission of 0.1425% plus a transaction tax of 0.3%. These calculations are encapsulated in functions to facilitate parameter optimization in the future.<\/p>\n<pre class=\"wp-block-preformatted\">def performance(tmp, n, init=1000000):<br\/>    df = tmp.copy()<br\/><br\/>    signal = 0<br\/><br\/>    df['his_low'] = df['\u6700\u4f4e\u50f9(\u5143)'].shift(n)<br\/>    df['his_high'] = df['\u6700\u9ad8\u50f9(\u5143)'].shift(n)<br\/><br\/>    df.loc[0, '\u73fe\u91d1'] = init<br\/>    df['\u9032\u51fa\u5834'] = 0<br\/><br\/>    for i in range(1, len(df)):<br\/><br\/>        if (df.loc[i-1, '\u6536\u76e4\u50f9(\u5143)'] &gt; df.loc[i-1, 'his_high']) &amp; (df.loc[i-1, 'BIAS'] &lt; 0) &amp; (signal == 0):   #\u524d\u4e00\u5929\u6536\u76e4\u50f9\u5c0f\u65bcn\u5929\u524d\u6700\u4f4e\u50f9\uff0c\u4e14\u6536\u76e4\u4e56\u96e2\u7387&lt;=0\uff0c\u5247\u7576\u5929\u958b\u76e4\u50f9\u9032\u5834<br\/><br\/>            df.loc[i, '\u80a1\u7968'] =  df.loc[i, '\u958b\u76e4\u50f9(\u5143)'] * 1000<br\/>            df.loc[i, '\u4ea4\u6613\u6210\u672c'] = round(-df.loc[i, '\u958b\u76e4\u50f9(\u5143)']*1000*0.001425)<br\/>            df.loc[i, '\u73fe\u91d1'] = df.loc[i-1, '\u73fe\u91d1'] - df.loc[i, '\u80a1\u7968'] + df.loc[i, '\u4ea4\u6613\u6210\u672c']<br\/>            df.loc[i, '\u9032\u51fa\u5834'] = '\u9032\u5834\u5efa\u5009'<br\/>            <br\/>            signal = 1<br\/><br\/>        elif (df.loc[i-1, '\u6536\u76e4\u50f9(\u5143)'] &lt; df.loc[i-1, 'his_low']) &amp; (df.loc[i-1, 'BIAS'] &gt; 0) &amp; (signal == 1):   #\u524d\u4e00\u5929\u6536\u76e4\u50f9\u5927\u65bcn\u5929\u524d\u6700\u9ad8\u50f9\uff0c\u4e14\u6536\u76e4\u4e56\u96e2\u7387&gt;=0\uff0c\u5247\u7576\u5929\u958b\u76e4\u50f9\u51fa\u5834<br\/><br\/>            df.loc[i, '\u80a1\u7968'] =  0<br\/>            df.loc[i, '\u4ea4\u6613\u6210\u672c'] = round(-df.loc[i, '\u958b\u76e4\u50f9(\u5143)']*1000*0.004425)<br\/>            df.loc[i, '\u73fe\u91d1'] = df.loc[i-1, '\u73fe\u91d1'] + df.loc[i, '\u958b\u76e4\u50f9(\u5143)']*1000 + df.loc[i, '\u4ea4\u6613\u6210\u672c']<br\/>            df.loc[i, '\u9032\u51fa\u5834'] = '\u51fa\u5834\u5e73\u5009'<br\/>            <br\/>            signal = 0<br\/><br\/>        elif signal == 1:<br\/><br\/>            df.loc[i, '\u80a1\u7968'] = df.loc[i, '\u6536\u76e4\u50f9(\u5143)'] * 1000<br\/><br\/>            df.loc[i, '\u73fe\u91d1'] = df.loc[i-1, '\u73fe\u91d1']<br\/><br\/>        else:<br\/><br\/>            df.loc[i, '\u73fe\u91d1'] = df.loc[i-1, '\u73fe\u91d1']<br\/><br\/>    df['\u80a1\u7968'] = df['\u80a1\u7968'].fillna(0)<br\/>    df['\u4ea4\u6613\u6210\u672c'] = df['\u4ea4\u6613\u6210\u672c'].fillna(0)<br\/>    df['\u7e3d\u6b0a\u76ca'] = df['\u73fe\u91d1'] + df['\u80a1\u7968']<br\/><br\/>    print('\u7e3d\u6b0a\u76ca\uff1a', df['\u7e3d\u6b0a\u76ca'].iloc[-1])<br\/>    print('\u4ea4\u6613\u6210\u672c\uff1a', df['\u4ea4\u6613\u6210\u672c'].sum())<br\/>    print('\u5831\u916c\u7387\uff1a', '%.2f'%((df['\u7e3d\u6b0a\u76ca'].iloc[-1] - init)\/init*100), '%')<br\/><br\/>    print('\u5e74\u5747\u5831\u916c\u7387\uff1a', '%.3f'%(df['\u7e3d\u6b0a\u76ca'].pct_change().mean()*252*100), '%')<br\/>    print('\u5e74\u5316\u6a19\u6e96\u5dee\uff1a', '%.3f'%(df['\u7e3d\u6b0a\u76ca'].pct_change().std()*np.sqrt(252)*100), '%')<br\/>    print('\u590f\u666e\u503c\uff1a', '%.3f'%((df['\u7e3d\u6b0a\u76ca'].pct_change().mean()*252*100) \/ (df['\u7e3d\u6b0a\u76ca'].pct_change().std()*np.sqrt(252)*100)))<br\/><br\/>    print('\u6700\u5927\u56de\u64a4\uff1a', '%.2f'%(((df['\u7e3d\u6b0a\u76ca'] \/ df['\u7e3d\u6b0a\u76ca'].cummax() - 1).cummin().iloc[-1])*100), '%')<br\/><br\/>    return df<br\/><br\/>result = performance(bias_cal(price, 20), 20) <\/pre>\n<p class=\"wp-block-paragraph\" id=\"867c\">\u9996\u5148\u7528\u4e00\u500b\u670820\u5929\uff0c\u8d77\u59cb\u91d1\u984d100\u842c\u70ba\u53c3\u6578\u4f86\u770b\u770b\u6210\u679c\uff0c\u53ef\u4ee5\u770b\u51fa\u5230\u4e86\u6700\u5f8c\u4e00\u5929\u53ea\u5269\u4e0b90\u842c\uff0c\u5176\u4e2d\u5927\u90e8\u5206\u90fd\u88ab\u4ea4\u6613\u6210\u672c\u7d66\u8017\u5149\u4e86\uff0c\u53ea\u8cfa\u5230\u5fae\u5e45\u7684\u5831\u916c1.17%\u3002<\/p>\n<figure class=\"wp-block-image aligncenter caption-align-center\"><img loading=\"lazy\" decoding=\"async\" alt=\"\" class=\"wp-image-17871\" height=\"216\" src=\"https:\/\/www.tejwin.com\/en\/wp-content\/uploads\/2026\/08\/image-270.png\" width=\"206\"\/><figcaption class=\"wp-element-caption\">N=20<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"c3de\">Visualizing the total equity and drawdown curve, we can see the results. It\u2019s evident that starting from mid-2021, there was a significant upward trend with TSMC. However, in recent times, due to factors like interest rate hikes and the Ukraine-Russia conflict, it has experienced its largest drawdown to date, giving back some of the previous gains.<\/p>\n<pre class=\"wp-block-preformatted\">n = 20<br\/>result = performance(bias_cal(price, n), n)<br\/>fig, ax = plt.subplots(2, 1, figsize=(20, 12), sharex=True)<br\/><br\/>ax[0].plot(result['\u5e74\u6708\u65e5'], result['\u7e3d\u6b0a\u76ca'])<br\/><br\/>ax[1].plot(result['\u5e74\u6708\u65e5'], result['\u7e3d\u6b0a\u76ca'].to_drawdown_series().to_list(), color='c')<br\/><br\/>dd = (result['\u7e3d\u6b0a\u76ca'].to_drawdown_series()*100).to_list()<br\/><br\/>ax[1].fill_between(result['\u5e74\u6708\u65e5'], np.zeros(len(result['\u7e3d\u6b0a\u76ca'])), dd, label='\u5927\u76e4 DD', color='blue', linewidth=1, alpha=0.8)<br\/><br\/>ax[0].set_title(f'\u53c3\u6578{n}\uff0d\u7e3d\u6b0a\u76ca &amp; \u56de\u64a4\u66f2\u7dda\u5716', fontsize=20)<br\/>ax[1].set_xlabel('\u6642\u9593', fontsize=20)<br\/><br\/>ax[0].set_ylabel('\u7d2f\u7a4d\u91d1\u984d', fontsize=20)<br\/>ax[1].set_ylabel('\u4e0b\u8dcc\u5e45\u5ea6(%)', fontsize=20)<br\/><br\/><br\/>plt.subplots_adjust(hspace=.0)<\/pre>\n<figure class=\"wp-block-image aligncenter caption-align-center\"><img loading=\"lazy\" decoding=\"async\" alt=\"\" class=\"wp-image-17875\" height=\"537\" src=\"https:\/\/www.tejwin.com\/en\/wp-content\/uploads\/2026\/08\/image-271.png\" width=\"865\"\/><figcaption class=\"wp-element-caption\">Total Equity and Drawdown Curve Chart<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\" id=\"1e72\">In the subsequent steps, we further optimize the parameters to find the best N days, ranging from 2 days to 50 days, selecting the parameter that results in the highest Sharpe ratio.<\/p>\n<pre class=\"wp-block-preformatted\">optim = []<br\/>for i in range(2, 51):<br\/>    df = performance(bias_cal(price, i), i)<br\/><br\/>    sharp = ((df['\u7e3d\u6b0a\u76ca'].pct_change().mean()*252) \/ (df['\u7e3d\u6b0a\u76ca'].pct_change().std()*np.sqrt(252)))<br\/><br\/>    ret = round(((df['\u7e3d\u6b0a\u76ca'].iloc[-1] \/ df['\u7e3d\u6b0a\u76ca'].iloc[0]) - 1), 4)*100<br\/><br\/>    optim.append((i, sharp, ret))<br\/><br\/>    result1 = pd.DataFrame(optim, columns=['\u5929\u6578', '\u590f\u666e\u503c', '\u5831\u916c\u7387'])<br\/><br\/>result1.sort_values('\u590f\u666e\u503c', ascending=False).head(5)<\/pre>\n<p class=\"wp-block-paragraph\" id=\"be9b\">After optimizing with different parameters, it was found that a 7-day calculation period performed the best in historical backtesting.<\/p>\n<figure class=\"wp-block-image aligncenter caption-align-center\"><img loading=\"lazy\" decoding=\"async\" alt=\"\" class=\"wp-image-17869\" height=\"213\" src=\"https:\/\/www.tejwin.com\/en\/wp-content\/uploads\/2026\/08\/image-269.png\" width=\"266\"\/><figcaption class=\"wp-element-caption\">Sharpe Ratio<\/figcaption><\/figure>\n<pre class=\"wp-block-preformatted\">result = performance(bias_cal(price, 7), 7)<\/pre>\n<p class=\"wp-block-paragraph\" id=\"e3c6\">The overall Sharpe ratio has improved from 0.055 to 0.826, and the return rate has increased significantly to nearly 40%. Additionally, the maximum drawdown has also decreased significantly.<\/p>\n<figure class=\"wp-block-image aligncenter caption-align-center\"><img loading=\"lazy\" decoding=\"async\" alt=\"\" class=\"wp-image-17873\" height=\"216\" src=\"https:\/\/www.tejwin.com\/en\/wp-content\/uploads\/2026\/08\/image-270.png\" width=\"206\"\/><figcaption class=\"wp-element-caption\">N=7<\/figcaption><\/figure>\n<p class=\"wp-block-paragraph\">From the equity curve, it can be seen that the strategy had good performance around the beginning of 2022 but was ultimately unable to withstand the recent strong interest rate hikes, resulting in the largest drawdown since 2015.<\/p>\n<figure class=\"wp-block-image aligncenter is-resized caption-align-center\"><img loading=\"lazy\" decoding=\"async\" alt=\"\" class=\"wp-image-17877\" height=\"513\" src=\"https:\/\/www.tejwin.com\/en\/wp-content\/uploads\/2026\/08\/image-272.png\" style=\"width:842px;height:513px\" width=\"842\"\/><figcaption class=\"wp-element-caption\">Total Equity and Drawdown Curve Graph<br \/><\/figcaption><\/figure>\n<h2 class=\"wp-block-heading\" id=\"21d9\">Conclusion<\/h2>\n<p class=\"wp-block-paragraph\">The strategy introduced in this session is one of the mean-reversion trading strategies. When the market is oversold (Bullish Divergence, BDI &lt; 0), and the closing price is higher than the highest price over a certain period, it\u2019s 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 &gt; 0), and the closing price is lower than the lowest price over a certain period, it\u2019s believed that the stock price has risen too much and has a downward trend. In this case, the long position is exited. However, it\u2019s important to note that this strategy involves frequent trading, which can be eroded by transaction costs and taxes. Therefore, it\u2019s recommended to combine other technical indicators to optimize entry and exit points.<\/p>\n<p class=\"wp-block-paragraph\">Finally, it\u2019s 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.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Create a price deviation ratio trading strategy using python and perform historical backtesting. Summary of Key Points in This Article Article Difficulty: \u2605\u2606\u2606\u2606\u2606 Calculate the N-day Price Deviation Ratio Indicator using unadjusted closing prices of individual stocks and use the N-day previous low and high prices as entry and exit signals. Reading Recommendation: This article [\u2026]<\/p>\n","protected":false},"featured_media":2177,"template":"","tags":[50,65,70,71],"insight_category":[13],"class_list":["post-2178","insight","type-insight","status-publish","has-post-thumbnail","hentry","tag-market-data","tag-python","tag-tej-api","tag-quantitative-strategy","insight_category-quant-research"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.tejwin.com\/en\/wp-json\/wp\/v2\/insight\/2178","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.tejwin.com\/en\/wp-json\/wp\/v2\/insight"}],"about":[{"href":"https:\/\/www.tejwin.com\/en\/wp-json\/wp\/v2\/types\/insight"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.tejwin.com\/en\/wp-json\/wp\/v2\/media\/2177"}],"wp:attachment":[{"href":"https:\/\/www.tejwin.com\/en\/wp-json\/wp\/v2\/media?parent=2178"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tejwin.com\/en\/wp-json\/wp\/v2\/tags?post=2178"},{"taxonomy":"insight_category","embeddable":true,"href":"https:\/\/www.tejwin.com\/en\/wp-json\/wp\/v2\/insight_category?post=2178"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}