เป้าหมายของบทความนี้มีสองเท่า ประการแรก เพื่อทำให้แนวคิดเบื้องหลังรายงานยอดนิยมนี้ง่ายขึ้น :”ความน่าจะเป็นของการทดสอบย้อนกลับที่ไม่เหมาะสมเกินไป” https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2326253 โดย Bailey , https://www.linkedin.com/in/david-h-bailey-72b471/, บอร์ไวน์ https://www.linkedin.com/in/jonathan-borwein-2157a835/, ปราโด https ://www.linkedin.com/in/lopezdeprado/ และ Zhu https://www.linkedin.com/in/qiji-zhu-1669b71a/ และนำเสนอในรูปแบบที่ผู้อ่านเข้าใจได้ง่าย และประการที่สอง เห็นภาพแนวคิดเหล่านี้เพื่อช่วยในการทำความเข้าใจเพิ่มเติม
บทความทางวิทยาศาสตร์จำนวนมากอาศัยสัญกรณ์ที่ซับซ้อนและคำศัพท์เฉพาะทาง ซึ่งอาจเป็นอุปสรรคสำหรับผู้อ่านที่ไม่คุ้นเคยกับสาขานี้ ด้วยการแบ่งแนวคิดออกเป็นคำอธิบายง่ายๆ ทีละขั้นตอน และให้รหัส Python แก่ผู้อ่านเพื่อทำซ้ำกระบวนการ ฉันหวังว่าจะทำให้แนวคิดนี้เข้าถึงได้มากขึ้น นอกจากนี้ ด้วยการแสดงภาพแนวคิด ฉันตั้งเป้าที่จะทำให้เข้าใจง่ายยิ่งขึ้น
การกำหนดโมเดลและการรวบรวมข้อมูล:
ส่วนประกอบแรกคือแบบจำลองพาราเมตริก ซึ่งหมายความว่ากลยุทธ์นั้นเป็นฟังก์ชันของชุดข้อมูลและพารามิเตอร์ n ตัว
พารามิเตอร์เหล่านี้มีค่าที่กำหนดไว้ล่วงหน้าซึ่งกำหนดพื้นที่รวมของโมเดล ซึ่งแสดงด้วยชุดค่าผสม n_plet ที่เป็นไปได้ทั้งหมด [p1, p2….pn]
n_plets ที่ไม่ซ้ำกันแต่ละอันจะสร้างสัญญาณการซื้อขายเฉพาะ และทุกสัญญาณจะสอดคล้องกับค่าเฉพาะของฟังก์ชันฟิตเนส ซึ่งเราเลือกเพื่อปรับเทียบโมเดล
การปรับเทียบโมเดลเกี่ยวข้องกับการระบุ n_plet ที่เพิ่มฟังก์ชันฟิตเนสให้สูงสุดในพื้นที่พารามิเตอร์
ตัวอย่างเช่น โมเดลตามฤดูกาลขั้นพื้นฐานซึ่งทุกๆ วันฉันเปิดการซื้อขายในเวลาที่กำหนดและปิดในเวลาอื่น จะมีการอธิบายโดยพารามิเตอร์ 2 ตัว: ชั่วโมงเข้าและชั่วโมงออก และพื้นที่พารามิเตอร์จะถูกสร้างขึ้นโดยทั้งหมด คู่ (รายการ T, T ออก)
หากเราถือว่าตราสารซื้อขาย 23 ชั่วโมงทุกวัน จะมีคู่ที่เป็นไปได้ 23*22 หรือ 2_plets
ขั้นตอนแรกคือการสร้างเมทริกซ์ที่ครอบคลุมอนุกรมเวลาของผลตอบแทนทั้งหมดที่เกี่ยวข้องกับปริภูมิพาราเมตริก จำนวนคอลัมน์ในเมทริกซ์นี้เทียบเท่ากับชุดพารามิเตอร์ที่เป็นไปได้ทั้งหมด (เช่น 23*22 ในตัวอย่างง่ายๆ ก่อนหน้านี้) ในขณะเดียวกัน จำนวนแถวจะถูกกำหนดโดยจำนวนการประทับเวลาในอนุกรมเวลาส่งคืน
ตัวอย่างเช่น สมมติว่าเราสังเกตการณ์ราคามาสิบปี และแต่ละปีมี 250 วันทำการซื้อขาย ในกรณีดังกล่าว จำนวนคอลัมน์ในเมทริกซ์จะเป็น (250*10)-1 (ไม่รวมคอลัมน์หนึ่งเนื่องจากการคำนวณเปลี่ยนจากราคาเป็นผลตอบแทน)
การกำหนด CSVC
CSVC ย่อมาจากการตรวจสอบข้ามแบบสมมาตรแบบผสมผสาน
ขั้นตอน CSVC เกี่ยวข้องกับการแบ่งชุดข้อมูลออกเป็นส่วน 2N ครึ่งหนึ่งของส่วนเหล่านี้จะใช้ในการปรับเทียบโมเดล และอีกครึ่งหนึ่งจะใช้ในการประเมินประสิทธิภาพของกลยุทธ์ด้วยข้อมูลที่มองไม่เห็น ขั้นตอนนี้ทำซ้ำในทุกวิถีทางที่เป็นไปได้ โดยสามารถเลือกส่วน N จากส่วน 2N ได้ และจำนวนวิธีที่เป็นไปได้กำหนดโดยสัมประสิทธิ์ทวินาม C(2N,N) ตามที่อธิบายไว้ในลิงก์: https:// en.wikipedia.org/wiki/Combination
เพื่ออธิบายแนวคิดนี้ ลองพิจารณาตัวอย่างที่ชุดข้อมูลแบ่งออกเป็น 6 ส่วน ในกรณีนี้ 3 ส่วนจะถูกใช้สำหรับชุดตัวอย่าง/การฝึกอบรม/การสอบเทียบ และอีก 3 ส่วนที่เหลือจะถูกใช้สำหรับชุดนอกตัวอย่าง/การทดสอบ/การตรวจสอบความถูกต้อง จำนวนชุดค่าผสมที่เป็นไปได้โดยสามารถเลือก 3 ส่วนจากชุด 6 ชุดได้ โดยค่าสัมประสิทธิ์ทวินาม C (6,3) = 20 รูปด้านล่างแสดงชุดค่าผสมที่เป็นไปได้ทั้งหมด 20 ชุด:
ในแต่ละพาร์ติชัน ฟังก์ชันเป้าหมายจะถูกคำนวณสำหรับชุดพารามิเตอร์ที่เป็นไปได้ทั้งหมดทั้งบนสีแดงและบนพื้นที่สีเขียว
เพื่อให้ชัดเจน เรามาลองนึกภาพวิธีการสำหรับโมเดลที่มี 5 ชุดค่าผสมที่เป็นไปได้ การรวมกันแต่ละครั้งจะก่อให้เกิดกลยุทธ์และจากนี้ไปเป็นเส้นอิควิตี้ ฟังก์ชันเป้าหมายได้รับการคำนวณแยกกันสำหรับช่วงชุดข้อมูลที่สอดคล้องกับพื้นที่สีแดงและพื้นที่สีเขียว
สมมติว่าตอนนี้กรอบงานชัดเจนแล้ว ขั้นตอนต่อไปคือการจัดอันดับชุดค่าผสมพารามิเตอร์ตามค่าของฟังก์ชันเป้าหมายในชุดข้อมูลทั้งในตัวอย่างและนอกตัวอย่าง
ในตาราง OSRIk คือการจัดอันดับนอกกลุ่มตัวอย่างของชุดพารามิเตอร์ที่จัดอยู่ในอันดับที่ 8 ในพาร์ติชันที่ k ของในตัวอย่าง/นอกตัวอย่าง
เมื่อพิจารณาเฉพาะแถวแรกของตารางก่อนหน้า เราจะได้รายการอันดับที่ไม่อยู่ในกลุ่มตัวอย่างที่สอดคล้องกับคำตอบที่ดีที่สุดในกลุ่มตัวอย่าง กล่าวอีกนัยหนึ่ง เรามีรายการที่บอกเราเกี่ยวกับทุกพาร์ติชั่นของชุดข้อมูล ว่าการรวมพารามิเตอร์ที่เพิ่มฟังก์ชันเป้าหมายให้สูงสุดมีประสิทธิภาพอย่างไรในกลุ่มตัวอย่างเมื่อเทียบกับชุดพารามิเตอร์อื่นๆ ทั้งหมด
แนวคิดก็คือ หากโมเดลไม่ได้ถูกติดตั้งมากเกินไป การรวมพารามิเตอร์ที่ทำงานได้ดีที่สุดในตัวอย่างในตัวอย่างจะไม่อยู่ในอันดับต่ำอย่างสม่ำเสมอเมื่อเทียบกับชุดพารามิเตอร์อื่นๆ หากทำการทดสอบนอกตัวอย่าง
ขั้นตอนต่อไปประกอบด้วยการใช้รายการอันดับนอกกลุ่มตัวอย่างตามที่กำหนดไว้ข้างต้น เพื่อประเมิน "ความน่าจะเป็นของการจัดลำดับที่มากเกินไป"
ตัวอย่าง:
ลองพิจารณาแนวคิดข้างต้นด้วยการตรวจสอบชุดของผลตอบแทนที่สร้างขึ้นอย่างปลอมๆ จำนวน 5,000 รายการ แต่ละชุดประกอบด้วยการสังเกต 1,000 รายการ หากนี่เป็นแบบจำลองในโลกแห่งความเป็นจริง ก็จะบอกเป็นนัยว่ามีชุดพารามิเตอร์ที่เป็นไปได้ 5,000 ชุด และชุดข้อมูลจะครอบคลุมช่วง 1,000 วัน
ให้เราตัดสินใจแบ่งการประทับเวลา 1,000 รายการออกเป็น 16 ส่วน วิธีเลือกองค์ประกอบ 16/2 = 8 ตัวจากชุด 16 ตัวกำหนดโดยสัมประสิทธิ์ทวินาม (16,8) = 12870 ซึ่งหมายความว่าจำนวนพาร์ติชันของตัวอย่างในตัวอย่าง/นอกตัวอย่างในตัวอย่างของเราเท่ากับ 12870 .
เพื่อแสดงเวกเตอร์ของการจัดอันดับนอกตัวอย่าง OSRik ด้วยวิธีที่สวยงามทางคณิตศาสตร์มากขึ้น เราสามารถใช้ฟังก์ชัน logit
โดยเฉพาะอย่างยิ่ง หากอันดับของชุดพารามิเตอร์ในตัวอย่างที่ดีที่สุดในชุดนอกตัวอย่างอยู่ที่ครึ่งล่าง ค่า logit จะเป็นลบ มิฉะนั้นจะเป็นค่าบวก
การพล็อตฮิสโตแกรมของค่า logit สามารถช่วยให้เราเห็นภาพการโอเวอร์ฟิตของโมเดลได้ หากฮิสโตแกรมมุ่งความสนใจไปที่ด้านลบ แสดงว่าโมเดลนั้นมีแนวโน้มว่าจะพอดีเกินไป ถ้ามันอยู่ในด้านบวก แสดงว่ามันไม่เหมาะสม
ในตัวอย่างเฉพาะของเรา ฮิสโตแกรมมีลักษณะดังนี้:
ในการคำนวณความน่าจะเป็นของการติดตั้งมากเกินไป เราเพียงหารจำนวนครั้งทั้งหมดโดยที่ค่าบันทึกน้อยกว่าศูนย์ด้วยจำนวนครั้งทั้งหมด
ภาคผนวก: สคริปต์ Python
ในส่วนนี้คุณจะพบสคริปต์หลามที่จะช่วยให้คุณสามารถคำนวณความน่าจะเป็นของการทดสอบย้อนกลับที่ให้มากเกินไป:
- dataframe ของผลตอบแทนที่แต่ละคอลัมน์แสดงถึงอนุกรมเวลาของผลตอบแทนของการรวมพารามิเตอร์ที่กำหนดของแบบจำลอง - ป้ายกำกับเป็นสรุป
- จำนวนส่วนที่ชุดข้อมูลถูกแยก -› ระบุว่าเป็น n_partition
from multiprocessing_utils import * from itertools import combinations import matplotlib.pyplot as plt import bottleneck as bn import numpy as np import pandas as pd class oft: def __init__(self, SUMMARY, n_partitions): # Initialize instance variables self.SUMMARY = SUMMARY # A pandas dataframe self.n_partitions = n_partitions # An integer self.len_sample = len(self.SUMMARY.columns) # The number of columns in the dataframe self.oft_combinations = self.create_IS_OS_combinations() # A list of tuples containing index sets self.logits = run_simulation(self.calc_logit, self.oft_combinations) # A list of logits self.show_results() # A method to print the results def create_IS_OS_combinations(self): # Create index sets for in-sample (IS) and out-of-sample (OS) combinations combs_IS = list(combinations(range(self.n_partitions), int(self.n_partitions/2))) combs_OS = [list(set(range(self.n_partitions)) - set(comb)) for comb in combs_IS] # Split the rows of the dataframe into partitions splitted = np.array_split(self.SUMMARY.index, self.n_partitions) # Combine the partitions into IS and OS sets OS = [np.concatenate([splitted[i] for i in [*comb]]) for comb in combs_OS] IS = [np.concatenate([splitted[i] for i in [*comb]]) for comb in combs_IS] # Return a list of tuples containing the IS and OS sets return zip(IS, OS) def find_OS_rank_of_best_IS(self, tuple_IS_OS): # Given an IS and OS combination, find the rank of the best IS in the OS set IS = tuple_IS_OS[0] OS = tuple_IS_OS[1] # Calculate the Sharpe ratio for the IS and OS sets ranked_sharpe_IS = self.rank(self.calc_sharpe, self.SUMMARY, IS) ranked_sharpe_OS = self.rank(self.calc_sharpe, self.SUMMARY, OS) # Find the position of the best IS in the ranked OS set postion_of_best_IS_in_OS = np.where(ranked_sharpe_OS.index == ranked_sharpe_IS.index[0])[0][0] return postion_of_best_IS_in_OS def rank(self, function, SUMMARY, mappa): # Apply a function to each row of the dataframe for a given index set and return a sorted dataframe combs = SUMMARY.loc[mappa].T.values return pd.DataFrame([function(i) for i in combs]).sort_values(by=0, ascending=False) def calc_logit(self, k): # Given an IS and OS combination, calculate the logit os_rank = self.find_OS_rank_of_best_IS(k) rel_os_rank = os_rank/self.len_sample logit = np.log(1/(0.5+rel_os_rank)) return logit def calc_sharpe(self,pl): N = len(pl) summa = bn.nansum(pl) if summa!= 0: sharpe = summa/np.sqrt(N*bn.ss(pl)-summa**2) return sharpe def show_results(self): df = pd.DataFrame(self.logits) fig, ax = plt.subplots(figsize=(15, 5)) df.hist(bins=400, ax=ax) ax.set_xlabel('OUT OF SAMPLE RANK OF THE BEST PARAMETER COMBINATION') ax.set_ylabel('number of occurences') ax.set_title("Logit Histogram") dpi = 500 # Save the figure to a file with the desired resolution plt.savefig('histogram_2.png', dpi=dpi) import multiprocessing import time def wrapper(args): return args[0](*args[1:]) def run_simulation(funct,combs): print(funct) pool = multiprocessing.Pool(multiprocessing.cpu_count()) start_time = time.time() print("Calibrating") results = pool.map(wrapper, [[funct,comb] for comb in combs]) pool.close() print("--- %s seconds ---" % (time.time() - start_time)) return results