← O artigo
Reprodutibilidade

Cada número do artigo, e de onde ele veio

Método suficiente para conferir a conta na mão, ou para reconstruir um estudo equivalente com dados públicos da Binance.

Dados

As barras de origem vêm de data.binance.vision/data/futures/um/, que é público e não exige credenciais. Seis séries são construídas por contrato: klines OHLCV de 30 minutos como série principal; klines de 1 minuto, reduzidas na chegada a máxima, mínima e o minuto de cada uma, guardadas apenas para resolver a ordem em que a máxima e a mínima de cada barra de 30 minutos foram impressas; eventos de funding em seus timestamps reais de liquidação, carregando funding_interval_hours por evento; klines de mark price de 30 minutos, que valoram o fluxo de funding; klines de premium index de 30 minutos, a entrada de basis do braço exógeno; e open interest e razões long-short de 5 minutos.

O intervalo de funding é lido por evento em vez de assumido, porque a Binance moveu vários contratos de oito para quatro horas durante 2025 e qualquer constante estaria errada em parte do período. O mark price publicado é usado em vez de substituir pela abertura do contrato, e o premium index é usado diretamente em vez de derivar basis de fechamentos do spot, então nenhum segundo instrumento entra no painel.

Preços nunca são preenchidos para frente através de uma barra faltante, nenhuma entrada é permitida enquanto um insumo obrigatório está ausente e um sinal cria ordem apenas para a barra imediatamente seguinte. Se essa barra faltar, a ordem é cancelada em vez de carregada para uma abertura posterior. Uma posição aberta atravessando um buraco de dados continua aberta, e os indicadores precisam completar todo o warm-up de novo antes de outro sinal.

Todo insumo que não é preço é alinhado as-of o fechamento da barra, de modo que um valor na barra t era conhecível no fechamento da barra t:

# open interest, long-short ratios: last observation at or before the bar close
panel = panel.join_asof(metrics, on="ts", strategy="backward")

# funding: the last settled rate at or before the close, and separately the rate
# whose settlement falls inside the bar, which is what the cost model charges
panel = panel.join_asof(funding.select("ts", "rate"), on="ts", strategy="backward")
panel = panel.with_columns(
    fund_settle_rate=pl.col("rate").filter(
        pl.col("settle_ts").is_between(bar_open, bar_close)
    )
)

Universo

O local é o mercado de futuros perpétuos da Binance margeados em USDT e a frequência é de 30 minutos. A elegibilidade é decidida só pelo trimestre de ranqueamento: cotado em USDT, volume negociado positivo em todos os dias de outubro a dezembro, não ser produto de índice como BTCDOMUSDT ou DEFIUSDT e não ser uma redenominação 1000X de um contrato já incluído. 141 contratos passam. O ranqueamento é por notional diário mediano no trimestre, e o universo é o top 30 e o bottom 30.

d = screen.filter(pl.col("rank_days_traded") >= pl.col("rank_days").max())
d = d.sort("median_daily_notional", descending=True)
keep = [s for s in d["symbol"] if s not in excluded]   # index + redenomination rules
H, L = keep[:30], keep[-30:]
ratio = median(notional[H]) / median(notional[L])      # 19.99 against a floor of 10

As medianas realizadas são US$ 138,8M por dia no estrato H contra US$ 6,9M no estrato L, e o contrato mais fino do universo congelado ainda negociava US$ 5,0M por dia. Contratos entre os dois estratos não são negociados e existem apenas para definir os postos. O piso de separação de 10 foi registrado antes de a triagem rodar, e uma estratificação abaixo dele teria sido abandonada em vez de estreitada até passar.

A existência de um arquivo não é evidência de negociação. Tanto a elegibilidade quanto o fim do histórico de um contrato usam volume negociado positivo, e o preenchimento da corretora depois do último negócio real é descartado. Contratos que morrem dentro do período permanecem no universo com suas mortes preservadas, e é por isso que os dados do MATICUSDT terminam na migração para POL e os do EOSUSDT na renomeação para Vaulta, em vez de numa varredura de deslistagem. Um universo construído da lista atual da corretora teria apagado os dois.

