IRAI — Plano consolidado (estado + rota até o Tactical Layer)

Projeto: IRAI — Intraday Risk Appetite Index · Criado: 2026-07-13 · Revisado e verificado: 2026-07-13

Método desta versão. Três revisores independentes (deep-reasoner, fable-reasoner, codex) analisaram o plano em paralelo, cegos entre si; depois cada achado foi verificado contra o código e contra o banco de produção. Todo achado carrega um selo de confiança — a versão anterior deste documento era união sem verificação e apresentava palpite não-checado com a mesma cara de fato.

SeloSignificado
ConfirmadoVerificado por mim no código ou no banco real. É fato.
Consenso≥2 revisores, com evidência arquivo:linha, não re-verificado.
Solo1 revisor; ninguém contestou nem confirmou. Não vira ação sem checar.
RefutadoVerificado e derrubado. Registrado para não voltar.

Achado principal da revisão

Há um lookahead de 6 horas vivo no modelo de produção

Nenhuma revisão anterior o viu. Ele reordena o plano: precisa ser consertado antes da Fundação NWE, porque contamina o P_up que o Tactical Layer usaria como feature. O DST (A6) foi confirmado empiricamente no banco, mas o plano errava as datas e o mecanismo. Dos 16 achados originais: 13 sobreviveram, 1 foi refutado, 2 tiveram a severidade corrigida.


1. Onde estamos

1.1 Concluído

BlocoEvidência
✅ Migração de charts (Recharts → lightweight-charts, 5/5)bundle 752→419 kB; recharts removido (eae208e)
✅ GEX — gamma walls IBOV → WIN$N e WDO$Nserviço ponta a ponta + timer systemd (4dd1273…39e6822)
✅ Pacote A — destravar o V2 (Fases 2, 3, 4 + B1) — 3/301e0b9b, 28ecf2a, e5f513f, 82e3727, ceec25d
✅ Markers de sinal por barrae235c03 + fixes 0236f19, 97f2cb7
✅ Corridas de request (bônus)guard de reqId em fetchSeries, dentro de 39e6822

Tudo o que restava do Pacote C do plano-mãe (Fases 1, 5, 6, 7) e o item "cesta do WDO" foi absorvido como pré-requisito dos planos novos. Este documento é a rota única.


2. As frentes (ordem corrigida)

  [ FRENTE 0 ]  <-- NOVA, e vem antes de tudo
  Causalidade do eixo temporal (engine.py:471-473)
  D1 (lookahead 6h nos fatores B3) + A6 (DST) — a MESMA função
        |
        v
  [ FRENTE 1 ]                [ FRENTE 2 ]                 [ FRENTE 3 ]
  Fundação NWE causal  ─────> Bloqueios de ambiente ─────> Tactical Layer
  (Fases 5 + 6)               (Fase 1, cesta do WDO)        (WIN$N, WDO$N)

A Frente 0 não existia na versão anterior. Ela vem primeiro porque consertar o NWE e deixar o P_up com lookahead produz exatamente a métrica inflada que o plano diz querer evitar — por um caminho que o achado C1 não cobria.


3. Achados verificados

3.0 Frente 0 — os dois bugs do eixo temporal (mesma função, consertar juntos)

D1 — Lookahead de 6 horas nos fatores domésticos Confirmado Crítico

Afirmação. engine.py:472 aplica o shift de +6h apenas ao símbolo do target (if is_b3 and d["symbol"] == data_target). Um fator que também seja da B3 permanece no eixo BRT e passa a ser lido 6 horas à frente do target.

Como verifiquei. A cesta viva (query em asset_models, banco de produção) contém o par cruzado nos dois sentidos:

Simulei o cursor da engine (engine.py:678-684) sobre a sessão real de 2026-07-10:

eixoWIN realWDO consumidodefasagem
15:0009:00 BRT15:00 BRT+6h — futuro
18:0012:00 BRT18:00 BRT+6h — futuro
19:00 → fim13:00 BRT →18:25 (close)congelado no fechamento da sessão

