NokiMo
阿程量化交易
阿程量化交易

patreon


策略實盤真係會有高 Sharpe Ratio? 【真‧搵Alpha策略系列 6 (連 codes)】

曾經聽過唔少大行 quant trader / PM 都呻一個問題,就係回測出黎的策略,實盤開始運行個陣都賺錢,但過左一段時間後,好快就倒蝕,之後要調參/轉新策略,一直無辦法得出一個「聖杯」交易系統。


以上係佢地講法,並唔代表我的睇法。一個策略「賺唔賺錢」,可以有好多原因:


其實如果作為一個 quant trader / PM (唔計做HFT/ MM/ Nearly Pure Arb),將「聖杯」同一個策略放埋一齊去講,好似策略失效就代表「聖杯」失效咁,通常俾我感覺係佢應該唔係真係做 quant trader / PM。因為做得 quant 相關職位,一定知道,通常都係一系列的交易策略同時運作,大家取長補短,有啲策略賺錢時有啲策略輸錢,總之賺的>輸的,PNL便會緩緩上升。所以,其中一個策略失效只係而個過程中自然會發生的一件事,關鍵在唔好有「搵到一個策略就可以行足成世」的心態,因為做交易係一件持續的事,要不斷改進,「聖杯」永遠講緊既係自己的研究及執行能力。


講到而度,就延伸到一個非常重要的問題。假設我岩岩到一間新 quant fund,而我有10個低相關度的交易策略可以實盤執行。如果作為一個比較風險緊慎的量化投資組合經理,想問你會有咩諗法?


我既諗法係: 我交一份有幾差的功課先會令我無左份工?


而個係真正內行人先會諗過的問題,因為外行的通常只係諗,我的策略的stat significance夠唔夠,回測的sharpe夠唔夠高,高就用啦。 然後一堆高 sharpe (及高相關) 策略擺埋一齊,就以為出黎的 sharpe 都會高。但實際情況係,因為好多時策略的成效未必係我地可以控制到,外在環境影響太大,有 N種原因可以令多個策略同時失效。


所以,我地需要的係一個機制去計出,就算多個策略同時失效,或者之前回測得出的 sharpe 因為各種原因而不能反映「真正」 sharpe 數值時,我們要預先為個回測策略打個折扣,咁個折扣應該要點計。


以下是根據我上一篇文提及的那位 Top Quant, Dr. Giuseppe Paleologo, 在他的書中提及的一個計出折扣 (hair-cut) 的方法 (我做左些少修改,因為以前用開類近的方法,但公式上無咁elegant)。作者稱呼這個方法為 RademacherAntiSerum,當中用到 Rademacher Complexity 及一些比較複雜的數學概念。有興趣的朋友可以去睇下佢的 Proof,我地而度就直接睇公式同埋理解點解而條公式 works 就可以。


   (Elements of Quantitative Investing (Draft), 2024)


調一調 d terms,即係 真Theta >= estimated theta - 2R -3[] - [],所以下限即  estimated theta - 2R -3[] - []


上面的公式見到有個真Theta 及 estimated theta (estimate 果個其實就係我地回測得出黎的 sharpe ratio),另外公式的右邊(R.H.S.) 可分成 3 部份,分別是:

1.

而個 R term (而個係 Rademacher complexity term) 越細越好,佢講緊如果我地策略既 returns series 越fit到d random noises,就代表其實我地策略得出的 returns 其實同 random noises 無分別,所以 R term 越大即overfitting機會更大。




2.

成個term 用黎解釋 estimation error,T越大成個數越細,打折扣亦越少。(T即係 number of trading days experienced,而 delta 係個 significance number)


3.

主要睇個 N term,定義為有幾多個策略我地而家同時考慮運行,間接也暗示以往測試數量。N數字越大,Actual sharpe的下限相對越低。(我知有朋友會問,咩唔係越多策略同一時間運行 sharpe 會越高嗎? 答案你是對的。而個 N term 點解會用黎 discount 係希望反映 multiple testing 問題。但要記住,如果我有10個策略而 sharpe 係2, 對比我有100個策略個 sharpe 係3,就算而個時侯因為N大左要比佢打折,其實打完折我的 sharpe 都好大機會比只有10個策略的為高。所以 N 大左係負影響個 sharpe 下限其實係唔緊要,最重要係你回測的sharpe本身都相對大左)