Famílias

O braço C tem oito famílias de indicadores conhecidos, todas apenas de preço: ATR, EMA, MACD, PPO, RSI, RSI_LEVEL, SMA e STOCHK. O braço M-price tem nove famílias de mecanismo, também apenas de preço: volcomp_break, tsmom, session_mom, weekend_rev, volclimax, btc_relstr, gap_fade, range_rev e keltner_trend. O braço M-exo tem onze famílias que adicionalmente leem funding, open interest, basis, fluxo de ordens ou posicionamento: funding_capit, liq_snap, basis_fade, oi_breakout, flow_absorb, smart_follow, lev_stress, funding_settle, cvd_exhaust, retail_contra e oi_elastic.

Cada família do braço M carrega uma descrição de mecanismo de um parágrafo, nomeando a contraparte, a razão pela qual ela negocia sob restrição e o observável que serve de proxy dessa razão. As descrições são serializadas, hasheadas com SHA-256 e congeladas junto com o resto da configuração antes de qualquer barra ser lida. São reconstruções escritas a partir do registro original de projeto, e não um documento selado na época, o que é a primeira limitação do artigo.

O btc_relstr lê o preço de um segundo instrumento e portanto é apenas de preço mas cross-asset. Ele fica no M-price e é sinalizado. As estatísticas de braço são médias com peso igual entre as famílias do braço, então o desequilíbrio de oito contra nove não obriga a descartar família nenhuma.

Uma estratégia é a tupla (arm, family, variant, threshold, max_hold, stop). Lookback e reward-to-risk são botões ajustáveis dentro da amostra, não estratégias separadas, e são reselecionados em cada janela. Toda família dos dois braços tira de uma única biblioteca compartilhada, copiada tal e qual do gerador que produziu os dois corpora originais e hasheada.

TRANSFORMS = ["base", "slope", "normalized_price", "roc", "bias", "volZ", "accel",
              "disFromMedian", "quant_stretch", "rank_resid", "fold_dev"]          # 11
CONFL      = ["RSIge40", "RSIge50", "Pge0.7", "Pge0.8", "BW_filter", "pi", "vr",
              "kurtosis", "kurtosis10", "skew", "skew0.75", "atr_pct", "atr_pct0.8",
              "burstfreq", "TinyBody", "NoNewLowGreen", "RangeSpike", "YesterdayPeak",
              "DeadFlat10", "InsideBar", "SameDirection", "TopOfRange",
              "VolContraction", "EMAHug"]                                          # 24

VARIANTS = TRANSFORMS + CONFL      # 35
THR      = [1.0, 1.5, 2.0]         # entry threshold, in z units
MAXH     = [24, 96, 288]           # 12 hours, 2 days, 6 days
SLS      = [1.0, 2.0, 4.0]         # stop, in entry-ATR units
N_CONFIGS = 35 * 3 * 3 * 3         # 945 attempted configurations

945 tentativas por família, por contrato, por janela, nos dois braços. Esse é o orçamento de busca igual: configurações tentadas iguais em vez de número igual de botões, e ele difere de propósito dos corpora originais, onde as famílias de catálogo iam de 12 a 8.932 configurações. Toda tentativa é persistida com seu motivo, inclusive as que nunca negociam, para que uma família que falha muito não pareça menos buscada do que uma que negocia o tempo todo. Um evento de sinal ocorre quando o estado desejado atual é diferente de zero e o estado anterior era neutro ou oposto, então uma condição persistente não pode disparar de novo a cada barra.

A tabela de scores realizada tem 28.576.800 linhas, em 60 contratos, 28 famílias e 18 janelas, o que dá 1.680 unidades contrato-família, cada uma carregando 945 configurações.

Execução