Nuance decisiva (não estava em nenhum revisor). O vazamento depende de quais barras já existem no banco no momento do compute:

Impacto. Acurácia histórica inflada; price_diverge_z e os markers de par do WIN gerados sobre um fator adiantado; e, se não for corrigido, o Tactical Layer treinaria P_up sobre uma feature que vê o futuro — o gate aprovaria um modelo que desaba ao vivo.

→ Emenda: shift por origem do símbolo (todo símbolo XP/B3 → +6h; Tickmill → 0), não por == data_target. Regressão: para target B3 com fator B3, a barra do fator consumida no eixo t tem de ter o mesmo instante de parede que a do target.

A6 — O shift fixo de +6h é cego a horário de verão Confirmado Crítico

Confirmado empiricamente no banco de produção. (O data/irai.db local está vazio — 0 tabelas — o que fez dois revisores marcarem isto como "não verificável".) Método: correlacionei retornos M5 de WIN$N (XP/B3, BRT fixo — o Brasil não tem DST) contra US500 (Tickmill), buscando mês a mês o lag que maximiza a correlação. O relógio do servidor se revela sozinho:

PeríodoLag medidoServidor Tickmill
jun–out/20256hUTC+3
nov/2025 – fev/20265hUTC+2
mar–jul/20266hUTC+3

Transições exatas (granularidade semanal): 02/11/2025 e 08/03/2026.

O plano anterior errava duas coisas:

  1. As datas. Dizia "~26/10/2025 a ~29/03/2026" — as datas do DST europeu. As reais são as do DST americano. Quem implementasse seguindo o plano erraria uma semana em cada ponta.
  2. O enquadramento. Tratava isso como contaminação da janela de treino. É bug vivo: hoje (jul/2026) o +6h está correto — por isso ninguém notou — mas a próxima virada é 01/11/2026, daqui a ~3,5 meses, provavelmente no meio do rollout do Tactical.

→ Emenda: shift date-aware por tabela de offsets derivada do dado (não por calendário europeu hardcoded), na mesma função do D1. Regressão cobrindo uma data de inverno e uma de verão.

3.1 Críticos restantes

C1 — O backtest tático não é point-in-time para o macro Consenso 3/3 Crítico

Duas pernas: (a) calibrate_universal.py:49-52 carrega market_bars sem filtro de data e não existe parâmetro de cutoff — o P_up replayado é in-sample em qualquer partição; (b) engine.py:618-622 só restaura o Kalman se state_ts < session_start → replay parte frio, sessão viva parte quente.

→ Emenda: replay cronológico com estado Kalman em memória atravessando sessões; cada fold com artefato macro imutável e cutoff anterior à janela de teste. ⚠️ Versionar só model_params não basta — calibrate_universal.py:337 sobrescreve factors in-place. ⚠️ Com o D1 vivo, "declarar o viés e endurecer o gate" não é saída aceitável: o viés não é de magnitude desconhecida, é um lookahead de 6h.

C2 — Máquina de estados contraditória Consenso 3/3 Alto (era Crítico)

Confirmado nos docs: resumo permite ARMADO a modelo reprovado vs §6 manda para NEUTRO; JSON usa ARMED, estados usam ARMADO; CONFIRMADO sem transição de saída; cooldown "3 barras ou até sair e voltar" sem precedência.

Severidade rebaixada (1 revisor, aceito): é ambiguidade de documento, não dado corrompido. Um sub-item foi corrigido: "thresholds por classe não são espelhos" → o certo é "não estão especificados" — é omissão, não assimetria provada.

→ Emenda: tabela (estado, condição, prioridade, próximo estado, evento) + enum canônico único, antes de qualquer código.

X3 — Eventos táticos podem nascer de barra em formação Confirmado Crítico

collector.py:98-101 usa INSERT OR REPLACE na barra mais recente, reescrevendo seu OHLCV a cada ciclo. Sem flag de fechamento, uma transição de estado pode ser persistida a partir de uma barra que ainda vai mudar — e a chave idempotente preserva o evento obsoleto.

