← A nota
Reprodutibilidade

Cada número da nota, e de onde ele veio

Método suficiente para conferir a aritmética à mão, ou para reconstruir um estudo equivalente a partir de dados públicos da Binance.

Praça, universo e datas

A praça é a de futuros perpétuos com margem em USDT da Binance e a data de seleção do universo é 31 de dezembro de 2022. Contratos elegíveis precisavam ser cotados e marginados em USDT, ter pelo menos 24 meses de histórico genuíno de volume negociado até aquela data, negociar entre outubro e dezembro de 2022 e não duplicar a exposição de outro contrato. Os contratos elegíveis foram então ranqueados pelo nocional diário mediano negociado entre outubro e dezembro de 2022, usando apenas informação disponível até a data de seleção, e os seis primeiros foram travados: BTCUSDT, ETHUSDT, DOGEUSDT, XRPUSDT, BNBUSDT e SOLUSDT.

A existência de um arquivo não é evidência de negociação. Tanto a elegibilidade quanto o fim do histórico usam volume negociado positivo, e o preenchimento da exchange depois do último trade real de um contrato é descartado.

O período primário fora da amostra vai de 1º de janeiro de 2023 a 30 de junho de 2026: quatorze janelas de três meses completas e sem sobreposição. Julho de 2026 é excluído por ser um trimestre incompleto.

Dados e calendário

Os seis contratos usam barras horárias em UTC desde o lançamento na Binance Futures até 30 de junho de 2026, com klines oficiais de um minuto mantidas para reconstruir a ordem em que a máxima e a mínima de cada hora ocorreram, e eventos de funding e aberturas horárias de mark price mantidos nos seus timestamps reais. Os lançamentos observados são BTC em 8 de setembro de 2019 às 17:00 UTC, ETH em 27 de novembro de 2019 às 07:00, XRP em 6 de janeiro de 2020 às 08:00, BNB em 10 de fevereiro de 2020 às 08:00, DOGE em 10 de julho de 2020 às 09:00 e SOL em 14 de setembro de 2020 às 07:00.

O painel mantém fronteiras de calendário comuns em vez de uma máscara de interseção, então o buraco de um mercado nunca apaga uma hora dos outros cinco, e preços nunca são preenchidos para frente através de uma barra ausente. Um sinal cria uma ordem apenas para a hora UTC imediatamente seguinte, e se essa hora não existe a ordem é cancelada em vez de carregada adiante. Uma posição aberta atravessando um buraco permanece aberta, e os indicadores precisam completar todo o warm-up de novo antes de outro sinal.

Uma linha horária só pode ser substituída por um agregado de um minuto ou por uma resposta REST atual quando a linha de origem é comprovadamente corrompida ou esparsa, e toda substituição guarda sua resposta de origem, motivo, timestamp e hash em um manifesto de reparos. Quatorze linhas de máxima e mínima e 240 linhas horárias ausentes foram reparadas sob essa regra antes do congelamento dos dados, deixando um painel de 333,424 linhas horárias e nenhum buraco.

As seis famílias de estratégia

Toda regra calcula um estado desejado depois da barra completa t, e um evento de sinal só dispara quando o estado desejado é diferente de zero e o estado anterior era neutro ou oposto, então uma condição persistente não consegue emitir um sinal a cada hora.

# a desired state becomes a signal only on a fresh transition
state = np.where(fast_ema - slow_ema >  threshold * atr,  1,
        np.where(fast_ema - slow_ema < -threshold * atr, -1, 0))
prior = np.roll(state, 1)
signal = (state != 0) & ((prior == 0) | (prior == -state))
FamíliaRegra de entradaDomínios numéricos
Tendência por EMAEMA rápida acima da lenta por um limiar em unidades de ATRrápida 12/24/48, lenta 48/96/168/336, limiar 0/0.25/0.50 ATR
Rompimento de DonchianFechamento além do canal rolante anterior, barra atual excluídalookback 24/48/96/168/336/720 h, buffer 0/5/10 bp
Momentum TSSinal do log-retorno de L horas além de um limiar escalado por volatilidadelookback 24/72/168/336/720, limiar 0/0.25/0.50/1.00
Reversão por RSIRSI de Wilder fora de um par de limites, operado na contramãocomprimento 6/14/28/56, limites 20/80, 30/70, 40/60
Fade de BollingerFechamento fora de uma banda, operado na contramãolookback 24/48/96/168, largura 1.0/1.5/2.0/2.5 dp
Compressão de volRompimento de canal com a vol curta abaixo de uma fração da vol longacurta 12/24, longa 72/168/336, razão 0.40/0.60/0.80, rompimento 12/24/48