O conjunto de informação e a sequência do trade, em ordem: features da barra completa t, estado desejado depois da barra t, entrada na abertura da barra t+1, barreiras ativas a partir de t+1, saída no primeiro entre stop, alvo ou a abertura depois de H barras completas seguradas.

Cada estratégia segura no máximo uma posição, um sinal na mesma direção enquanto ocupada é ignorado, e um sinal oposto sai e inverte na próxima abertura observada, cobrando duas execuções. As barreiras são escaladas por ATR em vez de percentuais fixos, porque num universo estratificado por liquidez um stop fixo de 1% é uma distância de risco diferente num major e num alt fino, o que confundiria o eixo de liquidez com um eixo de volatilidade.

Barras que tocam as duas barreiras são resolvidas caminhando pelo caminho de 1 minuto e tomando qual barreira foi cruzada primeiro. Resolver pela ordem em que os extremos foram impressos é outra pergunta, e foi medida enviesando resultados em até 12,5 bp por trade num martingale sem drift, mais que todo o modelo de custos. Quando um único minuto abrange as duas barreiras a ordem é genuinamente desconhecida nessa resolução, e o stop é tomado primeiro.

Custos e funding

gross = direction * (exit_price / entry_price - 1.0)
fee   = 5e-4 * fills                                   # 5 bp per fill, both legs
slip  = (0.5 * spread_month + 0.5 * 0.5 * spread_month) * fills
fund  = sum(direction * rate_k * qty * mark_k for k in settlements_held)
net   = gross - fee - slip - fund                      # reconciles to 1e-9 per row

A taxa é fixa em 5 basis points por execução, a taker VIP0 dos perpétuos margeados em USDT da Binance, e não varia por contrato. O spread é construído de daily/aggTrades como a mediana do repique entre negócios, a variação relativa absoluta de preço entre negócios consecutivos que trocam o lado agressor, tomada nos dias 8 e 22 de cada mês com até três recuos para frente quando um dia falta. Cada execução paga metade do spread medido mais metade disso de novo como provisão de slippage, então um round trip paga um spread medido inteiro mais 50% de provisão de impacto.

O bookTicker, o feed direto de cotação, é inutilizável aqui porque o arquivo só o carrega a partir de 2023-05-16, o que deixaria o primeiro terço do período sem custo. Estimadores de máxima e mínima são proibidos: medido nesta mesa, o Corwin-Schultz superestima o spread do BTC por um fator de 568 nesses tamanhos de barra, o que significa que ele lê volatilidade e não spread.

O estimador foi validado antes do freeze contra uma medição independente de bookTicker do BTCUSDT em 15 de julho de 2023 e a reproduz exatamente em 0.0330 basis points, sobre 440.267 negócios e 172.496 trocas de lado agressor. Essa concordância é um gate bloqueante, não uma nota de rodapé.

Um contrato-mês com barras negociadas e sem spread mensurável é falha de dados e derruba o contrato. Um contrato-mês sem barras negociadas é ausência estrutural e não carrega spread, porque não carrega posição. No universo congelado a divisão realizada é 3.607 contrato-meses medidos, 593 estruturalmente ausentes e zero falhas.

Componente de custoTratamento
Taxa5 bp por execução, fixa, nas duas pernas
SpreadMedido por contrato-mês, metade cobrada por execução
SlippageMais metade do spread medido por execução
FundingEvento observado e sinalizado na barra de liquidação, valorado pelo mark price publicado

Funding positivo debita comprados e credita vendidos, e o funding é apurado sobre a posição segurada imediatamente antes do timestamp de liquidação. Uma entrada nova nesse timestamp não paga nem recebe; uma posição carregada até ele e encerrada nele paga. A quantidade da posição é fixada em capital da estratégia dividido pelo preço bruto de entrada durante toda a vida do trade. O funding nunca é insumo de estratégia em nenhum dos braços, e no braço M-exo o caminho de custo e o caminho de feature são calculados de colunas separadas, com um gate explícito afirmando que nenhuma coluna de custo chega à matriz de features.

Walk-forward

