Цель этой статьи двоякая: во-первых, упростить концепции, лежащие в основе очень популярной статьи: Вероятность переобучения при ретроспективном тестировании https://papers.ssrn.com/sol3/papers.cfm?abstract_id=2326253 Бейли. , https://www.linkedin.com/in/david-h-bailey-72b471/, Borwein https://www.linkedin.com/in/jonathan-borwein-2157a835/, de Prado 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, которая максимизирует функцию пригодности в пространстве параметров.

Например, очень простая сезонная модель, в которой каждый день я открываю сделку в определенный час и закрываю ее в другой определенный час, описывается двумя параметрами: час входа и час выхода, а пространство параметров формируется всеми пары (T вход, T выход).

Если предположить, что инструменты торгуются 23 часа каждый день, существует 23*22 возможных пары или 2_плета.

Первым шагом является создание матрицы, охватывающей все временные ряды доходности, связанные с параметрическим пространством. Количество столбцов в этой матрице эквивалентно общему количеству возможных комбинаций параметров (например, 23*22 в предыдущем простом примере). При этом количество строк определяется количеством временных меток в возвращаемом временном ряду.

Например, предположим, что у нас есть десять лет наблюдений за ценами, и в каждом году 250 торговых дней. В этом случае количество столбцов в матрице будет (250*10)-1 (исключая один, поскольку расчет переходит от цены к доходности).

Определение CSVC.

CSVC означает комбинаторно-симметричную перекрестную проверку.

Процедура CSVC включает в себя разделение набора данных на 2N частей. Половина этих частей будет использоваться для калибровки модели, а другая половина — для оценки эффективности стратегии с невидимыми данными. Эта процедура повторяется для всех возможных способов выбора N частей из 2N частей, а количество возможных способов определяется биномиальным коэффициентом C(2N,N), как описано в ссылке: https:// en.wikipedia.org/wiki/Комбинация

Чтобы проиллюстрировать эту концепцию, давайте рассмотрим пример, в котором набор данных разделен на 6 частей. В этом случае 3 части будут использоваться для набора данных в образце/обучения/калибровки, а оставшиеся 3 части будут использоваться для набора данных вне образца/тестирования/проверки. Количество возможных комбинаций, в которых можно выбрать 3 детали из набора 6, определяется биномиальным коэффициентом C(6,3) = 20. На рисунке ниже показаны все 20 возможных комбинаций:

В каждом разделе вычисляется целевая функция для всех возможных комбинаций параметров как в красной, так и в зеленой области.

Чтобы было понятно, давайте изобразим методологию для модели с 5 возможными комбинациями параметров. Каждая комбинация порождает стратегию и, следовательно, линию капитала. Целевая функция рассчитывается отдельно для интервалов набора данных, соответствующих красным областям и для зеленых областей.

Предполагая, что структура теперь ясна, следующим шагом будет ранжирование комбинаций параметров на основе значения целевой функции как в наборах данных в выборке, так и вне выборки.

В таблице OSRik — это ранжирование вне выборки комбинации параметров, занимающей i-е место в k-м разделе в выборке/вне выборки.

Рассматривая только первую строку предыдущей таблицы, мы получаем список рангов вне выборки, соответствующих лучшему решению в выборке. Другими словами, у нас есть список, который сообщает нам для каждого раздела набора данных, как комбинация параметров, которая максимизирует целевую функцию, работает вне выборки по сравнению со всеми другими комбинациями параметров.

Идея состоит в том, что если модель не переоснащена, комбинация параметров, которая лучше всего работает во внутренней выборке, не будет постоянно занимать низкое место по сравнению с другими комбинациями параметров при тестировании вне выборки.

Следующий шаг состоит в использовании списка рейтингов вне выборки, как определено выше, для оценки «вероятности переобучения».

Пример:

Давайте рассмотрим приведенную выше идею, изучив набор из 5000 искусственно сгенерированных доходностей, каждая из которых содержит 1000 наблюдений. Если бы это была реальная модель, это означало бы, что существует 5000 возможных комбинаций параметров, а набор данных охватывает период в 1000 дней.

Давайте тогда решим разделить 1000 меток времени на 16 частей. Способ выбора 16/2 = 8 элементов из набора из 16 задается биномиальным коэффициентом (16,8) = 12870. Это означает, что количество разделов In-sample/Out of sample в нашем примере равно 12870. .

Чтобы представить вектор ранжирования вне выборки OSRik более математически элегантным способом, мы можем использовать логит-функцию.

В частности, если ранг наилучшей комбинации параметров в выборке во вневыборочном наборе находится в нижней половине, то логит-значение отрицательное; в противном случае он положителен.

Построение гистограммы значений логита может помочь нам визуализировать переоснащение модели. Если гистограмма сосредоточена на отрицательной стороне, то модель, вероятно, переоснащается; если он находится на положительной стороне, то он недообученный.

В нашем конкретном примере гистограмма выглядит так:

Чтобы вычислить вероятность переобучения, мы просто делим общее количество вхождений, где значение логита меньше нуля, на общее количество вхождений.

Приложение: скрипт Python.

В этом разделе вы найдете скрипт python, который позволит вам рассчитать вероятность переобучения при тестировании на исторических данных:

  1. Фрейм данных возвратов, в котором каждый столбец представляет собой временной ряд возвратов заданной комбинации параметров модели -> помечен как SUMMARY.
  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