A propriedade definidora da regra de compressão foi medida antes de qualquer exposição fora da amostra: seus eventos de sinal elegíveis tinham que de fato ocorrer durante o período de calibração de 2021-2022 em todos os mercados, porque um filtro que nunca filtra é um rótulo, não uma estratégia.

Saídas, execução e o caminho intrabarra

As seis famílias diferem apenas na lógica de entrada e compartilham uma única arquitetura de saída, o que impede que uma busca por saídas contrabandeie famílias novas. A entrada é na abertura da hora t+1 depois de um sinal na barra completa t, uma posição por sleeve, sinais na mesma direção ignorados enquanto a posição está ocupada, e um sinal oposto inverte na abertura seguinte e paga duas execuções. Fora isso, a posição sai no que vier primeiro entre stop, alvo ou hold máximo.

O ATR(14) de Wilder conhecido na entrada fixa as distâncias das barreiras por toda a vida do trade, e os domínios de saída são comuns a todas as famílias: stops a 1.0, 1.5, 2.0 ou 3.0 ATR de entrada, com reward-to-risk a 1.0, 1.5, 2.0 ou 3.0 e holds máximos de 12, 24, 48, 96 ou 168 horas.

O OHLC horário não consegue dizer qual barreira executou primeiro quando as duas ficam dentro de um mesmo candle, então o engine reconstrói o caminho de cada hora a partir de barras de um minuto como abertura, primeiro extremo registrado, segundo extremo registrado, fechamento. Se os dois extremos caem no mesmo minuto e a ordem fica indefinida, o stop executa primeiro. Um gap através de um stop preenche na primeira abertura disponível, enquanto um gap através de um alvo não preenche melhor que o alvo.

# events sharing one hourly timestamp resolve in a fixed order
for hour in window:
    apply_funding(hour)          # carried into this timestamp
    apply_gap_barriers(hour)
    apply_reversal(hour)         # closes old, opens new, two taker fills
    apply_max_hold(hour)
    apply_entry(hour)
    apply_intrabar_barriers(hour, minute_path[hour])

Cada família recebe exatamente 64 configurações tentadas em cada estratégia-mercado-janela. Um gerador determinístico de hipercubo latino discreto com seed 20260809 amostra os domínios de sinal e de saída, rejeita combinações duplicadas ou inválidas e escreve uma lista canônica de 64 linhas por família. As listas são geradas uma única vez, reusadas em todo mercado e toda janela, serializadas de forma canônica e com hash calculado antes de qualquer exposição fora da amostra, e o manifesto de busca declara n_searched = 2304, que são 64 tentativas em cada uma das 36 células família-mercado.

Uma tentativa só é elegível a vencer o bloco de parâmetros se fechar pelo menos 24 trades ao longo de pelo menos seis semanas calendário distintas. Tentativas que falharam, não finitas e inelegíveis permanecem na tabela de scores e ainda contam para as 64. Empates de parâmetro são desfeitos pelo menor turnover, depois pelo menor índice canônico; empates de seletor são desfeitos pelo menor turnover agregado do bloco do eixo, depois pela ordem congelada de mercado ou estratégia.

Cronograma walk-forward

Toda janela externa tem doze meses de histórico in-sample seguidos de três meses fora da amostra, avançando de três em três meses. Os primeiros nove meses ajustam os parâmetros numéricos de cada célula, os três meses finais repetem a configuração escolhida sem tocar nela, e só esses scores mantidos adiante decidem o eixo de mercado e o eixo de estratégia.

for window in windows:                       # 14 non-overlapping OOS quarters
    fit  = window.is_slice[:9_months]        # choose numeric parameters per cell
    axis = window.is_slice[9_months:]        # replay the chosen config, untouched
    picks = {
        "market":   best_mean_over(axis, group="market"),    # 6 strategies on it
        "strategy": best_mean_over(axis, group="family"),    # on 6 markets
    }
    apply_once(picks, window.oos_slice)      # frozen configs, frozen decisions

