เป้าหมายของบทความนี้มีสองเท่า ประการแรก เพื่อทำให้แนวคิดเบื้องหลังรายงานยอดนิยมนี้ง่ายขึ้น :”ความน่าจะเป็นของการทดสอบย้อนกลับที่ไม่เหมาะสมเกินไป” 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

ในส่วนนี้คุณจะพบสคริปต์หลามที่จะช่วยให้คุณสามารถคำนวณความน่าจะเป็นของการทดสอบย้อนกลับที่ให้มากเกินไป:

  1. dataframe ของผลตอบแทนที่แต่ละคอลัมน์แสดงถึงอนุกรมเวลาของผลตอบแทนของการรวมพารามิเตอร์ที่กำหนดของแบบจำลอง - ป้ายกำกับเป็นสรุป
  2. จำนวนส่วนที่ชุดข้อมูลถูกแยก -› ระบุว่าเป็น 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