As janelas são ancoradas no calendário: um bloco de 12 meses-calendário dentro da amostra seguido de um bloco de 3 meses-calendário fora da amostra, avançando de três em três meses, dezoito blocos cobrindo 1 de janeiro de 2022 a 30 de junho de 2026.

def quarters():
    out, d = [], date(2022, 1, 1)
    while d < date(2026, 7, 1):
        nxt = next_quarter(d)
        out.append((date(d.year - 1, d.month, 1), d, nxt))   # IS start, OOS start, OOS end
        d = nxt
    return out                                               # 18 windows

Todos os contratos compartilham as fronteiras das janelas, então um contrato que lista mais tarde contribui com menos janelas e um que morre contribui com menos ainda. Janelas ancoradas em barras, usadas pelos dois corpora originais, dão a cada contrato um calendário diferente e tornam os clusters semanais incoerentes no painel.

Dentro de cada bloco dentro da amostra, todas as 945 tentativas são pontuadas, lookback e reward-to-risk são selecionados pelo Sharpe dentro da amostra, e os botões selecionados são congelados e aplicados uma vez ao bloco fora da amostra. Um trade cujo intervalo de manutenção cruza a fronteira é excluído inteiramente do score dentro da amostra em vez de truncado. O warm-up de indicador pode ler barras antes do início do bloco, porque essas barras já eram conhecidas, mas linhas de warm-up nunca contribuem com resultado para o bloco que está sendo pontuado.

Pareamento

Uma estratégia-janela elegível tem pelo menos 20 trades fechados dentro da amostra em pelo menos 4 semanas calendário distintas e Sharpe finito dentro da amostra. O pareamento acontece dentro de uma célula (contract, window), nunca atravessando um estrato de liquidez, de forma gulosa por distância absoluta crescente de Sharpe dentro da amostra e sem reposição nos dois lados, com empates desfeitos pelos dois ids de estratégia para que o pareamento seja função pura da tabela de entrada.

order_vals = np.sort(m_sharpe)                  # candidates within the caliper by
lo = np.searchsorted(order_vals, c_sharpe - 0.05, side="left")   # binary search, not a
hi = np.searchsorted(order_vals, c_sharpe + 0.05, side="right")  # |A| x |B| matrix
# pairs taken in ascending |distance|, without replacement, ties broken by (c_sid, m_sid)

O caliper é de 0,05 unidades de Sharpe anualizado, com um único alargamento registrado para 0,10 disponível se o gate de sobreposição falhasse. Não foi necessário: 6.812.171 pares casaram em 0,05, e o gate registrado de sobreposição exige pelo menos 200 pares casados em cada janela e pelo menos 60% do braço menor casado no total, avaliado por estrato.

O pareador lê apenas colunas dentro da amostra. Rodá-lo contra uma tabela com as colunas fora da amostra apagadas produz um hash de pareamento idêntico, o que é um gate bloqueante e não uma alegação. O balanceamento depois do pareamento é reportado como diferença média padronizada no Sharpe dentro da amostra, e qualquer coisa acima de 0,10 seria falha da implementação de pareamento, não um resultado.

Os ids de estratégia são tornados únicos entre famílias antes de qualquer junção. O sid recomeça do zero a cada contrato-família, então a família EMA do braço C e a família de momentum do braço M carregam ambas ids de 0 a 944. Juntar só por sid é ambíguo, e num corpus de dois contratos isso transformou 119.178 pares em 8.580.816 linhas.

Inferência

Séries diárias pareadas de retorno líquido são construídas para os dois braços a partir do conjunto pareado. Dentro de cada janela fixa fora da amostra, o bootstrap reamostra clusters de semana calendário UTC de segunda a domingo, e uma fronteira de trimestre parte uma semana calendário, de modo que seus dias de cada lado formam fragmentos de janela-semana separados. Cada contrato, família, estratégia e braço dentro de um fragmento sorteado viaja junto, o que preserva a dependência transversal sem mudar a contagem de clusters de uma janela. Os dados realizados carregam 250 fragmentos em 1.642 dias de negociação.