Um trade cujo intervalo de holding cruza a fronteira de um bloco é excluído do score do bloco anterior, e nenhum rótulo, saída ou fluxo de caixa atravessa do bloco de parâmetros para o bloco de seleção de eixo, nem de in-sample para fora da amostra. O warm-up de indicadores pode ler barras anteriores ao início de um bloco, porque essas barras já eram conhecidas, mas as linhas de warm-up nunca contribuem P&L ao bloco sendo pontuado. A configuração escolhida não é reajustada nos doze meses completos depois da decisão de eixo.

Custos, funding e contabilidade

Toda entrada, perna de inversão, stop, alvo e saída por tempo é uma execução taker que paga 5 pontos-base de taxa e 2 pontos-base de slippage sobre o nocional absoluto executado, com a quantidade da posição fixada no capital do sleeve dividido pelo mid bruto de entrada por toda a vida do trade. O funding usa eventos observados da Binance nos seus timestamps reais, com funding positivo debitando comprados e creditando vendidos, apurado sobre a posição mantida imediatamente antes do timestamp de settlement.

# a buy fills adversely, and the gap is booked as slippage rather than hidden in gross
fill = raw_mid * (1 + 0.0002) if side > 0 else raw_mid * (1 - 0.0002)
fee = 0.0005 * abs(quantity * fill)
slippage = abs(quantity * (fill - raw_mid))
funding_cost = direction * funding_rate * quantity * mark_open   # positive when paid
net_pnl = gross_pnl - fee - slippage - funding_cost              # holds to 1e-9

Uma observação de funding esperada e ausente é uma falha de dados, não um zero e nem um convite a imputar. Toda linha do ledger guarda P&L bruto, taxa, slippage, funding e P&L líquido como colunas separadas, e a identidade acima reconcilia no corpus completo dentro de 5.55e-17 unidades de retorno.

Componente, todas as 36 células OOSbp de capital fixo de sleeve
P&L bruto+27,454.6
Taxas taker-168,288.4
Slippage-67,315.4
Funding-2,147.0
P&L líquido-210,296.2

Braços de portfólio

BraçoDecisão de eixo a partir do bloco mantido adiantePortfólio fora da amostra
Sem seleção de eixonenhumaas 36 células a 1/36
Seletor de mercadomelhor média de peso igual entre seis estratégiasseis estratégias nesse mercado a 1/6
Seletor de estratégiamelhor média de peso igual entre seis mercadosessa estratégia em seis mercados a 1/6

Todos os retornos de sleeve usam capital fixo, uma unidade de posição absoluta máxima e nenhuma capitalização. Sleeves parados, ausentes e inelegíveis ficam em caixa e os sleeves ativos nunca são renormalizados, então cada braço carrega o mesmo orçamento bruto máximo de 100% enquanto a exposição realizada e o turnover são resultados, não configurações. O Sharpe pode ser reportado de forma descritiva a partir do book diário sem sobreposição, mas ele nunca seleciona uma configuração, escolhe um braço ou decide uma conclusão.

Inferência

O engine constrói retornos líquidos diários pareados para os três braços, e dentro de cada janela fixa fora da amostra o bootstrap reamostra clusters de semana calendário de segunda a domingo em UTC, com uma fronteira de trimestre que divide uma semana deixando dois fragmentos separados. Todo mercado, estratégia e braço dentro de um fragmento amostrado viaja junto, o que preserva a dependência cross-sectional sem mudar a contagem de clusters de uma janela.

draws = paired_week_cluster_bootstrap(
    daily_by_arm, windows=14, n_boot=10_000, seed=20260809,
)
point = cumulative(market_picker) - cumulative(strategy_picker)   # +193.9 bp
low, high = np.percentile(draws, [2.5, 97.5])                     # -7868.3, +8194.8
ruling = "no_reliable_winner" if low < 0 < high else "winner"