→ Emenda: estados e eventos só avançam em barra fechada. Persistir bar_closed ou excluir deterministicamente a última barra até a abertura da próxima.

3.2 Altos

A1 — Fórmula do envelope NWE ambígua Confirmado Alto

Verifiquei cada sub-afirmação: o MAE usa o centro contemporâneo de cada barra (App.jsx:375 → Math.abs(allPrices[t-i] - center[t-i])) — um segundo implementador naturalmente usaria center[t]. "Barra válida" tem definição no código, só não no doc (App.jsx:347 → !d.is_ghost). O warm-up vive no endpoint (main.py:387-392, LIMIT 95) e a engine não o carrega — mover o NWE para a engine sem levar a carga quebra a paridade. A âncora é win_open em %. O número "~188 closes" confere (95 + 94: cada resíduo depende do próprio centro).

→ Emenda: declarar o computeNWE como referência normativa — com a ressalva do D2.

D2 — A emenda do A1, como escrita, canonizaria um lookahead Solo Alto

O computeNWE lê a barra t+1 (App.jsx:444-445: nextSlope/isTransition), e o doc da Fundação proíbe exatamente isso. Declará-lo "normativo fórmula a fórmula" faria a regressão de causalidade testar contra uma referência que viola a própria regra.

→ Emenda: normativo para centro/envelope/âncora (App.jsx:355-380, 397-441); explicitamente não-normativo para isTransition/nwe_up/nwe_down (:444-450, :471-472).

A2 — nwe_slope colide com campo existente Confirmado — e é colisão TRIPLA Alto

Não são duas semânticas, são três, todas com o mesmo nome:

Correção de número: o plano dizia "~1e-6" — falso. Isso é a precisão do round(...,6), não a magnitude.

→ Emenda: campo novo nwe_slope_price; e a migração do frontend elimina o booleano de App.jsx:421 na mesma passada.

A5 — Purge de 6 barras insuficiente Consenso 3/3 Alto

Para o label (3 e 6 barras), purge=6 basta. O furo é específico: MFE/MAE são medidos até 20 barras e alimentam as zonas de realização → entram na seleção. Falta também a regra "labels nunca cruzam a fronteira da sessão" (ausência confirmada).

→ Emenda: purge = max(horizonte_label, horizonte_MFE) = 20; label truncado na fronteira da sessão.

A7 — Gate estatístico frágil Consenso 3/3 Alto

ECE ≤ 0,10 sobre ~100 confirmações é enviesado por seleção (amostra truncada pelo próprio threshold), e o baseline do Brier nunca é definido. "Ridge multinomial" não identifica um modelo: RidgeClassifier não emite probabilidades nativamente.

→ Emenda: calibração medida sobre todas as previsões OOS bar-a-bar; as 100 confirmações ficam só para o gate econômico; nomear estimador/solver/calibração e baseline.

D4 — Calibrador e engine ancoram o fator em pontos diferentes Solo — não verificado Alto

Para alvos B3, o calibrador recorta os fatores globais em 09:00–18:00 EEST (calibrate_universal.py:56-67), enquanto a engine ancora o fator na 00:00 da janela (engine.py:566-570). Pesos e σ ajustados sobre uma variável, aplicados sobre outra. Atinge exclusivamente WIN$N e WDO$N — os dois do piloto.

⚠️ SOLO. Checar antes de agir.

D3 — A Frente 1 não é executável no Linux: o banco local está vazio Confirmado Alto

data/irai.db local tem 0 bytes / 0 tabelas (está no .gitignore). Paridade, ghost, gap, fuso e a auditoria DST precisam de barras reais.

Contornado nesta revisão: usei o banco de produção via SSH (host WSL, 806 MB). → Emenda: Etapa 0.5 — obter um snapshot do irai.db de produção antes da Frente 1.

3.3 Médios (consenso, salvo indicação)

3.4 Refutados (registrados para não voltarem)

3.5 Cortes para a v1 do Tactical (mantidos — os três concordaram)