rng = np.random.default_rng(20260810)
stat = []
for _ in range(10_000):
    draw = rng.integers(0, n_clusters, n_clusters)   # week fragments, not rows
    stat.append(diff[rows_in(draw)].mean())
point, (lo, hi) = observed_diff, np.percentile(stat, [2.5, 97.5])

10.000 réplicas, seed 20260810, intervalo por percentil. Reamostragem linha a linha é proibida, já que janelas de previsão sobrepostas não criam observações independentes. Os p-valores bilaterais são o dobro da menor cauda do bootstrap com a correção de réplicas finitas (count + 1) / (B + 1), limitados a um, antes de entrar no Benjamini-Hochberg. Nenhum ajuste de multiplicidade se aplica ao único contraste primário registrado, e os quatro secundários formam uma família BH a 5%.

A inclinação de decaimento é a única estatística que não pode agrupar por semanas. Sua observação é um par-janela, um Sharpe dentro da amostra contra um retorno fora da amostra, então ela agrupa pela própria janela e recebe 18 clusters em vez de 250. Os intervalos do artigo são correspondentemente largos, e isso é uma propriedade do estimando e não uma escolha.

Gates

Vinte gates bloqueantes rodam antes da análise, e cada um carrega um defeito plantado provando que ele pode falhar. Um gate que passa tanto na entrada limpa quanto na quebrada não consegue falhar e é reportado como quebrado. Todos os vinte passam e todos os vinte defeitos plantados são pegos.

Eles cobrem o hash da configuração congelada, o universo e sua razão de separação, a vivacidade das famílias nas duas direções, gates de regime filtrando o conjunto de candidatos de forma mensurável, truncar o futuro deixando sinais anteriores intactos, vazamento de features, resolução por primeiro toque no caminho de 1 minuto, sinal do funding, a identidade de custos, a contagem de 945 tentativas, ordenação e disjunção das janelas, a cegueira do pareador às colunas fora da amostra, sobreposição, balanceamento pós-pareamento, simetria ao trocar os rótulos de braço, um resultado futuro plantado, uma rotação de ações dentro da janela, controles de clones e o corpus nulo abaixo.

Duas outras verificações merecem menção. O motor ganha zero num caminho sem drift e bate com uma referência de primeiro toque dentro de 0,08 basis points por trade. A identidade do ledger gross - fee - slippage - funding = net vale por linha e no agregado, e depois do defeito de funding não-finito descrito no artigo ela agora rejeita valores não-finitos explicitamente antes de comparar magnitudes, porque um resíduo não-finito passa por uma comparação de magnitude e certifica um ledger corrompido como limpo.

O corpus nulo

O vocabulário de regras compartilhado carrega um artefato de autorreferência: um sinal derivado da série negociada, combinado com barreiras escaladas pela volatilidade defasada dessa mesma série, não ganha zero num corpus sem drift. Os dois braços perdem lá cerca de 550 basis points só pelo artefato. É propriedade do vocabulário e não defeito do motor, então testar que o nível é zero reprovaria um desenho que de fato está correto.

O teste registrado é portanto sobre a diferença, que é o estimando:

# same engine, same vocabulary, same matching, driftless GBM paths
D_null = matched_difference(null_corpus)
# 72.92 bp, 95% CI [-15.11, 166.60], 51,124 matched pairs, covers zero

Esse intervalo cobre o zero, que é o que o gate exige. Também é 33% do efeito observado na estimativa pontual, com limite superior acima do limite inferior do efeito observado, e é por isso que o artigo reporta o resultado principal e o artefato juntos em vez de só o resultado principal.

Todo gráfico do artigo é desenhado a partir de um único arquivo JSON publicado, exportado desses artefatos. O pipeline completo, o pacote de freeze e o ledger por trade não são distribuídos desta página. Se você quiser, escreva pedindo.

De volta ao artigo.

← Estratégias de trading precisam de uma razão econômica?