Cada p-valor secundário bilateral é o dobro da menor probabilidade de cauda do bootstrap com a correção de réplicas finitas (count + 1) / (B + 1), limitada a um, antes de os dois p-valores de seletor contra sem seleção entrarem em Benjamini-Hochberg a 5%, onde ambos retornaram 0.7513. Nenhum ajuste de multiplicidade se aplica ao único contraste primário registrado, e as quatorze diferenças por janela são diagnósticos descritivos, não unidades de bootstrap.

Decomposição

A decomposição usa os 36 retornos líquidos fora da amostra em nível de célula, sem ponderação, em cada janela completa, depois de os parâmetros por célula terem sido congelados, e calcula somas de quadrados ortogonais de efeitos fixos no cubo balanceado de mercado por estratégia por janela.

ss = orthogonal_fixed_effect_ss(cube)          # market x strategy x window
shares = {k: v / ss["total"] for k, v in ss.items()}
# market 0.46%, strategy 1.94%, interaction 6.98%, time 1.78%, residual 88.84%
boot = [shares_of(resample_weeks(cube)) for _ in range(n_boot)]
critical = np.percentile(max_abs_centered_error(boot), 95)   # 4.92 pp

Estas são parcelas descritivas de somas de quadrados, não componentes de variância populacional; chamá-las de estimativas de variância de efeitos aleatórios afirmaria uma permutabilidade que seis mercados deliberadamente selecionados não têm. Um componente só é chamado de dominante quando sua parcela pontual é a maior e seus intervalos simultâneos contra os outros dois componentes cross-sectional excluem o zero. A interação se qualifica: mercado menos interação vai de -11.45 a -1.61 pp e estratégia menos interação de -9.96 a -0.12 pp, enquanto mercado menos estratégia cobre de -6.40 a +3.44 pp.

Clones, transferência e sensibilidades

Os grupos de clones são determinados dentro de cada janela a partir dos vetores diários de posição com sinal do bloco de eixo das configurações já escolhidas, concatenados nos seis mercados. Duplicatas exatas sempre se fundem, quase-clones se fundem por linkage completo apenas quando toda correlação entre pares é de pelo menos 0.95, correlação negativa nunca funde estratégias inversas, e um par precisa de pelo menos 60 observações diárias finitas com variância diferente de zero antes de uma correlação sequer ser definida. Seis grupos unitários se formaram em toda janela, então o braço com clones colapsados é igual ao braço primário.

A transferência é testada repetindo as 36 configurações congeladas de cada janela nos seis mercados alvo ao longo do bloco de seleção de eixo e do bloco fora da amostra correspondente, com custos, funding, dimensionamento e execução idênticos. A etapa de transferência roda uma vez por janela em vez de pooled, porque uma invocação pooled deixaria períodos in-sample posteriores participarem de uma seleção avaliada em períodos fora da amostra anteriores.

A lista registrada de sensibilidades, fixada antes dos resultados, é um bloco de parâmetros com histórico expandido desde o lançamento, um evento de funding constante e positivo de 1 bp, um eixo de estratégia com clones colapsados, transferência pontuada por expectancy em vez de P&L total, seis painéis de seletor com um mercado deixado de fora, e a janela parcial de julho de 2026 apenas como descritiva. Nenhuma sensibilidade foi acrescentada depois que o resultado primário ficou visível, e nenhuma pode substituir o número principal.

EstimativaMercado menos estratégiaIntervalo de 95%
Primária registrada+193.9 bp-7,868.3 a 8,194.8
Histórico de parâmetros expandido-1,596.7 bp-8,488.4 a 5,322.1
Funding constante de 1 bp+1,359.1 bp-6,012.0 a 8,808.5
Mercado omitidoMercado menos estratégiaSeletor de mercadoSeletor de estratégia
BTC+928.4 bp-4,979.0-5,907.4
ETH+345.3 bp-3,320.1-3,665.4
DOGE+4,064.3 bp-3,646.1-7,710.4
XRP+940.3 bp-4,278.4-5,218.7
BNB-4,494.8 bp-6,474.6-1,979.7
SOL+465.2 bp-4,795.8-5,260.9

Gates de pré-registro