Zonas de realização (sem amostra condicional no gate), P_micro_30m (dobra a superfície de calibração) e tactical por barra na série (reconstrói-se de tactical_events). Ressalva não admitida antes: com a conferência da cesta do WDO bloqueada por ambiente, a v1 é de facto WIN-only — dizer isso evita planejar uma calibração de WDO que não pode ser aprovada.


4. Ordem de execução (reordenada pela revisão)

Etapa 0 — Emendar os planos-fonte

As emendas do §3 viram edição nos dois docs.

Etapa 0.5 — Frente 0: causalidade do eixo temporal ⚠️ NOVA — vem antes de tudo

  1. Snapshot do irai.db de produção para o Linux (D3).
  2. Corrigir engine.py:471-473 de uma vez: shift por origem do símbolo (D1) e date-aware por tabela de offsets derivada do dado (A6).
  3. Regressões: target B3 com fator B3 consome a barra do mesmo instante de parede; uma data de inverno e uma de verão.
  4. Recalcular a acurácia de WIN/WDO depois do conserto — os 69,0% e 73,9% atuais estão inflados por D1 e não devem ser usados como baseline.

Etapa 1 — Fundação NWE causal

  1. Regressões de causalidade antes da correção.
  2. Módulo puro NWE/VWAP/ATR (passada única, warm-up carregado pela engine).
  3. Snapshots enriquecidos; overview reutiliza o último da série.
  4. Threadpool + single-flight.
  5. Propagar por API e Firebase — incluindo o caminho Firebase na regressão de paridade.
  6. Migrar os charts; remover o computeNWE local (com a ressalva do D2).

Etapa 2 — Bloqueios de ambiente ⇉ (paralelo à Etapa 1)

  1. init_db() com a cadeia completa de migrações — é Python+SQLite puro, roda no Linux hoje.
  2. Conferir a cesta do WDO contra produção — depois do D1/D4, senão a divergência é medida contra um modelo desalinhado.

Etapa 3 — Tactical Layer (só depois de 0.5, 1 e 2)

  1. Migração de tactical_models/tactical_events/config antes da calibração.
  2. Extrator único de features.
  3. Backtester com replay point-in-time (C1).
  4. Calibração micro 15m + walk-forward + gate.
  5. Máquina de estados + eventos só em barra fechada (X3).
  6. API/Firebase aditivos; UI com feature flag desligada.
  7. Validação histórica → Windows → ativação individual, só após o gate.

5. Riscos que decidem o resultado

  1. D1 é lookahead vivo. Toda métrica histórica de WIN/WDO está inflada. Treinar o Tactical antes de consertá-lo produz um modelo que passa no gate e desaba ao vivo — o risco mais caro de detectar depois.
  2. A6 tem prazo. A próxima virada de DST é 01/11/2026. Se o shift não virar date-aware até lá, a produção volta a desalinhar 1h — possivelmente durante o rollout do Tactical, invalidando o próprio gate "aprovado no live".
  3. Paridade do NWE. Sem warm-up dentro da engine e sem a âncora win_open/% explícitas, a regressão de paridade falha e a divergência local-vs-prod volta.
  4. Ambiente. O banco local está vazio; sem o snapshot de produção a Frente 1 é tão bloqueada quanto a Frente 2.
  5. Escopo do Tactical. Sem os cortes da §3.5, o gate pode nunca ter amostra para aprovar nada.

6. Fora de escopo (reafirmado)

Execução automática de ordens, lote, copy trading — o IRAI é suporte à decisão. Volume Profile / book na v1. Expansão imediata aos 20 ativos. Recalibrar o macro sem evidência. Alegar validação live a partir do Linux. Polimento de frontend (Parte 3).


7. Lacunas — o que ninguém conseguiu verificar

Isto é resultado, não omissão.

Documentos-fonte: plano-mãe (convergência de sinal) · 2026-07-13-nwe-causal-backend-foundation.md · 2026-07-13-irai-tactical-layer-win-wdo.md