我寫左一段 python codes for大家去做 demo,大家可以直接 paste 到自己 python env 試下 run。要留意,策略係用日 sharpe 去做demo,如果要變返做 annaulized 年化 sharpe ,記得將佢乘返 square root (252)。你可以將 returns_dict 入面的value (array of returns) 變做自己策略的 returns 就可以了。


好喇,咁樣打完折有咩用? 就係當你要做 portfolio construction 的時侯,你用的 sharpe 唔會再用原先做 backtesting 得出的 naive shapre,而係用 sharpe 下限去做,咁樣變相你portfolio construction 得出的 metrics 就係相對保守,得出左一個 base之後就可以基於而個base去leverage 同埋做唔同的 asset allocation,但又唔洗怕其中一啲策略失效從而大程度影響整個交易組合的績效。而個方法先係真正行內人士點樣做 portfolio,而唔係單單用幾個望落去看似高sharpe 的策略去構建一個 portfolio,但最後容易輸得一敗塗地。


最後,如果有朋友覺得上面可能太過複雜或者太過理論,其實唔緊要,最緊要係明白而篇文想強調係 construct portfolio 時記得多用下限而個 concept,將你放入去 portfolio optimization 的 metrics 調到連現實都難以再低 (例如有某啲策略你見個 sharpe 一跌穿某個數你已經不考慮用) 的數值,咁樣你就知你成個 portfolio係 optimization完左後,係絕大部份時侯可以有幾差,有個底係手,再 build 上去做 capital allocation。


Codes: 

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt


class RademacherAntiSerum:

    def __init__(self, returns_dict):

        self.returns_dict = returns_dict

        self.N = len(returns_dict)

        self.T = max(len(returns) for returns in returns_dict.values())

        

    def compute_rademacher_complexity(self, num_samples=5000):

        rademacher_complexities = []

        

        for  in range(numsamples):

            supremum = 0

            for returns in self.returns_dict.values():

                T = len(returns)

                epsilon = np.random.choice([-1, 1], size=T)

                supremum = max(supremum, np.abs(np.dot(epsilon, returns)) / T)

            rademacher_complexities.append(supremum)

        

        return np.mean(rademacher_complexities)

    

    def compute_sharpe_ratios(self):

        return {k: np.mean(v) / np.std(v) for k, v in self.returns_dict.items()}

    

    def ras_for_sharpe(self, delta=0.05):

        sharpe_ratios = self.compute_sharpe_ratios()

        rademacher_complexity = self.compute_rademacher_complexity()

        

        lower_bounds = {}

        for k, sharpe in sharpe_ratios.items():

            T = len(self.returns_dict[k])

            lower_bounds[k] = sharpe - (

                2 * rademacher_complexity + 

                3  np.sqrt(2  np.log(2/delta) / T) +

                np.sqrt(2  np.log(2self.N/delta) / T)

            )

        

        return sharpe_ratios, lower_bounds

    

    def display_results(self):

        original_sharpe, lower_bounds = self.ras_for_sharpe()

        

        results = pd.DataFrame({

            'Original Sharpe': original_sharpe,

            'Lower Bound': lower_bounds

        })

        

        print(results)

        

        # Plotting

        plt.figure(figsize=(12, 6))

        strategies = list(original_sharpe.keys())

        plt.bar(strategies, original_sharpe.values(), alpha=0.5, label='Original Sharpe')

        plt.bar(strategies, lower_bounds.values(), alpha=0.5, label='Lower Bound')

        plt.xlabel('Strategy')

        plt.ylabel('Sharpe Ratio')

        plt.title('Original Daily Sharpe Ratios vs Lower Bounds Daily Sharpe Ratio')

        plt.legend()

        plt.xticks(rotation=45, ha='right')

        plt.tight_layout()

        plt.show()




def generate_returns(T, daily_mean, daily_std):

    return np.random.normal(loc=daily_mean, scale=daily_std, size=T)


returns_dict = {

    'Strategy 1': generate_returns(5000, 0.0012, 0.005),

    'Strategy 2': generate_returns(5000, 0.0011, 0.0048),

    'Strategy 3': generate_returns(5000, 0.0013, 0.0052),

    'Strategy 4': generate_returns(5000, 0.0010, 0.0045),

    'Strategy 5': generate_returns(5000, 0.0014, 0.0055)

}


ras = RademacherAntiSerum(returns_dict)

ras.display_results()



Cheers, 

阿程

2024年8月 - 第2篇 (真‧搵Alpha策略系列 6) 



 




策略實盤真係會有高 Sharpe Ratio? 【真‧搵Alpha策略系列 6 (連 codes)】

Related Creators