Dezenove gates bloqueantes tinham que passar depois da última mudança de código relevante e antes de qualquer barra fora da amostra ser lida, cada um com um defeito plantado que prova que o gate pode falhar. Eles cobrem concordância de hash entre dados, estratégias, busca, custos e análise; checagens de sinal, direção e atividade com dados reais para as seis famílias nos seis mercados; um filtro de compressão que filtra de forma mensurável; truncamento futuro deixando todo sinal, candidato e trade completo anterior inalterado; nenhum campo de funding, custo, timing de máxima e mínima ou desfecho chegando a um sinal; paridade de execução entre Python e Rust em fatias reais; ordem do caminho de um minuto mudando um resultado plantado de duplo acerto; sinais e contagens de funding reconciliando com os dados de origem; a identidade de P&L líquido valendo por trade e no agregado; as 64 tentativas persistindo, incluindo as que falharam; fronteiras de bloco ordenadas, purgadas e disjuntas; um painel plantado transposto trocando os dois seletores exatamente; contagens e pesos de sleeve por braço sem renormalização de caixa; controles de clone plantados produzindo os clusters esperados; um desfecho futuro plantado melhorando o otimizador de forma material e sendo detectado; uma rotação de ação dentro da janela preservando o turnover exatamente enquanto destrói a relação plantada; gates que falham propagando um código de saída diferente de zero; e o estado de prontidão reportando as checagens registradas como executáveis em vez de silenciosamente parciais.

O último deles é o gate de corpus nulo: em um corpus de random walk sem drift, usado só para teste, ao longo de 30 seeds independentes, nenhuma família teve média fora da amostra selecionada in-sample com um intervalo bootstrap de 95% inteiramente acima de zero.

Resultados de verificação

  • Corpus: 31,770,479 linhas de trades candidatos, 16,826 trades selecionados fora da amostra.
  • Identidade de custos: reconcilia em 5.55e-17 unidades de retorno.
  • Paridade de engine: 25,259 trades ao longo de 72 casos com dados reais, discordância máxima absoluta de P&L de 1.94e-16.
  • Screen de vazamento na mesma barra: correlação de -0.0001, contra +0.0051 do retorno futuro.
  • Checagem de fronteira: zero trades cruzando ou caindo fora da sua janela registrada fora da amostra.
  • Backtest guard: zero falhas automáticas nas checagens de walk-forward, ledger, custos e incerteza agrupada por semana.

Dois avisos automáticos foram resolvidos checando os dados diretamente em vez de por argumento. A checagem genérica leu o timestamp compartilhado entre in-sample e fora da amostra como uma purga de zero dias, quando o engine purga no nível do trade, o que a contagem direta de fronteira resolve. A etapa de inputs orientada a Sharpe da mesma checagem reportou poder baixo e uma dispersão de escala de ruído entre as famílias, o que não toca uma comparação decidida por P&L, mas descarta apresentar qualquer família individual aqui como um edge descoberto.

pre-OOS freeze   b7c102a3a6cbbbb0752e174cc610ed7c56a0f9fe8f94fd42ef184fa16ccd12cb
engine manifest  f2e70f0c2334751029be76673e7f8a429413944ef8026af149c1c9730f717a78
results          4cdf4ac1b42420f480528f3db6323b2e12b7d21ac160d99712e08983dc35451d
data manifest    34a26b3380223fe19afb73cf3d63a60d9e991fba82917e42adc8e7d66adae1e3

Qualquer mudança depois que um resultado fora da amostra fica visível exige uma nova raiz de execução, novos hashes e uma emenda escrita. Existem duas emendas, ambas pós-congelamento e ambas de apresentação: uma removeu um renome de coluna duplicado na exportação de janelas, e outra restaurou um rótulo de figura encurtado, escreveu as fronteiras de janela como datetimes sem fuso e fez o runner de checagens retornar falha quando qualquer checagem solicitada falha. Nenhuma delas tocou dados, configurações, trades, retornos, portfólios, inferência ou o veredito.

Os gráficos na nota são desenhados a partir das tabelas de resultado publicadas, as mesmas tabelas de onde vêm os números no texto.

Pedindo o pipeline completo

Esta página é suficiente para conferir cada número da nota à mão, ou para reconstruir um estudo equivalente a partir de dados públicos da Binance. Os scripts de produção, a construção dos dados, o engine em Rust, a análise e a geração das figuras não estão anexados. Envie um e-mail para daniel@daru.finance e eu envio para você.

← Voltar para a nota