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ília | Regra de entrada | Domínios numéricos |
|---|---|---|
| Tendência por EMA | EMA rápida acima da lenta por um limiar em unidades de ATR | rápida 12/24/48, lenta 48/96/168/336, limiar 0/0.25/0.50 ATR |
| Rompimento de Donchian | Fechamento além do canal rolante anterior, barra atual excluída | lookback 24/48/96/168/336/720 h, buffer 0/5/10 bp |
| Momentum TS | Sinal do log-retorno de L horas além de um limiar escalado por volatilidade | lookback 24/72/168/336/720, limiar 0/0.25/0.50/1.00 |
| Reversão por RSI | RSI de Wilder fora de um par de limites, operado na contramão | comprimento 6/14/28/56, limites 20/80, 30/70, 40/60 |
| Fade de Bollinger | Fechamento fora de uma banda, operado na contramão | lookback 24/48/96/168, largura 1.0/1.5/2.0/2.5 dp |
| Compressão de vol | Rompimento de canal com a vol curta abaixo de uma fração da vol longa | curta 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])Orçamento de busca e elegibilidade
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 decisionsUm 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 OOS | bp 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ço | Decisão de eixo a partir do bloco mantido adiante | Portfólio fora da amostra |
|---|---|---|
| Sem seleção de eixo | nenhuma | as 36 células a 1/36 |
| Seletor de mercado | melhor média de peso igual entre seis estratégias | seis estratégias nesse mercado a 1/6 |
| Seletor de estratégia | melhor média de peso igual entre seis mercados | essa 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 ppEstas 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.
| Estimativa | Mercado menos estratégia | Intervalo 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 omitido | Mercado menos estratégia | Seletor de mercado | Seletor 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ê.

