Status: ACTIVE | Ultima revisao: S152 (2026-03-30)
DASHBOARD (browser) → HTTP/WS → SERVIDOR (VPS FastAPI) → HTTP polling 1-3s → EAs MT5 (MQL5)
IDEIA / BUG REPORT
│
▼
CLAUDE INVESTIGA
│ ←── [100%] Guardian injeta patterns relevantes (59 ativos)
│ ←── [100%] Spec-loader carrega docs por topico
│ ←── [100%] Agent-router sugere subagent (VPS/EA/Dashboard)
│
▼
CLAUDE EDITA CODIGO
│ ←── [aviso] protect-specs.sh AVISA ao mexer em whitepaper (relaxado em S322; nao bloqueia)
│ ←── [100%] frontend-lint.py BLOQUEIA globals soltos, CSS injection
│ ←── [100%] af-stress-protocol.sh AVISA stress test se AF mudou
│
▼
CLAUDE COMMITA
│ ←── [100%] validate-commit.sh BLOQUEIA:
│ • Secrets no staged
│ • Spec protegido sem [allow-spec]
│ • .mqh mudou sem EA_BUILD++
│ • Drift codigo ↔ spec (via spec-code-map.json)
│ • 3+ arquivos codigo sem memory
│
│ ←── [100%] post-commit:
│ • git push vps master (background)
│ • sync-docs-vps.sh → specs mudados vao pro /guide
│
│ ←── [100%] post-commit-learn.sh sugere /learn se commit tipo fix:
│
▼
CLAUDE FAZ DEPLOY
│ ←── [100%] pre-deploy-quality.sh BLOQUEIA se property tests falharem
│ ←── [100%] protect-ea-deploy.sh BLOQUEIA cp/mv .ex5 direto
│ ←── [100%] post-deploy-verify.sh checa saude apos deploy
│
▼
SESSAO TERMINA
│ ←── [100%] auto-capture.py BLOQUEIA se licoes nao capturadas
│ ←── [100%] verify-memory.sh verifica memoria salva
│
▼
PROXIMA SESSAO
│ ←── [100%] Guardian com novo pattern aprendido
└── CICLO SE REPETE (auto-melhoria continua)
| Nivel | Mecanismo | Garantia | Quando usar |
|---|---|---|---|
| 1 | Hook (31 ativos) | 100% — deterministico | Regras criticas, protecoes |
| 2 | CLAUDE.md | ~90% — pode esquecer | Guidelines gerais |
| 3 | Spec .md | ~70% — so carrega por keyword | Referencia detalhada |
| 4 | Memory .md | ~50% — le se consultado | Dados operacionais |
Regra de ouro: Se algo precisa ser 100%, precisa de HOOK. Regra so no CLAUDE.md nao basta.
| Hook | Quando | O que bloqueia |
|---|---|---|
| validate-commit.sh | git commit | Secrets, drift, specs, build |
| protect-specs.sh (Tier 1) | Edit/Write | Whitepapers sem aprovacao |
| pre-deploy-quality.sh | scp .py → VPS | Property tests falhando |
| protect-ea-deploy.sh | cp/mv .ex5 | Deploy manual (forcar deploy_ea.sh) |
| frontend-lint.py | Edit JS | Globals soltos, CSS injection |
| auto-capture.py | Fim sessao | Licoes nao capturadas |
| Hook | Quando | O que avisa |
|---|---|---|
| protect-specs.sh (Tier 2) | Edit/Write | Hooks, config, Patterns data |
| af-stress-protocol.sh | Edit AF engine | Lembrete de stress test |
| post-commit-learn.sh | Commit fix: | Sugere /learn |
| Hook | Quando | O que faz |
|---|---|---|
| Guardian (59 patterns) | Cada mensagem | Injeta warnings por keyword |
| Spec-loader | Cada mensagem | Carrega docs relevantes |
| Agent-router | Cada mensagem | Sugere subagent |
| session-diagnostics.sh | Inicio sessao | VPS health, git, EA build |
| framework-health.py | Inicio sessao | 9 pilares OK? |
| spec-freshness.py | Inicio sessao | Specs desatualizados? |
| sync-docs-vps.sh | Post-commit | Sincroniza specs → VPS /docs/ |
| track-skill-usage.py | Qualquer skill | Registra uso |
| post-deploy-verify.sh | Post-deploy | Health + invariantes SQL |
| Processo | Frequencia | O que faz |
|---|---|---|
| healthcheck_v2.sh | 2min | Auto-restart se cair |
| monitor.sh | 5min | Telegram se problema |
| backup_daily.sh | 3h UTC | DB + configs |
| _auto_close_expired_pendings | 15s | Fecha sinais expirados |
| _periodic_cleanup | 15min | Limpeza DB |
| _run_drawdown_checker | 5min | Alerta drawdown Telegram |
| Ferramenta | Melhor para | Tokens |
|---|---|---|
| Playwright MCP | Unico browser MCP (snapshot, screenshot, automacao) | ~114k |
| Skill | Trigger | Garantia |
|---|---|---|
| /validate | "validar", "varrer tudo", "pre-deploy", "deploy feito", "simular fluxo", "E2E" | ~70% (mesclada S271 de /prove + /validate-loop + /quality-gates) |
| /brainstorming | "design", "trade-off" | ~70% |
| /debug-sistematico | "debug", "mesmo erro" | ~70% |
| /learn | Licao descoberta | ~90% (CLAUDE.md) |
| /compound | Fim de sessao | ~90% (CLAUDE.md) |
| /commit | Commitar | Manual |
| /deploy | Deploy VPS | Manual |
| /status | Diagnostico | Manual |
Bug encontrado → /learn captura pattern → Guardian injeta na proxima sessao
│ │
│ ▼
│ Claude evita repetir
│ │
└──── 3x repetido? → Promover pro CLAUDE.md ──┘
Para quem chega agora. Este é o mapa do maquinário que vigia o próprio trabalho neste
projeto: o que existe, onde mora, o que garante de verdade e onde ele é frágil.
Números medidos em 2026-08-01 — envelhecem; o comando para remedir está no fim.
O harness é um sistema imunológico: um conjunto de fiscais automáticos que rodam sozinhos
antes, durante e depois de cada ação, mais um acervo de lições que se injeta no contexto por
palavra-chave. Ele existe porque regra escrita em documento acerta ~90% das vezes, e trava
automática acerta 100%.
| Peça | Quantos | O que é |
|---|---|---|
Fiscais (hooks) |
220 | scripts que rodam em gatilhos do sistema |
| — dos quais BLOQUEIAM | 60 | param a ação; os outros só avisam |
Lições (patterns) |
738 | acervo no cofre, todas chegando à projeção que é lida |
| Specs | 464 | documentos de referência, incluindo estes guias |
| Skills | 23 | procedimentos que se carregam sob demanda |
| Comandos | 22 | atalhos que o dono digita (/validate, /learn…) |
| Testes | 883 arquivos | a rede que segura tudo isso |
Peso real: os fiscais consomem ~12% do relógio de uma sessão. Em 96.702 execuções
medidas, 94,1% não produziram nada — o silêncio é o estado normal e esperado.
Esta hierarquia é a coisa mais importante deste guia. Ela decide onde escrever uma regra
nova.
| Camada | Garantia | Quando usar |
|---|---|---|
| Fiscal (hook) | 100% — determinístico, não dá para esquecer | regra crítica, proteção, bloqueio |
CLAUDE.md |
~90% — pode se perder no ruído | orientação geral |
| Spec | ~70% — só carrega por palavra-chave | referência detalhada |
| Memória | ~50% — lê se alguém consultar | dado operacional |
Anti-padrão nº 1 do projeto: regra importante só no CLAUDE.md. Se precisa ser 100%,
precisa de fiscal.
Corolário medido, e ele já custou caro: quando duas regras do harness se contradizem, a
contradição não fica neutra — vence a que tem trava, em silêncio. Mudou a doutrina? Procure
o hook que injeta a versão velha, ou ela continua valendo.
| O que | Caminho |
|---|---|
| Fiscais | .claude/hooks/ |
| Registro dos fiscais (quem roda quando) | .claude/settings.json |
| Doutrina de fundo | .claude/knowledge/*.md |
| Procedimentos | .claude/skills/<nome>/SKILL.md |
| Comandos do dono | .claude/commands/*.md |
| Perfis de ajudante | .claude/agents/*.md |
| Lições (fonte da verdade) | ~/Dropbox/KnowledgeHub/linniu-copytrade/patterns/ |
| Projeção lida pelo vigia | .claude/hooks/guardian-index.json (gerada — nunca editar à mão) |
| Decisões com trade-off | specs/decisions/NNNN-*.md |
| Testes | scripts/tests/ |
/brainstorm → desenha, e a spec nasce com as linhas de verificação obrigatórias
↓
/plan → quebra em tarefas; a última já inclui a conferência adversarial
↓
/rapid → até 3 arquivos, teste-primeiro
↓
/validate → roteiro → 5 lentes em paralelo → junta achados → cético
↓
/learn → lição capturada NA HORA, nunca "depois"
A porta do cético é o /validate. Disparar revisor por fora entrega achado pingado e faz
o dono remontar o quadro de cabeça — é desperdício medido, não preferência de estilo.
Esta foi a última reforma grande, e ela mudou uma coisa que valia desde sempre.
O laço fecha quando uma passada devolve zero achado de gravidade alta ou crítica. Média e
baixa viram nota — não contam, não bloqueiam, não viram card.
Rede: orçamento de 8 revisores por obra. Conserto nascido de achado gasta do orçamento
e nunca o reabre.
Por quê: aplicando a matemática clássica de inspeção de software aos nossos próprios 307
achados, a estimativa é de ~3.884 defeitos existentes — achamos 7,9%. Revisar até zerar não
termina, e não é por indisciplina. Detalhe, fontes e o preço da troca:
specs/decisions/0038-portao-por-gravidade-e-orcamento-de-revisao.md.
O que conta como alta ou crítica não é a olho: a régua com os quatro níveis, a pergunta que
decide cada um e exemplos reais está em
specs/regua-de-gravidade-do-cetico.md.
Não estão aqui todas as 60 — só as que protegem coisa que não volta:
| Trava | O que impede |
|---|---|
pede-ok-antes-de-subir.py |
promoção para produção sem o "pode subir" do dono |
block-scp-vps-deploy.sh |
copiar arquivo direto para o servidor, fora do caminho canônico |
block-manual-ea-upload.sh |
subir o robô sem passar pelo script que confere a assinatura |
check-ea-no-ordersend.sh |
o robô mandar ordem por programação em vez de imitar clique humano |
quarentena-so-leitura.py |
ajudante de leitura agir em produção |
p367-commit-staging-guard.sh |
um chat varrer o trabalho do outro num commit sem escopo |
validate-commit.sh |
commitar com a bateria vermelha, segredo no diff ou documento fora de sincronia |
| Fraqueza | Número (medido 2026-08-01, 3ª medição) |
|---|---|
| Fiscais sem teste | 82 de 200 (41%) |
| Dos que BLOQUEIAM, sem teste | 0 de 52 |
| Fiscais que nunca dispararam | ver seção seguinte — era o buraco de verdade |
Método, para o número ser auditável (sem isto ele não vale nada — ver o aviso abaixo):
denominador = arquivos .py/.sh em .claude/hooks/, fora as bibliotecas _* e os
próprios arquivos de teste; corpus = todo arquivo de teste enumerado do disco (rglob),
fora .venv*, .git e worktrees — deu 992 arquivos em 26 pastas. "Tem rede" = o nome do
fiscal aparece em algum desses arquivos.
⚠️ Terceira versão destes números. As duas primeiras estavam erradas, pelo MESMO motivo,
e a segunda foi a correção da primeira.
Versão Publicado Erro 1ª 137 de 220e28 de 60contei exit 2dentro de comentário como bloqueio; busquei testes em 1 pasta2ª 106 de 210e1 de 53busquei em 3 pastas — o repositório tem 26. Faltaram .claude/hooks/tests/(27 arquivos) e a raiz de.claude/hooks/(8)3ª a tabela acima corpus enumerado do disco, denominador sem libs nem testes A 2ª versão foi publicada junto com a lição
P756, que ensina exatamente a não fazer
isso — e cuja primeira redação repetia o defeito, afirmando que "o repositório tem três
pastas de teste". Quem achou foi um cético independente, relendo a correção.(Da 1ª versão sobra ainda um acerto:
check-ws-auth-tracker.pyfoi tirado da lista de
"código morto" — é chamado porvalidate-commit.sh:1076e tem 11 testes.)
Ressalva de contagem: 1 dos 52 é falso-bloqueador. check-delegation-prompt-report.py
está registrado só em PostToolUse — ali o arquivo já foi salvo, então o exit 2 vira
recado, não impedimento. O próprio hook diz isso no cabeçalho; a contagem é que não distinguia.
Achado em 2026-08-01 por cético independente, e confirmado com medição própria: sete fiscais
nunca rodaram uma única vez, e um deles guarda dinheiro.
A causa não estava em nenhum deles. Estava no registro em .claude/settings.json: o padrão
Edit(*/static/js/*) exige um diretório antes de static, e o caminho que o harness casa
não tem. Comparação limpa, em 125.351 registros de telemetria:
| Padrão de registro | Fiscal que o usa sozinho | Execuções |
|---|---|---|
Edit(*server/*.py) — sem barra |
auto-test.sh |
48 ✅ |
Edit(**/tests/**) |
check-test-grep-based.py |
308 ✅ |
Edit(*.md) |
check-jargon-leigo.py |
187 ✅ |
Edit(*/server/*.py) — com barra |
7 fiscais | 0 ❌ |
Os sete que estavam mudos: p284-no-raw-prop-name-compare (guarda de dinheiro — impede duas
contas da mesma empresa de virarem par), frontend-lint, check-trycatch-around-api,
check-cross-layer-rule-dup, check-error-msg-jargon, check-state-machine-drift e
check-test-token-login. Os 16 gates foram corrigidos ((*/ → (*).
A lição, que vale para todo o repositório: um teste de fiscal roda o arquivo do fiscal
direto, com um pacote montado à mão. Ele prova que a lógica dele decide certo. Ele não
prova que o harness algum dia entrega esse pacote a ele. São duas perguntas, e só a segunda
diz se a proteção existe de verdade:
| Pergunta | Quem responde |
|---|---|
| A lógica decide certo? | o teste em scripts/tests/ |
| Ele é chamado alguma vez? | a telemetria em .claude/.fiscais-medidos.jsonl |
Por isso a tabela de fraqueza acima ("1 de 53 sem teste") diz menos do que parecia: cobertura de
teste alta convivia com sete guardas desligadas. Um fiscal com 0 execuções e 8 testes verdes é
pior que um sem teste nenhum — porque o verde convence.
Conferência periódica (quais fiscais estão registrados e nunca dispararam):
python scripts/quality/fiscais-mudos.py
python scripts/quality/regua_de_corte.py --versao-min 2
Ele mede peso, silêncio e categoria de cada fiscal na janela recente. Se os números aqui
divergirem muito dele, ele está certo e este documento está velho.
| Assunto | Onde |
|---|---|
| Quando parar de revisar, e a escada de lentes | .claude/knowledge/quando-parar-de-revisar.md |
| Régua de gravidade | specs/regua-de-gravidade-do-cetico.md |
| Como orquestrar vários ajudantes | .claude/knowledge/workflow-house-style.md |
| O fluxo de 6 passos | specs/decisions/0042-o-fluxo-de-6-passos-medir-antes-de-desenhar.md |
| Guia das lições | .claude/knowledge/patterns-guide.md |
| Confiabilidade de ajudante | specs/subagent-confiabilidade.md |
Status: DRAFT — o diagnóstico está fechado; o desenho não foi aprovado
Tier: L — toca o injetor de contexto, o portão do commit, o quadro de metas, o acervo de lições e o ciclo inteiro de 12 passos; atravessa .claude/hooks/, scripts/quality/, memory/ e specs/.
Autor: Claude + Usuário
Data: 2026-08-10
Sessão: obra do harness, sessão 1
Última conferência contra o CÓDIGO: 2026-08-11, sonda de 5 lentes + 5 céticos — os cinco vereditos voltaram SOBREVIVE: NÃO e 20 números foram corrigidos, três deles invertendo a leitura anterior (§3.19). (Revisões anteriores: 2026-08-10, a 1ª achou 6 contradições; a 2ª achou mais 15, quase todas criadas pela própria 1ª — ver §18)
Esta é a SSoT (fonte única da verdade) desta obra. Toda decisão, medição e mudança de
rumo entra aqui. Se algo só existe na conversa, considere que não existe — foi
exatamente esse o defeito que originou a obra.Substitui: o primeiro desenho de 5 portões, derrubado no mesmo dia (ver §9).
⚠️ Nota de nascimento, e ela prova a tese da obra: esta spec foi escrita sem a linha
Tier:e o guarda que exige a seção de pós-implementação deixou passar em silêncio —
porque, não achando a declaração de tamanho, ele libera. É o defeito nº 2 do dono
("mentir que terminou") acontecendo dentro do documento que existe para curá-lo.
No acervo há 78 specs com tamanho indeterminado contra 72Le 16M.
Para quem chega agora, ou volta depois de uma compactação. Se você só ler esta seção, sai
sabendo o suficiente para decidir se continua lendo.
| Pergunta | Resposta em uma linha |
|---|---|
| Por que esta obra existe? | O dono nomeou dois defeitos: eu esqueço e eu minto que terminei. Tudo aqui tem que matar um dos dois (§1) |
| O que já se sabe? | 26 medições, todas com fonte e data (§3). A mais citada é a lei dos 9% (§3.10) — mas ela levou uma ressalva medida: fecha #N tem lembrete há 22 dias e continua em 8%. O que rende não é o lembrete: é o lembrete pedir o que quem escreve já tem na mão |
| O que já foi decidido? | 27 decisões (§4). As quatro que mais mandam: o alvo são os 2 defeitos (D5), peça nova apaga uma velha (D8/D18), desenho novo confere o código e nunca esta spec (D19), e nada se aposenta pela régua de custo — ela morreu (D24) |
| O que já foi construído? | Nada do desenho. Sete desenhos foram propostos e os sete morreram no cético (§9) — o sétimo levou 3 de 3 vereditos contra, o primeiro placar unânime da obra. O que foi construído são seis consertos avulsos achados durante a medição — cada um com o alvo que ele move, em §4-B |
| Qual é o próximo passo? | ⚠️ A sonda de 11/08 (§3.19) matou as duas pistas que existiam para o sexto desenho e devolveu três fatos acionáveis: o mutation_check.py pronto e desligado, o canal único em 23,1% de adoção, e o validate-commit.sh com 68,5% do conserto da vida dele no último mês. Nenhum deles é desenho ainda |
| O que pode mandar parar tudo? | A inversão de §3.11 — mas ela mudou de natureza em 11/08: não é pico, é patamar de ~3 semanas (60,9% → 60,3%). O dono decidiu não parar e otimizar (D16); o número continua de pé e agora tem idade |
O estado, sem enfeite: o diagnóstico está maduro e caro (três varreduras, uma sonda de 11
ajudantes, ~3,2 milhões de tokens, nove céticos). O desenho está zerado. Esta spec vale pelo
que ela impede de ser re-proposto (§8 e §9), não pelo que ela já entregou.
⚠️ E ela já mentiu de novo: em 11/08 três dos seus números estavam invertidos e um não
fechava com a própria aritmética (§3.19). O CA-4 — "a spec não se contradiz" — reprovou pela
terceira vez. Isto não é sinal de que a revisão falhou; é o custo conhecido de um documento
que carrega número medido. A defesa que funciona é a D19: conferir o código, nunca este texto.
| Se você quer… | Vá para |
|---|---|
| entender por que a obra existe | §1 o alvo · §2 os 6 critérios |
| ver os números e conferir você mesmo | §3 (26 medições, cada uma com fonte e data) · §3.19 é a mais recente e corrige 20 números das outras |
| saber o que já foi decidido e não re-litigar | §4 (27 decisões) · §4-B (qual alvo cada movimento move) |
| ver onde o fluxo vaza | §5 os 12 passos · §6 o mapa peça por peça |
| comparar com as ferramentas de fora | §7 o placar · §7.2 a régua da indústria |
| saber o que NÃO entra — e não repropor | §8 · §8.1 · §9 (os 7 desenhos mortos) |
| saber o que ainda não se sabe | §10 as perguntas abertas |
| retomar depois de uma compactação | §0 (esta) → §4 → §10 → §18 o diário |
| saber o que FAZER, e em que ordem | ⬅️ specs/os-vigias-do-harness.md — a fatia de EXECUÇÃO deste tema |
🧭 A divisão de papéis entre este documento e a lista de execução (2026-08-13)
Este arquivo é o por quê: diagnóstico, números, decisões e desenhos mortos. A lista do
o quê fazer mora emspecs/os-vigias-do-harness.md, com
13 metas que fecham por commit (fecha meta N @os-vigias-do-harness) — logo o estado é
escrito por máquina e duas conversas podem trabalhar nele sem se atropelar.Por que a separação, e não uma lista aqui: este arquivo tem 2.043 linhas e status DRAFT. Uma
lista de estado dentro dele seria a segunda lista respondendo "o que falta?" — o defeito
que esta obra existe para combater. Um documento, um papel.⚠️ E a lista de execução carrega, no topo, as TRÊS ideias que este documento já matou com
medição (D2a poda dos mudos §3.14-B · o despachante único §3.12 · a régua de custo §3.15).
Elas foram re-propostas em 2026-08-13 por quem não tinha lido este arquivo — a liçãoP779em
estado puro. O apontador cruzado existe para que isso não aconteça uma terceira vez.
Estas regras existem para o documento não apodrecer — que é o defeito nº 1 de spec nesta casa.
| Regra | Por quê |
|---|---|
| Toda decisão vira linha na §4, com data e motivo | decisão sem motivo é re-litigada em duas semanas |
| Todo número vem com o comando que o produziu | número sem comando não pode ser reconferido, e vira folclore |
| O que fica de FORA é obrigatório (§8) | lição P884 desta casa: eu estreito o escopo em silêncio e o dono precisa perguntar "cadê o resto?" |
| Nada aqui é escrito por máquina, por enquanto | quando a §10 virar quadro automático, esta linha muda — e a lição P742 avisa: arquivo que vira projeção continua aceitando escrita à mão, e ela some no próximo rebuild, sem erro |
| Toda seção que descreve defeito VIVO carrega a data da conferência | D19. Sem a data, o texto é histórico, não estado — e foi assim que a §3.7 matou um desenho inteiro |
| Número derrubado por medição posterior some do texto — vira uma linha de registro, no passado | é a lição P904, e esta spec tinha o defeito em 6 lugares até 2026-08-10: afirmava 76% e 171 como fato em seções que outras seções já haviam desmentido |
| Diário no fim (§18), uma linha por sessão | é onde se vê se a obra andou ou parou |
"os projetos do github que prometem resolver o principal problema da LLM em planos, que é
esquecer as coisas, mentir que terminou X coisa, isso é grave, meu foco é esse"
— 2026-08-10"agora confio mais em pegar algo pronto e copiar e melhorar aquilo que já está
comprovando" — 2026-08-10
São dois defeitos, não um harness bonito. Tudo que entrar nesta obra tem que matar um dos
dois. O resto é enfeite e fica de fora.
| Defeito | Nome operacional | Onde ele mora no ciclo |
|---|---|---|
| Eu esqueço | a saída do passo vive só na conversa | passos 3 e 6 do ciclo (§5) |
| Eu minto que terminei | a conclusão não tem recibo | passos 7 e 11 do ciclo (§5) |
Toda peça — nossa ou copiada — passa por estes seis. Os três marcados 🔑 são os que mudaram
o rumo desta pesquisa e não estavam na lista inicial.
| # | Critério | O que decide |
|---|---|---|
| 1 | Quem julga? | máquina (devolve 0 ou 1) · outra IA · o dono |
| 2 | Qual é o recibo? | o que sobra em disco provando que o passo aconteceu |
| 3 | 🔑 Sobrevive à compactação? | passo cuja saída vive só na conversa vai ser esquecido |
| 4 | 🔑 Falha barulhenta ou calada? | falhar calado é pior que não existir — você acredita que rodou |
| 5 | 🔑 Custa quanto quando NÃO precisa? | portão que cobra pedágio no caso fácil é contornado |
| 6 | Dá pra desfazer? | irreversível exige aprovação; reversível pode rodar sozinho |
Os critérios 3 e 4 são os dois defeitos do dono, em forma de pergunta.
Regra: nenhum número entra aqui sem a fonte. Onde a fonte é fraca, está dito.
📅 Leia a data antes do número (
D19). Tudo aqui foi medido em 2026-08-10 — a obra
tem um dia de vida —, mas em momentos diferentes do dia, e isso importa:
Seções Quando Reconferidas depois? 3.1 a 3.11 madrugada ❌ não — são janelas móveis, e refazê-las custa a varredura inteira (§8.1) 3.12 a 3.14 manhã ✅ pelos três céticos da obra dos guardas 3.15 a 3.18 tarde ✅ nasceram de cético, com o número reproduzido Seção marcada ✅ descreve defeito morto; sem ✅, o defeito está vivo na data indicada.
Reconferido em 2026-08-11 (§3.19). A coluna "% que é conserto" é a medida mais estável
do eixo: nenhuma das quatro se moveu mais de 1 ponto em 24 horas.
Medida (30 dias, 2026-07-12 → 2026-08-11) |
Valor conferido 11/08 | Era, em 10/08 |
|---|---|---|
| Commits totais | 2.128 | 2.101 — ⛔ não comparável, ver o aviso abaixo |
% que é conserto — guardas (.claude/hooks/) |
55,3% (380 de 687) | 56% |
% que é conserto — ferramentaria (.claude/ + scripts/) |
52,1% (513 de 985) | 60% |
% que é conserto — servidor (server/) |
48,2% (251 de 521) | 49% |
% que é conserto — a tela (static/) |
18,7% (86 de 460) ← a única saudável | 18% |
git log -n 99999 --since="30 days ago" --format='%s' -- .claude/hooks/ | grep -cE '^fix(\(|:)' # 380
git rev-list --count --since="30 days ago" HEAD -- .claude/hooks/ # 687
A tela custa 3× menos que os guardas para manter, no mesmo repositório e no mesmo mês. É a
prova de que 55% não é lei da natureza — é escolha de desenho em algum lugar.
⚠️ Armadilha medida:
git log --since=... | wc -ldevolve 50 quando o real é 2.092.
Erro de 40×, reproduzido 4 vezes. Use sempregit rev-list --count. (A causa é ortk, não
ogit— §10.)⛔ Armadilha nova, e ela custou a conclusão principal desta seção (lição
P927):
janela deslizante medida ontem não se compara com ela mesma hoje. O "2.101 → 2.177" parecia
aceleração de 70 commits e era queda de 49 — ao avançar um dia, a janela ganha o dia novo
e perde um dia inteiro na outra ponta. Agravante: a medição de 10/08 foi feita no MEIO de um
dia que fechou com 142 commits. Tendência só se mede com janelas fixas e disjuntas, com
--untilexplícito e hora na data (--since=2026-08-11devolve 0 onde--since="2026-08-11 00:00"devolve 18).
| Janela | Guardas novos/semana — 10/08 | Guardas novos/semana — 11/08 |
|---|---|---|
| 180 dias | 11,6 | 10,0 |
| 90 dias | 13,3 | 9,9 |
| 30 dias | 24,5 | 15,4 |
| 7 dias | 34,0 | 16,0 |
git log -n 99999 --since='7 days ago' --diff-filter=A --format='' --name-only -- '.claude/hooks/*.py' '.claude/hooks/*.sh'
⚠️ As duas colunas aceleram, e é só isso que se pode afirmar. A diferença entre elas não é
desaceleração — é divergência de fórmula, e a fórmula de 10/08 não ficou registrada. Testado
em 11/08: sem filtrar as subpastas (tests/, _lib/), o número de 7 dias salta de 16 para
41 — ou seja, o pathspec sozinho explica um fator de 2,6×. Enquanto a fórmula de ontem não
for recuperada, não use a comparação entre as colunas, só a tendência dentro de cada uma.
💡 Lição de método: eu reportei 34/semana, um revisor me corrigiu para 13,3, e os dois
estavam certos — era aceleração. Número em uma janela só mente. (E a lição de 11/08
completa a de 10/08: número na mesma janela, medido em dois dias, mente igual —P927.)
Esta seção afirmava um fato que a §8 usa para excluir uma ideia. O fato caiu; a exclusão
perde a base. É o achado mais grave da sonda de §3.19.
| Grupo | Guardas | Consertos por guarda (90d) |
|---|---|---|
| ~~Com teste que os cite~~ | ~~131~~ | ~~3,52~~ |
| ~~Sem nenhum teste~~ | ~~87 (39,9%)~~ | ~~1,05~~ |
O que o código diz: o 87 é quem TEM rede, não quem está sem. A fonte é o campo
guardas de .claude/canarios/_exercitados.json, e o próprio
painel_de_guardas.py:505-517 documenta que esse
arquivo lista quem a bateria RODA. O sinal estava trocado.
| Medida | Valor conferido em 11/08 |
|---|---|
Guardas com rede (_exercitados.json) |
87 |
| Guardas sem rede | 100 (painel) a 110 (reconstrução crua) |
| Desses "sem rede", quantos têm teste dedicado em disco que passa | 32 de 110 → ≥29% de falso-positivo |
| Idade da base de cobertura | congelada em 2026-08-03, com 305 commits e 154 arquivos distintos tocados em .claude/hooks/ desde então |
python -c "import json;d=json.load(open('.claude/canarios/_exercitados.json',encoding='utf-8'));print(len(d['guardas']))" # 87
pytest .claude/hooks/tests/test_p846_quarto_arquivo_sem_plano.py # 19 passed — e ele consta "sem rede"
pytest scripts/tests/test_p600_fala_que_nao_chega.py # 116 passed — idem
⚠️ O que fica de pé e o que cai:
| Afirmação | Estado |
|---|---|
| "Guarda com teste conserta 3,35× mais" | ❌ SUSPENSA. A razão foi calculada com os dois grupos trocados — ela pode se manter, inverter ou sumir. Não é fato até ser refeita |
| "É viés de sobrevivência: os testados são os que estão em uso" | ⚠️ continua plausível, mas agora é hipótese sem número |
| "15 guardas concentram 43,3% dos toques de conserto" | ⚠️ ver §3.19: em 30 dias, 74,2% do conserto está FORA do top-5, e 144 de 252 guardas (57,1%) levaram conserto. Há cabeça gorda e cauda longa |
Consequência que atravessa o documento: a §8 exclui "escrever teste para cada guarda" com o
motivo "medido: guarda com teste conserta 3,35× mais". Esse motivo está suspenso — a linha
foi marcada lá também. A causa que o repositório dá para o fenômeno segue de pé e é outra, escrita
em replay_fiscal.py:3:
"Escrevi um fiscal novo, escrevi 23 testes, todos passaram, e o fiscal errava 13 de 14 vezes
em produção — precisão de 7%. Os testes mediam o fiscal contra a minha IMAGINAÇÃO do que eu
falo. O replay mede contra o transcrito real."
| Medida | Valor |
|---|---|
stage-sensor-gate.py — execuções |
1.217 |
| Bloqueios | 1 — e foi falso positivo (barrou comando só-leitura) |
A frase que ele exige (Prova: CA-N via [...]) em todo o histórico |
0 commits |
O bilhete que só avisa (Leigo:) |
3.013 commits |
A forma livre Prova:, que ninguém cobra e a casa escreve sozinha |
25 commits |
A casa construiu a fechadura e nunca cortou a chave. E o formato que dispara portão é
evitado; o que não dispara é adotado espontaneamente.
| Medida | Valor |
|---|---|
| Lições no acervo | 875 (todas com dado — zero sem medição) |
| Já apareceram ao menos 1 vez | 263 (30,1%) |
| Medidas e nunca apareceram | 612 (69,9%) |
| Das que apareceram — mediana | 1 vez (máximo 50) |
Guardar não é lembrar. O injetor escolhe 3 por turno sobre 875 candidatas — a conta não
fecha por construção.
| Medida | Valor |
|---|---|
| Lições sem parente textual | 773 de 875 (88,3%) |
| Lições em família | 102 (11,7%), em 41 famílias |
| A maior família | 8 lições sobre a mesma raiz: "declarei pronto sem prova" |
Régua usada: a própria da casa, max(Jaccard, Overlap) ≥ 0,3
(check-pattern-duplicate.py:125).
⚠️ Honestidade: minha hipótese era que o acervo inteiro fosse repetição. Não é. A
régua vê palavras, não raízes, então subconta — mas eu estava exagerando e o número corrigiu.
✅ CONSERTADO no commit
4f5b8cf77, no mesmo dia em que foi achado. O descarte deixa
recibo visível na saída ([SUMIU: <lente> <n>ch]) e grava no log de erro. Rede permanente:
.claude/hooks/tests/test_descarte_deixa_recibo.py, 7 testes.⚠️ Esta seção ficou 8 horas descrevendo como VIVO um defeito já morto — e isso custou um
desenho inteiro. À tarde, oito ajudantes leram esta spec, não leram o código, e propuseram
consertar o que já estava consertado. Foi o defeito crítico nº 1 do quarto desenho
(§9.1). A lição virou a decisãoD19.
| Medida | Valor |
|---|---|
| Teto do injetor | 5.000 caracteres |
| O que acontece ao estourar | descarta o bloco inteiro e segue, sem registrar no log |
| Turnos que já saem colados no teto | 79 de 213 (37,1%) |
| O que a telemetria registra | "falou em 213 de 213" — falso-verde |
A linha do defeito é unified-context-injector.py:199:
um continue sem chamar o registrador de erro que o próprio arquivo mantém.
Rodando o injetor ao vivo com prompt rico, a lente de sugestão de agente desaparece
inteira, calada. É o critério 4 (falha calada) no coração da máquina.
Por que isto é o achado mais grave da obra: numa conversa, o dono percebe quando eu
esqueço. Num laço autônomo não há ninguém — as travas sumiriam em 1 turno a cada 3 e o
erro andaria sozinho por horas.
Correção contra a §6.2 desta própria spec, que marcava a peça como "manter" sem ler o que
ela diz sobre si mesma.
O comentário está no próprio código, em
meta_da_obra.py:270:
"escrever
fecha meta Nna mensagem do commit depende de EU LEMBRAR — e a lei desta casa
diz que humano lembrando morre."
| Medida na única obra que adotou | Valor |
|---|---|
| Commits que citam meta | 7 de 75 (9%) |
| Deriva mediana | 14 commits |
| Tabelas de meta no mesmo arquivo | 2 — a da máquina e uma à mão, divergindo em 5 pontos |
Consequência de desenho: propagar o formato sem apagar a lista à mão não mata o quarto
lugar de verdade — cria o quinto.
| Proposta | Devolveria | Veredito |
|---|---|---|
| Aposentar os 72 guardas sem ato útil | 5,8 min/dia | não paga a obra |
| Porteiro único (1 processo no lugar de 38) | ~8 a 16 min/dia (estimado) | não paga a obra |
Piso de processo medido nesta máquina: python + imports típicos = 51,2 ms; bash vazio =
17,1 ms. Cada guarda paga dois arranques.
⚠️ Aqui havia um número que caiu: esta seção afirmava que "~76% do tempo é só ligar
processo". A sonda de §3.12 mediu 22,5% — o76%era uma estimativa de piso dividida
pela duração média, não uma medição do trabalho real. Fica como registro, no passado, porque
ele sustentou uma proposta (o porteiro único) que morreu por causa disso.
Medida em três mecanismos idênticos, independentes um do outro. Nasceu de o dono contar como
usa o roadmap: "tenho noção pelo número de cards" — ou seja, o produto do roadmap é o
número, e card aberto depois de pronto é mentira no único número que ele lê.
Os três são a mesma coisa: uma frase na mensagem do commit.
| A frase | O que ela faz | Tem quem cobre? | Adoção |
|---|---|---|---|
fecha #N |
fecha o card no roadmap | ❌ ninguém pergunta | 8,0% |
fecha meta N |
fecha a meta na spec | ❌ ninguém pergunta | 9,0% |
Leigo: |
só documenta, não fecha nada | ✅ um gancho avisa na hora | 83,4% |
git log -n 99999 --since='30 days ago' --regexp-ignore-case --extended-regexp --grep='(fecha|closes|resolve) #[0-9]+' --oneline
A lei, em uma frase: mecanismo que depende de eu lembrar de escrever uma frase chega a
~9%; a mesma frase, com um lembrete no momento de escrever, chega a 83%.
A diferença entre 9% e 83% não é desenho — é o lembrete no instante certo. E o lembrete é
a peça mais barata do repertório: já foi provada aqui, em 3.013 commits.
Detalhe que reforça: dos 209 usos de
fecha #Nem toda a história, 208 aconteceram nos
últimos 90 dias e 170 nos últimos 30. O hábito é novo e está crescendo — mas a taxa
está travada em 8%. Crescimento de uso não conserta taxa de adoção.
Achado do ângulo advogado-do-diabo da terceira varredura. Refeito com janelas fixas em
2026-08-11 (§3.19), depois que o cético derrubou a leitura de "acelerou desde ontem".
A série em semanas FIXAS e disjuntas — é ela que mostra tendência; a janela deslizante não
mostra (P927):
| Semana fixa | Total | Ferramenta | Produto | Ferramenta % | Produto % |
|---|---|---|---|---|---|
| 15/07 → 22/07 | 562 | 119 | 374 | 21,2% | 66,5% |
| 22/07 → 29/07 | 349 | 160 | 175 | 45,8% ⬅️ a virada | 50,1% |
| 29/07 → 05/08 | 568 | 346 | 163 | 60,9% | 28,7% |
| 05/08 → 12/08 | 546 | 329 | 128 | 60,3% | 23,4% |
git rev-list --count --since="2026-08-05 00:00" --until="2026-08-12 00:00" HEAD # 546
git rev-list --count --since="2026-08-05 00:00" --until="2026-08-12 00:00" HEAD -- .claude/ scripts/ # 329
git rev-list --count --since="2026-08-05 00:00" --until="2026-08-12 00:00" HEAD -- server/ static/ Experts/ Include/ # 128
A inversão tem ~3 semanas de idade e as duas últimas semanas são planas (60,9% → 60,3%).
Isso muda a natureza do problema: não é um pico da semana desta obra, é o patamar em que a
casa passou a viver. E os 60,3% são piso, não teto — há 334 commits em 30 dias que não
caem em nenhum dos dois lados (specs/ 397, memory/ 119, .claude/knowledge/ 23 na mesma
janela), e boa parte disso é documentação da própria ferramenta.
| Janela mais longa | Ferramenta | Produto | Conferido |
|---|---|---|---|
| 180 dias | 30,2% | 56,3% | 11/08 |
| 90 dias | 25,2% | 61,4% | 11/08 |
| 14 dias | 59,7% | 27,8% | 11/08 |
| ~~30 dias~~ | ~~42,0%~~ | ~~47,2%~~ | ⛔ linha INVÁLIDA — ver abaixo |
⛔ A linha de 30 dias de 10/08 não fecha com a própria aritmética e foi descartada: o mesmo
briefing dizia 1.161 commits de harness em 2.101 totais, que é 55,3%, não 42,0% — e 42,0% de
2.101 daria 882. Não use aquela linha como base de nada. Medida hoje, a janela de 30 dias dá
45,4% × 43,1%.
⚠️ Interseção declarada (D25): 82 commits em 30 dias (8,5%) tocam ferramenta e
produto e são contados dos dois lados — em 7 dias são 15, em 180 dias são 382. As percentagens
acima somam mais de 100% por isso, e não por erro.
Ressalvas honestas, dos dois lados:
| Contra o alarme | A favor do alarme |
|---|---|
| ~~7 dias é janela curta~~ — caiu: são 3 semanas de patamar, não um pico | referência de mercado põe manutenção de ferramenta em 15-20% do esforço saudável — estamos a 3-4× disso |
| o produto não parou: 128 commits na última semana fechada | os guardas crescem e 55,3% do que se faz neles é conserto, contra 18,7% na tela (§3.1) |
a cobertura de teste é pior do que a spec dizia? ⚠️ indefinido — o 87 estava invertido (§3.3) |
há 100 a 110 guardas sem rede, mas com ≥29% de falso-positivo nessa conta (§3.3) |
A frase do advogado do diabo, que fica registrada porque é a consequência real:
"Com 60,7% da semana na ferramenta, o próximo defeito que o dono vai sentir não vai ser 'o
agente esqueceu' — vai ser uma conta de prop firm morrendo enquanto eu conserto o guarda que
fiscaliza o guarda."
2026-08-10, conferido no código e nos números. 223.392 execuções, janela de 2,74 dias.
O dono pediu para medir antes de construir; foi o que evitou construir a coisa errada.
Os guardas de um lote já rodam em PARALELO — em 10 de 10 eventos o relógio do lote fica
colado no guarda mais lento, não na soma.
| Evento | Soma dos guardas | Relógio real | O maior |
|---|---|---|---|
PreToolUse (12 por lote) |
2.624 ms | 537 ms | 418 ms |
UserPromptSubmit (31 por lote) |
7.181 ms | 751 ms | 733 ms |
Stop (17 por lote) |
6.044 ms | 2.057 ms | 1.948 ms |
| O que se acreditava | O que a medição diz |
|---|---|
| despachante único dá 4,2× | sequencial seria 2,7× PIOR; paralelo dá 1,46× |
| 76% do tempo é ligar processo | 22,5% da soma do trabalho |
| a fonte externa mede 8,16× | não existe lá: são 6,75× / 8,36× / 12,85×, e a causa é reescrita em Rust |
A régua que nasceu daqui — quanto do relógio cada guarda POSSUI: num lote paralelo,
acelerar o mais lento só rende até o segundo colocado. Então o valor de um guarda é a
soma, sobre os lotes em que ele foi campeão, de (ele − o segundo). Com essa régua o ranking
muda, e o teto total é 16,5 a 21,2 min/dia de 84,4 min/dia de relógio da frota.
O conserto que saiu disso (commit 27688bfd1): dois guardas pagavam o preço inteiro
sem ter nada a julgar — porque o discriminador barato estava por último.
| Guarda | Antes | Depois | |
|---|---|---|---|
block-worktree-deploy.sh |
752,5 ms | 78,8 ms | 9,5× |
auto-format.sh |
277,6 ms | 54,3 ms | 5,1× |
O padrão não foi inventado aqui: dangerous-commands.sh já usava esse atalho desde
2026-07-21, com o mesmo comentário explicando o porquê. Foi extensão, não invenção.
⚠️ A bancada mentiu, e a telemetria de produção me pegou. Os dois primeiros atalhos
mediram 9,5× e 5,1× na bancada e renderam ~1,0× em produção. A causa: eles
procuravam o texto-gatilho no pacote inteiro, e o pacote de um Edit carrega o
conteúdo do arquivo — como o trabalho de rotina desta casa é escrever sobre código, o
atalho quase nunca disparava. Consertado nos commitsbde1c907ae99b6ee1c9: agora leem o
caminho de verdade, e o caso realista foi de 375,2 → 72,2 ms e 173,3 → 44,0 ms.Como eu descobri, e o método vale mais que o conserto: o antes/depois da telemetria dava
pior depois — inclusive nos guardas que eu não tinha tocado. Usei os 97 guardas
intocados como CONTROLE: a mediana deles ficou 1,74× mais lenta na mesma janela, porque
a máquina estava sob carga (eu mesmo, com 15 ajudantes e baterias). Sem o controle eu teria
concluído "piorou" — ou, pior, teria acreditado na bancada.
O que eu tinha recusado — e o dono mandou consertar na raiz. sync-memory-on-stop.sh
possuía 2,17 min/dia, e eu me recusei a aplicar o atalho barato em bash: o discriminador
teria que comparar caminhos, e o caminho de uma worktree contém o do projeto como
prefixo. Em bash, isso trocaria 0,3 s por perda de lição.
Medindo para o card #915, apareceu algo pior — e vale mais que o tempo. O
grep -Fvx -f dele contra o caderno real (54.917 linhas / 8,1 MB) não terminou em
600 s, e o prazo do gancho é 10 s. Ou seja: desde que o caderno cresceu, toda
sessão de worktree encerrada teve esse guarda morto no meio, calado, e a lição daquela
sessão nunca voltou. A telemetria registrou as 327 execuções como "ok" — porque, de
fora, morrer no prazo e não ter nada a dizer são a mesma coisa.
Consertado na raiz (commit 7d12bdd67): o arquivo virou Python — um processo em vez de
oito. A comparação virou conjunto (linear), e a pergunta "estou numa worktree?" virou
identidade de diretório (os.path.samefile), que ignora grafia e não cai na armadilha
do prefixo — a mesma armadilha que me fez recusar o conserto em bash simplesmente não
existe em Python.
| Antes | Depois | |
|---|---|---|
| Caminho comum (fora de worktree) | 377,7 ms | 89,7 ms (4,2×) |
| Caminho da worktree, 60 mil linhas | > 600 s (morto aos 10 s) | < 5 s |
Rede: .claude/hooks/tests/test_sync_memory_on_stop.py, 9 testes com repositórios de
mentira em pasta temporária — inclui o teto de 5 s com 60 mil linhas, que é a regressão da
bomba, e um teste que declara a lacuna que o guarda NÃO cobre (a grafia POSIX do Git Bash,
/c/Users/...). Lição: P895.
Por que a lacuna é declarada em vez de consertada: medidos 1.084 de 1.084 pacotes
reais, todos trazem o caminho com contrabarra. A grafia POSIX é alcançável no papel e
inalcançável na prática — e o teste existe justamente para que a próxima pessoa saiba disso
sem ter que descobrir de novo.
git status acordava os porteiros do commitAchado depois dos primeiros consertos, no mesmo método: medir com carga relevante e
irrelevante. Medido e conferido em 2026-08-10.
Fonte dos números: bancada local, cada guarda alimentado com o pacote real de um
git status e de um git commit, 15 amostras por guarda, mediana — não é um comando
colável; é reprodutível rodando o guarda com o pacote na entrada padrão.
Seis guardas nasceram do mesmo molde e todos começam pela mesma pergunta de verdade
(git commit). Mas o atalho barato deles só saía quando o comando não tinha git — então
git status, git log e git diff, os comandos mais frequentes da casa, atravessavam e
pagavam um interpretador Python cada um.
Filtrar por commit é superset estrito da regra real: todo comando que casa
\bgit\s+commit\b contém, por definição, a palavra commit — inclusive
git commit --no-verify, que é o caso que a sentinela do portão existe para pegar.
E o atalho revelou o segundo custo, que era maior: com o trabalho caro cortado, o que
sobrou no relógio foi o preâmbulo — as três linhas que todo guarda roda antes de qualquer
decisão ($(cat) para ler o pacote, $(dirname) para achar a pasta). Dois processos por
guarda, em todo evento, mesmo quando não há nada a julgar. Trocados por expansão nativa do
bash: IFS= read -r -d '' e ${BASH_SOURCE[0]%/*}.
O caminho inteiro, medido na mesma máquina, com git status:
| Estado | Soma dos cinco check-* |
|
|---|---|---|
| Como estava de manhã | 1.024,4 ms | — |
Depois do atalho por commit |
372,3 ms | 2,75× |
| Depois do preâmbulo nativo | 116,8 ms | ✅ 8,8× |
Individualmente, agora: 22,6 / 22,8 / 23,1 / 23,4 / 24,8 ms. O piso do bash nesta máquina é
21,3 ms — os cinco custam o arranque do interpretador e praticamente nada além disso. O
portão (validate-commit.sh) saiu de 309,7 para 65,6 ms (4,72×) só com o atalho.
⚠️ Nota de honestidade sobre a medição, e ela é a lição
P900se cobrando: numa passada
ocheck-price-single-door.shapareceu com 140,9 ms. Era ruído de carga — repetindo com
15 amostras deu 24,8 ms (mínimo 22,0). Número solitário não vira conclusão.
Rede: 57 testes (test_familia_check_atalho_commit.py), incluindo um que lê a linha do
atalho e cobra que ela continue filtrando por commit — é o que impede alguém de alargá-la
de novo sem perceber — e outro que exige stderr limpo quando o guarda não bloqueia, que
é a rede que teria pego o defeito da contrabarra (§3.14).
Replay do motor real (scripts/check-carimbo-de-fato.py) contra o conteúdo de cada arquivo
na época, commit a commit, nos 6.002 commits de 90 dias. 2026-08-10.
| Medida | Valor |
|---|---|
Guardas que chegaram em .claude/hooks/ na janela |
185 |
| ... destes, com dente (conseguem barrar) | 31 |
| ... destes, sem carimbo e sem válvula | 30 |
... usaram a válvula [sem-fato-ok] |
0 |
Posteriores ao nascimento da lei (02467c161, 2026-07-29) |
4 — e um é arquivo de teste |
Posteriores ao portão nativo do git (30b7138f2, 2026-08-07) |
0 |
A explicação inteira está na data. A lei existe desde 29/07, mas o portão nativo
(.git/hooks/commit-msg chamando validate-commit.sh) só foi ligado em 07/08. Antes
disso ela só rodava na cópia que roda antes do git add, com a lista errada. Os três escapes
reais são todos anteriores a 07/08.
Ela morde HOJE — testado, não deduzido. Criei um guarda de mentira com dente e sem
carimbo, pus na fila do git e rodei o portão: saída 2, com a mensagem certa. O portão
inteiro levou ~60 s (a bateria rodou 15 arquivos em 58,6 s — não os 446 s que o comentário
do próprio arquivo cita para o caso pior).
⚠️ O limite dela, medido junto: com o arquivo fora da fila do git, o portão sai em
1,17 s e não vê nada. A lei enxerga o que o git enxerga — nem mais, nem menos.
Resíduo consertado no mesmo dia (commit 658b4f5b0): p600-fala-que-nao-chega.py e
p846-quarto-arquivo-sem-plano.py eram os dois legados que barravam sem declarar o fato.
Ambos carimbados.
2026-08-10. Três agentes de contexto limpo, três lentes distintas, disparados em paralelo
sobre o mesmo diff. Um deles me derrubou — e é o mais valioso dos três.
| Lente | Veredito | O que devolveu |
|---|---|---|
| Falso-negativo dos atalhos | ✅ SOBREVIVE: SIM |
0 falsos-negativos em 3.314 commits e 11.856 edits reais varridos — mas achou 3 documentos meus mentindo |
| Paridade da reescrita | ✅ SOBREVIVE: SIM |
sha256 idêntico entre o .sh morto e o .py novo sobre o caderno real |
| Preâmbulo nativo | ❌ SOBREVIVE: NÃO |
quebra real: ${BASH_SOURCE[0]%/*} não entende contrabarra |
O defeito que o terceiro achou, e por que ele importa mais que o tempo que a obra ganhou:
a expansão nativa do bash corta no /. Num caminho do Windows (C:\...\guarda.sh) ela não
acha barra nenhuma e devolve . — então source ./_telemetry.sh falhava, log_hook_event
deixava de existir e todo bloqueio parava de ser registrado, em silêncio.
🕳️ E aqui está o buraco na minha própria rede: os 52 testes passavam com o
source
quebrado, porque só olhavam o código de saída. O guarda liberava certo, com o canal de
erro gritandoNo such file or directory, e ninguém via. Virou teste permanente:
guarda que não bloqueia tem que sair calado.
O presente que ninguém pediu: a troca do atalho de git por commit curou um mudo
pré-existente. Com tabulação entre as duas palavras (git⇥commit), o filtro antigo — que
procurava git com espaço literal — não casava, e os cinco guardas saíam calados. Medido pelo
cético: antes exit=0, agora exit=2. Um portão que não barrava passou a barrar.
O que ficou como nota, e não virou código (baixa, com o número que a torna inalcançável):
o corte do valor no JSON não desescapa, então um caminho contendo aspa emudeceria o guarda —
e aspa é ilegal em nome de arquivo no Windows. O leitor de pacote tem custo linear, e pesaria
acima de ~10 KB — o máximo real medido é 6,8 KB.
As sete lições que a obra deixou (todas no acervo, todas com dado):
| Lição | O que ela guarda |
|---|---|
P894 |
desenho novo confere o código, nunca a spec — virou a D19 |
P895 |
guarda com prazo + trabalho que cresce com o dado = falha calada que a telemetria conta como OK |
P899 |
lição do acervo pode citar mecanismo já aposentado |
P900 |
antes/depois em produção sem grupo de controle mede a carga da máquina, não o conserto |
P901 |
trocar o mecanismo deixa a bancada órfã, e o conserto óbvio a deixa verde pelo motivo errado |
P904 |
a explicação da versão morta fica por cima, descrevendo a viva |
P905 |
comparador A/B cego devolve zero por construção — e zero parece a boa notícia |
Palavra de conclusão desta frente, pelo vocabulário P676: Blindado — três céticos
independentes, nenhum achado ALTO ou CRÍTICO aberto. (O preço, dito: os dois últimos commits
de conserto não têm cético em cima deles — conserto nascido de achado gasta orçamento e não
reabre o laço.)
2026-08-10, antes de escrever uma linha do quinto desenho. A primeira coisa que a sonda fez
foi matar a minha ideia favorita — e é para isso que ela existe.
A ideia era: "41,5% dos guardas nunca bloquearam nem falaram; podar esses resolve a
manutenção". Parecia óbvia e estava errada.
| Medida (90 dias de git × 188 guardas registrados) | Valor |
|---|---|
| Guardas mudos — zero bloqueio, zero fala | 78 de 188 = 41,5% |
| Quanto da manutenção eles consomem | 23,7% ❌ |
| Quanto vai para os guardas que funcionam | 76,3% |
Toques em guarda que são fix: (conserto, não construção) |
514 de 814 = 63,1% |
| Quanto os 15 maiores concentram | 40,2% |
A manutenção mora nos guardas que funcionam. Podar 41,5% da pilha devolveria menos de um
quarto do esforço — e custaria a proteção que os podados eventualmente dariam. A D2, tomada de
manhã contra a poda por tempo, ganha aqui a segunda perna: não paga por manutenção também.
⚠️ Esta seção esteve FALTANDO até a segunda revisão de 10/08. Os números existiam, tinham
sido medidos e tinham matado uma ideia — e nada disso entrou na spec; só uma menção de passagem
dentro da §3.15. É a liçãoP884em estado puro: eu estreito o escopo em silêncio, e quem lê
não tem como saber de onde veio a conclusão.
E foi desta sonda que nasceu a régua que morreu na sequência — a mesma população, olhada por
outro ângulo, com uma aritmética errada. Ver §3.15.
2026-08-10, tarde. Três céticos independentes atacaram o quinto desenho; os três derrubaram
algo meu, e o terceiro reproduziu a minha régua na casa decimal antes de demoli-la.
A régua era: custo de um guarda = toques de conserto no git ÷ (bloqueios + falas). Com ela
eu afirmei que guarda que julga texto custa 200× mais que guarda que julga fato. Os seis
defeitos, todos medidos:
| Defeito | O número |
|---|---|
| Janelas incompatíveis | numerador de 90 dias (git) ÷ denominador de 1,311 dia (telemetria) — fator 68,6× |
| O topo é divisão por zero | 13 dos 15 "mais caros" têm ZERO atos. O valor 18,0 é 9 ÷ 0,5 — a constante de escape que eu mesmo pus |
| A lista dos caros É a lista dos mudos | os mesmos 41,5% que a sonda da manhã tinha declarado irrelevantes. Eu os matei e os ressuscitei com outro nome |
| O topo nem são guardas | aparecem duas bibliotecas, o próprio medidor e um arquivo de teste |
| As duas pontas são independentes | correlação de postos entre toques e atos: 0,096 |
| Dupla contagem | quem bloqueia e fala conta 1 ato como 2 |
E o erro que teria custado caro: eu propus aposentar check-declare-done-sem-ritual.py
dizendo que ele tinha zero bloqueios. A telemetria diz 22 bloqueios em 31 horas — ele é o
3º guarda que mais barra coisa na casa inteira. E ele julga TEXTO, o que faz dele o
falsificador da minha tese, dentro dos dados que eu usei para escrevê-la.
🏠 A resposta estava em casa, escrita antes de eu errar. O próprio
scripts/quality/painel_de_guardas.pyjá adverte contra exatamente este viés de janela, no
comentário deFATIA_RECENTE— "julgar pela janela é o defeito que este par existe para
consertar". A casa documentou o defeito e eu o repeti. É a liçãoP779em estado puro.(A versão anterior desta nota citava números de linha. Eles envelheceram em três horas,
quando eu mesmo mexi no arquivo — por isso a citação agora é pelo nome do conceito.)
O que sobrevive da régua: nada como número. Como pergunta, sim: "quanto conserto este
guarda consome por ato útil?" continua sendo a pergunta certa — e responder exige janela alinhada,
ATOS = 0 virando "sem medida" em vez de "caro", e a população filtrada para guardas de verdade.
✅ CONSERTADO no commit
3e26c68a4, no mesmo dia. A causa não era o gatilho, nem o papel,
nem a decisão: era de onde ele lia. O cético grava o laudo em disco antes de responder
e só depois monta a resposta com o bloco de escopo no fim — dois pedaços em dois lugares, e o
guarda escolhia um. Agoracarimba()junta as duas fontes.Prova com o código real contra os laudos reais (livro desviado para pasta temporária):
17 laudos → 0 carimbos antes, 4 depois — e os 4 são exatamente os de veredito positivo.
Rede: 4 testes novos, 326 verdes em toda a família.⚠️ E um número meu que estava errado, pelo mesmo erro da
P908— cometido 40 minutos depois
de eu escrevê-la. Eu disse "2,9% dos laudos trazem o bloco". Esse número mistura duas
populações separadas por uma mudança: a instrução que manda o cético declarar o escopo entrou
às 05:37 do mesmo dia.
População Laudos Com o bloco Antes da instrução 478 0 — 0,0% Depois da instrução 49 15 — 30,6% A adesão nunca foi 2,9%: era zero antes e 30,6% depois. Janela misturada vira número que
engana, e o alvo do engano fui eu mesmo.
O texto abaixo é o diagnóstico como estava antes do conserto, mantido porque é o que explica a causa.
Medido em 2026-08-10 por mim e, independentemente, por dois céticos. É o defeito real que
sobrou de pé depois de os três movimentos do quinto desenho caírem.
carimba-blindado-do-cetico.py nasceu às 06:36 de 10/08 (commit 05684ea70) e está
registrado em SubagentStop. Ele faz, derivado, o que o quinto desenho ia construir à mão:
lê o laudo do cético e sobe o degrau do livro de bordo — sem sinal → mexido → testado →
validado → blindado.
| Medida | Valor |
|---|---|
| Execuções desde que nasceu | 116 |
| Eventos que ele gravou | 0 |
| Livro de bordo | 425 anotações, 100% commit — zero validado, zero blindado |
| Céticos que terminaram nesse período | 65 — e 53 chegaram com o relatório vazio |
| Chances reais (veredito positivo) | 3 |
| Carimbos | 0 |
A causa, reproduzida na bancada: ele exige que o laudo traga um bloco declarando onde o
cético olhou. Medido nos laudos que existem em disco:
| Laudos de cético gravados | 525 |
|---|---|
| Com o bloco exigido | 15 — 2,9% |
| E o que são esses 15 | laudos sobre o próprio carimbo |
O formato só foi cumprido quando o assunto era o formato. É a mesma família da lei dos 9%:
mecanismo que depende de alguém lembrar de escrever um bloco no fim rende 2,9%.
⚠️ Não consertar por reflexo. A saída provável não é cobrar melhor o bloco — é
derivar o escopo do que o cético já produz, coerente com aD13. Mas isso é desenho, e o
desenho desta obra morreu cinco vezes. Mede primeiro.
bloqueios e queria dizer bloqueios em PreToolUse — CONSERTADO✅ CONSERTADO no commit
046510e42, no mesmo dia. O campo viroubloqueios_no_portao
— o nome carrega o escopo — e nasceu um segundo,bloqueios_fora_do_portao, para o número
parar de sumir. Os dois saem juntos na ficha; só o do portão entra na taxa de "apita demais",
porque fora dele a saída 2 é o protocolo do evento, não trabalho travado.A prova, contra a telemetria real: 359 bloqueios estavam invisíveis, em 4 guardas.
Guarda No portão Fora Execuções check-declare-done-sem-ritual.py0 23 242 notify-subagent.py0 319 522 auto-capture.py0 10 234 check-nonexistence-claim.py0 7 240 Rede: 4 testes novos — um cobra que o nome curto não volte, outro roda contra a telemetria
real e falha se o guarda deStopreaparecer com zero. 103 verdes na bateria do painel.
O diagnóstico abaixo é como estava antes do conserto, mantido porque explica a causa.
Conferido no código em 2026-08-10 — e não era bug, era decisão de desenho documentada.
O comentário do campo, no próprio painel, declarava: "só PreToolUse — bloqueio nos outros
eventos tem outro sentido". E faz sentido: saída 2 em Stop é o protocolo daquele evento, não
trabalho barrado.
O defeito é o nome. Um campo chamado bloqueios que vale zero para um guarda que barra 22
vezes por dia leva quem consome o painel — eu — à conclusão exata oposta.
| Medida | Valor |
|---|---|
| Guardas onde painel e fonte crua divergem nos atos | 35 de 188 (18,6%) |
Guardas com bloqueios = 0 no painel que barram de verdade |
4 |
| O maior deles | notify-subagent.py: 314 bloqueios reais, 0 no painel |
Isto é a lição P788 acontecendo no instrumento de medição da casa: um proxy barato de
observar vira o número em que se confia, e ele produz confiança porque produz número.
2026-08-10, ordem do dono: "sweetspot e robusto". O sweetspot não era peça nova.
O defeito que motivou: contagem que fica para trás numa revisão aconteceu três vezes num
dia, sempre pelo mesmo mecanismo — a revisão acrescenta seções, e todo número escrito no
resumo envelhece no mesmo instante. É conferível por máquina, então passou a ser.
Onde entrou, e por que aí: scripts/quality/referencias_da_spec.py já existia, já fazia a
metade documento × código (o padrão DOCER) e já era chamado pelo portão do commit.
Ganhou a segunda metade — documento × ele mesmo:
| O que ela pega | O caso real que a motivou |
|---|---|
secao-inexistente |
§7 citado num documento que não tem seção 7 |
contagem |
o resumo dizia 23 decisões; a tabela tinha 26 |
linha-fora-do-arquivo |
uma nota citava arquivo.py:241; 3 horas depois o arquivo mudou e o número passou a mentir |
data-no-futuro |
datas de "amanhã", dando impressão de conferência que não houve |
Custo no dia-a-dia: zero. Não é gancho, não roda em todo comando — roda onde o verificador
já rodava. Zero guarda novo, zero peça nova: é a D18 cumprida por absorção.
E ela achou um defeito na primeira passada: a lista de "arquivos protegidos, não mexer"
desta spec protegia o caminho antigo server/signals.py, que não existe no repositório. Os reais são
server/af/signals.py e server/routes/signals.py — e o segundo ficava de fora da
proteção enquanto o texto dava a impressão contrária.
A contagem que não dá para adivinhar: o autor DECLARA. A conferência de decisões funciona
porque | **D7** | é convenção inequívoca — mas ela não generaliza, e a primeira versão parou
aí, deixando "21 medições" sem conferência com a justificativa de que "generalizar acusaria
prosa legítima". É verdade, e mesmo assim era desculpa: o problema não era o filtro, era o
texto não dizer o que estava contando.
A saída é a mesma da indústria quando a ferramenta não pode adivinhar a intenção — uma
anotação inline, invisível no texto renderizado, da família de # type: ignore e
<!-- prettier-ignore -->:
| O que já se sabe? | <!--conta:secoes:3-->21 medições, todas com fonte |
| Propriedade | Como fica |
|---|---|
| Falso positivo | zero por construção — sem anotação, nada é conferido |
| Anotação com nome errado | ❌ grita: conta:decisoess não existe — trava desligada em silêncio é o defeito nº 2 do dono |
| Exemplo em bloco de código ou crase | ignorado — senão a própria documentação da ferramenta seria a primeira coisa que ela acusa |
| Contadores | decisoes · secoes (com argumento: secoes:3 conta as 3.x) |
⚠️ E há um número aqui que eu deliberadamente NÃO anotei: os "5 desenhos mortos". A spec
tem 3 seções 9.x, porque dois dos cinco desenhos não ganharam seção própria. Anotar
secoes:9 faria a ferramenta cobrar 3 onde o texto diz 5 — e a resposta certa não é mudar
o texto para 3, que estaria errado. Contagem que não é derivável do documento não deve ser
anotada; anotar seria trocar um número velho por uma conferência falsa.
Rede: 60 testes na bateria. A maioria é de ruído — de propósito, porque aviso que erra
é desligado mentalmente. Dois rodam contra esta spec, um contra as 325 specs reais.
A peça foi entregue como "nível sênior" e submetida a duas lentes. As duas devolveram
PARCIAL, e — sem se falarem — apontaram o mesmo mecanismo raiz.
| O que mediram | Valor |
|---|---|
| Acusações nas 325 specs reais | 12 — e as 12 eram FALSAS (o filtro irmão, no mesmo arquivo, nasceu com 93%) |
| Quantas eram o mesmo homônimo | 7 — constants.py:1583 casando o arquivo de 99 linhas em vez do de 2.713 |
| Travessia de diretório | provada — leu um arquivo do Python, fora do repositório |
| O typo na anotação | mudo em 7 de 7 formas, contra a promessa escrita de que "grita" |
conta:decisoes:7 |
contava todas — o argumento era aceito e ignorado, e o número batia |
| No portão do commit | zero avisos em 2.200 commits — esta spec não está na lista das publicadas |
A raiz era uma só: a metade nova não herdou os dois filtros que a metade velha tinha há
meses — exigir a barra no caminho, e recusar caminho de fora do repositório. Um conserto, no
mesmo lugar, matando o falso positivo de 58% e a travessia.
Depois da reforma: 0 acusações nas 325 specs · 21 de 21 casos de prova como
esperado (os 7 typos passaram a morder; os 4 falsos positivos calaram) · custo das 325 caiu
de 1.224 para 915 ms, porque o cache pagou mais do que os filtros custaram.
⚠️ O que ficou DECLARADO e não consertado, porque não dá para consertar honestamente: a
cobertura do detector de linha é 1,9% — ele só morde se o arquivo encolher 68%. O
defeito que motivou a peça (a citação que envelheceu em 3 horas) não é pego por ele. Com
zero falso positivo ele ainda vale o que custa, mas não é o que eu disse que era.
Medido pelo advogado do diabo em 2026-08-10, sobre os 111 commits do espelho do roadmap. É o
número que a D16 (não congelar) precisa encarar de frente.
| No roadmap | Valor |
|---|---|
| Cards de produto abertos (servidor, robô, painel, contas) | 62 |
| Parados há mais de 30 dias | 20 |
| Parados há 50 dias ou mais (e é piso censurado — o espelho só começa em 22/06) | 13 |
| Cards de ferramenta nascidos nos últimos 14 dias | 54 de 65 (83%) |
Entre os parados, três que envolvem dinheiro real: o canário que precisa passar antes de entrar
conta real de mesa proprietária (#381), a morte por piso passando em silêncio
(#592/#594/#595) e o anti-troca de dia cego das 22h às 00h (#627).
E a ressalva que esta spec fazia caiu. A §3.11 dizia "7 dias é janela curta". Em 14 dias
a inversão persiste: 58,3% ferramenta contra 29,6% produto. São duas semanas, não uma.
O que o roadmap revelou, contra a minha hipótese: o número de cards não cresce por falta de
fechamento —fecha #Njá responde por ~75% dos fechamentos, com apenas 8% dos commits
usando-o. Ele cresce por criação: cerca de 340 cards nasceram em 20 dias.
⚠️ Nenhum número desta seção foi reconferido em 11/08. Os cards moram no Postgres e a sonda
de §3.19 foi só-leitura de git e disco. Esta é a maior mancha de "não conferido" que sobrou.
SOBREVIVE: NÃODisparada antes de escrever o sexto desenho, cumprindo a D19 (conferir o código, nunca
esta spec) e o CA-5 (medição que matou uma ideia tem seção própria). 11 ajudantes ·
1.150.445 tokens · 37 minutos. Nenhuma linha de código escrita; nenhuma peça proposta.
O veredito, em uma frase: o diagnóstico da obra continua de pé e do tamanho que ela dizia —
o que caiu foi a leitura dele, e as duas pistas que eu tinha para o sexto desenho.
| A spec afirmava | O código diz | Onde foi corrigido |
|---|---|---|
| "87 guardas SEM teste" | 87 é quem TEM. Sinal invertido; sem rede são 100-110, com ≥29% de falso-positivo | §3.3 |
| "a inversão acelerou desde ontem" | Datada em 22-29/07; as duas últimas semanas são patamar (60,9% → 60,3%) | §3.11 |
| "30 dias: 42,0% × 47,2%" | Aritmética não fecha — 1.161 de 2.101 é 55,3%. Linha descartada | §3.11 |
| Pista | O que a derrubou |
|---|---|
| "reformar um punhado de arquivos resolve" | 74,2% do conserto está FORA do top-5, e 144 de 252 guardas vivos (57,1%) levaram conserto em 30 dias. Há cabeça gorda e cauda longa |
| "a ferramenta é mais concentrada que o produto" | Invertido, e por falta de grupo de controle (D22). Em 90 dias, 40 arquivos do produto cobrem metade dos toques contra 122 da ferramenta. Concentração é propriedade de repositório, não patologia da ferramenta |
| Fato | Número | Comando |
|---|---|---|
| 🟡 Peça pronta, sem gatilho automático | scripts/quality/mutation_check.py, atrás de 3 variáveis de ambiente. ⛔ A linha dizia "zero execuções em 530 sessões" e era FALSA — derrubada em 11/08: são 20 execuções à mão (9 em 10/08, 7 em 22/07, 4 em 07/08). O medidor que dizia zero (.claude/.fiscais-medidos.jsonl) só enxerga ganchos; script solto rodado por comando nunca apareceria ali |
sed -n '40,46p' scripts/quality/pre-deploy.sh · contagem: tool_use de Bash casando python.*mutation_check\.py\s+\S nos 519 transcritos |
| Canal único com adoção parcial | _lib/entrega.py em 55 de 238 arquivos (23,1%) — 44 .py + 11 .sh |
listagem dos .sh que citam entrega |
| A peça mais reincidente da casa | validate-commit.sh: 37 dos 54 consertos da vida dele (68,5%) no último mês; 115 commits no total; nasceu em 27/02/26 |
git log -n 99999 --format='%s' -- .claude/hooks/validate-commit.sh \| grep -cE '^fix' |
D6 (zero descartes) — é baixo, e a palavra "nunca" não se sustenta44 guardas (23% dos 191 registrados) não registraram bloqueio nem fala na janela medida e
constam sem rede. O que eles custaram:
| Medida | Valor |
|---|---|
| Fatia do conserto da pasta, em 90 dias | 7,70% (78 de 1.013 commits) — ou 5,72% por par commit×arquivo |
| Commits dedicados a eles | 6 de 86. Os outros 80 foram carona (mediana 4 arquivos/commit, máx 26) |
| Quantos tiveram zero commit em 90 dias | 14 dos 44 |
⛔ O cético anulou a palavra "nunca", e o motivo importa mais que o número. As duas janelas,
lado a lado (D25): telemetria = 25,64 h contra git = 90 dias — razão 84:1. E as
25,6 h não são escolha analítica: são o resto de uma poda automática
(rotacao_telemetria.py:45, teto de 200 mil linhas,
guarda 150 mil). A janela ENCOLHE conforme a casa fica mais movimentada — então guarda de
evento raro (deploy, compactação, Playwright) aparece mudo por artefato de poda.
Duas provas de que a classificação é frágil:
loop-detector.py, orcamento-de-cetico.py, block-sync-subagent.py,completeness-critic.py e canonical-carteiro-enforcer.py.loop-detector.py: 2 bloqueios em 6.673 execuções (1 a cada 3.336). Pela regra de três,A frase honesta é: "44 guardas não registraram ato em 25,6 horas". Não é "44 guardas
inúteis".
Quatro denominadores em circulação, e dois scripts da própria casa discordam em 7 lendo o mesmo
settings.json:
| Fonte | Diz |
|---|---|
ls .claude/hooks/*.py *.sh |
238 (188 .py + 50 .sh) |
painel_de_guardas.py |
191 registrados |
fiscais-mudos.py |
184 |
| Briefing de 10/08 | 218 |
Sem um denominador único, toda percentagem sobre "os guardas" é indefinida — e há 29
ferramentas de medição em scripts/quality/.
CA-1 — o que JÁ existe e não precisa ser construído| Peça | Estado | Número |
|---|---|---|
_medidor.py |
LIGADO | 235 de 238 registros (98,7%) |
scripts/quality/painel_de_guardas.py |
LIGADO (SessionStart) | 191 guardas · 1 mudo · 2 apita-demais · 7 fala-pro-vazio · 100 sem rede |
scripts/quality/regua_de_corte.py |
LIGADO | 192 fiscais · 151.551 execuções · 5,0% das EXECUÇÕES falaram (não "5% dos guardas" — troca de unidade corrigida) |
scripts/quality/fiscais-mudos.py |
LIGADO | 0 mudos · 5 raros (1-5 execuções) |
scripts/quality/regras-orfas.py |
LIGADO | 9 docs · 2 órfãs: limites-da-ferramenta.md, tier-l-decision-criteria.md |
.claude/hooks/_lib/entrega.py |
PARCIAL | 55 de 238 (23,1%) |
.claude/canarios/_exercitados.json |
DEFASADA | congelada em 03/08; 305 commits em .claude/hooks/ desde então |
scripts/quality/escuta_de_canarios.py + derivar_canarios.py |
SEM GATILHO | existem, funcionam, e ninguém os dispara: o comando que os roda mora num docstring. Ver §3.20 |
~~.claude/hooks/tests/test_loop_detector.py — "NÃO RODA, timeout 6m40s"~~ |
✅ A AFIRMAÇÃO ERA FALSA | o caminho estava errado (o teste é tests/hooks/test_loop_detector.py) e ele passa: 10 passed in 0.67s. Achado pelo conferente de referências da própria casa, referencias_da_spec.py, minutos depois de eu escrever a linha |
Hipótese testada e DESCARTADA: guarda órfão genuíno — 0 confirmados. Dos 53 candidatos do
grep ingênuo, ~20 foram abertos à mão e todos eram falso-positivo por indireção (fan-out do
unified-context-injector.py, ou instalador de gancho nativo ensure-*-commit-hook.sh).
Lista obrigatória (P884): eu estreito o escopo em silêncio e o dono precisa perguntar "cadê o
resto?".
| Buraco | Por quê |
|---|---|
| A causa real de 61,6% dos consertos de guarda (306 de 497 em 30 dias) | A classificação foi por texto no assunto do commit; a causa mora no corpo ou no diff. 497 corpos não foram lidos |
| Se a reforma do canal único funcionou ou falhou | Os dois detectores da família nasceram dentro da janela (03/08: 9516a48da, 75210d5bb), e 67% dos consertos são em guardas de fev-jun. Pode ser doença viva ou termômetro novo expondo passivo de 5 meses — os dados aceitam as duas |
| Todos os números do Roadmap (§3.18) | Moram no Postgres; a sonda foi só-leitura de git e disco |
| Oito números da lente de causa | Citavam classify2.py como comando — o arquivo não existe no repositório, era artefato volátil de outra sessão. Irreprodutíveis |
| Se os ~33 candidatos a órfão não abertos escondem um genuíno | Só ~20 dos 53 foram verificados à mão |
Se test_loop_detector.py passa |
Morreu por timeout sem uma linha de resultado |
| A fórmula de "guardas novos/semana" usada em 10/08 | Sem ela, a comparação entre as duas colunas da §3.2 é indefinida |
🔎 A lição de método que a sonda pagou, e que vale além desta obra: três dos vinte números
corrigidos caíram pelo mesmo defeito — janela relativa usada para afirmar tendência. Virou
a liçãoP927. Qualquer número medido em--since='N days ago'nesta spec deve ser lido
como fotografia daquele instante, nunca como ponto de uma série.
Medido em 2026-08-11, logo depois da §3.19, seguindo a diretriz do dono: "focando na raiz e
não no sintoma, e também focando em unificação". Esta seção existe porque a medição matou
uma hipótese minha e achou outra — é o CA-5.
O sintoma é conhecido: ~100 guardas aparecem como "sem rede", e a §3.19 mediu ≥29% de
falso-positivo nessa conta. A causa não é a que eu supus.
"O painel só olha 2 das 10 pastas de teste da casa
(painel_de_guardas.py:71), então perde os
testes que moram fora delas."
❌ Falsa como causa principal. O código diz o contrário, e o próprio docstring da função já
tinha aprendido a lição em 02/08:
"CITAÇÃO NÃO É COBERTURA. A primeira versão marcava 'tem teste' todo guarda cujo NOME
aparecesse em algum arquivo de teste. Medido: isso marcava 98 guardas, e 44 deles nunca eram
rodados. A verdade de terreno é outra: quem tem rede é quem a bateria RODA."
— painel_de_guardas.py:498-507
As duas pastas são o plano B, usado só quando o arquivo de verdade some — e o painel
avisa na tela quando cai nele ("só CITAÇÃO em teste (fonte fraca)"). O desenho está certo.
| # | O defeito | A prova |
|---|---|---|
| 1 | A colheita não tem gatilho. O comando que produz a verdade mora num docstring; nenhum gancho, nenhum portão, nenhum cron o dispara | busca por escuta_de_canarios em todo o repositório: zero chamadas executáveis — só .planning/, docstrings e um teste que o importa |
| 2 | A colheita, quando roda, olha as mesmas 2 pastas de 10 | o comando publicado é python -m pytest scripts/tests .claude/hooks/tests -q -p escuta_de_canarios (escuta_de_canarios.py:32) |
O estado do arquivo de verdade, hoje:
| Medida | Valor |
|---|---|
Último commit de .claude/canarios/_exercitados.json |
fa89e3825, 2026-08-03 |
Commits em .claude/hooks/ desde então |
305 — e 154 arquivos distintos tocados |
O campo medido_em |
literalmente "regravado sem data — passe CANARIO_DATA" |
| Guardas listados | 87 |
git log -n 8 --format='%h %ad %s' --date=short -- .claude/canarios/_exercitados.json
python -c "import json;print(json.load(open('.claude/canarios/_exercitados.json',encoding='utf-8'))['medido_em'])"
Esta seção afirmava, em 11/08 pela manhã:
~~"toda decisão de aposentar, absorver ou proteger um guarda depende deste número. A
D6×
D8é decidida por ele. A régua de utilidade mínima da §7.2 é decidida por ele."~~
FALSO, e é a D19 violada por mim: herdei a frase do texto da própria spec sem abrir o
código. É a mesma causa de morte do quarto desenho (§9.1), cometida na seção escrita para
denunciá-la.
| O que eu afirmei | O que o código diz | Prova |
|---|---|---|
| o número decide aposentadoria | tem UM consumidor executável, e ele imprime | os únicos arquivos que leem _exercitados.json são os dois produtores e painel_de_guardas.py:512 |
| ele é cobrado em algum portão | roda uma vez, em SessionStart --curto |
.claude/settings.json:268 |
| "sem rede" é alarme | não está na tupla graves — cai na linha de resumo |
painel_de_guardas.py:788: graves = (SUMIU, QUEBRADO, MUDO, APITA_DEMAIS, FALA_PRO_VAZIO) |
| ele pode barrar algo | main() devolve 0 em todos os caminhos (fail-open declarado) |
painel_de_guardas.py:900-906 |
a D6×D8 é decidida por ele |
as três decisões que eu nomeei já estavam mortas ou excluídas pela própria spec | D2 (não podar) · D6 (zero descartes) · D24 (nada se aposenta pela régua) · §3.14-B (a sonda matou podar guardas mudos) |
E o "~100 mal classificados" também encolheu, pelo mesmo tipo de erro — eu contei quem está
fora da lista guardas e chamei de "desconhecido", mas o arquivo tem três listas:
| Quantos | O que é | |
|---|---|---|
fora da lista guardas |
106 | ⛔ o número que eu publiquei |
| vistos e descartados por REGRA | 71 | a colheita os VIU rodando com pacote vazio e decidiu, de propósito, que "rodar vazio não é julgar" — derivar_canarios.py:86-91 |
| nunca vistos por nenhuma das três | 35 | ✅ estes sim são "não sei" |
O próprio produtor documenta a escolha de régua: "contando qualquer execução, o painel diria
25 guardas sem rede; contando só quem julgou um pacote, são 102"
(derivar_canarios.py:77-80). A distância entre 25
e ~100 é escolha de régua, não fotografia velha.
O defeito é real, e é menor e mais preciso do que eu escrevi: um instrumento de tela
apresenta 35 guardas que ele nunca mediu como se fossem "sem rede" — afirma o que não
sabe. É o defeito nº 2 do dono ("mentir que terminou") dentro do medidor, e vale conserto.
O que ele não é: um número que decide alguma coisa.
E a LEI da obra continua valendo para a fotografia — só não com a importância que eu dei:
Escrito por máquina disparada por evento = vive. Escrito por humano que precisa
lembrar = morre — inclusive quando existe lembrete automático.**
Colheita PARCIAL é pior que colheita velha. Se o gatilho for pendurado no portão do commit,
que roda uma bateria seletiva (§3.19: "porteiro seletivo, 40 de 129"), o resultado seria
"os guardas cujo teste não rodou desta vez perderam a rede" — o instrumento passaria a
mentir ativamente em vez de apenas envelhecer.
A saída de desenho que isso obriga: o artefato tem que deixar de ser fotografia que
precisa estar completa e virar livro-caixa que acumula, com data por guarda. Aí colheita
parcial soma em vez de apagar, e a idade fica visível por guarda em vez de global.
Nenhuma linha foi escrita. O desenho vai a cético antes — é o que ficou 250× mais barato (§9.0).
Lente "a peça já existe" — a que matou cinco desenhos. Veredito: SOBREVIVE: SIM, as quatro
portas confirmaram o diagnóstico. Mas ele achou quatro defeitos adjacentes, e o primeiro é meu.
| # | O que caiu | O certo | Prova |
|---|---|---|---|
| A1 | "512 commits desde a foto" — ⛔ peguei um número de janela de 14 dias e colei numa afirmação de "desde o commit da foto" | 305 commits · 154 arquivos distintos | git rev-list --count fa89e3825..HEAD -- .claude/hooks/ → 305; o 512 vinha de --since="14 days ago", que hoje dá 509 |
| A2 | "o mesmo mecanismo que o conftest já usa" — ⚠️ não é simétrico |
o precedente (teto_de_subprocesso) tem aplica() e devolve(), chamados de pytest_configure/pytest_unconfigure; o escuta_de_canarios remenda no corpo do módulo e nunca desfaz |
conftest.py:41-55 contra escuta_de_canarios.py:172-174; busca por pytest_unconfigure ou devolve no plugin: zero |
| A3 | o artefato tem 3 listas, não 1 | guardas 87 · so_rodaram_vazio 92 · so_carregados 16 |
leitura do JSON |
| A4 | o docstring do painel já envelheceu na direção que esta seção denuncia | ele afirma "são 64, não 98"; o arquivo tem 87 | painel_de_guardas.py:507 |
O A1 importa mais pelo que ele é do que pelo tamanho. O argumento sobrevive inteiro em 305 —
o que não sobrevive é a confiança, e este é o documento cuja única função é não mentir. Foi
achado por um cético lendo o rascunho três horas depois de eu escrever a §3.19, que existe
justamente para registrar 20 números que eu tinha errado. (A §0 avisa que revisar documento
longo cria contradição nova. Esta é a prova, ao vivo, no mesmo dia.)
O A2 muda o desenho, e para pior. O remendo global era o risco marcado como "o que mata o
desenho", e a mitigação que eu tinha era "o mecanismo já está provado no conftest". Não
está: a peça citada como precedente restaura, esta não. Ligar por import deixaria
subprocess.run, Popen e spec_from_file_location remendados até o processo morrer.
Consequência: o movimento do gatilho passa a exigir aplica()/devolve() no plugin — deixou
de ser fiação de 1 linha.
Do desenho morto sobrou um conserto, e ele é no consumidor, não no produtor. Entregue em
2026-08-11.
A regra, em uma frase: "não está na lista" tinha duas causas, e o painel chamava as
duas de sem rede — afirmando o que não sabia sobre metade delas.
| Causa | Quantos | O honesto |
|---|---|---|
a colheita viu o guarda e decidiu que aquilo não era rede (so_rodaram_vazio, so_carregados) |
71 | sem rede ✅ já estava certo |
| a colheita nunca viu — o guarda nasceu depois dela | ~30 | ⚠️ era sem rede, virou nao da pra dizer |
O estado certo já existia no arquivo, criado pelo mesmo raciocínio 9 dias antes, e o
docstring de classificar já explicava por quê:
"
porta_usadaaceitaNonede propósito: ... TratarNonecomoFalseseria chamar de
'sem tráfego' o que na verdade é 'não medido' — o erro de 2026-08-02, em escala."
— painel_de_guardas.py:221-223
Era o mesmo erro, no eixo do lado. É a lição P779 em cheio: a resposta estava em casa, no
arquivo que o desenho já estava lendo.
A prova, no dado real — a linha do bootstrap antes e depois:
antes: 2 evento sem canal · 100 sem rede · 0 nao da pra dizer · 0 sem trafego · 78 saudavel
depois: 2 evento sem canal · 70 sem rede · 31 nao da pra dizer · 0 sem trafego · 78 saudavel
| O que foi conferido | Resultado |
|---|---|
| bateria do painel | 107 verdes (era 104; entram 3 testes novos) |
| mutação dirigida — apagar o ramo do "não medido" | ❌ morre (1 falha) |
| mutação dirigida — fonte cega virar "não sei" para todos | ❌ morre (1 falha) |
| restaurado | ✅ 107 verdes |
A armadilha que o próprio teste da casa pegou, e vale registrar: a primeira versão do
conserto tratava "a fonte não diz quem mais ela viu" como "não medido" — e isso apagaria o
estado sem rede inteiro para qualquer base antiga. O teste ponta a ponta que já existia
ficou vermelho e me corrigiu. A regra final é mais estreita: o nao da pra dizer só aparece
com prova positiva de que a colheita enxergou parte do mundo.
⚠️ O que este conserto NÃO faz, dito: não regenera a colheita (ela segue de 03/08, sem
gatilho), não muda os 71 legítimos, e não faz o número decidir nada — o painel continua
imprimindo e devolvendo 0. Ele faz o relatório dizer a verdade, que era o tamanho honesto do
problema.
Nasceu da cobrança da lição P884: a pesquisa entregou 6 peças e o desenho cobriu 2.
Antes de desenhar o sétimo, varri no CÓDIGO o que cada uma das seis já tem — que é a lente
CA-1, a que matou 5 dos 6 desenhos. Desta vez ela rodou antes, não depois.
A pesquisa-spec-driven-e-harness.md §10 marca dois dos quatro momentos de verificar como
buraco: "antes da 1ª linha de código" ❌ falta, e "as N tarefas têm N provas?" ⚠️
"existe o mecanismo, falta ligar ao plano". A segunda afirmação está errada — e a
primeira está certa por um motivo diferente do que a pesquisa supunha.
# o medidor completo ficou no rascunho da sessão; a regra que ele aplica:
# tarefa = cabeçalho `##|### (Task|Fatia|Frente|Peça|Bloco|A1…)`; comando = ferramenta
# executável (python/pytest/npx/curl/git/psql/ssh/bash/scripts/) em qualquer posição
# de uma linha com rótulo de verificação, ou nas 5 linhas seguintes.
ls ~/.claude/plans/*.md | wc -l
| O que | Quantos | % |
|---|---|---|
| Planos aprovados nesta máquina | 19 | — |
| …com seção de verificação no nível do plano | 19 | 100% |
⛔ O RESTO DESTA MEDIÇÃO FOI DERRUBADO no mesmo dia, por três céticos independentes. Ficam
registrados como o que são — um ponto arbitrário dentro de uma faixa larga:
| Eu publiquei | O que outros dois mediram, com critérios próprios |
|---|---|
| 124 tarefas | 113 (um) · 126 (outro, aplicando a regra que ESTA seção descreve) |
| 103 com campo (83,1%) | 82 |
| 72 com comando (58,1%) | 25 a 76 — ou seja, 22,1% a 67,3%, só trocando o critério |
A regra publicada aqui não reproduz os números publicados aqui — ela gera 126/82/55. Um dos
dois está errado e não dá para saber qual, porque o medidor foi descartado no rascunho.
🎯 A leitura honesta: "quantas tarefas carregam comando de prova" não tem resposta
conhecida nesta casa. Não existe gabarito contado à mão, e o mesmo corpo de 19 planos já
devolveu 3,6%, 58,1%, 87,1%, 22,1% e 67,3% conforme quem conta. O que
sobrevive é a pergunta, não o número.
A causa raiz, e ela é mais interessante que o número: os 19 planos usam 15 formas de
marcar uma tarefa e 23 de declarar a verificação dela, com rotatividade de ~1 forma nova por
plano. Nada nunca exigiu uma gramática — então qualquer leitor automático não lê um formato,
adivinha um dialeto. E o próprio plan.md alimenta a confusão: manda o campo se chamar
Verificacao na linha 133 e escreve Validacao nas linhas 100, 111 e 197.
plan-approved-task-tracker.py roda em PostToolUse ExitPlanMode — o momento exato da
aprovação do plano, antes da primeira linha de código. É o momento que a pesquisa chamou de
"a única peça do mercado que vale enxerto".
| O que ele recebe | O que ele faz com isso |
|---|---|
tool_input.plan — o texto inteiro do plano |
❌ nada |
tool_input.planFilePath — o caminho do arquivo |
❌ nada |
| — | ✅ injeta um lembrete de texto e grava .claude/.plano-aprovado.json |
Confirmado em 26 chamadas reais de ExitPlanMode nos transcritos (eu tinha escrito 5 — era
uma fatia; a varredura completa dos 519 arquivos, deduplicada por identificador de chamada, dá
26): o campo plan chega com 4.604 a 23.038 caracteres, e planFilePath vem em 25 das
26. E a varredura de .claude/hooks/ por planFilePath / .claude/plans devolve uma linha
só — uma fixture de teste passando "..." como espaço reservado
(tests/test_plan_task_tracker.py:54). Nenhum guarda da casa lê o texto de um plano.
🎯 A frase que resume: o sensor já está de pé na porta certa, com o envelope na mão, e
nunca o abriu. Não falta guarda novo — falta abrir o envelope.
A primeira versão deu 3,6% e estava errada: ela exigia que a linha começasse com a
ferramenta, e a forma real é - **Verificação:** \cd server && pytest …`— o rótulo vem antes.
Falso negativo em **13 de 13** tarefas de um plano. Uma versão anterior a essa, frouxa demais
(qualquer crase contava), deu **87,1%**. **Os três números saíram do mesmo dado.** Só a
conferência à mão de um plano separou o verdadeiro dos dois falsos — é o anti-padrão
*"medidor caseiro"* da/sonda`, pego dentro da própria sonda.
| # | Peça da pesquisa | Estado no código, 11/08 |
|---|---|---|
| 1 | Quem julga é sensor que devolve 0/1, não o agente | ✅ existe e é a força da casa — 44 guardas no portão do commit |
| 2 | Quem julga ≠ quem fez | ✅ existe e tem trava — pretask-preflight.py, orcamento-de-cetico.py, check-declare-done-sem-ritual.py, os três registrados |
| 5 | Teste verde não é prova (SpecBench → mutação) | ⚠️ pronto, sem gatilho automático — mutation_check.py em pre-deploy.sh:43, com 20 execuções à mão em 3 dias distintos. (Eu tinha escrito "0 ocorrências em 187.911 linhas de telemetria": aquele diário só mede ganchos, e este é script solto — ele nunca apareceria ali, rodando ou não.) |
| 6 | 1 conversa = 1 tarefa | ❌ contradiz a política atual (compactar em vez de fechar). É decisão do dono, não achado técnico |
Achado a partir de duas perguntas do dono — "já não temos a skill plan e o template pra spec?"
e "e a spec template, achou ela?". As duas eram a lente CA-1, e as duas mudaram a resposta.
Esta seção é a mais importante da spec: ela mostra que sete desenhos tentaram inventar um
sistema que esta casa desenhou, construiu, testou com 42 casos e declarou IMPLEMENTADO há dois
meses e meio.**
specs/harness-3-portoes.md, S384, 2026-05-27Ela ataca, nomeando, os mesmos quatro furos que a pesquisa de agosto "descobriu": premissa
não re-ancorada · cobertura por arquivo em vez de por requisito · sensor visual decidido no chute
· juiz funcional sendo o mesmo cérebro. Cita as mesmas fontes (harness engineering, ATDD,
"quem julga tem que ser ferramenta que devolve 0/1").
| Portão | O que faz | Estado medido em 11/08 |
|---|---|---|
| A — premissa | exige confirmar o problema contra o terreno real antes de desenhar | ✅ vivo — 0g em REQUIRED["L"], tratado em brainstorm-phase-auditor.py:188-193, registrado |
| B — carimbo | cada critério de aceite leva [logic] · [journey] · [visual] |
⚠️ parcial — está no specs/TEMPLATE.md, e chegou em 35 de 328 specs |
| C — sensor verde | o commit da tarefa só fecha com o sensor provado; [visual] sem prova de navegador BLOQUEIA |
❌ órfão — vivo e registrado em Bash(*git commit*), esperando um token que ninguém emite |
O contrato tem três elos. Medidos:
| # | Elo | Adesão | |
|---|---|---|---|
| 1 | a spec traz CA-N com carimbo de sensor |
35 de 328 = 10,7% | ⚠️ |
| 2 | o plano diz qual CA-N cada tarefa satisfaz |
1 de 19 = 5,3% | ❌ |
| 3 | o commit carrega Prova: CA-N via [sensor] |
1 de 4.000 = 0,025% | ❌ |
# elo 3, o mais fácil de reconferir:
git log --format=%B -n 4000 | grep -ciE 'prova\s*:\s*CA[-_ ]?\w+\s+via'
O elo 2 nunca foi construído. O .claude/commands/plan.md não menciona critério de aceite uma
única vez — nem CA-N, nem Prova:. E git log -S "Prova: CA" nesses arquivos volta vazio:
a linha que o guarda espera nunca existiu no template que deveria emiti-la.
O stage-sensor-gate.py declara no próprio texto que o token é "mandado pelo template do
/plan". Ele foi construído contra um produtor que não foi construído junto.
🔎 Esta é a
D8ao contrário e vale como lei: a casa criou uma peça nova (o portão) sem
criar o elo que a alimenta — e o resultado não foi um portão ruim, foi um portão mudo por
dois meses e meio, disparando em todo commit e nunca vendo nada. Peça nova precisa nascer com
quem a alimenta, não só com quem a chama.
Ligar o elo 2 hoje emitiria Prova: CA-N apontando para critérios que 90% das specs não têm
carimbados. E o elo 3 tem dente: [visual] sem prova de navegador barra o commit. Ligar a
corrente com dois elos fracos transforma um guarda correto em falso-alarme — que é o defeito que
esta obra mais mediu. Decisão do dono, 11/08: conferir antes de ligar. Os dois furos foram
conferidos e estão acima; o terceiro (o acervo de specs sem carimbo) segue aberto.
O dono mandou analisar specs/painel-de-progresso-do-harness.md, fechada em 11/08 com 10 de 11
metas. Veredito: ela fez bem, e o resultado mais transferível dela não é o painel — é que
medir encolheu a meta em 6 de 11 casos (metas 3, 4, 6, 8, 11 encolheram; a 9 foi cancelada
por já estar pronta). Esta seção é a triagem, obrigatória pela lição P884: o que entra E o que
fica de fora, com motivo.
| # | Peça | Serve? | Por quê / onde entra aqui |
|---|---|---|---|
| 1 | A LEI: escrito por máquina disparada por evento = vive; escrito por humano que precisa lembrar = morre — inclusive quando existe lembrete automático** | ✅ | é a nossa lei dos 9% endurecida: a prova são dois arquivos mortos que tinham lembrete no arranque |
| 2 | O trailer Meta: + o conserto no commit-msg (--in-place, padrão do Change-Id do Gerrit) |
✅ a mais valiosa | é a receita pronta do Prova: CA-N via [sensor] órfão (§3.22). Nasceu depois de 5 remendos numa expressão regular, e a raiz foi trocar prosa por trailer do git |
| 3 | O formulário do cético — bloco alvo \| lente no laudo |
✅ | vocabulário fechado, já construído. É o "declare, não adivinhe" que os 3 céticos de 11/08 exigiram |
| 4 | Alarme por limiar tentado e DESCARTADO com número — limiar 5 apitaria em 76% dos pontos; limiar 40 em 15% e atravessaria calado o buraco de 50 commits | ✅ | disciplina de falso positivo, do mesmo teto de 10% da D20 |
| 5 | A unidade "desde a última mudança de HEAD" |
✅ | uma régua servindo dois guardas (orçamento de cético e trava do 4º arquivo) |
| 6 | A tabela dos 9 passos originais daquela obra | ❌ | congelada de propósito, e ela mesma registra que mentiu por um dia em 5 passos |
| 7 | A pesquisa do formato universal de resultado de teste | ❌ | concluiu "não cobre o alvo" e não mudou o desenho |
| 8 | A aba no site + os passos de instalação do gancho na VPS | ❌ | entrega específica daquela obra; zero transferência |
| 9 | O bloco de histórico de 313 commits | ❌ | gerado por máquina; ruído para quem lê |
Três sistemas desta casa respondem à mesma família de pergunta. Descobri hoje que dois deles
são complementares e nunca foram ligados:
| Sistema | Responde | Vocabulário | Nasceu |
|---|---|---|---|
| Degraus do livro de bordo | quão fundo a prova foi | mexido · testado · validado · blindado |
09-11/08 |
| Carimbos dos 3 portões | qual sensor prova | [logic] · [journey] · [visual] |
27/05 |
A palavra de conclusão (P676) |
o que eu afirmo ao dono | Entregue · Validado · Blindado | — |
O primeiro e o terceiro já estão alinhados — mesmas palavras, mesmo significado, sem
conflito. O que falta é o fio entre os dois primeiros: se o commit da tarefa carregasse
Prova: CA-N via [journey], o carimbo do degrau sairia sozinho, sem ninguém digitar. É a
mesma obra vista de dois lados, feita com dois meses e meio de distância — e o elo que falta é
exatamente o da §3.22.
O dono perguntou por elas por nome (travamento de commit, "pythons fantasmas"). As duas
primeiras têm spec; a terceira é peça de uma delas.
| Obra | Onde | Estado |
|---|---|---|
| Travamento do commit · cadeado de 238 min · sobrescrita entre conversas | specs/varias-conversas-no-mesmo-repositorio.md |
CONSTRUÍDO 11/08 — 3 peças: comando único de commit (7f4ef8d89), trava anti-sobrescrita (55d8991cb), teto de tempo (76e7edd71) |
| O diário do atrito do commit (fatias, cadernos, cards) | specs/PLAN-fechar-o-atrito-do-commit.md |
fatias A-D fechadas, F medida |
| Os "pythons fantasmas" | sem spec própria — é a 3ª peça da primeira: scripts/_lib/teto_de_subprocesso.py |
medido: 237 de 537 chamadas de subprocesso sem teto de tempo; um travamento entupia a máquina. O contador do arranque vem de .claude/hooks/fotografo-de-fantasmas.py |
1.261 linhas de spec para uma obra de dois dias. É o mesmo inchaço deste documento, e a
própria seção de pós-implementação dela confessa ter carregado uma régua revogada em
01/08 — dentro da spec de uma obra que existe para perseguir documento que mente.
Numeradas, com data e motivo. Decisão sem motivo volta a ser discutida.
| # | Data | Decisão | Motivo |
|---|---|---|---|
| D1 | 2026-08-10 | Não trocar o modelo de spec pelo spec-kit |
1 portão real dele contra 44 aqui; e ele não instala sem quebrar — a trava do 4º arquivo barra a primeira linha de código dele |
| D2 | 2026-08-10 | Não podar guardas para ganhar tempo | devolve 5,8 min/dia. A justificativa original estava inflada 6,98× |
| D3 | 2026-08-10 | Não construir porteiro único | ~8-16 min/dia. Relógio nunca foi o gargalo — manutenção é |
| D4 | 2026-08-10 | O primeiro desenho (5 portões) morre | cético: SOBREVIVE: NÃO, 2 críticos. Detalhe em §9 |
| D5 | 2026-08-10 | O alvo são os 2 defeitos do dono, não "um harness melhor" | ordem dele, literal |
| D6 | 2026-08-10 | Zero descartes — nada do que existe vai pro lixo | o mapa (§6) não achou uma única peça errada; achou peças desligadas |
| D7 | 2026-08-10 | Copiar o princípio do projeto pequeno e o hábito do grande | o único que julga com máquina tem 7 estrelas; o de 126 mil só confere se o arquivo existe |
| D8 | 2026-08-10 | Toda peça nova tem que apagar ou absorver uma existente | a pilha cresce 34/semana; somar sem tirar piora |
| D9 | 2026-08-10 | Portão só liga em modo sombra primeiro | o portão existente mordeu 1 de 1 vez, e mordeu errado |
| D10 | 2026-08-10 | Esta spec é a cobaia da própria obra | se as caixas dela ficarem por marcar, a obra se autodesmentiu |
| D11 | 2026-08-10 | Nenhuma peça desta obra pode depender de eu lembrar de escrever uma frase sem um lembrete no momento certo | a lei dos 9% (§3.10), medida em 3 mecanismos: sem lembrete = 8-9%; com lembrete = 83,4% |
| D12 | 2026-08-10 | O alvo do roadmap é o número não mentir, não o painel ficar bonito | palavras do dono: "tenho noção pelo número de cards" — o número é o produto |
| D13 | 2026-08-10 | Estado derivado (git, banco, telemetria) vence estado escrito à mão, sempre que houver escolha | é a única forma que aguenta dois chats no mesmo working tree (§11-B) |
| ~~D14~~ | ~~2026-08-10~~ | ~~🚩 LINHA VERMELHA: se a versão final desta obra propuser mais de ~5 arquivos novos de guarda, congelar o harness por 30 dias e voltar pro robô~~ · ⛔ REVOGADA no mesmo dia por D16 | a inversão de §3.11: 60,6% da semana na ferramenta contra 23,3% no produto |
| D15 | 2026-08-10 | A obra só se justifica no tamanho de fiação: emendas de 1 a 3 linhas em peças que já rodam, zero guardas novos · (relaxada por D18) |
as três varreduras concordam: a casa já tem quase tudo; o que falta é a última ligação de cada peça |
| D16 | 2026-08-10 | ⛔ Congelar o harness está FORA. O alvo é otimizar o harness, gastando os recursos necessários. D14 fica no documento riscada, como registro de uma proposta minha que o dono recusou |
ordem dele, literal: "não quero congelar o harness, o foco é otimizar ele, vamos gastar os recursos necessários, prossiga com as pesquisas e análises". A inversão de §3.11 continua verdadeira e continua medida — o que mudou foi o que se faz com ela: o número deixa de ser gatilho de parada e vira alvo de otimização |
| D17 | 2026-08-10 | O alvo da otimização são os três juntos — não mentir/não esquecer · menos manutenção · menos atrito — e não um deles. Cobrança em §4-B | ele recusou escolher um: "é as 3 coisas que vc citou e mais né, fica difícil focar só em 1". Consequência prática: o desenho final tem que dizer, movimento por movimento, qual dos três ele move — movimento que não move nenhum sai |
| D18 | 2026-08-10 | Guarda novo pode nascer, desde que apague ou absorva um existente — D15 (zero guardas novos) fica relaxada, D8 fica ratificada. E abre-se uma frente de pesquisa nova: qual é o sweetspot da indústria para guardas em ganchos |
escolha dele entre 3 opções, com o preço na mesa. Palavras dele: "opção 1, hoje em dia, na indústria, qual o sweetspot pra guardas em hooks? quero entender onde podemos estar errando" |
| D19 | 2026-08-10 | 🚨 Desenho novo confere o CÓDIGO, nunca esta spec. E toda seção que descreve defeito vivo passa a carregar a data da última conferência — sem ela, o texto é histórico, não estado | quatro desenhos morreram do mesmo defeito, e o quarto morreu por culpa deste documento: a §3.7 ficou 8 horas dizendo "defeito vivo" depois do conserto, e 15 ajudantes construíram em cima do texto velho (§9.1) |
| D20 | 2026-08-10 | A régua da indústria é tempo, não quantidade. Parar de perguntar "são guardas demais?" e passar a perguntar "quanto tempo se fica parado esperando?" | nenhuma fonte publica teto de quantidade para harness de agente — isso foi procurado por 5 ângulos e não existe. O que existe é teto de tempo, taxa de falso positivo (Google desliga acima de 10%) e proporção do acervo que fica ligada (ESLint: 22%) |
| D21 | 2026-08-10 | O valor de um guarda é quanto do relógio ele POSSUI — (ele − o segundo mais lento do lote), somado sobre os lotes em que foi campeão. Não é a duração dele, e não é quantas vezes ele roda | os guardas rodam em paralelo (§3.12): acelerar o campeão só rende até o segundo colocado. Sem essa régua, "o guarda dura 1,4 s" leva a otimizar o que não move o relógio |
| D22 | 2026-08-10 | Medição de conserto exige grupo de controle. Antes/depois em produção só vale com um conjunto de peças não tocadas medido na mesma janela | lição P900: o antes/depois deu "pior" nos guardas que eu não tinha tocado — a máquina estava sob carga. Sem o controle eu teria concluído o oposto do verdadeiro |
| D23 | 2026-08-10 | Rede que só confere o código de saída não é rede. Guarda que não bloqueia tem que sair calado — o canal de erro entra no teste | 52 testes passaram com o source quebrado e a telemetria muda por baixo (§3.14). Foi um cético de fora que viu, não a bancada |
| D24 | 2026-08-10 | ⛔ A régua de custo por guarda está MORTA. Nada se aposenta por ela — nem os "4 guardas inertes", nem os 78 mudos | ela divide 90 dias por 1,3 dia, o topo é divisão por zero, e ela ia me fazer desligar o 3º maior bloqueador da casa (§3.15) |
| D25 | 2026-08-10 | Toda medição que cruza duas fontes declara as DUAS janelas, lado a lado, antes do resultado. Sem isso o número não entra | é o defeito de D24 em uma frase. E o aviso já estava escrito dentro do próprio medidor da casa (painel_de_guardas.py:241-247) — a lição P779: a resposta estava em casa |
| D26 | 2026-08-10 | O foco é arrumar o harness primeiro, antes de voltar ao produto | ordem do dono, literal: "o foco agora é em arrumar o harness primeiro" — dada depois de ver os números do custo de oportunidade (§3.18), não antes |
| D27 | 2026-08-10 | ✅ A conferência de coerência da spec ABSORVE o verificador que já existe — não vira script novo, nem guarda novo. Ela roda onde ele já roda: no portão do commit | ordem do dono ao escolher o caminho: "sweetspot e robusto". Robusto porque o portão do commit já chamava a peça (garantia de 100%, não de memória); sweetspot porque custa zero no dia-a-dia — não é gancho, não roda em todo comando. É a D18 cumprida por absorção, que é a forma boa dela (§3.17-B) |
D17 — qual dos três alvos cada movimento moveO dono recusou escolher um alvo. A consequência que ele mesmo apontou é esta tabela:
movimento que não move nenhum dos três sai do desenho.**
O quinto desenho foi escrito e morreu (§9.0). Fica registrado o que ele propunha e o que
cada movimento moveria — porque proposta morta sem motivo escrito volta em duas semanas:
| Movimento do quinto desenho | 🎯 | 🧹 | 🪶 | Entrou? |
|---|---|---|---|---|
| Palavra de conclusão vira linha no commit | ✅ | ✅ | ✅ | ❌ a peça já existia, e derivada |
Aviso do fecha #N no commit |
✅ | — | ✅ | ❌ existe há 22 dias, e a coluna que ele leria não existe |
| Guarda de texto nasce com replay | ✅ | ✅ | — | ❌ alcança 3 de 186 guardas |
Os seis movimentos já executados, medidos pela mesma régua — não são o desenho, são
consertos avulsos achados durante a medição:
| Movimento executado | 🎯 | 🧹 | 🪶 | Prova |
|---|---|---|---|---|
Recibo no descarte do injetor (4f5b8cf77) |
✅ forte | — | — | 37,1% dos turnos deixavam de entregar, calados (§3.7) |
sync-memory reescrito em Python (7d12bdd67) |
✅ forte | ✅ | ✅ | a lição da sessão em worktree nunca voltava; > 600 s → < 5 s (§3.12) |
Atalho + preâmbulo nos guardas (198c9d023 e irmãos) |
— | — | ✅ forte | soma dos cinco: 1.024,4 → 116,8 ms (§3.12-B) |
Carimbo nos dois guardas legados (658b4f5b0) |
✅ | — | — | barravam sem declarar o fato (§3.13) |
Carimbo de conclusão passa a enxergar o laudo (3e26c68a4) |
✅ forte | — | — | 116 execuções → 0 carimbos; agora 4 nos mesmos laudos (§3.16) |
O campo bloqueios passa a dizer de que evento fala (046510e42) |
✅ forte | ✅ | — | 359 bloqueios estavam invisíveis, em 4 guardas (§3.17) |
O que essa tabela denuncia, e continua denunciando: dos seis movimentos executados, só
dois tocam 🧹 manutenção, e nenhum a ataca de frente — que é justamente o eixo em que a
§3.11 mostra a casa a 3-4× da referência de mercado. O sexto desenho, se houver, tem que
atacar a coluna do meio — ou repete o padrão dos cinco anteriores.
🔎 E há um padrão nos seis que vale mais que a tabela: nenhum deles foi desenhado.
Todos apareceram durante uma medição, olhando um número que não fazia sentido. Cinco
desenhos morreram; seis consertos nasceram de sondas. É a evidência mais forte desta obra a
favor do passo 2 do fluxo da casa — medir antes de desenhar.
Cor do veredito = critério 3 (o que sobra em disco). É o critério do defeito "eu esqueço".
| # | Passo | O que sobra em disco | |
|---|---|---|---|
| 1 | Princípios | CLAUDE.md, relido toda mensagem |
✅ |
| 2 | Dizer o que eu quero | a spec | ✅ |
| 3 | Tirar a ambiguidade | nada próprio — a resposta depende do resumo | ❌ |
| 4 | Medir a premissa | o número, só se eu escrever | ⚠️ |
| 5 | Decidir o como | a escolha, só se eu escrever | ⚠️ |
| 6 | Virar tarefas | o plano é arquivo; o estado das caixas não | ⚠️ |
| 7 | Conferir antes de codar | — | ❌ |
| 8 | Construir | o código | ✅ |
| 9 | Provar cada tarefa | o commit — mas o portão de etapa está morto (o stage-sensor-gate.py da §3.4; a lei do carimbo é outro portão e esse morde, §3.13) |
⚠️ |
| 10 | Reconciliar plano × real | em obra, card #888 |
⚠️ |
| 11 | Declarar conclusão | ⬅️ corrigido 10/08: existe mecanismo — carimba-blindado-do-cetico.py, ligado em SubagentStop, deriva o degrau do laudo do cético. Rendia 0 em 116 execuções e foi consertado em 2026-08-10 (§3.16) |
⚠️ |
| 12 | Guardar a lição | escreve bem, devolve mal (69,9% mudas) | ⚠️ |
O padrão: passo que produz arquivo sobrevive; passo que produz conversa evapora.
Os três vermelhos são exatamente os três que só produzem conversa.
Onde o spec-kit fica: cobre 7 dos 12 (passos 1, 2, 3, 5, 6, 7, 8). Não tem os passos
4, 9, 10, 11 e 12. Cinco a um a nosso favor — e o único que ele tem e nós não é o passo 7.
Vocabulário: manter = está bom · melhorar = existe e está furado · substituir =
o defeito é de desenho · descartar = não paga o que custa.
| Comando | Veredito | Por quê |
|---|---|---|
/sonda |
manter | é a vantagem sobre todo o mercado — nenhuma das 13 ferramentas tem |
/brainstorm |
manter | |
/plan |
melhorar | vira arquivo, mas ninguém marca as caixas — 4 de 5 planos nunca tiveram uma marcada |
/rapid |
manter | |
/validate |
manter | é a porta única do cético, já decidido |
/learn |
melhorar | escreve bem, devolve mal |
/compound |
manter | |
/explica |
manter | |
/compact |
manter | |
/gsd-* |
manter parcial | usar map-codebase, debug, scan; ignorar os que duplicam /plan |
A coluna conferido é a D19 em forma de tabela: sem data, a linha é história, não estado.
| Mecanismo | Veredito | Prova | Conferido |
|---|---|---|---|
Portão do commit (validate-commit.sh) |
manter | a peça mais forte da casa; 8,8× mais rápido desde 10/08 (§3.12-B) | 10/08 |
pre-compact-save.py + rehidratação |
manter | funcionou nesta conversa, duas vezes | 10/08 |
completeness-critic.py |
melhorar | lê a fila do chat, não lê o plano | 10/08 |
| Guardião de lições | substituir | 3 vagas para 875 candidatas | 10/08 |
stage-sensor-gate.py |
substituir | 1.217 execuções, 1 bloqueio, falso | 10/08 |
A LEI DO CARIMBO (validate-commit.sh:2216) |
manter ⬅️ promovido | morde hoje, testado com guarda de mentira: saída 2. Os "171" eram ruído — foram 30, e só 3 contam (§3.13) | 10/08 |
mutation_check.py |
manter e dar gatilho | ⚠️ corrigido 2×: ele não está solto — está no pre-deploy.sh:41, atrás de um opt-in de três variáveis. E não tem zero execuções: são 20, à mão, em 3 dias distintos. A frase "lei dos 9% chegando a 0%" cai junto |
11/08 |
meta_da_obra.py |
melhorar | o próprio código confessa que depende de eu lembrar; 9% de adoção (§3.8) | 10/08 |
unified-context-injector.py |
melhorar | ✅ o defeito de §3.7 foi consertado (4f5b8cf77). O "melhorar" que sobra é outro: ele escolhe 3 de 875 lições por turno |
10/08 |
carimba-blindado-do-cetico.py ⬅️ novo no mapa |
manter — ✅ consertado | nasceu 10/08 e rendia 0 em 116 execuções; agora acende (§3.16, 3e26c68a4) |
10/08 |
painel_de_guardas.py ⬅️ novo no mapa |
manter — ✅ consertado | escondia 359 bloqueios reais atrás de um nome de campo (§3.17, 046510e42) |
10/08 |
| Trava do 4º arquivo sem plano | manter | 10/08 | |
pede-ok-antes-de-subir.py |
manter | 10/08 |
Placar atualizado: 16 manter · 5 melhorar · 2 substituir · 0 descartar. (Duas peças entraram
no mapa nesta revisão — elas existiam e não estavam listadas, que é a mesma falha da §3.14-B.)
# a conferência do mutation_check, que derrubou o "nunca ligado a nada":
grep -n "mutation_check" scripts/quality/pre-deploy.sh
| Lugar | Quanto tem | Veredito |
|---|---|---|
specs/ |
321 documentos | melhorar — é acervo, não índice vivo |
| Cards no roadmap | ~900 | melhorar — fila que só cresce |
| Lições | 875, com 612 mudas | substituir a devolução |
memory/ |
estado da sessão | manter |
scripts/quality/ |
29 ferramentas de medição | manter — vale ouro e está subusado |
Placar: 14 manter · 6 melhorar · 2 substituir · 0 descartar.
Confiança: medido = li o código ou a issue · inferido = deduzido do desenho.
| Projeto | 1 julga | 2 recibo | 3 sobrevive | 4 falha alto | 5 custo fácil | 6 desfaz |
|---|---|---|---|---|---|---|
| spec-kit 126k ⭐ | ⚠️ | ✅ | ✅ | ❌ | ❌ | ✅ |
| OpenSpec 64k | ⚠️ | ✅ | ✅ | ⚠️ | ⚠️ | ✅ |
| ruflo 67k | ❌ | ⚠️ | ⚠️ | ❌ | ✅ | ⚠️ |
| BMAD 51k | ❌ | ⚠️ | ⚠️ | ❌ | ✅ | ⚠️ |
| task-master 28k | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ |
| ccpm 8,3k | ❌ | ✅ | ✅ | ❌ | ⚠️ | ✅ |
| groundtruth 7 ⭐ | ✅ | ✅ | ⚠️ | ✅ | ⚠️ | ⚠️ |
| AWS Kiro fechado | ✅ | ⚠️ | ⚠️ | ✅ | ⚠️ | ⚠️ |
As colunas 3, 5 e 6 são inferidas — nenhuma ferramenta foi instalada.
| A briga | Quem paga o preço |
|---|---|
| Juiz rigoroso × custo no caso fácil | recibo custa trabalho em toda tarefa, inclusive a boba |
| Falhar alto × ser adotado | medido aqui: o que avisa = 3.013 commits; o que bloqueia = 0 |
| Memória perfeita × contexto limpo | recarregar tudo piora a resposta |
| Servir a todos × ser forte | só dá pra conferir o que é universal: "o arquivo existe?" |
A quarta é a decisiva, e é visível no placar: a única ferramenta que julga com máquina é a
menor por três ordens de grandeza (7 estrelas contra 126 mil). Não é talento — é
consequência. Portão que serve a 126 mil pessoas só pode conferir o que é universal.
O que isso significa aqui: o motivo de nenhum ser 10/10 é o motivo pelo qual o nosso
pode ser — um usuário, uma máquina, um jeito de rodar teste.
O preço: ninguém mantém a nossa ferramenta. Bug no spec-kit, 126 mil pessoas acham. Bug
aqui, o dono acha — e a conta já é 673 consertos/mês contra 363 do produto.
Pergunta do dono, literal: "qual o sweetspot pra guardas em hooks? quero entender onde
podemos estar errando". Cinco ângulos + síntese + cético, 2026-08-10. O cético derrubou 3 dos
5 movimentos e 2 dos 5 achados — o que está abaixo é só o que sobrou de pé.
A frase que resume: a indústria não limita quantos porteiros o prédio tem — limita quanto
tempo você fica parado na portaria. Nós vínhamos contando porteiro.
| Dimensão | A régua de fora | Nós | |
|---|---|---|---|
| Guardas por pessoa | Google: ~100 analisadores para 25 mil engenheiros | 238 em disco / 191 registrados para um dono — ⚠️ o "218" de 10/08 era um quinto denominador; a casa tem quatro em circulação (§3.19) | 🔴 |
| Teto de quantidade publicado | não existe para harness de agente — procurado por 5 ângulos | — | ⬜ |
| Proporção do acervo que fica ligada | ESLint: 65 de 294 = 22%; o resto existe e custa zero |
100% — não temos o estado "existe mas está desligado". ⚠️ Conferido em 11/08 e continua verdade: o único análogo achado é o mutation_check.py, que está desligado por opt-in de ambiente, não por um estado declarado |
🔴 |
| Utilidade mínima | Google desliga acima de 10% de falso positivo efetivo | ⚠️ INDEFINIDO. O "72 de 187 (38,5%) nunca fizeram ato útil" foi refeito: são 44 que não registraram ato numa janela de 25,6 h — e a palavra "nunca" não se sustenta (§3.19) | ⬜ |
| Manutenção da própria ferramenta | teto de 50% (regra de toil, SRE) | 55,3% nos guardas · 52,1% na ferramentaria — acima do teto, mas em patamar, não acelerando (§3.11) | 🔴 |
| Avisar × bloquear | Facebook: a mesma análise deu ~0% de adesão em lote noturno e >70% falando no momento da decisão | 83,4% × 0 aqui — o mesmo formato | ✅ nosso padrão está certo |
| "Peça nova apaga a velha" | é literalmente a política publicada de depreciação do ESLint |
D8/D18 |
✅ |
O único movimento que sobreviveu ao cético — e que morreu na sonda, um dia depois. Era
um despachante por evento, em vez de dois processos Python por guarda: hoje 230 dos 233
ganchos rodam python _medidor.py <guarda>, dois arranques cada. O teto estimado era 4,2×
(o "8,16×" que a síntese citou não existe na fonte — o cético abriu o benchmark: são 6,75× /
8,36× / 12,85×, e a causa lá é reescrita em Rust, não despachante).
⛔ Este movimento está MORTO desde 2026-08-10. A sonda de §3.12 mediu o que a régua de
fora não podia saber: os guardas de um lote já rodam em paralelo. Um despachante
sequencial seria 2,7× PIOR; paralelo daria 1,46×, não 4,2×. Foi aD3sendo
confirmada por medição própria depois de ter sido tomada por estimativa. O que a régua de
fora devolveu de útil não foi o movimento — foi a régua (D20: a pergunta certa é "quanto
tempo se fica parado", não "são guardas demais?").
A ressalva que o cético fez e que fica registrada: nenhuma fonte de fora cobre o eixo em que
somos genuinamente diferentes — o guarda aqui fala dentro do contexto de quem decide, não
num relatório que alguém abre depois. Toda transferência de número acima é por analogia, não
por equivalência.
Seção obrigatória. Lição P884: eu estreito o escopo em silêncio e o dono precisa perguntar
"cadê o resto?".
| Ideia | Por que não entra |
|---|---|
Trocar o modelo de spec pelo spec-kit |
D1 |
| Podar guardas para ganhar tempo | D2 — 5,8 min/dia |
| Porteiro único | D3 — relógio não é o gargalo |
| ~~Escrever teste para cada guarda~~ | ⚠️ A EXCLUSÃO PERDEU A BASE em 11/08, e em 19/08 ganhou NÚMERO — que não a ressuscita. O motivo era "guarda com teste conserta 3,35× mais", e a razão foi calculada com os dois grupos trocados (§3.3). Medido em 19/08 com grupo de controle limpo (âncora 43d404e68, 81 com prova · 73 sem): o efeito real é 1,18×, e um placebo (feat/docs/chore contados como conserto) dá 0,82× — o controle negativo que fecha o caso. Mas 1,18× REMOVE A CONDENAÇÃO, não apoia a proposta: é quase-nulo, é correlação (quem escreve prova escolhe o guarda que já dá trabalho), e o custo de escrever as ~73 provas que faltam nunca foi medido por ninguém. É esse custo, não o 3,35×, que decide. A ideia segue na mesa, sem defesa e sem condenação — o que mudou é que agora se sabe exatamente o que falta medir para tirá-la de lá. Recontar: python scripts/quality/custo_de_manutencao_do_guarda.py |
| Atestado de proveniência (SLSA / Sigstore / in-toto) | desenhado para cadeia com várias partes; aqui não há contraparte |
| Recibo para o carimbo de jornada | copiaria um recibo falso — o marcador nasce de qualquer chamada ao navegador |
| Segundo portão para guarda novo | já existe e bloqueia (validate-commit.sh:2216) |
| Mutação no gancho de fim de turno | o gancho tem teto de 15 s; mutação leva ~600 s |
| Freio na pilha de guardas como portão | não há evento de gancho que o sustente — vira doutrina, não trava |
| Perguntar "qual é a raiz?" | não existe UMA raiz em sistema complexo; a pergunta que funciona é "quantos remendos já estão neste mesmo lugar?" |
A P884 obriga esta lista: eu estreito o escopo em silêncio e o dono precisa perguntar
"cadê o resto?". Aqui está o resto, dito antes de ele perguntar.
| Não conferido | Por quê | Risco de estar velho |
|---|---|---|
| ~~Os números da §3.1 a §3.11~~ | ✅ CONFERIDOS em 11/08 (§3.19) — e a aposta de que "a ordem de grandeza não muda" estava errada em três deles. O 87 estava invertido, a inversão tinha 3 semanas e não 1 dia, e a linha de 30 dias não fechava com a própria aritmética |
⬜ resolvido — e a lição é que "ordem de grandeza não muda" não é motivo para não conferir |
| Os números do Roadmap (§3.18) ⬅️ nova | os cards moram no Postgres; a sonda de 11/08 foi só-leitura de git e disco | 🟡 médio — é a maior mancha que sobrou |
| A causa de 61,6% dos consertos de guarda ⬅️ nova | a classificação de 11/08 leu o assunto do commit; a causa mora no corpo ou no diff | 🟡 médio — desenhar sem isso é apostar na família que sobrou por acaso no topo |
| Seis peças da §6.2 (o crítico de completude, o guardião de lições, o sensor de etapa, o quadro de metas, a trava do 4º arquivo, o pede-OK) | não foram tocadas pela obra dos guardas; conferi só as cinco que a obra mexeu | 🟢 baixo — e a data 10/08 está na tabela, que é exatamente o ponto da D19 |
| O placar das ferramentas de fora (§7) | D1 decidiu não instalar nenhuma; as colunas 3, 5 e 6 seguem inferidas e vão seguir |
🟢 baixo — a decisão já foi tomada |
| O quinto desenho | ele não existe. Esta revisão arrumou o diagnóstico, não escreveu solução | — |
Registrado para não ser re-proposto. Todos devolveram SOBREVIVE: NÃO de um cético de contexto limpo.
Três céticos em paralelo, lentes distintas. Os três devolveram SOBREVIVE: NÃO — foi a
primeira vez nesta obra que o placar deu 3 de 3. Nenhuma linha de código escrita.
O que ele propunha: o gancho plan-approved-task-tracker.py, que roda na aprovação do plano
e já recebe o texto inteiro dele, passaria a ler o plano e a nomear as tarefas sem comando
de prova, no lembrete que ele já injeta.
| Lente | Causa da morte | |
|---|---|---|
| R2 o alvo | O mecanismo é prosa, e prosa não move nada — agora medido. O MESMO envelope faz dois pedidos: um vira chamada de ferramenta (TaskCreate, 97,6% de adesão) e o outro vira texto meu (o placar N/M, 1,70% — reproduzido por mim em 1,93% sobre 34.801 mensagens). O bloco novo era da família do placar. Adesão esperada: 2 a 6%, abaixo da própria lei dos 9% |
❌ crítico |
| R3 o interpretador | 91,5% de falso positivo, medido implementando a regra. Ela acusaria 71 tarefas nos 19 planos e 65 das acusações são falsas. O teto da casa é 10% (D20); o pior falso-alarme já aberto aqui é 38,7%, e 13,2% já pagou uma obra inteira de conserto |
❌ crítico |
| R1 a premissa | O número que justificava a obra não se reproduz. Mesmo corpo, 22,1% a 67,3% conforme o critério. A mitigação — "o parser bate 72/124" — valida o interpretador contra ele mesmo, e pior: o medidor foi descartado, então o alvo nem é reconstruível | ❌ crítico |
| " | Três fatos meus caíram junto: mutation_check.py tem 20 execuções e não zero · são 26 chamadas de aprovação e não 5 · 6 planos sem estrutura de tarefa e não 3 |
⚠️ alta |
| O exemplo | O caso escolhido à mão para VENDER o desenho era ele próprio um falso positivo: a tarefa que eu citei como "sem prova" já traz Verificação: clique real no dashboard resolvendo um par de simulador. Metade dos exemplos da proposta acusava tarefa correta |
❌ crítico |
🔎 A causa raiz é a mesma de sempre, num disfarce novo. Desta vez eu não li a spec em vez do
código — eu medi. Mas medi com régua que eu mesmo escrevi, sem gabarito contado à mão, e
tratei o resultado como fato. É a liçãoP935, registrada por mim duas horas antes, cometida
dentro do desenho escrito para consertar medição.
O que sobreviveu, e é bastante:
CA-1 confirmada por três varreduras).TaskCreate (que é gravada, vira estado derivado e um sensorplanFilePath que já chega — sem acusar ninguémTrês céticos em paralelo, lentes distintas. R1 (a peça já existe) devolveu SOBREVIVE: SIM;
R3 (isto acerta o alvo?) devolveu SOBREVIVE: NÃO com 4 achados altos ou críticos. Nenhuma
linha de código foi escrita.
O que ele propunha: dar gatilho automático à colheita de cobertura dos guardas (o plugin
escuta_de_canarios carregado por conftest.py), e transformar o artefato de fotografia em
livro-caixa com data por guarda.
| Movimento | Causa da morte | |
|---|---|---|
| Todo o desenho | O número que ele refresca não decide nada. Um consumidor executável, que imprime numa linha de resumo do bootstrap, fora da lista de alarmes, num script que devolve 0 sempre | ❌ crítico |
| " | 70% do problema vem de REGRA, não de idade. Dos ~100 "sem rede", 71 foram VISTOS pela colheita e descartados de propósito ("rodar vazio não é julgar"); só 35 são de fato desconhecidos. A distância entre 25 e 102 está documentada no produtor como escolha de régua | ❌ crítico |
O gatilho (M1) |
A outra metade do encanamento continua sem gatilho. derivar_canarios.py — quem escreve o arquivo — tem zero chamadores executáveis. Ligar só o ouvinte pagaria o custo na bateria inteira com o arquivo seguindo congelado |
❌ alta |
| " | Contradição interna: a seção "o que não entra" excluía o portão do commit, e o conftest.py da raiz alcança o portão por construção — validate-commit.sh roda pytest da raiz em 5 pontos. O risco do remendo global iria parar dentro de portões que fazem exit 2 |
❌ alta |
| " | O precedente citado não é simétrico: teto_de_subprocesso tem aplica()/devolve(); escuta_de_canarios remenda no corpo do módulo e nunca desfaz |
⚠️ alta (achado do R1) |
A absorção declarada (D8) |
absorver um ritual que nunca rodou é trocar 0 execuções por N. D8 declarada, não cumprida |
⚠️ média |
O que sobreviveu, e é pouco mas é real: o princípio do livro-caixa (o recibo é derivado,
não escrito à mão — D13 cumprida, ao contrário do quinto desenho), e um conserto de 1 linha
no consumidor que entrega a parte honesta sem nada do risco (§3.20).
🔎 A causa raiz da morte, e ela é a mesma de sempre: eu construí sobre a frase "toda
decisão depende deste número", que estava nesta spec e que eu nunca conferi no código.
É aD19violada dentro da seção escrita para denunciá-la — e a quinta vez que um desenho
morre por ler o documento em vez do repositório.A diferença desta vez: o cético que matou levou 18,5 minutos e o desenho nunca entrou
na spec como proposta. O ciclo de matar ideia ruim continua barato.
Três céticos, três lentes, 1.135 s o mais longo. Contra os 15 ajudantes e 1,82 milhão de
tokens que o quarto levou para morrer.
| Movimento proposto | Causa da morte | |
|---|---|---|
| A palavra de conclusão vira trailer do commit | a peça já existe e é melhor — carimba-blindado-do-cetico.py, derivada, nascida às 06:36 do mesmo dia. Pela quinta vez, o mesmo defeito |
❌ crítico |
| " | o mecanismo do Leigo: é o gancho commit-msg, que só recebe o caminho da mensagem — sem transcrito, sem sessão, sem o meu texto. 3 das 4 responsabilidades do guarda que eu ia absorver dependem do texto |
❌ crítico |
| " | o trailer é escrito à mão e afirma fato não verificável, contra a própria D13. Precedente na §3.4: o formato que afirma fato tem 0 commits; o que não afirma nada tem 83,4% |
❌ |
O aviso do fecha #N |
já existe há 22 dias, instalado em .git/hooks/commit-msg. E a coluna que eu ia consultar (doing) não existe no roadmap |
❌ crítico |
| Guarda de texto nasce com replay | o princípio sobrevive — é a única ideia que ataca manutenção de frente. O mecanismo não: a lei em que eu ia me apoiar só cobra de quem bloqueia, e 4 dos 5 alvos só avisam; e o replay exige um contrato que 3 de 186 guardas cumprem | ⚠️ meio |
| (a régua que sustentava tudo) | morreu junto — ver §3.15 | ❌ crítico |
A diferença desta vez: nenhuma linha de código foi escrita, o desenho nunca entrou na spec, e
o custo total foi de três agentes. O ciclo de matar ideia ruim ficou ~250× mais barato — que
é, sozinho, o único resultado de desenho que esta obra produziu até agora.
Duas varreduras, 15 ajudantes, 1,82 milhão de tokens. Dois relatórios, dois SOBREVIVE: NÃO.
| Proposta | Causa da morte | |
|---|---|---|
| Recibo no injetor de contexto | já estava construído há 8 horas — commit 4f5b8cf77, do mesmo dia. O desenho leu a §3.7 desta spec, que ainda dizia "defeito vivo", e não leu o código |
❌ crítico |
| Conferente antes de codar, no gancho do plano | o gancho de plano disparou 4 vezes em 2,73 dias; o guarda que ele apagaria disparou 1.486. Trocar 1.486 por 4 e chamar de "100% de cobertura" é a lei dos 9% ao contrário | ❌ crítico |
| "43 guardas acordam em todo comando" | falso: 19 já são filtrados por condição, só 12 estão soltos | ❌ |
"[allow-spec] é a senha do portão" |
ele não abre portão nenhum — só silencia um aviso (validate-commit.sh:721-724) |
❌ |
"O portão do carimbo foi consertado em 30b7138f2" |
aquele commit era sobre quebra de linha. O diagnóstico real segue de pé: o portão morre de relógio, com 446 s de teste na frente dele (validate-commit.sh:287-297) |
❌ |
| Mexer na memória de lições | arrancaria uma peça declarada indivisível (zera-marcadores-da-sessao.py:44-52) e ressuscitaria o update-scores.py, aposentado por medição |
❌ |
| "despachante único dá 8,16×" | número emprestado e inflado. A correção da época dizia 4,2× — e essa também caiu no dia seguinte: medindo, sequencial é 2,7× PIOR e paralelo dá 1,46× (§3.12) | ⚠️ |
A causa raiz, e ela é minha: consertei um guarda de manhã, não atualizei a spec, e à tarde
o desenho inteiro foi construído em cima do texto velho. A spec que existe para ser a fonte da
verdade virou a fonte do erro. Decisão que nasceu daqui: D19.
| Portão proposto | Causa da morte |
|---|---|
| Pedir o carimbo no commit | apresentado como "só avisa", mas seu único efeito é acionar um bloqueador — e o bloqueador mordeu 1 de 1 vez, errado |
| Recibo para jornada | clonaria um recibo falso |
| Guarda novo prova que apita | já existia no mesmo arquivo e no mesmo gatilho, há meses |
| Freio na pilha | não nomeia evento de gancho |
Blindado exigir mutação |
impossível no evento escolhido (15 s × 600 s) |
Três defeitos do portão existente, reproduzidos:
| Defeito | A prova |
|---|---|
| Barra comando que não é commit | casa git commit dentro de string de outro código |
| Um ponto-e-vírgula desliga o portão | ele corta o comando no primeiro ;, e os commits reais usam ; |
| O carimbo visual é recibo falso | nasce de qualquer chamada ao navegador, sem vínculo com teste, arquivo ou resultado |
Ordenadas por quanto elas travam o próximo passo. As respondidas ficam, com a resposta —
apagá-las faria a pergunta voltar em duas semanas.
✅ A FILA DE PESQUISA ESVAZIOU EM 2026-08-19 (meta 15 da obra
os-vigias-do-harness).
14 das 17 perguntas têm veredito: 8 respondidas, 6 aposentadas com o motivo escrito.As 3 que sobram não são pesquisa — são o desenho do G2 em si, e por isso não se
respondem lendo nem medindo: como atacar o esquecimento (🔴 é a obra, 5 desenhos mortos
atrás dela), onde se conserta — onde o plano é lido ou onde ele nasce (🔴 a pista viva) e
o quinto desenho move a coluna manutenção (🔴). Enquanto elas estiverem abertas, o que
falta é decidir e construir, não medir.⚠️ Aposentar não é resolver, e a diferença está dita linha a linha. Três delas
(N provas · a lição é a certa · os 115 guardas prestam) foram aposentadas porque
responder exigiria um gabarito humano ou um sinal de utilidade que não existe — o preço
de reabrir está escrito em cada uma. Duas (colunas do placar · passo 7) não eram
pergunta: eram decisão já tomada, guardada em forma de dúvida.
| Pergunta | Estado | Trava o desenho? |
|---|---|---|
| Como atacar o esquecimento? | ⚠️ varredura feita (15 ajudantes); o desenho morreu cinco vezes, o diagnóstico ficou (§9) | 🔴 sim — é a obra |
| Onde se conserta: onde o plano é LIDO ou onde ele NASCE? ⬅️ nova, 11/08 | ⚠️ a pergunta mudou de lugar depois de 3 céticos. Ler o plano depois de pronto e acusar por nome dá 91,5% de falso positivo, porque não há gramática: 15 formas de marcar tarefa × 23 de declarar verificação, ~1 forma nova por plano. Os dois céticos convergiram, sem combinar, em consertar onde o plano nasce | 🔴 sim — é a pista viva |
| As N tarefas têm N provas? ⬅️ nova, 11/08 | ⚰️ APOSENTADA 19/08 como pergunta de MEDIÇÃO — ela não tem resposta e agora se sabe o custo de ter uma. Para respondê-la seria preciso um gabarito contado à mão: alguém lê os 19 planos e marca tarefa por tarefa. Enquanto não houver, todo medidor automático continuará devolvendo um número diferente (3,6% · 22,1% · 58,1% · 67,3% · 87,1% — cinco medidores, cinco respostas). A pergunta útil que sobrevive no lugar dela é a linha seguinte (onde se conserta), porque ela não precisa deste número. Segue registrado: ❌ SEM RESPOSTA CONHECIDA, e isto é o achado. Não há gabarito contado à mão; o mesmo corpo de 19 planos já devolveu 3,6% · 22,1% · 58,1% · 67,3% · 87,1%. O que se sabe com segurança é só o nível do plano: 19 de 19 (100%) | 🟡 parcial |
D6 (zero descartes) briga com D8/D18 (peça nova apaga velha) |
✅ RESOLVIDA 19/08: a briga era falsa, e o terceiro caminho a dissolve (linha abaixo). Elas só se opõem se existir e custar forem a mesma coisa; medido, não são — o custo segue a atenção, e o efeito residual é 1,19×. D6 guarda a EXISTÊNCIA, D8 guarda a SUPERFÍCIE ATIVA, e um terceiro estado (congelado) satisfaz as duas ao mesmo tempo. O que falta é o estado no contrato, não a decisão. Contexto anterior, que continua valendo: ⚠️ COM PREÇO (11/08, §3.19): o conjunto que a D6 protege custa 7,70% do conserto em 90 dias, e só 6 de 86 commits foram dedicados a ele. A briga vale menos do que parecia — e a régua que a decidiria não existe: "nunca bloqueou" mede 25,6 h, não a vida do guarda |
🟡 parcial — deixou de ser bloqueio de primeira ordem |
Existe um terceiro caminho para a briga D6×D8? ⬅️ nova, 11/08 |
✅ RESPONDIDA 19/08: existe, tem dois nomes na indústria, e nenhum deles é "desligar". (a) Depreciação sem remoção — ESLint: a política publicada diz que uma regra nunca é removida, salvo se outra regra a substituiu; depreciada, ela entra em congelamento de manutenção — a equipe deixa de fazer conserto, melhoria e até atualização de documentação dela, e ela segue utilizável. (b) Estado opcional com graduação — Tricorder, do Google: análises nascem opcionais, melhoram por feedback, juntam usuários e graduam para ligada-por-padrão. Em janeiro de 2018 eram ~70 análises opcionais e 2.500 projetos com pelo menos uma ligada. O critério de promoção é o mesmo que esta casa já cita em regua_de_corte.py: ruído acima de ~10% derruba do caminho. Por que isto resolve a briga: D6 (zero descartes) e D8 (encolher a superfície ativa) só brigam se "existir" e "custar" forem a mesma coisa. Medido nesta casa em 19/08 (scripts/quality/custo_de_manutencao_do_guarda.py), não são: o custo de um guarda segue a ATENÇÃO que ele recebe (toques), não a existência dele — a razão bruta de 2,68× vira 1,19× depois do controle. Guarda congelado custa perto de zero. O que falta pra usar: o contrato Guarda (onde_moram_os_guardas.py:128) não tem terceiro estado — estar no censo é estar ativo. Acrescentá-lo é acréscimo, não modificação, mas todo consumidor passa a ter que tratá-lo, e é aí que o preço aparece. ⚠️ O 294/65 = 22% citado na versão anterior desta linha NÃO se confirma: a página de regras do ESLint não publica totais agregados. O que se confirma é a POLÍTICA, que é mais útil — e ela fala em congelar, não em desligar. ~~❌ não investigado.~~ O ESLint entrega 294 regras e liga 65 (22%) — nada vai pro lixo (D6 satisfeita) e a superfície ativa encolhe (objetivo da D8). A §7.2 registra que não temos o estado "existe mas está desligado", e a sonda confirmou: o único análogo é o mutation_check.py, desligado por variável de ambiente, não por estado declarado |
🔴 sim |
| Por que o carimbo de conclusão rende zero? | ✅ RESPONDIDA e CONSERTADA 10/08 (3e26c68a4): ele lia o arquivo do laudo, e o bloco de escopo vive na resposta — 13 de 17 casos. Provado: 17 laudos reais, 0 → 4 carimbos (§3.16) |
— |
| O quinto desenho move a coluna 🧹 manutenção? ⬅️ nova | ❌ em aberto. Dos quatro movimentos já feitos, nenhum ataca manutenção (§4-B) — e é o eixo em que estamos a 3-4× da referência | 🔴 sim |
| Quantos guardas a casa tem? ⬅️ nova, 11/08 | ✅ RESPONDIDA 19/08: são 211, e os três leitores concordam. A unificação da meta 18 (18/08) matou a pergunta e ninguém voltou pra fechá-la. Medido hoje, chamando cada um: onde_moram_os_guardas.guardas_declarados() 211 · fiscais-mudos.registro() 211 · porta_foi_usada.registro_completo() 211 — e o painel imprime 211 no cabeçalho. Os quatro números antigos (238 · 191 · 184 · 218) eram quatro leituras próprias do mesmo settings.json; hoje há uma fonte só e os outros são cascas sobre ela. (238 era contagem de ARQUIVO em disco — hoje 241 — e nunca foi denominador de guarda registrado: a pasta tem peça compartilhada e teste dentro.) ~~❌ quatro denominadores em circulação~~ — 238 em disco · 191 no painel · 184 no fiscais-mudos.py · 218 no briefing de ontem. Dois scripts da própria casa discordam em 7 lendo o mesmo settings.json. Sem denominador único, toda percentagem sobre "os guardas" é indefinida |
🟡 parcial — mas envenena todo número relativo |
| "Escrever teste para cada guarda" volta para a mesa? ⬅️ nova, 11/08 | ✅ VOLTA — a acusação foi medida e vale 1,18×, não 3,35×. O motivo da exclusão era o custo de manutenção; com o controle aplicado (linha acima) ele quase desaparece. Isso não aprova a ideia: o preço de escrever as provas que faltam continua sem número, e o painel mede hoje 68 guardas sem rede. O que mudou é que a objeção que a barrava não se sustenta. ~~⚠️ a condenação caiu, a absolvição não veio.~~ O motivo da exclusão (3,35× mais conserto) foi calculado com os grupos trocados (§3.3). A ideia não está aprovada — está sem número, dos dois lados | 🟡 parcial |
| Os outros 115 guardas prestam? | ⚰️ APOSENTADA 19/08 — impossível hoje, e a causa é ESTRUTURAL, não falta de esforço. São dois buracos independentes, e nenhum se fecha medindo mais: a telemetria não registra "o que este guarda falou mudou uma decisão" (a métrica que o Google usa), e a janela dela é sobra de poda automática — remedida em 32,7 h. Cortando a amostra ao meio, 15% dos guardas trocam de rótulo. Reabrir exige construir o sinal de utilidade, que é obra própria. Segue registrado: ❌ impossível julgar hoje, e 11/08 mostrou por que é pior do que parecia: a telemetria não conta "o que este guarda falou mudou uma decisão" (a métrica do Google) e a janela dela é o resto de uma poda automática — remedida em 19/08: 32,7 h (194.531 registros, 48,7 MB), ainda encolhendo conforme a casa fica movimentada; recontar pelo primeiro e o último ts de .claude/.fiscais-medidos.jsonl. Cortando a amostra ao meio, 15% dos guardas trocam de rótulo |
🟡 parcial |
| A lição devolvida é a certa? | ⚰️ APOSENTADA 19/08 pelo mesmo motivo da linha acima, e com o mesmo preço dito. Responder exige gabarito humano — alguém marcando, para uma amostra de mensagens, qual lição deveria ter voltado. Sem isso só se mede variedade, e variedade sozinha premia errar mais espalhado. Enquanto ninguém pagar o gabarito, a resposta honesta é "não sabemos", não um número novo. Segue registrado: ❌ não existe gabarito. Sem ele só dá pra medir variedade — e variedade sozinha premia errar mais espalhado | 🟡 parcial |
| "Guarda com teste = 3,35× mais conserto" significa o quê? | ✅ RESPONDIDA 19/08: significa quase nada — e o número é 1,19×, não 3,35×. Refeita com os grupos certos e o controle já corrigido por cético independente (âncora 43d404e68 · 2026-08-19): 81 com prova · 73 sem — só quem a colheita VIU. ⚠️ E há 44 guardas (22%) FORA DA CONTA: a colheita nunca os viu, e o descarte não é neutro — 18 deles TÊM prova, são os mais novos, e guarda novo recebe mais conserto na janela. O placar passou a anunciar isso (achado de cético de fecho, 19/08); consertos por guarda 5,37 vs 2,37 = 2,27×. Aí entra o CONTROLE que faltava — contar TODO toque, não só conserto: 8,19 vs 4,25 = 1,93×. Ou seja, guarda com prova é simplesmente guarda mais mexido. Normalizando, a fração de toques que são conserto fica 65,6% vs 55,8% — 1,18×. O efeito real é um décimo do que a frase original acusava. ⚠️ Continua sendo correlação, não causa: quem escreve prova escolhe o guarda que já dá trabalho. ⚠️ A 1ª versão desta linha dizia 130 sem / 2,68× / 2,25× / 1,19×, e o grupo de controle estava CONTAMINADO — montado por subtração, ele fundia "medido e não tem" com "nunca foi visto" (56 de 129), e ainda carregava 12 guardas registrados sem arquivo no disco, entrando como zeros. Derrubado por cético; a conclusão sobreviveu porque o residual é fração de somas e é imune a zeros — sobreviveu pela métrica escolhida, não pelo método. Lição P1122. Recontar (o controle é obrigatório por construção — a peça não sabe imprimir a razão crua sozinha, e o placar traz HEAD + data porque o censo muda entre rodadas): python scripts/quality/custo_de_manutencao_do_guarda.py. Rede: scripts/tests/test_custo_de_manutencao_do_guarda.py (12 provas, 6 de 6 mutantes mortos). ~~⛔ A PERGUNTA CAIU ANTES DA RESPOSTA (11/08).~~ Ela pressupunha que 87 era o grupo sem teste — e era o grupo com (§3.3). A razão inteira precisa ser refeita antes de significar qualquer coisa |
🟡 parcial |
| O passo 7 (conferir antes de codar) tem justificativa? | ⚰️ APOSENTADA 19/08 — a resposta já está dada e é "não, e sabemos por quê". Segue registrada: ⚠️ enfraquecida: o /speckit.analyze inteiro tem zero sensores — os 6 passes são texto de prompt, e o único julgamento de máquina são 3 checagens de "o arquivo existe". Era o nosso único enxerto aprovado |
⬜ não |
| As colunas 3, 5 e 6 do placar (§7) se confirmam rodando? | ⚰️ APOSENTADA 19/08 — não é pergunta, é decisão já tomada. A D1 decidiu não instalar nenhuma ferramenta de fora; logo elas seguem inferidas por desenho, e não por dívida. Reabrir isto exige revogar a D1, o que é decisão do dono |
⬜ não |
| A causa do truncamento em 50 | ✅ RESPONDIDA 10/08: é o rtk, não o git. git log \| wc -l = 8.506; rtk git log \| wc -l = 50. Medida 3× (lente, cético, à mão). Manual global corrigido |
— |
| Por que a lei do carimbo deixou passar 171 guardas? | ✅ RESPONDIDA 10/08, e não eram 171: eram 30, dos quais 3 contam. Ver §3.13 | — |
O mutation_check.py está mesmo solto? |
✅ RESPONDIDA 10/08, e a metade do número foi CORRIGIDA em 11/08: não está solto — vive no pre-deploy.sh:41, atrás de opt-in de 3 variáveis. Mas não tem zero execuções: são 20, à mão. O "zero" vinha de um medidor que só enxerga ganchos |
— |
| O quê | Onde |
|---|---|
| A pesquisa das 14 ferramentas, fact-check e crenças derrubadas | pesquisa-spec-driven-e-harness.md |
| A spec derrubada, com os 4 erros escritos dentro | guardas-quanto-cada-um-custa.md |
O painel de progresso (obra irmã, card #888) |
painel-de-progresso-do-harness.md |
| A obra irmã do atrito de commit (outro chat, mesmo repositório) | PLAN-fechar-o-atrito-do-commit.md |
| A régua de custo por guarda | regua_de_corte.py |
| A bancada que mede guarda contra transcrito real | replay_fiscal.py |
| A biblioteca de estudos do dono | C:/Users/mrodr/OneDrive/Documentos/Estudos IA/ |
As redes que a obra deixou (101 testes, todos verdes em 2026-08-10):
| Rede | Testes | O que ela impede de voltar |
|---|---|---|
tests/test_familia_check_atalho_commit.py |
57 | alguém alargar o filtro por commit · guarda que libera falando no canal de erro |
tests/test_protect_specs_atalho.py |
25 | a lista do atalho sair de sincronia com a lista vigiada |
tests/test_guardas_lentos_atalho_barato.py |
10 | o atalho comer o veredito de quem deveria falar |
tests/test_sync_memory_on_stop.py |
9 | a bomba de 600 s voltar · e declara a lacuna POSIX |
tests/test_descarte_deixa_recibo.py |
7 | o injetor voltar a engolir bloco em silêncio |
Fontes externas que mudaram o rumo:
| Fonte | O que ela fez |
|---|---|
| Bossavit, Leprechauns of Software Engineering | derrubou a justificativa clássica: a curva "bug custa 100× mais tarde" é folclore |
| DORA 2026 (1.110 engenheiros do Google) | adoção de IA sobe throughput e instabilidade juntos |
| The Specification as Quality Gate (arXiv 2603.25773) | IA revisando IA tem erro correlacionado — separar o revisor não basta |
| SpecBench (arXiv 2605.21384) | 97% na bateria visível, 0% na escondida |
| Senge, Shifting the Burden | o remendo alivia e o alívio tira a pressão de consertar a raiz |
| Dekker / Allspaw | "não existe uma raiz" — causas são cada uma necessária, só juntas suficientes |
Pergunta do dono, 2026-08-10. Não é hipótese: há outro chat trabalhando no repositório
neste momento, na mesma branch, no mesmo working tree (.git compartilhado via Dropbox).
| Medida — 90 dias, 5.941 commits | Valor |
|---|---|
| Commits que tocam 1 área só | 66,0% |
| Tocam 2 áreas | 22,4% |
| Tocam 3+ áreas (zona de mistura) | 11,0% |
| Tocam 4+ | 3,2% |
Válvula [commit-ok] usada |
0 vezes |
Válvula BYPASS_COMMIT_GUARD usada |
1 vez |
| Mecanismo | Aguenta 2 chats? | Por quê |
|---|---|---|
Guarda de staging (p367-commit-staging-guard.sh) |
✅ sim, e é o modelo | feito exatamente pra isso, depois de a contaminação acontecer 3× · 0 usos da válvula em 90 dias |
| Trava do 4º arquivo sem plano | ✅ | conta só o que ela viu passar nesta sessão, não git status — foi corrigida por causa deste problema |
| Cards (banco Postgres) | ✅ | é servidor, tem transação |
| Acervo de lições | ✅ | já corrigido: a porta tem trava, depois de perder 3 de 120 linhas |
| Injetor de contexto | ✅ | roda por processo, não compartilha estado |
meta_da_obra.py |
⚠️ | derivar do git é a decisão certa para dois chats (ambos veem a mesma verdade); o risco é a frase do commit casar com a meta do outro |
memory/todos.md (espelho) |
⚠️ | dois chats regenerando o mesmo arquivo: a última escrita ganha |
| Esta spec, e toda spec | ❌ buraco sério | dois chats editando o mesmo .md no mesmo working tree = última escrita ganha, em silêncio |
Working tree compartilhado não gera conflito de merge — gera sobrescrita. Se dois chats
editam o mesmo documento, o git nem percebe: não há duas versões para reconciliar, há um
arquivo e a última gravação venceu. A fonte da verdade é a peça mais frágil do desenho.
| O que protege hoje | Garantia |
|---|---|
| Um dono por obra (este chat nesta spec; o outro no painel) | combinado, não travado |
| Commitar cedo e miúdo, encurtando a janela | disciplina, ~90% |
| Estado importante derivado do git, não escrito à mão | é a única saída 100% — e é por isso que o desenho do quadro de metas está certo, mesmo com 9% de adoção |
Consequência de desenho, registrada: quanto mais estado desta obra for derivado
(lido do git, do banco, da telemetria) e menos escrito à mão, mais ela aguenta dois
chats. Toda vez que a escolha aparecer, ela pende para o derivado — e o motivo não é elegância,
é este.
Carimbo de sensor: [logic] = teste automatizado · [journey] = fluxo ponta a ponta ·
[visual] = tela real. Estes são os critérios da fase de desenho; os da construção nascem
quando o desenho for aprovado.
[logic]: todo número da §3 reproduz rodando o comando escrito ao lado dele[logic]: nenhuma peça proposta já existe no repositório — auditoria com busca[logic]: toda proposta declara os três: quem julga, qual o recibo e aD13 da própria spec[journey]: esta spec sobrevive a uma compactação — alguém que só tenha oD10 virando teste[ ] CA-4 [logic]: a spec não se contradiz — nenhum número afirmado como fato numa
seção pode estar derrubado em outra
→ ❌ REPROVOU DUAS VEZES no mesmo dia, e a segunda é a informativa:
| Revisão | Achados | De onde vieram |
|---|---|---|
| 1ª | 6 | medições da madrugada derrubando afirmações da noite anterior |
| 2ª | 15 | ⚠️ quase todos criados pela própria 1ª revisão — contagens que ficaram para trás (13 medições, 23 decisões, 4 desenhos), citações de linha que envelheceram em 3 horas, e o §0 se contradizendo consigo mesmo |
A lição que isso ensina, e que a 1ª revisão não sabia: revisar um documento longo
cria contradição nova, porque a revisão acrescenta seções e as contagens ficam para
trás. Conferir contagem e citação de linha passa a ser parte do fecho de toda revisão,
não um extra
→ ❌ REPROVOU PELA TERCEIRA VEZ em 2026-08-11, e desta vez o defeito é de outra
natureza — não é contagem desatualizada, é fato invertido:
| Revisão | Achados | Natureza |
|---|---|---|
| 3ª (sonda de §3.19) | 20 | 3 inversões de leitura, 1 linha que não fecha com a própria aritmética, e 16 correções de método (janela, denominador, unidade) |
O que a terceira ensina, e é a mais dura: as duas primeiras revisões liam o
documento; esta leu o código. Nenhuma quantidade de auto-revisão acha um número
invertido — só a medição acha. É a D19 sendo provada pela terceira vez, e o motivo de
CA-1 e CA-4 nunca poderem ser marcados de forma permanente
- [x] CA-5 [logic] ⬅️ novo, e nasceu APROVADO: nenhuma medição que matou uma ideia
pode ficar de fora — se um número mudou o rumo, ele tem seção própria, não menção de
passagem
→ ✅ corrigido em 10/08: a sonda que matou a poda dos guardas mudos (23,7% da
manutenção, 63,1% de fix:) existia, tinha matado uma ideia e não estava na spec —
só citada dentro de outra seção. Virou a §3.14-B. É a lição P884 virando critério
| Lente | O que devolveu |
|---|---|
Em casa (busca em specs/, .claude/hooks/, scripts/) |
4 das 6 peças já existiam: mutation_check.py (pronto, desligado), completeness-critic.py (bloqueia, lê a fila errada), replay_fiscal.py (mede guarda contra transcrito real), meta_da_obra.py. Mais regua_de_corte.py e painel_de_guardas.py. Os dois desenhos morreram por propor construir o que já estava lá |
| Fora (doc oficial, issues, nome do padrão) | Os nomes: stage gate · evidence-based verification · durable execution · context rot · shifting the burden (Senge) · "there is no root cause" (Dekker/Allspaw). E o fact-check: 4 de 10 afirmações do texto que originou tudo eram falsas |
| O que o padrão CUSTA | Portão forte só existe em ferramenta de um usuário — o de 126 mil estrelas só confere se o arquivo existe. O preço de poder ser forte: ninguém mantém a nossa ferramenta (673 consertos/mês contra 363 do produto) |
Alternativas descartadas e o motivo: ver §8, que é obrigatória e não pode ficar vazia.
| Risco | Probabilidade | Impacto | Mitigação |
|---|---|---|---|
| Somar à pilha em vez de reformar | alta | 34 guardas/semana viram 40 | D8 — toda peça nova apaga ou absorve uma existente |
| Ligar portão que morde errado | média | trabalho legítimo barrado, e o dono perde a confiança na trava | D9 — modo sombra primeiro, sempre |
| A ferramenta quebra e o produto para junto | média | dia de trabalho parado | tudo reversível em um comando; nada substitui peça viva (D6) |
| Esta spec apodrecer | alta | vira mais um dos 321 documentos que enganam | D10 — ela é a cobaia; CA-3 é o teste |
| Propor o que já existe | alta (aconteceu 2×) | obra inteira desperdiçada | CA-1 — auditoria com comando registrado antes de qualquer proposta |
| A spec se contradizer ⬅️ novo | alta — já aconteceu, em 6 lugares | pior que apodrecer: um leitor que abre no meio encontra o número derrubado afirmado como fato, e constrói em cima | CA-4 + a regra de manutenção: número derrubado sai do texto e vira linha de registro, no passado (P904) |
| Ninguém revisar isto de novo | média | a revisão de 10/08 achou 6 defeitos em 1 dia de deriva; em 30 dias seriam mais | nenhuma mitigação automática existe — é a única linha desta tabela sem rede, e fica dita |
Experts/, Include/CopyTrade/, server/af/,server/routes/signals.py, migrations/. Nada desta obra toca o robô, o motor de hedge ou⚠️ Esta lista protegia um arquivo que não existe. O caminho antigo
server/signals.py
foi substituído em 2026-08-10 — ele não está no repositório; os reais são
server/af/signals.py(já coberto porserver/af/) eserver/routes/signals.py. Quem
lesse acreditaria estar protegido, e o arquivo de verdade ficava de fora. Quem achou
foi o verificador de referências, na mesma passada em que ele ganhou a metade nova.
⚠️ Esta lista é documentação, não proteção — o guarda lê uma lista fixa de 6 arquivos
num acervo de 494 specs. Ela vale como contrato lido, e fica pronta para o dia em que a
spec for registrada.
specs/decisions/) quando o desenho for aprovado. Pendente.D3 porteiroD16 não congelar, D18 guarda novo apaga velho, D19 conferir o código.)(Média e baixa viram NOTA: não contam pro fecho, não bloqueiam e não viram card.)
| Data | O que mudou |
|---|---|
| 2026-08-10 | Obra nasce. Pesquisa de 14 ferramentas concluída e commitada (934a4df). Primeiro desenho (5 portões) proposto e derrubado por cético no mesmo dia. Mapa do fluxo atual levantado: 14 manter, 6 melhorar, 2 substituir, 0 descartar. Decisões D1 a D10 registradas. Esta spec commitada em b69b312. |
| 2026-08-10 (mesmo dia, depois) | Varredura do esquecimento voltou — segundo desenho também derrubado (SOBREVIVE: NÃO). Dois achados entram na spec: §3.7 o injetor de contexto engole 37,1% do que deveria entregar, calado (verificado à mão); §3.8 o quadro de metas depende de eu lembrar, com 9% de adoção. Dois vereditos da §6.2 corrigidos. Commit b61032697. |
| 2026-08-10 (mesmo dia, noite) | Spec alinhada ao molde da casa — e o alinhamento revelou que ela nascera sem a linha de tamanho, passando pelo guarda em silêncio. Análise de dois chats no mesmo repositório (§11-B), com o index.lock do outro chat aparecendo ao vivo durante o commit. A lei dos 9% (§3.10) nasce da informação do dono sobre como ele usa o roadmap, e gera D11, D12 e D13. Terceira varredura disparada: spec que apodrece · fila que só cresce · contexto injetado · dois agentes · e um advogado do diabo contra a obra inteira. Nada foi construído ainda. |
| 2026-08-10 (madrugada→manhã) | O dono revoga o congelamento (D16), pede os três alvos juntos (D17) e autoriza guarda novo que apague um velho (D18). Manda medir o despachante antes de construir — e a premissa cai (§3.12): os guardas já rodam em paralelo. Nasce a régua de posse do relógio (D21). Manda acelerar os 5 mais lentos e consertar a bomba do #915 na raiz: a família dos seis sai de 1.024,4 → 116,8 ms (§3.12-B) e o sync-memory sai de > 600 s → < 5 s. A lei do carimbo é medida por replay em 6.002 commits e sobrevive (§3.13). |
| 2026-08-10 (madrugada) | Três céticos independentes sobre a obra dos guardas (§3.14). Um devolve SOBREVIVE: NÃO — a expansão nativa do bash não entende contrabarra, e o registro de todo bloqueio se perdia em silêncio. Consertado, com a rede que teria pego (stderr limpo). Descoberto de brinde que o diff curou um mudo pré-existente (git+TAB+commit). Sete lições no acervo: P894, P895, P899, P900, P901, P904, P905. Palavra: Blindado. |
| 2026-08-10 (tarde — o quinto desenho e três céticos) | Escrevi o quinto desenho em rascunho, fora da spec, e mandei três céticos independentes atacá-lo antes de qualquer linha de código. Os três derrubaram algo meu. O desenho inteiro caiu (§9.0) e, junto, a régua de custo por guarda que eu tinha criado pela manhã (§3.15) — ela dividia 90 dias por 1,3 dia e ia me fazer aposentar o 3º maior bloqueador da casa. Sobraram três fatos: o carimbo de conclusão roda 116× e rende zero (§3.16), o painel chama de bloqueios o que é bloqueios em PreToolUse (§3.17), e o custo de oportunidade tem nome — 62 cards de produto abertos, 13 há 50+ dias (§3.18). Entram D24, D25, D26. Custo de matar o desenho: três agentes, contra 15 ajudantes do anterior. |
| 2026-08-10 (noite — a fila de "arrumar") | O dono manda arrumar o harness primeiro, e a fila tinha dois itens, os dois consertados. O carimbo de conclusão (3e26c68a4): rodava 116× e rendia zero porque lia o arquivo do laudo, e a lista do que o cético examinou vive na resposta — 13 de 17 casos. Provado com o código real: 0 → 4 carimbos. O nome do campo do painel (046510e42): bloqueios contava um evento só, e 359 bloqueios reais estavam invisíveis em 4 guardas. Virou bloqueios_no_portao + bloqueios_fora_do_portao, com teste que impede o nome curto de voltar. Lição P908 registrada. |
| 2026-08-10 (noite — SEGUNDA revisão completa da spec) | O dono pede para conferir se tudo está anotado. Mais 15 defeitos, e o achado é sobre revisão em si: quase todos foram criados pela primeira revisão — contagens que ficaram para trás (13 medições onde há 20, 23 decisões onde há 26, 4 desenhos onde há 5), citações de número de linha que envelheceram em 3 horas quando eu mesmo mexi no arquivo, e o resumo de 60 segundos se contradizendo consigo mesmo em duas linhas vizinhas. E faltava conteúdo: a sonda que matou a poda dos guardas mudos (23,7% da manutenção) nunca tinha virado seção — virou a §3.14-B. Nascem CA-5 (medição que matou ideia tem seção própria) e a régua de que revisar cria contradição nova. |
| 2026-08-11 (tarde — o sexto desenho nasce e morre no mesmo dia) | Escrevi o sexto desenho em rascunho, fora da spec, e mandei três céticos em paralelo, com lentes distintas. R1 (a peça já existe) devolveu SOBREVIVE: SIM — confirmou que não há gatilho por nenhuma das formas de carregar plugin de pytest — mas derrubou um número meu escrito 3 horas antes: o "512 commits" era uma janela de 14 dias colada numa afirmação de "desde a foto"; o certo é 305. R3 (isto acerta o alvo?) devolveu SOBREVIVE: NÃO e matou pela raiz: o número que o desenho refresca não decide nada — um consumidor executável, que imprime numa linha de resumo, fora da lista de alarmes, num script que devolve 0 sempre. E 71 dos ~100 "sem rede" vêm de uma REGRA deliberada, não de defasagem; só 35 são de fato desconhecidos, e o próprio produtor documenta a escolha de régua. A frase que sustentava o desenho — "toda decisão de aposentar depende deste número" — era desta spec, e eu nunca a conferi no código: D19 violada dentro da seção escrita para denunciá-la. Registro em §9.-1. Sobrou um conserto de 1 linha no consumidor, que entrega a parte honesta sem nenhum do risco. |
| 2026-08-11 (a sonda — e o dia em que a spec foi conferida contra o CÓDIGO, não contra si mesma) | O dono manda seguir no harness. Em vez de escrever o sexto desenho, disparei uma sonda de 5 lentes + 5 céticos (11 ajudantes, 1.150.445 tokens, 37 min) sobre o eixo manutenção — porque a própria spec mede que 5 desenhos morreram e 6 consertos nasceram de sondas. Os cinco vereditos voltaram SOBREVIVE: NÃO e 20 números foram corrigidos (§3.19). Três invertem a leitura: o 87 era quem TEM teste, não quem está sem (§3.3, e isso derruba a exclusão de "escrever teste para cada guarda" na §8); a inversão está datada em 22-29/07 e é patamar, não aceleração desde ontem (§3.11); e a linha de 30 dias não fechava com a própria aritmética. A sonda matou as duas pistas que eu tinha para o sexto desenho — "reformar um punhado de arquivos" (74,2% do conserto está fora do top-5) e "a ferramenta é mais concentrada que o produto" (invertido: o produto é mais). Sobraram três fatos acionáveis: o mutation_check.py pronto e desligado, o canal único em 23,1%, e o validate-commit.sh com 68,5% do conserto da vida no último mês. Nasce a lição P927 (janela deslizante medida duas vezes não mede tendência) e a pergunta nova do terceiro caminho da briga D6×D8 (o modelo ESLint: 294 existem, 65 ligadas). |
2026-08-11 (a cobrança da P884 — as 6 peças da pesquisa, conferidas no código) |
O dono manda voltar à obra. Em vez do sétimo desenho, varri no código as 6 peças que a pesquisa entregou e que o desenho anterior cobriu pela metade — a lente CA-1 rodando antes, não depois. Resultado em §3.21. Duas peças já existem com trava (sensor 0/1 no portão; juiz ≠ executor), uma está pronta e desligada (mutation_check.py, 0 de 187.911 linhas de telemetria), uma é decisão do dono (1 conversa = 1 tarefa). Das duas que a pesquisa marcava como buraco: "N tarefas = N provas" estava errada — a adesão é 83,1%, e o buraco real é que só 58,1% carregam comando executável, com 25% escrevendo prosa onde a regra pede comando. E "conferir antes da 1ª linha" ganhou nome e endereço: plan-approved-task-tracker.py roda no instante exato, recebe o texto inteiro do plano (4.604-9.957 caracteres, confirmado em 5 chamadas reais) e não o lê — a varredura de .claude/hooks/ por leitura de plano devolve uma fixture de teste. O medidor errou duas vezes antes de acertar (3,6% ancorado errado, 87,1% frouxo demais, 58,1% conferido à mão) — o anti-padrão "medidor caseiro" pego dentro da própria sonda. Nenhuma linha de código de desenho foi escrita. |
| 2026-08-11 (noite — o sétimo desenho e o primeiro placar 3 a 0) | O dono escolheu desenhar a abertura do envelope e com cético antes do código. Escrevi o sétimo desenho em rascunho e mandei três céticos de lentes distintas. Os três devolveram SOBREVIVE: NÃO — inédito nesta obra. R2 mediu o golpe em vez de argumentar: o MESMO envelope pede duas coisas, uma vira chamada de ferramenta (97,6%) e a outra vira prosa minha (1,70%; reproduzi em 1,93% sobre 34.801 mensagens) — e o meu bloco era da família da prosa. R3 implementou o meu interpretador e rodou nos 19 planos: 71 acusações, 65 falsas — 91,5%, contra teto de 10% da casa, num lugar onde 13,2% já pagou uma obra de conserto. R1 derrubou os números: o 58,1% vira faixa de 22,1% a 67,3%, o motor de mutação tem 20 execuções e não zero, são 26 chamadas de aprovação e não 5. E o exemplo que eu escolhi à mão para vender o desenho era ele próprio uma acusação falsa. Corrigidos 6 pontos da spec, incluindo o "zero execuções" que estava aqui desde 10/08 e que eu repeti sem conferir. O que sobrevive: a porta está certa, o canal entrega, e a raiz é a gramática ausente — 15 formas de marcar tarefa × 23 de declarar verificação. Duas saídas sem interpretador nenhum ficaram nomeadas. |
| 2026-08-10 (revisão completa — "quero ele 10/10") | Revisão da spec inteira contra o código, como manda a D19. Seis contradições internas encontradas e corrigidas: 76% afirmado como fato em duas seções que a §3.12 já havia derrubado para 22,5% · 171 guardas na §6.2 contra os 30/3 da §3.13 · o despachante ainda descrito como "o movimento que sobreviveu" depois de morto · 4,2× em §9.1 · e a afirmação de que o mutation_check.py nunca foi ligado a nada — o código diz que ele está no pre-deploy.sh:41, atrás de um opt-in de três variáveis, com zero execuções em 530 sessões. Entram a §0 (a obra em 60 segundos), a §4-B (a cobrança da D17 — qual alvo cada movimento move), o CA-4 (a spec não se contradiz, que reprovou e por isso existe) e as decisões D22 e D23. Decisões reordenadas. |
Este arquivo é para o dono, não para a máquina. Ele explica, sem uma linha de jargão, as 18
regras que eu sigo para decidir se uma coisa vale a pena, como fazer, e quando dizer que acabou.
O texto técnico mora em.claude/skills/sweetspot/references/clausulas.md; aqui fica a versão
que se lê de fora.
⚠️ Isto é uma SEGUNDA CÓPIA das mesmas regras, e duas cópias divergem — então a ordem de quem
vence vai escrita na primeira tela. Se este guia e o arquivo técnico discordarem sobre o que uma
regra manda, o técnico vence. A duplicação é decisão, não descuido: obrigar você a abrir o
arquivo técnico para entender a própria régua era o mesmo que não ter régua.
⚠️ Os números deste guia são fotografias, não medidores. Onde eu digo "48 de 189" ou "6.109
execuções", eu vi aquilo num dia. O placar que se atualiza sozinho fica em
.claude/skills/sweetspot/references/placar.md. Número que envelhece e ninguém reconta é a falha
que estas próprias regras mais perseguem — e um guia leigo não pode carregar os comandos de
reconferir sem virar o jargão que ele veio substituir. O preço está dito: a ordem de grandeza
está aqui, o valor exato mora lá.
⚠️ Esta seção existe porque eu me perdi, e o dono viu antes de mim. Passei uma obra medindo
uma coisa e chamando de melhoria da sweetspot. Eram camadas diferentes, e confundi-las é o
erro mais caro que dá para cometer aqui.
A pergunta dele, que abriu o assunto: "isso é pra skill sweetspot ou é outro conceito?"
Era outro conceito.
┌─────────────────────────────┐
uma decisão ──► │ 1. QUE AÇÃO ISSO DISPARA? │ ◄── NÃO é "que pergunta é essa"
│ dá pra desfazer? │ é "o que acontece se der errado"
│ quem mais é atingido? │
└──────────────┬──────────────┘
│
┌────────────────────┴────────────────────┐
▼ ▼
🔒 IRREVERSÍVEL / ATINGE FORA 🔧 REVERSÍVEL / SÓ AQUI DENTRO
subir pra produção · dinheiro card · formato · conserto
apagar · mensagem em seu nome medir · escrever spec
│ │
▼ ▼
┌──────────────────────┐ ┌─────────────────────────────┐
│ PORTÃO DURO │ │ 2. A RÉGUA DECIDE │
│ mecanismo, não texto │ │ ◄── É AQUI que a │
│ ⚠️ confiança alta │ │ `sweetspot` mora │
│ NÃO compra passagem │ └──────────────┬──────────────┘
└──────────┬───────────┘ │
▼ ▼
⏳ PARA E ESPERA ┌─────────────────────────────┐
│ 3. MEDIR CONTRA O QUE ELE │
│ DE FATO RESPONDEU │
└─────────────────────────────┘
| camada | o que responde | onde mora | garantia |
|---|---|---|---|
| 1. o portão | esta decisão pode ser confiada a uma régua? | hooks — pede-ok-antes-de-subir e ato-de-producao-por-mcp barram de verdade |
✅ ~100% |
| 2. a régua | dado que pode, qual caminho? | ✅ a sweetspot, as 18 cláusulas |
⚠️ ~90% (é texto) |
| 3. a medição | o julgamento acertou? | os 947 pares de gabarito-do-dono.json |
⚠️ sem rotina ainda |
A tabela de garantias desta casa já responde: hook vale ~100%, texto vale ~90%. Uma skill é
texto. Pôr o portão do irreversível dentro dela seria trocar uma parede por um lembrete.
E a literatura de 2026 diz a mesma coisa com outras palavras: "alta confiança não compra o
direito de fazer uma ação irreversível sem supervisão". O padrão adotado em produção classifica
cada ação por reversibilidade e raio de estrago — e só depois deixa a régua julgar.
✅ A boa notícia: este desenho já está montado aqui, só não estava nomeado. O portão já é
hook; a skill já é a camada 2.
Os quatro níveis da literatura foram testados nos 929 pares — e aqui eles são DOIS, não quatro:
| delegou | taxa | intervalo | |
|---|---|---|---|
| 🔒 tier 1 — irreversível / atinge fora | 18/168 | 10,7% | [ 6,9..16,3] |
| 🔧 tiers 2, 3 e 4 juntos | 188/761 | 24,7% | [21,8..27,9] |
Os intervalos não se sobrepõem — a separação é real. Mas os tiers 2, 3 e 4 são
indistinguíveis entre si (25,9% · 24,5% · 24,1%), então o eixo é binário: irreversível, ou
não.
⚠️ E o ganho preditivo global é pequeno, porque o tier 1 é só 18% dos casos:
| eixo | Brier (menor é melhor) |
|---|---|
| chute fixo (a taxa média) | 0,1726 |
| ✅ tier de reversibilidade | 0,1696 |
| régua de texto (a cascata) | 0,1705 |
Os três praticamente empatam. A leitura honesta: a reversibilidade funciona como portão
binário, não como preditor.
Medir a minha confiança e usá-la como portão é o conserto intuitivo — e é o que falha:
| achado | consequência |
|---|---|
| confiança verbalizada é sistematicamente superconfiante | agentes preveem 73% de sucesso e acertam 35% |
| usar confiança crua como portão | ⛔ faz escalar DE MENOS justo onde mais precisa |
| agentes já escalam de menos por padrão | é o modo de falha dominante da indústria |
Fontes: Knowing When to Ask for Help · Act or
Escalate? · On Verbalized Confidence
Scores ·
confidence-escalation
A camada 1 tem mecanismo só nas duas pontas que já viraram hook — subir pra produção e as
três ferramentas de produção. Fora delas, ela vive em texto, e eu obedeço ~90% das vezes.
✅ E os 90% estão funcionando, medido: no tier irreversível a delegação cai para 10,7%
contra 24,7% no resto. Comparado ao que a literatura mede — agentes que escalam de menos
por padrão —, estamos do lado certo da falha.
Sintoma de que isso quebrou: eu subir algo para produção sem parar e perguntar.
python .claude/medicoes/2026-08-28-variacoes-da-regua/reversibilidade.py
Você não precisa decorar número nenhum. Cada regra tem um NOME curto; o número existe só para a
máquina anotar a nota dela. Quando eu disser "parei pela do teto", é destas que estou falando.
| família | a pergunta que ela responde | as regras | o eixo do tempo |
|---|---|---|---|
| O jeito único | quantos jeitos de fazer isso existem? | Caminho único · Unificação |
antes (impede nascer) × depois (junta o que separou) |
| Procurar, não lembrar | eu busquei, ou achei que sabia? | Os irmãos · Aberto/Fechado · O mundo já resolveu |
antes de construir × depois de achar defeito |
| O número honesto | esse número quer dizer o quê? | Alarme ou ferramenta · Conta o pedaço · Peça nova · Sweetspot · Não medi |
antes de olhar × ao olhar |
| O silêncio não vale | o que eu deixei de dizer? | Robusto · As três linhas · A palavra |
tudo no fecho |
| Onde é a causa | consertei o que apareceu ou o que causou? | A raiz · Está sangrando? |
agora (estanca) × depois (a raiz) |
| Quando acaba | como sei que terminei? | Sem defeito sintético · O teto |
é o fim, por natureza |
O eixo do tempo foi o dono quem viu, olhando Unificação e Caminho único: "uma pra arrumar
e outra pra já nascer certo". O padrão se repete em quase toda família — quase sempre há uma
regra que age antes e outra que age depois.
| marca | origem | quando brigam |
|---|---|---|
| 🟦 | as 8 do dono — ele as escreveu num bilhete, com as palavras dele | as dele vencem |
| ⬜ | as 9 minhas — cada uma nasceu de um defeito medido aqui | cedem |
A maioria tem nome consagrado lá fora, e onde há nome ele vem marcado com 📚 no verbete —
conte os 📚 e você tem o número deste arquivo, sem precisar acreditar em mim. Isso não é
enfeite: nome de indústria carrega décadas de gente aprendendo na dor.
(A primeira versão desta linha dizia "quinze das 18" — um número que não se confere lendo o
arquivo, porque nem todo verbete com âncora traz o 📚. Um revisor contou 11 e me derrubou.
Afirmação que o próprio documento não sustenta é a falha que a regra Conta o pedaço persegue,
cometida no documento que a explica.)
"se as coisas tiverem um meio só de fazer tal coisa, nada nasce quebrado" — palavras do dono,
e são quase literalmente as da indústria.
Caminho único — o que impede o defeito de nascerQue exista UM jeito de fazer aquilo.
No dia-a-dia: tomada de três pinos. Ninguém precisa de aviso, de treinamento nem de fiscal —
o plugue errado simplesmente não entra.
Como sei que cumpri: eu conto quantos jeitos de fazer aquilo existem hoje. Mais de um, e o
defeito vai nascer no jeito não pavimentado — é só questão de tempo.
📚 Lá fora chama-se caminho pavimentado, e a frase que resume é governa-se por construção, não
por inspeção.
⚠️ E não é ordem, é conveniência — isto muda tudo. A literatura é explícita: não é "você DEVE
usar", é "se usar, sua vida fica mais fácil". A porta que ninguém usa não é caminho único — é
mais um jeito.
(Isto era a regra Canal único, a 18ª. Aposentada em 21/08 por decisão do dono: ela era um caso particular vestido de princípio — "todo aviso sai pela mesma porta" é literalmente "que exista um jeito só de fazer aquilo". Caso particular só pega o caso dele, e foi por isso que ela terminou com zero acertos. O conteúdo ficou; a regra separada, não.)
Todo aviso sai pela mesma porta, no mesmo formato: o que houve · onde · o que fazer agora.
No dia-a-dia: correspondência chega na caixa do correio. Bilhete jogado por cima do muro
também "foi entregue" — e ninguém lê.
A porta única não serve para o seu caso? Mude a porta. Nunca abra a segunda.
⚠️ Foi a mais fraca de todas, e vale contar por quê — porque a história é sobre régua, não sobre
a regra. Medido em 21/08, com duas réguas diferentes:
| como se mede | o número |
|---|---|
| pelo formato do código (quem fala sem usar a porta) | 91 de 189 |
| quem não fala nada (e portanto não precisa de porta) | 50 |
| quem usa a porta | 48 |
| pelo efeito — quem de fato falou pro vazio numa janela de ~28h | 8 guardas, 1.054 falas perdidas |
A distância entre 91 e 8 é a resposta inteira. Converter 91 arquivos para consertar 8 casos
observados é exatamente o que a regra Sweetspot proíbe. E o pior dos 8 nem é um arquivo do tipo
que uma conversão em massa alcançaria.
As três falharam pelo mesmo motivo — eu procurei de cabeça — e as três foram consertadas do
mesmo jeito: colar o comando que rodei. É a família mais coesa das seis.
(Esta seção esteve aninhada sob Unificação até 21/08, e o lugar errado ensinava a regra
errada como dona do caso mais usado dela. A cláusula Canal único foi fundida em Caminho
único, não em Unificação.)
Unificação — o que arruma depois que já separouAo juntar duas peças que fazem a mesma coisa, a que fica tem que ter as garantias das duas.
No dia-a-dia: duas fechaduras na mesma porta e você vai ficar com uma. A nova é bonita e tem
chave eletrônica; a velha é feia e tem tranca de segurança. Ficando com a nova sem olhar, a
porta perdeu a tranca — e ninguém percebe até alguém empurrar.
Nunca a mais nova, nunca a mais usada, nunca a mais limpa. A que fica é a SOMA.
Como sei que cumpri: escrevo uma tabela lado a lado — o que a condenada tinha, o que a eleita
tem, cada item marcado como levado ou descartado com motivo. Se eu não consigo listar o que
a condenada fazia, eu não a li o bastante para apagá-la.
⚠️ Caso real: elegi a mais nova e 88 peças rodaram um dia inteiro com uma proteção a menos.
A "velha" tinha um limite de tempo que a nova não tinha.
📚 Lá fora chama-se a cerca de Chesterton: não derrube a cerca antes de descobrir por que ela
foi posta. Há literatura específica sobre agente de IA removendo o que não entende — que foi
exatamente o que aconteceu nas 88.
E se somar as garantias sair caro, não unifique. Duas peças de propósito é decisão legítima.
Os irmãosAchou um defeito, vá procurar os parentes dele — por busca, nunca de cabeça.
No dia-a-dia: o eletricista achou uma tomada queimada. O bom vai olhar as outras do mesmo
circuito antes de guardar a caixa. O ruim troca a tomada e vai embora.
Como sei que cumpri: eu colo a busca que rodei e quantas coisas ela achou. Dizer "procurei"
não vale — não dá para conferir.
É a pior de todas na minha ficha — mais falhas que acertos, e o número exato muda toda semana
(ele mora no placar, não aqui). Nas vezes que errei, eu tinha lido a regra, achei que sabia de
cabeça, e o irmão estava 25 linhas acima, no mesmo arquivo.
⚠️ E ela falhou de novo em 21/08, dentro do commit que criou este guia. Busquei uma frase e
declarei "9 irmãos, todos atualizados"; um revisor independente refez a busca sem diferenciar
maiúscula de minúscula e achou mais dois vivos — um deles em caixa alta, no cabeçalho da
regra Sweetspot, dizendo quatro sobre uma tabela de cinco. Busca que só pega a grafia que
você lembrou é busca de memória com roupa de comando.
📚 Lá fora chama-se análise de variantes. O Google mede uma coisa assustadora: metade dos
ataques novos que eles encontram são variantes de um bug que já tinha sido corrigido — porque
ninguém foi olhar os irmãos depois do conserto.
Aberto/FechadoCoisa nova entra por um encaixe. Se para acrescentar eu preciso abrir o motor que já funciona, eu
paro.
No dia-a-dia: a caixa de ferramentas tem slots. Régua nova entra num slot vazio. Você não
serra a caixa para caber a régua — e se serrar, todas as outras ferramentas ficam bambas.
Como sei que cumpri: eu colo a busca pelo encaixe. "Não havia encaixe" dito de cabeça é
opinião; a busca colada é fato.
📚 Este é seu E é da indústria ao mesmo tempo: é o O do SOLID, de 1988. Até 20/08 esta regra
existia aqui sem o nome — eu tinha traduzido a ideia e apagado o rótulo, e quando você releu a
lista não reconheceu o próprio critério. Nome consagrado carrega literatura; apelido meu carrega
só a minha explicação.
É a que mais pega defeito de todas (o número está no placar, que se atualiza sozinho). E mesmo
assim falhou 6 vezes numa obra só — e nas 6 o encaixe existia. Numa delas, a peça certa estava
importada 1.260 linhas acima, no mesmo arquivo.
⚠️ E ela falhou de novo em 21/08, com a variante mais traiçoeira: o encaixe tinha DUAS metades e
eu achei uma. Para este guia aparecer na página do site, é preciso (1) copiar o arquivo pra
máquina e (2) inscrevê-lo na lista de páginas. Eu fiz a primeira, colei a busca, e prometi a você
duas vezes que ele apareceria sozinho. Não apareceria. O comentário do próprio código dizia a
frase que me desmentia: "copiar é automático, publicar não". Encaixe achado não é encaixe
completo — a pergunta certa é "esta é a única metade?"
O mundo já resolveuAntes de desenhar, ver se já existe. A ordem é: casa → internet → casa de novo.
No dia-a-dia: antes de inventar gambiarra para a porta que range, você olha na sua caixa de
ferramentas. Não achou? Pergunta o nome da peça na loja — e volta para casa procurar de
novo com o nome certo, porque muitas vezes ela estava lá e você não sabia como se chama.
A volta é a parte que quase todo mundo pula, e ela é medida: em 12 de 12 achados derrubados
por revisor independente, a resposta já estava neste projeto. Ir direto para a internet não é
diligência — é pular a fonte que tinha a resposta.
Mas quando eu não sei nomear o que procuro, a internet é o dicionário. Um nome técnico vindo de
fora destravou 307 achados que já estavam no disco aqui. O dado estava dentro; o nome estava
fora.
A pergunta não é "como faço isso melhor?" — é "isso deveria ser feito assim?"
Foi esta regra que trouxe todos os nomes de indústria deste guia, num único dia: descobrimos que
quatro regras nossas já eram padrão consagrado sem o nome, e que a nossa análise de causa tinha
dois dos três passos que a definição canônica exige.
Cinco regras, uma pergunta só: esse número quer dizer o quê? É a maior família, e não por
acaso — quase todo erro caro que eu cometi aqui começou num número sem régua.
Alarme ou ferramenta — declare ANTES de ver o númeroA mesma quietude significa o oposto nos dois casos.
No dia-a-dia: um extintor que nunca foi usado está ótimo. Uma furadeira que nunca foi
usada foi dinheiro jogado fora.
| é um... | serve quando | zero uso significa |
|---|---|---|
| alarme (trava, proteção) | não dispara | está funcionando |
| ferramenta (peça que alguém chama) | é chamada | está morta |
Quem chega com o número sem ter declarado antes é ferramenta, sem apelação — senão eu escolho o
rótulo depois de ver o número, que é o mesmo que não ter régua.
⚠️ Aqui mora um buraco que eu achei comparando com a literatura, e ele era numa regra minha:
dizer que alarme calado = está funcionando é conclusão que não se tira olhando. Extintor
quieto pode estar cheio ou vazio. A literatura de controles é explícita: proteção silenciosa
exige teste ativo. Hoje a regra exige a prova que exercita — e a peça que faz isso já rodava
aqui, só faltava a regra depender dela.
Conta o pedaçoMeça o trecho que você vai mexer, nunca a peça inteira — e todo número vem com a régua ao lado.
No dia-a-dia: uma tomada não funciona. Você não conta quantas vezes a casa é usada; conta
quantas vezes aquela tomada é usada. Pode ser a da geladeira ou a de trás do armário, e a
decisão é oposta.
A segunda metade é mais importante que a primeira. Número sem régua não quer dizer nada:
28 trechos repetidos ← errado: "trecho" não quer dizer nada
2 LINHAS INTEIRAS idênticas ← certo: dá para conferir
⚠️ O caso, e ele é meu: reportei 28 onde havia 2. Contei com uma janela que passa de
palavra em palavra, então o mesmo pedaço era contado uma vez por posição. O número chegou até você
como fato.
⚠️ E aconteceu de novo no dia seguinte, neste próprio guia: publiquei "55 de 189 usam o canal"
misturando duas contagens — o numerador incluía subpastas, o denominador não. O número certo é
48. Régua misturada é o defeito que esta família inteira existe para pegar, e ele me pegou duas
vezes em dois dias.
📚 Lá fora chama-se definição operacional, do Deming, e a frase dele é a régua inteira: "não
existe valor verdadeiro de coisa nenhuma; existe o resultado de aplicar um procedimento". Mudou o
jeito de contar, mudou o número.
Sintoma de que eu errei: você pergunta "28 o quê, exatamente?" e eu preciso voltar no código
para lembrar como contei.
Peça nova precisa de casoQuantas vezes a situação já aconteceu de verdade? Zero na janela que dá para medir = não
construo.
No dia-a-dia: não compre guarda-chuva porque pode chover. Compre quando tiver se molhado.
E se já existe peça cuidando daquele mesmo momento, eu mudo ela em vez de criar a próxima.
O número que sustenta isso aqui dentro: temos cerca de 210 travas registradas, a maioria
nunca agiu, e cerca de 8% delas estão quebradas neste momento — e cada uma vira manutenção para
sempre. Se eu não consigo dizer numa frase por que a minha é diferente, eu não construo.
(A frase antes dizia "8% de chance de nascer quebrada", e isso é outra coisa: o número mede
quantas estão quebradas hoje, não quantas nascem assim. Uma é foto, a outra é taxa — e eu não
medi a taxa. Um revisor pegou.)
📚 Lá fora chama-se YAGNI — "você não vai precisar disso", do Extreme Programming. A nossa é
mais dura que a original: lá diz "não construa para o futuro imaginado"; aqui é não construa
até o caso ter acontecido E sido contado.
SweetspotA maior garantia pelo menor custo que se paga TODO DIA. Números escritos antes de qualquer
código:
| número | a pergunta | o corte |
|---|---|---|
| Preciso | de cada 100 vezes que falar, quantas serão à toa? | mais de 10 em 100 → nasce desligada |
| Barato | quantos segundos soma no caminho de todo mundo? | compete com o portão, que já leva ~32s |
| Saldo | apaga ou absorve alguma peça que já existe? | — |
| Valor | quantas vezes fez algo útil numa janela declarada? | zero → sem medida, nunca "cara" |
| Silencia | de cada 100 observações, em quantas ela está verde? | abaixo de 50% ela virou paisagem |
| Quem sente | quem percebe a diferença, e COMO? | "ninguém percebe" não reprova — mas vai escrito, e aí a peça é para a oficina, não para você |
No dia-a-dia: alarme de carro que dispara com o vento não é segurança, é barulho — em uma
semana você desliga. E aí, no dia que precisava, ele não estava ligado.
⚠️ O corte do Valor não é enfeite. Sem ele, a régua mataria a trava de comandos perigosos
desta casa: 6.109 passagens pelo portão do commit, para 2 bloqueios. Ela fica calada em 99,9%
das vezes porque comando perigoso é raro. Peça muda e barata é alarme de incêndio: fica.
(A régua vai junto de propósito: são as passagens pelo portão, não as execuções totais, que
naquele dia foram 6.324. Trocar um pelo outro é a mesma família de erro que a regra Conta o
pedaço persegue, e um revisor pegou este aqui.)
📚 O Google mede alarme por quatro atributos, e dois são iguais aos nossos. O Silencia é o
quinto, e nasceu de um buraco achado em 21/08: faltava medir quanto tempo o alarme fica tocando
depois que o problema acabou. Alarme vermelho permanente não é sinal, é parede — e é exatamente o
estado de um alarme desta casa hoje.
O número decide?Antes de escolher uma coisa porque ela teve a melhor nota, prove que a nota separa as duas.
Ordem sua, de 28/08: "este é um princípio primordial, testes robustos pra medição".
No dia-a-dia: duas balanças de farmácia, uma marcando 80,1 kg e outra 80,3. Você não
conclui que a segunda é mais pesada — a diferença cabe dentro do erro do aparelho. É preciso
saber de quanto o aparelho erra antes de acreditar na diferença.
O caso que a criou foi meu, e eu ia errar: pontuei sete réguas contra um gabarito e o placar
deu 66,7% · 63,3% · 63,3%. Escolhi a primeira. Quatro conferências depois, nenhuma
sustentava a escolha — a vantagem inteira era de 3 respostas certas, e uma delas até
inverteu quem ficou em primeiro.
| a pergunta, antes de escolher pelo número | o que ela reprova |
|---|---|
| quanto acerta quem só chuta a resposta mais comum? | ganhar pouco de quem chuta é ruído |
| a diferença cabe na margem de erro? | margens que se sobrepõem não separam nada |
| a nota está inflada porque quase tudo é da mesma classe? | numa prova em que 90% é passou, quem diz passou sempre acerta 90% |
| quem montou o gabarito? | se fui eu, e eu também escrevi as réguas, medi a minha própria cabeça |
⚠️ Ela NÃO manda fazer conta em toda medição. "5 casos em 1.481 arquivos" não precisa de
nada disso — ali não há duas opções disputando. Ela só acorda quando o número vai ESCOLHER.
O que ela custa: uns 10 minutos quando dispara, e às vezes não há amostra que baste. Aí a
saída honesta é dizer "não consegui distinguir" e escolher pelo critério de sempre — dizendo
que foi assim.
Como você vê que eu falhei nela: eu te apresentar um pódio cuja diferença é de um ou dois
casos.
Não medi"Não sei" é resposta. "Achei que" não é.
No dia-a-dia: o mecânico que diz "não testei o freio, não deu tempo" é honesto e você decide
o que fazer. O que diz "o freio deve estar bom" é o perigoso.
Não consegui medir? Escrevo "não medi X" e sigo. Proibido inventar um medidor caseiro para
produzir o número que eu quero, e proibido abrir investigação de meia hora por causa disso. Seguir
calado é que não vale.
⚠️ Cuidado que um revisor pegou: dizer "não medi" não me libera para construir. Sem esta
linha, eu escapava da regra Peça nova precisa de caso só dizendo que não tinha medido.
Três regras sobre o que eu deixo de dizer. Nenhuma delas é sobre código: são sobre o que
chega até você no fim.
RobustoTodo caminho de erro diz, escrito, o que faz quando dá problema: tranca, ou abre avisando o que
se perde.
No dia-a-dia: a catraca do prédio quando falta luz. Ela pode travar (ninguém entra, nem
morador) ou destravar (todo mundo entra, inclusive quem não devia). As duas são decisões
válidas. O inaceitável é ninguém saber qual delas o prédio escolheu.
Não existe "não vai quebrar". Existe "quando quebrar, faz isto" — e isto vai escrito na
própria linha, não na cabeça de quem construiu. Quando nada está escrito, o padrão é trancar.
📚 Lá fora chama-se padrão à prova de falha, e é de 1975. O que a nossa acrescenta: lá diz que
o padrão deve ser o estado seguro; aqui a escolha vai escrita, porque padrão implícito é
exatamente onde o "abre calado" se esconde.
⚠️ A própria trava de segurança desta casa tem 20 saídas de erro e nenhuma declaração — e duas
delas devolvem "nada encontrado", que ali significa o comando passa.
As três linhasToda entrega termina com três coisas, e a falta de qualquer uma reprova sozinha:
O QUE ELA NÃO PEGA .... um caso concreto que pode mesmo acontecer
O QUE ELA COBRA ....... um número medido, nunca um adjetivo
COMO SABER QUE FALHOU . o sintoma, e onde ele aparece
No dia-a-dia: a bula do remédio. Para que serve, a dose, e o efeito colateral. Remédio sem
bula funciona igual — até o dia que não funciona.
É a que mais some no aperto. Quando a resposta está longa e eu quero fechar, é a primeira coisa
que eu corto — e é justamente ela que impede a entrega de virar propaganda.
A palavraTrês palavras com sentido travado, ditas sem você perguntar.
| palavra | significa exatamente |
|---|---|
| Entregue | escrito, testado, no ar e conferido ao vivo — e ninguém tentou derrubar |
| Validado | o acima + revisado em laço até o árbitro dizer que o laço fechou para o alvo que você declarou (com o alvo em médio são DUAS passadas limpas seguidas, não uma — descer o alvo encarece a palavra) |
| Blindado | o acima + alguém independente tentou derrubar e não sobrou achado alto nem crítico EM ABERTO (+ mutação dirigida, se for obra de risco) + rodou em produção pelo menos uma vez |
No dia-a-dia: a diferença entre "montei o carro", "o mecânico conferiu" e "rodou 1.000 km
na estrada". As três são verdade; só a terceira você aposta a viagem.
Zero produção = Entregue, por mais teste e revisão que a obra tenha levado. Esta parte é rigor
acima do que a indústria costuma exigir, e nasceu de um caso concreto: usei Blindado numa peça
com 15 provas verdes e 3 revisores independentes — que nunca disparou uma vez sequer, por um
defeito conhecido do fornecedor.
A raiz, não o sintomaConsertar o que causou, não o que apareceu — e são TRÊS passos, não dois.
No dia-a-dia: o teto pinga. Passo 1: a telha está trincada. Passo 2: se eu trocar a
telha e continuar pingando, eu estava errado — e escrevo isso ANTES de trocar. Passo 3: o que
impede a trinca de voltar no ano que vem.
O passo 2 é o que separa causa de palpite. Causa que não tem jeito de estar errada é sintoma com
nome bonito.
O passo 3 é novo e nasceu de um buraco meu: eu explicava a causa lindamente e o aprendizado
morria no texto. Consertei três defeitos numa manhã e nenhum dos três virou proteção — viraram
parágrafo. O quarto lugar da mesma falha só não voltou porque um humano leu.
"Não deixei proteção, e o motivo é X" é resposta legítima. O que não vale é o silêncio, que se lê
como "está resolvido".
Está sangrando? — a exceção legítimaDefeito acontecendo neste minuto se conserta antes de qualquer discussão.
No dia-a-dia: a casa está alagando. Você fecha o registro primeiro e descobre depois qual cano
estourou. Discutir a causa com a água subindo é luxo.
Como sei que é sangramento de verdade: eu consigo nomear quem sofre e onde. "Pode
acontecer" é palpite; "a conversa X leva o commit errado" é fato. Não consigo apontar a vítima?
Então não está sangrando — e aí a causa vem primeiro, que é o caso normal.
📚 Lá fora chama-se estancar o sangramento e é a primeira regra de resposta a incidente do
Google: "você não está ajudando o usuário se o sistema morrer enquanto você investiga".
Sem defeito sintéticoA prova tem que exercitar a peça de verdade — e morrer junto com ela.
Os dois nomes são seus, e querem dizer coisas diferentes:
| o termo | o que é | como se pega |
|---|---|---|
| sintético | a peça "funciona" só porque o teste foi feito sob medida para ela | a prova fica verde com a peça vazia |
| latente | está errado, correto no papel, e só aparece no dia raro | o caminho nunca rodou de verdade |
No dia-a-dia: apertar o botão de teste do alarme de fumaça prova que a pilha tem carga. Não
prova que ele dispara com fumaça. São coisas diferentes, e só a segunda importa.
Como sei que cumpri: eu esvazio a peça e rodo a prova. Ficou verde? A prova não vale nada, e
é ela que precisa de conserto, não o código.
📚 Lá fora chama-se teste de mutação e defeito latente. O segundo é termo formal de teste de
software — você chegou nele sozinho.
(Escrito a pedido seu, em 2026-08-24: "achei bem interessante o conceito de saber apontar quando
está errado também, e não quando acerta apenas".)
A sua pergunta foi: se ele falha, então ele não acerta — não é a mesma coisa? Não é, e o motivo
é que existem duas formas OPOSTAS de um alarme quebrar — e cada teste sozinho é cego para uma:
| o detector está | testar "apita com fumaça?" | testar "fica quieto sem fumaça?" |
|---|---|---|
| apitando o tempo todo | ✅ passa — e engana você | ❌ pega |
| com a pilha arrancada | ❌ pega | ✅ passa — e engana você |
Um alarme que nunca dispara é perfeito no teste de não incomodar. Um que dispara sempre é perfeito
no teste de pegar ladrão. Por isso os dois testes existem, e por isso eles são pares — não
substitutos.
| o que é | a pergunta que ele faz | |
|---|---|---|
| 🐦 canário | o par de testes, sempre os DOIS juntos | "ele apita quando deve E cala quando não deve?" |
| 🧪 mutante | eu estrago a peça de propósito e rodo a prova | "se eu arrancar a pilha, alguém percebe?" |
A diferença em uma linha: o canário testa o alarme; o mutante testa quem vigia o alarme.
Mutante verde é o pior resultado da casa — quer dizer que a rede de provas era decoração, e isso
não aparece de nenhum outro jeito, porque enquanto não há fumaça ela parece perfeita.
⚠️ A regra que vale para qualquer medição, e ela custou caro em 24/08: quando eu for provar que
uma régua nova é melhor, a população de teste tem de incluir os casos em que a régua VELHA já
acertava. Sem esses, o teste só pode confirmar o que eu quero ouvir. Foi assim que um número
meu virou o contrário: medindo só as falhas, a régua nova parecia pior; incluindo os acertos,
apareceu que a antiga acusava 41% a 56% do trabalho BOM.
📁 Onde eles vivem aqui: os canários em .claude/canarios/*.json (um arquivo por guarda, com os
dois lados escritos); os mutantes em scripts/quality/mutation_check.py, que planta os defeitos e
conta quantos a rede matou. Quando um guarda não dá para testar dos dois lados, isso vai escrito
em .claude/canarios/_lacunas.json, com o motivo medido — lacuna declarada vale mais que canário
fingido.
O tetoQuando eu paro de tentar e devolvo a bola. Por número, nunca por cansaço.
No dia-a-dia: você não procura a chave perdida para sempre. Você decide antes: "olho a casa
inteira três vezes, e depois chamo o chaveiro". Sem o combinado, você vira a casa até de noite.
Paro no primeiro destes que acontecer, e digo qual foi, com o número:
| o que dispara | por que este número |
|---|---|
| 5 rodadas que ainda acharam coisa | parou de render. Rodada limpa não gasta — ela é progresso |
| 8 revisores na mesma obra | orçamento; commitar o que está pronto zera a conta |
| 3 tentativas falharam no mesmo ponto | é o desenho que está errado, não o código |
| a mesma família de defeito pela 3ª vez | é raiz — vale a regra A raiz |
Bater no teto sem chegar em Validado é resultado legítimo, não fracasso. Eu reporto assim:
"parei no teto tal, depois de tantas; cheguei em Entregue; falta X para Validado".
📚 Lá fora chamam-se regras de parada, e há literatura séria sobre isso na revisão de
documentos e de código — inclusive uma família de métodos que estima quantos defeitos ainda
faltam para decidir se compensa continuar.
⚠️ O teto não é permissão para parar antes. Ele só vale depois do laço ter rodado — parar na 1ª
rodada citando o teto é a violação que ele mais mata.
Cada vez que um revisor independente derruba um trabalho meu, ele anota qual regra estava escrita
e não me impediu. Isso vira uma ficha:
| coluna | significa |
|---|---|
| pegou | a regra achou defeito real |
| falso | ela apitou à toa |
| não protegeu | ela estava lá, eu li, e o defeito passou assim mesmo |
A terceira é a única que mede efeito em vez de obediência — e é por isso que ela vem do revisor,
não de mim. O placar vivo fica em .claude/skills/sweetspot/references/placar.md, e é gerado por
máquina.
O que a ficha já mostrou, e vale como regra geral: as regras que viram conta acertam quase
sempre; as que viram lembrete erram metade das vezes. Foi por isso que quatro delas passaram a
exigir a busca colada em vez de eu afirmar que procurei.
Esta seção existe porque o dono perguntou "está salvando todos achados, conclusões e
medições no.mdda skill?". A resposta honesta era meio: os 13 achados estavam no log
técnico (references/desempenho.md), e este guia — o que ele lê — não tinha nenhum.
Foram 6 investigações, 13 achados e 49 artefatos de medição em 7 pastas sob
.claude/medicoes/2026-08-28-*. Cada uma tem os dados crus e um analise.py que reproduz os
números. O que mudou no seu dia-a-dia está nas duas tabelas abaixo.
| a coisa | antes | agora | o número que decidiu |
|---|---|---|---|
| ideia nova sendo julgada | perdia por 6 pontos só por ter menos eixos medíveis | compara-se por média, não por soma — a nota volta a decidir | a regra de 27/08 piorou a desvantagem em 2 pontos (era 4) |
| quando escolher os critérios | podia ser depois de olhar as opções | antes, sempre | constructed criteria, Uhlmann & Cohen 2005 — 3 estudos, e comprometer-se antes eliminou o viés |
| o peso dos 6 eixos | igual, sem ninguém ter medido | continua igual, e agora se sabe que não deveria | 3 juízes cegos declararam garante 35 / alcança 25 / manter 20 / cabe 11 / instalar 9 — e a campeã de uma matriz real muda A → C |
| a afirmação | quem a fez | o veredito |
|---|---|---|
| "a ficha tem 0 matrizes de campo" | eu, publicado como o achado principal | ❌ são 7, em 4 arquivos — e duas delas decidiram NÃO construir |
| "o fiscal precisa aprender os 6 eixos" | eu, ia commitar | ❌ minha régua acusaria 232 linhas inocentes; a régua do conjunto acusa 20 blocos |
| "a casa já escolhe régua por contexto" | eu | ❌ são 3 eras; a régua é prevista pela data em 30 de 30 |
| "3 critérios não discriminam" | eu | ❌ veio de 3 matrizes didáticas; nas de campo eles variam |
"o falso foi registrado ontem" |
você | ❌ são 3, e o primeiro é de 24/08 (99002e6c3) |
| o que | por que não foi feito | o número |
|---|---|---|
| o fiscal não enxerga a ficha principal | minha 1ª régua reprovou na própria cláusula 10 | 0 de 6 eixos na lista dele |
| o peso dos eixos continua igual | mexer nele muda pódio de decisão passada — é porta de mão única | 1 matriz de 12 opções, N = 1 |
| a ficha nunca foi cobrada de ninguém | ela é doutrina, não guarda — vale ~90% | 71 notas registradas, 7 matrizes escritas |
⚠️ O que a medição NÃO alcançou, dito: ninguém mediu se você usa os 6 eixos de cabeça,
sem escrever a tabela. Todo número acima é sobre tabela escrita. Se você pontua mentalmente,
o "7 matrizes" subconta — e só você sabe disso.
Ordem do dono: "primeira etapa é ver se o teste é robusto pra realmente nos dar
resultados que façamos ver qual é o melhor". Ele está certo em pôr isso antes de mais
variações — sem isto, cada variação nova produz um número que não decide nada.
Reconferir: python .claude/medicoes/2026-08-28-variacoes-da-regua/robustez.py
| # | a prova | o resultado |
|---|---|---|
| 1 | kappa de Cohen (concordância descontado o acaso) | melhor = 0,432 — faixa "ajustável"; nenhuma chega a 0,60 |
| 2 | baseline: chutar sempre a classe majoritária | 56,7% contra 66,7% da melhor — ganho de 3 perguntas |
| 3 | McNemar entre as três melhores | p = 1,000 — elas são estatisticamente empatadas |
| 4 | validação cruzada de 5 dobras, 70 perguntas | inverte a ordem: a que perdia passa a liderar |
A acurácia crua mente. Com D valendo 56,7% da amostra, uma régua que só chutasse D
acertaria 56,7% com kappa zero. É o exemplo canônico da literatura: "se 90% das saídas são
pass, um juiz que sempre diz pass acerta 90% e tem kappa perto de zero".
Escolher entre as três melhores é ruído. McNemar entre V5 e V7 dá p = 1,000 — a
diferença de uma pergunta não distingue nada. O que o teste distingue de verdade são as
três piores (p ≈ 0,012).
⚠️ E a validação cruzada INVERTE o pódio:
| variação | validação simples (30) | 5 dobras (70) | desvio entre dobras |
|---|---|---|---|
| V5 cascata | ✅ 66,7% | 71,4% | 10,1 |
| V6 | 63,3% | 78,6% | 9,0 |
| V7 | ❌ 63,3% | ✅ 84,3% | ✅ 5,3 — a mais estável |
⚠️ Mas esse resultado está CONTAMINADO e vai dito: as 5 dobras incluem as 40 perguntas em
que a V7 foi ajustada. Ela leva vantagem por construção. A validação cruzada limpa exigiria
reajustar a régua dentro de cada dobra — não foi feito.
Logo o pódio continua indefinido PELA ACURÁCIA, e essa é a resposta honesta.
⚠️ Isto foi resolvido na 2ª rodada, e por outro caminho: o pódio fica definido quando se
pesa o custo do erro em vez de contar acertos — porque as duas colocadas erram em direções
opostas, e só uma delas é cara. A seção O ACHADO QUE INVERTE A CONCLUSÃO, mais abaixo, traz
o número. A frase acima segue valendo para o que ela mede: pela acurácia crua, empate.
A pergunta que veio antes de ampliar foi: quanto seria preciso ampliar? A conta do
poder estatístico respondeu, e a resposta encerra o caminho.
| o que | número |
|---|---|
| perguntas pareadas para distinguir a 1ª da 2ª colocada | 1.164 |
| perguntas por grupo, se fossem medições independentes | 3.213 |
| perguntas distintas que existem no acervo inteiro | ❌ 632 |
⚠️ Classificar TODA pergunta que já existiu nesta casa daria pouco mais da metade do
necessário. Não é questão de tempo nem de disposição: a população não comporta a
medição. Recontar leva 5 s — varrer AskUserQuestion nos 4.774 transcritos.
Por que o número explode: o teste pareado só olha os casos em que uma régua acerta e a
outra erra. Entre a 1ª e a 2ª, isso acontece em 16,7% das perguntas — cinco casos, com
placar 3×2. Cinco casos não sustentam decisão nenhuma, e mais amostra só chega lá devagar.
✅ As duas piores, sim, são distinguíveis — bastam 25 perguntas para separar a melhor
da V1 palavra solta. O teste distingue bom de ruim; ele não distingue bom de bom.
Embaralhei o gabarito 10.000 vezes e comparei com o acerto observado.
| variação | observado | com o gabarito embaralhado | p |
veredito |
|---|---|---|---|---|
| V5 cascata completa | 66,7% | 41,2% | 0,0015 | ✅ lê a pergunta |
| V6 · V7 | 63,3% | 37,6% | 0,0010 | ✅ lê a pergunta |
| V3 pronome primeiro | 50,0% | 31,8% | 0,0098 | ✅ lê a pergunta |
| V1 · V2 | 36,7% | 23,4% | 0,0249 | ✅ lê a pergunta |
| V4 pelas opções | 40,0% | 28,2% | 0,0635 | ❌ não distinguível do acaso |
Isto é o teste do vazio aplicado à estatística: se embaralhar o gabarito produzisse o
mesmo acerto, a régua não estaria lendo nada. Seis das sete leem. A V4 — a única que
julga pelas opções em vez da pergunta — é a que não passa, e isso é coerente: as opções
são escritas por mim, não pelo dono.
⚠️ Dois fatos só apareceram na matriz de confusão, e os dois mudam o resultado.
Ela devolve ? quando nenhuma marca casa. Abster não é errar: abster é perguntar, que é
o comportamento seguro e o padrão de hoje.
| variação | cobertura | acerto global | acerto quando opina |
|---|---|---|---|
| V5 cascata completa | 80,0% | 66,7% | ✅ 83,3% [64..93] |
| V6 · V7 | 76,7% | 63,3% | 82,6% [63..93] |
| V3 pronome primeiro | 63,3% | 50,0% | 78,9% [57..91] |
| V1 · V2 | 50,0% | 36,7% | 73,3% [48..89] |
A melhor régua acerta 5 de cada 6 vezes em que abre a boca — e cala nas outras. O 66,7%
que eu vinha reportando somava as duas coisas.
| erro | o que acontece na prática | custo |
|---|---|---|
D virou R |
eu decido sozinho o que era seu | ❌ ALTO — é o passo 4 do protocolo violado |
R virou D |
pergunto o que a régua já resolvia | ⚠️ baixo — uma interrupção |
virou ? |
pergunto por não saber | ✅ zero — é o padrão de hoje |
⚠️ O peso 10× foi declarado ANTES de rodar — é a cláusula 18 aplicada a si mesma
(constructed criteria). E a conclusão é conferida contra pesos de 3 a 30: ela não muda.
| variação | erro CARO | erro barato | custo | resolve |
|---|---|---|---|---|
| ✅ V5 cascata completa | 1 | 3 | 13 | 24/30 |
| V3 pronome primeiro | 1 | 3 | 13 | 19/30 |
| V1 · V2 | 2 | 2 | 22 | 15/30 |
| ❌ V6 · V7 | 3 | 1 | 31 | 23/30 |
| V4 pelas opções | 4 | 2 | 42 | 18/30 |
⛔ A V6 e a V7 erram TRÊS VEZES MAIS no lado caro. Pela acurácia global elas estavam
empatadas com a V5 (p = 1,000, indistinguíveis). Pelo custo real, a distância é de 2,4×
— e ela aparece na direção que importa: elas decidem o que é do dono.
Desempate declarado: custo igual → ganha quem responde mais. É como a V5 passa à
frente da V3: mesmo erro, mais perguntas resolvidas.
V5 cascata completaEla ganha em todos os pesos de 3 a 30, tem a maior cobertura, o menor erro caro e o maior
acerto quando opina. E — o que importa mais que o pódio — os consertos que a "melhoraram"
(V6, V7) pioraram exatamente o que não podia piorar.
⚠️ O que isto NÃO diz: que a V5 serve como mecanismo. Ela ainda deixa 1 decisão do
dono passar como minha em 30, e o protocolo dos 5 passos continua sendo julgamento meu.
A recomendação de não construir guarda sobre classificação por palavra FICA de pé.
A 1ª versão desta medição tinha duas tabelas se contradizendo: a principal dava V5 em
1º, a de sensibilidade dava V3 — porque só a primeira aplicava o desempate. Consertado, e
a cicatriz fica escrita no código: "ordenar aqui por outro critério faz as duas tabelas se
contradizerem".
O item 1 da lista "o que falta" era um segundo avaliador, porque eu escrevi as réguas E o
gabarito. Eu tinha escrito que custaria um agente cego. Custa zero: a resposta de cada
pergunta está gravada no transcrito, ao lado dela.
AskUserQuestion com resposta (bruto) |
2.984 |
| pares (pergunta, resposta) distintos | 947 (fator de repetição: 3,15×) |
fora da conta ([No preference], vazia) |
18 |
| dentro da conta | 929 |
| ⛔ ele mandou eu aplicar a régua | 206 — 22,2% |
| ✅ ele exerceu escolha | 723 — 77,8% |
⛔ Uma em cada 4,5 perguntas minhas não precisava existir — e não é interpretação minha,
são as palavras dele, gravadas.
⚠️ Ela mede coisa DIFERENTE dos números anteriores, e tratá-las como concorrentes seria o
erro que esta obra existe para medir:
| medida | o que olha | o que responde | valor |
|---|---|---|---|
9,5% |
a pergunta, por palavra | quantas eu não podia resolver | 9,5% |
30,0% · 56,7% |
a pergunta, meu gabarito à mão | idem, lendo uma a uma | 30% e 56,7% |
22,2% |
a RESPOSTA dele | quantas ele disse que eu devia ter resolvido | 22,2% |
2,3%, e o número real é 10× maiorA 1ª régua devolveu 20 casos. Só fui olhar o outro eixo porque a doutrina da casa obriga:
"falso positivo e falso negativo são dois eixos; medir só um garante escolher errado quando o
defeito mora no outro".
| eixo | o que a auditoria à mão achou |
|---|---|
| falso positivo | 6 de 20 — 30%: instrução de RITMO ("sem pausas", "vou dormir") não é delegação de DECISÃO |
| falso negativo | 8 de 40 — 20%: ⛔ a forma mais comum de delegação, invisível para a régua |
⚠️ A forma que faltava é a mais frequente de todas: ele não diz "você decide" — ele
responde com os CRITÉRIOS. caminho sweetspot e robusta · Robusto, nível sênior, foco na
raiz · e na forma mais curta possível, /sweetspot. Isso é o mesmo que dizer "aplique a
régua", e não tem uma palavra de delegação dentro.
Corrigidos os dois eixos: 20 → 206. A régua nova foi auditada de novo, 25 de cada lado:
zero falso positivo claro, zero falso negativo. E o 206 não se move quando o corte de
texto varia de 10 a 200 caracteres.
⚠️ Um terceiro defeito, e ele é do meu próprio TESTE: a prova do [No preference] usava a
fala pura — 15 caracteres, abaixo do mínimo de instrução —, então ela caía na gaveta certa
pelo caminho errado. Apagar a regra inteira deixava a prova verde. Pego por mutação;
hoje as falas de teste são longas de propósito, com um assert exigindo isso.
O protocolo dos 5 passos já previa isto, no passo 1: "pontuo as opções pelas cláusulas
ANTES de cogitar perguntar". Se eu tivesse feito isso, 206 perguntas não teriam existido.
⚠️ E o passo 4 continua intacto: deploy, produção, dinheiro, preferência dele e rumo da
obra param e esperam. Os 77,8% em que ele exerceu escolha provam que perguntar não é o
defeito — perguntar o que a régua já resolve é.
⚠️ Isto NÃO autoriza construir guarda. A régua lê a RESPOSTA: ela só sabe que eu errei
depois de eu já ter perguntado. O valor é medir a taxa, não impedir a pergunta. E a
recomendação de não classificar a PERGUNTA por palavra continua valendo.
Detalhe completo: .claude/medicoes/2026-08-28-variacoes-da-regua/ACHADO-O-DONO-JA-RESPONDIA.md
python .claude/medicoes/2026-08-28-variacoes-da-regua/gabarito_do_dono.py
0,147 — a rubrica é AMBÍGUACom o 2º avaliador em mãos, a pergunta que a obra inteira perseguia finalmente se
responde: o meu gabarito mede a realidade, ou mede a minha cabeça?
| perguntas da amostra com resposta gravada dele | 50 de 70 |
| concordância crua | 56,0% |
| esperada por acaso | 48,4% |
| KAPPA | ⛔ 0,147 |
O piso de "ajustável" é 0,40. Descontado o acaso, sobra quase nada.
⚠️ O que este número NÃO prova, e vai dito: os dois avaliadores respondem perguntas
diferentes — o meu gabarito responde "quem DEVIA decidir?", o dele responde "quem ele
MANDOU decidir?". Kappa baixo entre perguntas diferentes é esperado.
✅ O que ele PROVA, e isto basta: a minha classificação não prediz o comportamento
dele — e era exatamente por essa predição que eu ia escolher a régua.
EU \ ELE D R
D 18 5
R 14 6
Em 14 de 50 casos (28%) eu classifiquei como a régua decide — e ele exerceu escolha de
verdade. Se eu tivesse obedecido o meu próprio gabarito, teria decidido sozinho 14 coisas
que ele quis decidir.
É o erro CARO da cláusula do custo assimétrico, e ele é 2,8× mais frequente que o barato.
⚠️ Ressalva declarada: "ele exerceu escolha" não prova que ele PRECISAVA escolher —
pode ter respondido só porque eu perguntei (preferência revelada superestima a
necessidade). Mesmo assim, 28% é grande demais para vir todo de cortesia.
| o que | veredito |
|---|---|
| classificar a pergunta por palavra | ❌ morto — 66,7% com sobreajuste, kappa ≤ 0,43 |
| escolher entre as réguas por acurácia | ❌ morto — precisaria de 1.164 perguntas e existem 632 |
| escolher entre as réguas por custo assimétrico | ✅ decide (V5), e é robusto de peso 3 a 30 |
| o meu gabarito como referência | ⛔ kappa 0,147 contra o avaliador externo |
| o passo 4 do protocolo (produção, dinheiro, preferência, rumo) | ✅ intacto, e reforçado — o erro caro é 2,8× o barato |
| os 5 passos como julgamento meu (~90%) | ✅ segue — nenhuma automação os substitui |
python .claude/medicoes/2026-08-28-variacoes-da-regua/kappa_entre_avaliadores.py
Duas provas CONGELAM este veredito: se o kappa subir de 0,40 ou o erro caro inverter,
elas ficam vermelhas e mandam remedir antes de citar qualquer número desta página.
Saber que 22,2% das perguntas não precisavam existir não diz o que fazer. Recortando por
tema, dá:
| tema | delegou | total | taxa | |
|---|---|---|---|---|
| medir / investigar | 9 | 22 | 40,9% | ⛔ acima |
| conserto / defeito | 28 | 81 | 34,6% | ⛔ acima |
| prioridade / por onde | 20 | 68 | 29,4% | |
| doutrina / regra | 12 | 43 | 27,9% | |
| formato / apresentação | 19 | 72 | 26,4% | |
| construir peça / trava | 39 | 156 | 25,0% | |
| card / fila | 10 | 58 | 17,2% | |
| produção / deploy | 9 | 96 | ✅ 9,4% | ✅ bem abaixo |
(média geral 22,2%; temas com menos de 15 perguntas ficam de fora — não sustentam percentual)
✅ produção / deploy é o tema em que ele MENOS delega — menos da metade da média. É
evidência empírica, vinda do comportamento dele, de que o passo 4 está certo: deploy,
dinheiro e produção param e esperam.
⛔ E o que ele mais delega é decisão TÉCNICA — medir e consertar. É ali que a régua da casa
tem de decidir sozinha.
| fatia | período | taxa | intervalo de 95% |
|---|---|---|---|
| 1 | 20/07 a 04/08 | 10,8% | [ 7,1..16,1] |
| 2 | 04/08 a 11/08 | 31,9% | [25,6..38,9] |
| 3 | 11/08 a 16/08 | 18,4% | [13,5..24,6] |
| 4 | 16/08 a 23/08 | 20,5% | [15,3..26,9] |
| 5 | 23/08 a 28/08 | 29,1% | [23,1..35,9] |
⛔ Erro 1, que eu escrevi antes de conferir: "SUBINDO, está PIORANDO", comparando só a 1ª
com a última. A curva NÃO é monotônica — 3 subidas e 1 descida. É um degrau seguido de
platô oscilante, não uma trajetória. A leitura honesta: a 1ª fatia é separada das demais; do
resto não dá para dizer se melhora ou piora, e afirmar qualquer das duas é achismo.
⛔ Erro 2: a explicação alternativa óbvia era "ele passou a usar a palavra nova".
Conferida e descartada: a skill sweetspot nasceu em 19-20/08 (fatia 4) e o pico é da
fatia 2 (04-11/08) — antes dela existir.
Escrevi "99,0% das delegações citam sweetspot". É circular: a régua usa essa
palavra para classificar como delegação, então o resultado é ~100% por construção — mede a
régua, não o comportamento dele.
A pergunta honesta ancora no OUTRO caminho de classificação: das 16 delegações achadas
por "vc decide" — que não menciona a palavra —, 8 a citam assim mesmo: 50,0%. E 46
respostas são só a palavra e nada mais.
python .claude/medicoes/2026-08-28-variacoes-da-regua/estou_melhorando.py
| # | o que falta | por que | estado |
|---|---|---|---|
| ~~1~~ | ~~2º avaliador cego~~ | ✅ feito, e de graça — é a resposta dele, 947 pares | ✅ feito |
| ~~2~~ | ~~kappa entre avaliadores~~ | ⛔ feito, e REPROVOU: 0,147 | ✅ feito |
| ~~3~~ | ~~curva de aprendizado~~ | ⚠️ feita, e é INDECIDÍVEL — degrau, não tendência | ✅ feito |
| ~~4~~ | ~~recorte por tema~~ | ✅ feito, e validou o passo 4 | ✅ feito |
| 5 | Krippendorff alpha | generaliza o kappa para 3+ avaliadores | ⚠️ o único que ainda vale |
| 3 | ~~amostra maior~~ | ⛔ morto: precisaria de 1.164 e existem 632 | ❌ impossível |
| 4 | ~~curva de aprendizado~~ | ✅ substituída pelo cálculo de poder, que é mais direto | ✅ feito |
| 5 | ~~teste de permutação~~ | ✅ 6 das 7 réguas leem a pergunta | ✅ feito |
| 6 | ~~matriz de confusão~~ | ✅ foi ela que achou a abstenção e a assimetria | ✅ feito |
| 7 | ~~bootstrap × Wilson~~ | ✅ os dois intervalos concordam — o n pequeno não distorceu |
✅ feito |
| 8 | ~~baseline estratificado~~ | ✅ 50,8% (contra 56,7% do majoritário) | ✅ feito |
| 9 | calibração em conjunto congelado | re-medir e alertar se cair 2 erros-padrão | não rodado |
Como reconferir tudo — 2 comandos:
python .claude/medicoes/2026-08-28-variacoes-da-regua/poder.py
python .claude/medicoes/2026-08-28-variacoes-da-regua/abstencao.py
O QUE ELE NÃO PEGA — ele não me obriga a nada. É texto, e texto tem cerca de 90% de adesão
contra quase 100% de uma trava de verdade. Algumas regras têm trava atrás: O teto, A
palavra, Sem defeito sintético e Caminho único têm um guarda que barra o trabalho quando eu
desobedeço. Quantas das 18 têm trava eu não contei — e deixar isso escrito, em vez de chutar um
número, é a regra Não medi sendo aplicada a este próprio guia.
O QUE ELE COBRA — nada no seu dia. Ele é lido por você quando você quiser, e por mim quando eu
abrir a régua completa.
COMO SABER QUE FALHOU — você lê uma resposta minha, vê um número solto sem a régua ao lado, ou
uma entrega sem a palavra de fecho, e precisa perguntar "isso está validado mesmo?". A pergunta
é o sintoma.
Medido em 2026-08-11 08:00 UTC · a anotação mais recente tem 0.0 dia(s).
Gerado sozinho a cada commit. Não edite à mão — suas edições serão sobrescritas.
| parte | degrau | por quê | idade | anotações | |
|---|---|---|---|---|---|
| 🟡 | Banco de dados | mexido | a prova (blindado) é anterior ao último commit — envelheceu | 1d | 4 |
| 🟡 | Configuração do projeto | mexido | código mudou, nenhuma prova anotada | 0d | 11 |
| 🟡 | Conversa robô↔servidor | mexido | código mudou, nenhuma prova anotada | 0d | 3 |
| 🟡 | Documentação | mexido | código mudou, nenhuma prova anotada | 0d | 105 |
| 🟡 | Endpoints do servidor | mexido | a prova (blindado) é anterior ao último commit — envelheceu | 1d | 9 |
| 🟡 | Login e acesso | mexido | código mudou, nenhuma prova anotada | 0d | 1 |
| 🟡 | Motor de hedge (AF) | mexido | a prova (blindado) é anterior ao último commit — envelheceu | 1d | 4 |
| 🟡 | Núcleo do servidor | mexido | a prova (blindado) é anterior ao último commit — envelheceu | 1d | 7 |
| 🟡 | Rede de testes da tela | mexido | código mudou, nenhuma prova anotada | 1d | 23 |
| 🟡 | Rede de testes do servidor | mexido | a prova (blindado) é anterior ao último commit — envelheceu | 1d | 24 |
| 🟡 | Resto da tela | mexido | código mudou, nenhuma prova anotada | 0d | 43 |
| 🟡 | Sem tema mapeado | mexido | código mudou, nenhuma prova anotada | 0d | 1 |
| 🟡 | Simulador E2E | mexido | a prova (blindado) é anterior ao último commit — envelheceu | 1d | 5 |
| 🟡 | Tela da bancada do robô | mexido | código mudou, nenhuma prova anotada | 2d | 2 |
| 🟡 | Tela de contas | mexido | código mudou, nenhuma prova anotada | 2d | 1 |
| 🟡 | Tela do hedge | mexido | código mudou, nenhuma prova anotada | 1d | 11 |
| 🔵 | Ferramentas de trabalho | testado | portão do commit: testes passaram em scripts/git/commitar.py, scripts/tests/test_commitar.py | 0d | 205 |
| degrau | quer dizer | |
|---|---|---|
| ⬜ | sem sinal | nada foi anotado — não confunda com 'está ruim', é ausência de informação |
| 🟡 | mexido | o código andou e nenhuma prova recente cobre o que ele é hoje |
| 🔵 | testado | o portão do commit rodou os testes desta parte e eles passaram |
| 🟢 | validado | passou pelo laço de validação sem achado alto nem crítico |
| 🛡️ | blindado | o acima + um cético independente tentou derrubar e não conseguiu |
O degrau cai sozinho quando o código é tocado depois da prova, ou quando a prova passa de 30 dias.
Nenhum degrau é digitado à mão — todos vêm de anotação com nome de parte.
Status: ACTIVE | Criado: 2026-07-19 (S-atual) | Documento vivo — atualizar selos a cada fase
entregue e promover itens do censo a seção completa (card #568 do Roadmap acompanha).
Fontes: ADR 0029 (a ata oficial da decisão de arquitetura do Portão Único),
specs/identidade-terminal-portao-unico.md,specs/superficie-comunicacao-e-unificacao.md.
Este é o atlas dos pontos únicos do sistema: cada seção mostra um lugar onde antes havia
N cópias espalhadas e hoje (ou em breve) existe UMA fonte de verdade. Este mapa é o índice —
o detalhe fundo vive nos outros livros desta estante (links ao longo do texto).
Como ler os selos: ✅ no ar (te protege hoje) · 🔨 em obra (sendo construído agora) ·
📋 desenho aprovado (decidido, ainda não construído).
Pensa num prédio. A chave que o robô carrega é o crachá: prova que a máquina é
conhecida e diz em qual ala ela entra (o "mundo": real, simulação, stress ou bancada de
dev). O rosto — número do login MT5 + servidor da corretora — escolhe a sala (a conta)
dentro da ala. A chave nunca escolhe a sala.
O caminho, passo a passo (fluxo-alvo, ADR 0029):
O que cada credencial pode — e não pode:
| Credencial | No prédio | Decide | Nunca decide |
|---|---|---|---|
| A chave (no arquivo da máquina) | Crachá + seletor de ala | "Pode entrar?" e "em qual mundo?" | Qual conta é |
| O rosto (login MT5 + servidor) | A cara de quem entra na sala | Qual conta, dentro da ala | Em qual mundo está |
Este fluxo é o desenho aprovado do sistema (ADR 0029, 19/jul). Parte dele já vale hoje;
o resto está entrando por fases — a régua de progresso está na seção 2 abaixo.
O problema que essa reforma mata: cada canal de entrada do servidor resolvia sozinho "de qual
conta é esse pedido?". Onde há N porteiros improvisados, sempre sobra um que confere errado.
Antes — cada canal com seu porteiro (fragmentado): quatro portas laterais, cada uma com
critério próprio — a conexão instantânea (WebSocket) e a consulta periódica (Poll HTTP)
resolviam por conta própria; o batimento decidia pela CHAVE (ignorando a corretora) e o
cadastro decidia pelo NÚMERO solto (idem). Agravantes: DOIS classificadores de "mundo" que
discordavam entre si, e o guarda-de-código tinha uma lista de exceção que desligava o alarme
justamente nos infratores. Foi essa classe de brecha que gerou o incidente 83832 — a chave
escolheu a sala errada.
Depois — um portão na borda (unificado): todas as portas dão no mesmo saguão. O portão
(resolve_identidade) confere o crachá, lê a ala numa coluna guardada no banco (não numa
string deduzida) e acha a sala pelo rosto, DENTRO da ala. Sai um objeto pronto —
Identidade { terminal · mundo · conta } — e todo canal só consome; ninguém re-resolve.
Blindagens estruturais: índice único por mundo (cruzar de ala é impossível por construção,
não por disciplina) e um guardião que quebra o build se qualquer canal tentar resolver
pela chave fora do portão — sem lista de exceção.
Antes, a segurança dependia de cada canal LEMBRAR de conferir o rosto; depois, o canal não
tem como esquecer — ele nem resolve, só recebe a identidade pronta. É a diferença entre
disciplina e estrutura.
Régua das fases — o que te protege hoje vs o que vem:
| Fase | O que entrega | Status |
|---|---|---|
| F0 | O "mundo" vira coluna no banco (com trava de valores) + preenchimento das contas existentes. Invisível pra frota. | ✅ no ar |
| F1 | Portão roda em sombra: decide em paralelo e mede divergência — só avança com divergência ZERO na frota real. | ✅ no ar (sombra; divergência ZERO medida) |
| F2 | Liga o portão canal por canal (cada um atrás de um interruptor reversível). Fecha os 3 buracos achados na auditoria. | 🔨 construída (flag OFF) — aguarda revisão + deploy do dono |
| F3 | A bancada de simulação ganha rosto próprio — e morre o último desvio ("na dúvida, decide pela chave") que só ela usava. | 📋 aprovado |
| F4 | Faxina final: consolidar os índices e aposentar de vez a chave gravada na conta. Adiada de propósito (reversibilidade primeiro). | 📋 adiada |
Honestidade do mapa: a reforma anterior já fez os canais principais conferirem o rosto — o
que sobrou são 3 cantos (achados por uma auditoria com 13 revisores independentes) que
ainda resolvem pela chave/número. A F1 só mede, sem mexer; quem fecha esses cantos de verdade
são as fases F2 e F3.
Toda ordem nasce num balcão único (create_signal_canonical): o sinal, o pendente, o vínculo
e o recibo são criados juntos, num ato só — ninguém monta ordem "na mão" em outro canto do
sistema. Livro fundo: Carteiros.
O mesmo pacote de dados (_build_ws_payload) viaja pela conexão instantânea (WebSocket) E
pelo poll HTTP — um molde só para as duas rotas, impossível os canais divergirem no conteúdo.
Livro fundo: Carteiros, seção do Entregador.
Um único caminho de publicação (deploy-canonico.sh → porteiro na VPS, o servidor alugado,
que confere, sincroniza, reinicia e testa) — e as versões novas do robô chegam sozinhas nas
contas (auto-catchup). Livro fundo: Servidor.
Real, simulação, stress e bancada de dev são um EIXO de primeira classe do sistema. A bancada
de teste não é exceção nem gambiarra: é só um valor não-real desse eixo, isolado por
construção. Livro fundo: Camadas Pool/Sim.
Cada linha é um lugar onde N cópias viraram UMA fonte de verdade. Qualquer linha pode ser
promovida a seção completa deste mapa depois. Censo de 19/jul.
| Mecanismo | O que virou ponto único | Status |
|---|---|---|
| Carteiro (criação) | Toda ordem nasce num balcão só (create_signal_canonical): sinal + pendente + vínculo + recibo num ato atômico. Migração concluída em mai/26. |
✅ no ar |
| Conferente (recibo) | Quando o robô responde "executei / falhei", UM conferidor (apply_ack_canonical) processa o recibo — antes cada rota conferia do seu jeito. |
✅ no ar |
| Plantonista (batimento) | O "estou vivo" do robô passa por UM plantonista (apply_heartbeat_canonical), seja pela conexão instantânea ou pelo poll. |
✅ no ar |
| Entregador (pacote único) | O mesmo pacote de dados (_build_ws_payload) vai pela conexão instantânea E pelo poll — um molde, duas rotas. |
✅ no ar |
| Porta única no robô | Dentro do EA, a recepção de sinal tinha DUAS cópias (instantânea e poll) — e só uma armava a proteção. Virou UMA recepção; a classe do bug "perna nua" morreu. | ✅ no ar |
| Deploy canônico + versão única | Um caminho só de publicação, e a versão exibida em todo lugar sai de UMA fonte que sobe a cada deploy (antes: número cravado em 4 lugares, congelado). | ✅ no ar |
| Métricas com linhagem | Cada trade carrega um número de jornada (journey_id) e as métricas saem de UM cérebro — antes, telas diferentes calculavam números que divergiam. |
✅ no ar |
| Simulador herda o painel real | As métricas mastigadas viraram patrimônio do caminho principal (Real Path); o painel paralelo do SOAK foi absorvido e removido. | ✅ no ar |
| Vigia de saúde | O healthcheck que mentia (exames mudos há meses) virou UM vigia honesto + UMA aba Saúde no site. | ✅ no ar |
| Roadmap = TODOs | A lista de tarefas vive numa tabela só no banco; a aba Roadmap e o arquivo local são espelhos gerados — ninguém edita cópia à mão. | ✅ no ar |
| Avisos em português claro | Toda mensagem de erro que chega em você passa por UM tradutor (user_messages.py) — jargão técnico cru é barrado por guarda automática. |
✅ no ar |
| Duas chaves do robô | Chave de entrada (limitada, só cadastro) vs chave por-conta (opera) — papel de cada uma definido num desenho só, trava ligada na frota desde mai/26. | ✅ no ar |
| Catraca de teste na porta | Um teste-guarda varre TODAS as portas HTTP do servidor e quebra o build se alguma nascer sem tranca — esquecimento virou impossível (6 furos históricos fechados). | ✅ no ar |
| Sininho filtra por mundo | Todo aviso nasce com etiqueta do mundo da conta, e o sino do site filtra na entrada — cenário de simulação não dispara alarme vermelho falso. O Telegram já filtrava; o site passou a filtrar também. | ✅ no ar |
Cada item nasceu de uma análise de divergência (drift: duas versões da mesma coisa se
desalinhando a cada mudança). Item entregue sobe pro censo; item novo achado em auditoria
entra aqui.
| Candidato | A divergência de hoje | Estado |
|---|---|---|
| Portão Único de identidade | 3 cantos ainda resolvem conta por chave/número. A fase 1 JÁ RODA em sombra na produção (divergência ZERO medida); a fase 2 (ligar canal a canal) está construída atrás de interruptor desligado, aguardando revisão. É a régua da seção 2. | 🔨 em obra (F1 no ar em sombra) |
| Catraca única de entrada (nega-por-padrão) | Hoje cada porta do servidor tranca sozinha (funciona, com a guarda de teste como rede). O desenho final é UMA catraca que barra tudo que não foi liberado. Adiada de propósito: a guarda já entrega ~90% do benefício com risco quase zero. | 📋 desenho pronto |
| Piso de fechamento | DOIS temporizadores concorrentes decidem "já posso fechar a posição?" — o desenho aprovado colapsa num piso único com precedência firma > pool > global. | 📋 aprovado |
| Situação única da conta | A conta codifica estado em ~8 interruptores soltos e 3 telas separadas de "precisa de você". O desenho aprovado: UMA etiqueta de situação calculada + UMA caixa de entrada de decisões. | 📋 aprovado |
Status: ACTIVE | Ultima revisao: S194 (2026-04-06) — property tests atualizados (test_risk_engine.py), skills /rapid + /triage adicionadas (S192)
SSoT para: Scripts de qualidade, testes automaticos, monitoramento em producao
Relacionados:specs/debugging.md,specs/sweetspot-pilares.md
Sistema de 3 camadas que previne e detecta problemas automaticamente:
1. Monitoramento — roda 24/7 na VPS, avisa no Telegram
2. Testes preventivos — roda antes de deployar, encontra bugs antes de irem pra producao
3. Testes de resiliencia — simula falhas pra ver se o servidor aguenta
scripts/quality/
quality_monitor.py <- Roda na VPS a cada 15min (cron)
(watchdog.py APOSENTADO S433 -> dobrado no vigia-mutuo in-server, health_checks.py)
invariant_check.py <- Versao standalone dos invariantes (backup)
reconcile.py <- Versao standalone da reconciliacao (backup)
fault_injection.py <- Simula falhas (rodar manualmente)
.state/ <- Arquivos de estado (heartbeat, dedup)
tests/property/
test_pure_functions.py <- 30 testes: volume, P&L, NaN, market hours
test_risk_engine.py <- 22 testes: risk, daily DD, OSB, dist/cush
.claude/hooks/
pre-deploy-quality.sh <- Hook: roda testes antes de SCP pro VPS
A cada 15 minutos, roda 9 queries SQL no banco verificando regras que nunca devem ser quebradas. Se alguma quebrar, manda Telegram.
*/15 * * * * quality_monitor.py (9 checks)
# watchdog.py APOSENTADO S433 -> dobrado no vigia-mutuo in-server (health_checks.py, 5min)
| # | Check | Severidade | Quando roda | O que verifica |
|---|---|---|---|---|
| 1 | same_sign_pnl | CRITICAL | Sempre | Ambos lados do hedge com mesmo sinal P&L |
| 2 | duplicate_af_trades | CRITICAL | Sempre | Ticket duplicado (lucro contado 2x) |
| 3 | net_exceeds_risk | HIGH | Sempre | Custo hedge > risco configurado |
| 4 | stale_heartbeat | HIGH (sem Telegram**) | Sempre* | EA sem heartbeat (15min dia util, 30min FDS) |
| 5 | phantom_positions | HIGH | Mercado aberto | DB diz posicao aberta, EA nao ve |
| 6 | signal_without_ack | HIGH | Mercado aberto | Signal enviado mas ninguem respondeu |
| 7 | ghost_positions | MEDIUM | Mercado aberto | EA ve posicao que DB nao conhece |
| 8 | orphan_trade_links | MEDIUM | Sempre | Trade links abertos ha 24h+ |
| 9 | pnl_suspect_unresolved | MEDIUM | Sempre | Pares suspeitos sem atencao ha 12h |
*Check 4 (stale_heartbeat) roda sempre mas com tolerancia adaptativa:
- Dia util (mercado aberto): alerta apos 15 minutos sem heartbeat
- FDS/fora de horario: alerta apos 30 minutos (cobre restarts de PC)
Check 4 nao manda Telegram (decisao do dono, 2026-07-30). Motivo: EA offline
JA e' avisado pelo vigia de saude (scripts/vps/healthcheck_v2.sh --full), com
nome da conta, ha quanto tempo e desde quando. Este check repetia a MESMA noticia
15min depois como stale_heartbeat: 4x — sem nome, sem tempo. Ele continua
rodando normalmente: aparece no log, no arquivo de batimento e e' consultavel por
/saude no Telegram; so o alerta duplicado sumiu. A severidade continua HIGH de
proposito — rebaixar pra MEDIUM so pra conseguir silencio seria mentir sobre a
gravidade. O silencio e' declarado no campo telegram: False do catalogo, e
send_alerts() o respeita. Rede: server/tests/test_saude_sob_demanda.py.
telegram: False no catalogo (hoje so o 4)O antigo watchdog.py (cron */30) foi aposentado; vigiar se o quality_monitor parou passou pro vigia-mutuo IN-SERVER (server/routes/health_checks.py::schedule_watchdog_freshness_check, laço de 5min, lê o mesmo heartbeat file). Se passar de 30min sem rodar, avisa no Telegram: "O quality monitor parou de rodar". Detalhe: specs/malha-de-saude.md.
Editar quality_monitor.py, adicionar entrada no dict CHECKS:
"nome_do_check": {
"description": "O que verifica em linguagem simples",
"severity": "CRITICAL|HIGH|MEDIUM",
"market_only": True|False,
"action": "O que fazer quando violacao encontrada",
"query": "SELECT ... FROM ... WHERE <condicao_de_violacao>"
}
Extrai funcoes do servidor (sem precisar de banco de dados) e joga milhares de inputs aleatorios tentando quebrar. Se quebrar, mostra exatamente qual input causou o problema.
pytest tests/property/ -v # Rodar todos (52 testes, ~30s)
pytest tests/property/ -q # Resumido
| Arquivo | Grupo | Testes | O que verifica |
|---|---|---|---|
| test_pure_functions.py | calculate_volume | 5 | Lot size nunca negativo, nunca NaN, sempre 2 decimais |
| test_pure_functions.py | validate_pair_pnl | 6 | Validacao hedge: mesmo sinal rejeitado, oposto aceito |
| test_pure_functions.py | clean_nan_json | 8 | NaN/Infinity limpos do JSON do EA |
| test_pure_functions.py | market_hours | 8 | Sabado fechado, domingo abre 18h, etc |
| test_pure_functions.py | financial_invariants | 3 | Propriedades matematicas do hedge |
| test_risk_engine.py | individual_risk | 5 | Risk nunca negativo pra conta viva, nunca > hard cap $3k, nunca > daily DD |
| test_risk_engine.py | calc_risk | 3 | Risk do par sempre >= 0 |
| test_risk_engine.py | daily_dd | 5 | DD remaining nunca negativo, nunca excede limite operacional |
| test_risk_engine.py | osb_weight | 5 | Anti-OSB detection correto |
| test_risk_engine.py | dist_cush | 4 | Distancia/cushion nunca negativo, dead account risk <= cushion |
Nota S194:
test_risk_engine.pyrefatorado — importa funcoes reais deapp.af.engine(SSoT) em vez de manter copias locais (FakeProp/FakePA removidos, -154 linhas).
Quando eu faco SCP de arquivo .py pro VPS, o hook pre-deploy-quality.sh roda automaticamente:
- Se testes passam → deploy prossegue
- Se testes falham → deploy BLOQUEADO com mensagem de erro
Manda requests "errados" de proposito pro servidor e verifica que ele nao crasha (retorna 500).
python scripts/quality/fault_injection.py --all # Todos os 8 cenarios
python scripts/quality/fault_injection.py --scenario X # Um cenario especifico
python scripts/quality/fault_injection.py --dry-run # Mostra o que faria
| Cenario | O que simula | Esperado |
|---|---|---|
| duplicate_signal | Signal repetido (mesmo ticket) | Anti-loop ignora o 2o |
| ack_without_signal | ACK pra signal que nao existe | 404 (nao 500) |
| malformed_heartbeat | Dados com NaN/Infinity | Trata sem crashar |
| rapid_fire_signals | 10 signals em 2 segundos | Nao crasha |
| stale_ack | ACK pra signal antigo | Trata com graca |
| wrong_direction | direction="SIDEWAYS" | Nao crasha |
| zero_volume | volume=0 | Nao crasha |
| huge_values | Numeros gigantes | Nao crasha |
| Data | Bug | Como encontrou | Impacto |
|---|---|---|---|
| 2026-03-29 | calculate_volume crash NaN | Property test (tick_value=inf) | Poderia travar calculo de volume |
| 2026-03-29 | clean_nan_json arrays | Property test ([NaN,...]) | NaN passava pro banco |
| 2026-03-29 | same_sign_pnl par 195524 | Quality monitor (1a rodada) | Edge case mercado, nao bug |
| Metrica | Como medir | Meta |
|---|---|---|
| Violations no monitor | Log + Telegram | 0 CRITICAL, < 3 HIGH/dia |
| Property tests passando | pytest tests/property/ |
52/52 (100%) |
| Fault injection passando | fault_injection.py --all |
8/8 (100%) |
| Bugs encontrados por property test | Contagem no changelog | Crescente |
| Tempo sem bug silencioso | Dias desde ultimo bug encontrado pelo monitor | Crescente |
Status: ACTIVE | Ultima revisao: S315.1 (2026-05-02) — Onda 5 cleanup: Etapa 4 aponta canonical (
create_signal_canonical+dispatch_pending_to_eas). Detalhes emspecs/signal-dispatch-canonical.md.
Versao: 2.3
Data: 2026-05-02
Status: v2.3 (S315.1 Onda 5): Etapa 4 atualizada — Signal+PendingSignal+TradeLink+SignalAck criados via create_signal_canonical() (SSoT em server/signal_dispatch.py). WS push pos-commit via dispatch_pending_to_eas() (substitui _push_signals_to_eas legacy, removido). Origin agora canonico via enum OriginType (lista exaustiva — ver specs/signal-dispatch-canonical.md). Fallback signal.origin or "ea" removido em ACK logging (signals legacy ficam origin=NULL, dashboard renderiza "(legacy)"). v2.2: Adicionado secao Trace ID. v2.1: EA remote logs pipeline, mapa de funcoes EA/servidor, ACK validation V1-V4, reconciliation pos-trade, grupo-only routing (S1015).
SSoT para: Ciclo completo de vida de um sinal — da deteccao ate o trade_link
┌─────────────────────────────────────────────────────────────────────────────┐
│ CICLO DE VIDA DE UM SINAL │
│ │
│ EA Master Servidor EA Slave │
│ ───────── ───────── ───────── │
│ 1. Detecta trade ──WS/HTTP──> 2. Valida + armazena │
│ (OTT ou SE) Signal + PendingSignal │
│ + TradeLink (origin) │
│ 3. Distribui ──WS push──> 4. Recebe sinal │
│ (aplica invert) HTTP poll (safety net) │
│ 5. Executa via GUI │
│ 6. Recebe ACK <──HTTP POST── 6. Envia ACK │
│ SignalAck + TradeLink │
│ PendingSignal.resolved │
└─────────────────────────────────────────────────────────────────────────────┘
O EA tem 2 mecanismos paralelos que detectam trades. Ambos alimentam a mesma fila.
| Aspecto | Detalhe |
|---|---|
| Arquivo | Experts/LinniuC.mq5:1342 |
| Trigger | TRADE_TRANSACTION_DEAL_ADD (qualquer deal executado) |
| Latencia | ~0ms (evento nativo MT5) |
| Campo | detection_method = "OTT" |
Fase 1 (sempre executa, mesmo com g_busy=true):
- Armazena SE_DealHint via SE_StoreDealHint() com: positionId, symbol, entry (IN/OUT), closeReason (SL/TP/STOP_OUT/MANUAL), dealVolume, dealPrice
- TTL do hint: 10 segundos | Buffer: 10 hints max (SE_MAX_DEAL_HINTS)
Fase 2 (so se g_busy=false):
- Chama SnapshotEngine_ProcessCycle() + SnapshotEngine_FlushQueueWeb() imediatamente
| Aspecto | Detalhe |
|---|---|
| Arquivo | Include/CopyTrade/SnapshotEngine.mqh:628 |
| Trigger | Timer a cada InpPollingMs (200ms) + OnTick() |
| Latencia | 0-200ms (pior caso = intervalo do timer) |
| Campo | detection_method = "SE" |
Logica de comparacao (SE_CompareSnapshots, linha 252):
| Situacao | Acao |
|---|---|
Ticket em curr mas nao em prev |
SE_EnqueueOpen() |
| Ticket em ambos, SL/TP/volume mudou | SE_EnqueueModify() (com anti-echo) |
Ticket em prev mas nao em curr |
SE_EnqueueClose() (com anti-echo) |
Como decide OTT vs SE: No momento do enqueue, verifica SE_HasDealHint(ticket):
- Hint existe → detection_method = "OTT"
- Hint nao existe → detection_method = "SE"
Na primeira chamada, o SE apenas armazena o snapshot atual como
g_se_prevSnap. Nenhum sinal e gerado. Isso evita que todas as posicoes existentes sejam reportadas como "novas" ao iniciar o EA.
Arquivo: SnapshotEngine.mqh
| Aspecto | Valor |
|---|---|
| Capacidade | SE_MAX_QUEUE = 200 sinais |
| TTL por item | 300 segundos (5 min) — expirado = descartado |
| Estrutura | SE_QueueItem: action, ticket, symbol, direction, volume, sl, tp, retryCount, enqueuedTick, close_reason, detection_method, deal_price (preco de fill exato do OTT), profit (DEAL_PROFIT + COMMISSION + SWAP) |
| Max retries | SE_MAX_RETRIES = 5 — sinal descartado apos 5 falhas |
| detection_method | OTT (OnTradeTransaction), SE (SnapshotEngine), af_engine (AF hedge), af_push_modify (AF MODIFY push) |
CLOSE vai para o INICIO da fila (linha 364-377). OPEN e MODIFY vao para o final.
MODIFY ja existente na fila para o mesmo ticket: atualiza sl, tp, volume no lugar (nao duplica).
Se fila tem 200 itens, novos sinais sao descartados com log
[ALERTA] Fila CHEIA.
Arquivo: SnapshotEngine.mqh, funcao SnapshotEngine_FlushQueueWeb() (linha 424)
| Tipo | Cooldown | Efeito |
|---|---|---|
| CLOSE | 500ms | CLOSE nao eh bloqueado por OPEN recente |
| MODIFY | 1000ms | Evita flood de MODIFY rapidos |
| OPEN | 500ms | Evita flood de OPEN rapidos |
| Tentativa | Delay |
|---|---|
| 1a | 1s |
| 2a | 2s |
| 3a | 4s |
| 4a | 8s |
| 5a | 16s (cap) |
| Max retries | 10 — depois descarta com log "orphan checker no servidor ira compensar" |
Funcao: SE_SendSignalViaWS() (linha 389)
{
"type": "signal",
"action": "OPEN",
"ticket": 123456,
"symbol": "XAUUSD",
"direction": "BUY",
"volume": 0.10,
"price": 2650.00,
"sl": 2640.00,
"tp": 2660.00,
"close_reason": "",
"signal_channel": "ws",
"detection_method": "OTT",
"queued_duration_ms": 15
}
Se WS_IsConnected() retorna false OU SE_SendSignalViaWS() falha → fallback imediato para HTTP.
Funcao: WebBridge_PostSignal() (WebBridge.mqh:686)
Endpoint: POST /api/signals
Headers: X-API-Key, X-Account-Num, Content-Type: application/json
| Tipo | Tentativas HTTP |
|---|---|
| CLOSE | 3 tentativas com Sleep(500*attempt) entre elas |
| OPEN/MODIFY | 1 tentativa |
Arquivo: /opt/copytrade-server/app/routes/signals.py
Endpoint: POST /api/signals
SSoT canonica (S314.6+, parcial — Onda 4 lifecycle pendente): Signal+PendingSignal+TradeLink+SignalAck sao criados via
create_signal_canonical()emserver/signal_dispatch.py(B7+B11 fix — single source) em rotas dashboard (broadcast_*) + AF engine 100% (af/signals.py + routes/af.py — S315.0). Master OPEN do endpoint EAPOST /api/signals(batch insert ~linha 762) ainda usasa_insert(Signal).values(signal_rows)batch insert manual (NAO o helpercreate_signal_canonical()) — sera migrado em Onda 4 lifecycle. Atualizacao S361 (doc-fresh):originesignal_sourceJA usam enums canonicos (OriginType.EA.value/SignalSource.EA.value, linhas ~747/755), NAO mais strings cruas"ea"/"manual"— o drift R9 de string hardcoded foi resolvido; resta apenas o batch insert nao passar pelo helper unico. WS push pos-commit viadispatch_pending_to_eas(db, trade_group_id)100% canonical em todos os paths. Detalhes completos + escopo Onda 4:specs/signal-dispatch-canonical.md.
X-API-Key → verify_api_key)actiontrade_group_id (UUID)sl_distance = abs(price - sl), tp_distance = abs(tp - price)create_signal_canonical(origin=OriginType.EA, signal_source=SignalSource.EA, create_pending_signal=False, create_trade_link=True, is_origin_link=True, create_origin_ack=True) — cria Signal + TradeLink(is_origin=True) + SignalAck("ORIGIN") atomicogroup_id (exceto master)peer.invert != account.invert → caller aplica _invert_direction/_invert_sl_tp ANTES (R8: caller invert, canonical e agnostica)create_signal_canonical(create_pending_signal=True, expires_in_seconds=PENDING_SIGNAL_TTL_SECONDS) — cria Signal + PendingSignal (TTL 60s)await dispatch_pending_to_eas(db, trade_group_id) faz WS push uniforme via _build_ws_payload (R11)Se conta slave tem >=
CIRCUIT_BREAKER_MAX_POSITIONS(10) posicoes abertas, o OPEN e bloqueado para aquela conta.
TradeLink ativo pelo ticket do mastertrade_group_idtrade_group_id + MODIFY, ignora (anti-echo)create_signal_canonical(action="MODIFY", ticket=local_ticket_do_peer, ...) — cria Signal + PendingSignalpeer.invert != account.invertawait dispatch_pending_to_eas(db, trade_group_id)TradeLink ativo pelo ticketprofit + close_price do link existente e retorna "profit_updated" (sem re-fechar)TradeLink da conta master (is_closed=True, closed_at=now)deal_price do EA (preco exato de fill do deal) — prioridade maximadeal_profit do EA (DEAL_PROFIT + COMMISSION + SWAP consolidado)OpenPosition cache (ultimo bid do heartbeat)trade_group_idtrade_group_id + CLOSE, ignoracreate_signal_canonical(action="CLOSE", ticket=local_ticket_do_peer, ...) — cria Signal + PendingSignalSuppressMarker com TTL 10s para o trade_group_id (anti-echo servidor)log_trade_event(event_type="CLOSE", close_reason, profit, close_price)await dispatch_pending_to_eas(db, trade_group_id)Tabela: pending_signals
| Campo | Descricao |
|---|---|
account_id |
Conta slave destino |
signal_id |
FK para signals |
trade_group_id |
UUID do grupo |
expires_at |
now + 60s (1 minuto, centralizado em constants.py) |
resolved |
false ate execucao ou expiracao |
PendingSignal expira em 60s (1 min). Sinais OPEN nao executados sao auto-fechados pela background task (a cada 15s). Sinais MODIFY/CLOSE expirados sao marcados
resolved=Truepelo cleanup periodico (loop a cada 15min emmain.py).
Endpoint
POST /api/admin/cleanup/pending-signalspara forcar limpeza.
O servidor chama ea_ws_manager.notify_account(account_id, "new_signal") que envia o sinal completo via WS.
Endpoint: GET /api/signals/pending
Funcao EA: WebBridge_PollSignals() (WebBridge.mqh:760)
| Situacao | Intervalo de poll |
|---|---|
| WS conectado e ativo | 10 segundos (safety net) |
| WS desconectado | 1-3 segundos (configuravel InpPollInterval) |
O EA SEMPRE faz poll HTTP, mesmo com WS ativo. O WS acelera a entrega, o HTTP garante que nada se perde.
Resume semantics — EA informa "ate onde ja processou":
g_ws_lastSignalId (long) persistido em MQL5/Files/ws_last_signal_id.txtLNC_MarkSignalProcessed (monotonic: so cresce){"type":"auth", "api_key":"...", "account_num":"...", "last_signal_id":N}_flush_pending_signals em ea_ws.py) filtra WHERE signal_id > last_signal_idPor que: antes, ao reconectar, servidor re-enviava TODOS os pendings unresolved. Se EA ja processou via HTTP poll no intervalo (comum — HTTP 1-3s vencia WS reconnect 22s backoff na S252), o buffer circular g_processedSignalIds[100] protegia mas gerava log noise + trafego redundante.
Fail-open (backward compat):
- EA velho sem campo: last_signal_id=0 -> envia tudo (comportamento pre-S253 preservado)
- Arquivo corrompido: StringToInteger retorna 0 -> mesmo fallback
- Escrita em disco falha: global atualizado mas nao persistido -> proximo restart re-flusha (buffer 100 IDs protege)
Monotonic: WS_SetLastSignalId so aceita sigId > g_ws_lastSignalId. Evita regressao se LNC_MarkSignalProcessed receber IDs fora de ordem.
_detect_missing_positions (detecta posicoes orfas comparando HB vs TradeLink) agora roda em AMBOS os paths:
- WS heartbeat (_handle_ws_heartbeat): desde S224+
- HTTP heartbeat (POST /api/heartbeat): desde S253 Onda 2 (antes: delay de 15min via orphan task)
Anti-race: Advisory lock Postgres (pg_try_advisory_lock(account_id)) impede concorrencia. Se WS path ja esta rodando, HTTP path skipa (nao-blocking). Evita double-close de posicao.
Arquivo: Experts/LinniuC.mq5 + Include/CopyTrade/GUIExecution.mqh
| Check | Detalhe |
|---|---|
| Anti-retry | g_processedSignalIds[] (buffer circular de 100 IDs). Sinal ja processado → ignora, mesmo que execucao tenha falhado |
| Idade maxima | (TimeGMT() - signalTime) > InpSignalMaxAge (60s) → descarta com ACK SKIPPED |
GUIExecution_Open, GUIExecution.mqh:765)GUI_AcquireGUILock() — lock exclusivo em arquivo (TTL 30s para stale locks)GUI_EnsureVisible() — restaura MT5 se minimizadoGUI_CloseAllMT5Dialogs() — fecha dialogos residuaisGUI_OpenF9() — WM_COMMAND 32848 abre dialogo "Nova Ordem"GUI_WaitForDialog() — timeout InpTimeoutMs (5000ms)GUI_SelectSymbol() — seleciona simbolo no combo (ID 10331)GUI_WaitForSubDialog() — aguarda controles carregaremGUI_TryConfirmBrokerPopup() — confirma popups (leverage, risk) ou detecta erros (failed, insufficient)Pos-execucao:
- GUIExecution_DetectNewTicket() — compara lista de posicoes antes/depois
- Fallback: SE_FindRecentOpenDeal() usa DealHint do OTT
- SE_SuppressTicket(newTicket, 5) — suprime re-deteccao por 5s (Pattern #34, S84)
GUIExecution_Modify, GUIExecution.mqh:844)GUI_AcquireGUILock()GUI_OpenPositionDialogSafe(ticket) — localiza na ListView (10328), duplo-cliqueGUI_VerifyDialogTicket() — verifica titulo contem ticket esperadoSE_SuppressTicket(localTicket, 10) — suprime por 10 segundosGUIExecution_Close, GUIExecution.mqh:948)GUI_AcquireGUILock()GUI_EnsureVisible()GUI_ForceCloseResidualDialogs() entre elasGUI_OpenPositionDialogSafe(ticket) — ListView → duplo-cliqueSE_SuppressTicket(localTicket, 10) — suprime por 10 segundosPrincipio-mestre: o tempo NUNCA decide sucesso/falha — so o FATO (evento OnTradeTransaction, estado real de posicao, ou popup de rejeicao). O teto de tempo e' so o orcamento de "esperar travado" antes de entregar pra rede de reconciliacao. Caso rapido (broker normal) = identico a antes (~100ms).
| Mecanismo | Comportamento |
|---|---|
| Cap de confirmacao configuravel (D2) | Laco de confirmacao pos-clique (OPEN/MODIFY/CLOSE) usa g_gui_confirmTimeoutMs (servidor manda confirm_timeout_ms default 15000; fallback local 2000) em vez do for(t<2000) fixo. NAO confundir com a janela-de-aparecer (g_gui_timeoutMs=5000, intocada) nem com o settle de campo do MODIFY. |
| Fail-fast em rejeicao (R4) | GUI_TryConfirmBrokerPopup seta g_gui_lastRejection ao detectar popup de ERRO; os 3 lacos quebram cedo (nao esperam o cap a toa). O caller usa pra separar FAILED-real de cap-estourou. |
| Fila de confirmacao adiada (D3) | Cap estourou SEM fato E SEM rejeicao → LNC_ScheduleConfirm enfileira (g_confirmQueue[4]) + LNC_MarkSignalProcessed (anti-dobra) + solta o lock. LNC_CheckConfirmQueue (no OnTimer, roda mesmo com mercado parado) re-confere: OPEN via SE_FindRecentOpenDeal + fallback HistorySelect correlacionado por tempo; CLOSE via !PositionSelectByTicket. FILLED tardio (deferred_*_confirmed) ou FAILED-real so apos CONFIRM_TOTAL_WINDOW_MS=30s. Anti-orfa: nunca declara FAILED por impaciencia. |
| MODIFY em ordem fechada → FILLED-noop (D4) | Bail !PositionSelectByTicket ANTES de GUIExecution_Modify → ACK FILLED reason="modify_noop_position_closed" (nao FAILED). Evita o alerta enganoso "par desalinhado". Servidor: TradeLink so criado em action=="OPEN" (MODIFY-noop nao ressuscita ticket fechado). |
| Aviso de delay leigo (D5) | ack_helpers.py [DELAY]/[DELAY GUI] usam user_messages.delay_cause_message() — explica a causa (corretora lenta na tela vs espera de entrega) sem jargao. Gate is_test/is_simulator preservado. |
| Simulacao de latencia (D7) | sim_broker_delay_ms (so sandbox; pool SEMPRE 0 por gate server-side) — GUI_SimGatePassed segura a confirmacao pra exercitar o caso lento de forma deterministica. |
Pendente (TODO #376): D6 leitura-direta-do-texto da linha (
LVM_GETITEMTEXTW) — otimizacao de caso raro (row-shift), adiada. SSoT completo:specs/broker-latency-resilience-S395.md.
Arquivo: WebBridge.mqh:808
Endpoint: POST /api/signals/{signalId}/ack
{
"status": "FILLED|FAILED|SKIPPED",
"local_ticket": 789012,
"error_msg": "",
"open_price": 2650.12,
"applied_sl": 2640.00,
"applied_tp": 2660.00,
"actual_volume": 0.10,
"receive_channel": "ws_push|http_poll",
"gui_duration_ms": 1450
}
| Status | Quando |
|---|---|
FILLED |
Execucao GUI bem-sucedida |
FAILED |
GUI falhou ou ticket nao encontrado |
SKIPPED |
Sinal muito antigo (> InpSignalMaxAge) |
| Fase | Tentativas | Backoff |
|---|---|---|
| Imediata | 3x | 1s, 2s (exponencial) |
| Fila persistente | Ilimitado (max 20 na fila) | 5s → 10s → 20s → 30s (cap) |
SignalAck com todos os camposFILLED: cria TradeLink com is_origin=False, local_ticket do slavePendingSignal.resolved=Trueapplied_sl vs target_sl, applied_tp vs target_tp — mostra divergencia entre o que foi pedido e o que o broker aplicoulog_trade_event(event_type="ACK", status, action="OPEN"|"MODIFY", context)| Campo | Descricao |
|---|---|
server_received_at |
Timestamp de chegada no servidor |
server_processing_ms |
Tempo do recebimento ate commit no DB |
receive_channel |
ws_push ou http_poll (como o slave recebeu) |
gui_duration_ms |
Tempo de execucao GUI no slave |
total_delay_ms |
De signal.created_at ate ack.executed_at (latencia total) |
Capturados pelo EA via SymbolInfoTick() 1 linha antes de GUIExecution_Open/Close/Modify. Servidor calcula slippages no handler ACK. Backwards-compat: ACKs pre-S336 (EA build < 3.85.3) tem campos NULL.
| Campo | Tipo | Descricao |
|---|---|---|
bid_at_request |
Float | BID via SymbolInfoTick no momento do click GUI (R1) |
ask_at_request |
Float | ASK via SymbolInfoTick no mesmo instante (par sempre junto — INV4) |
exec_slip_pts |
Float | Slippage GUI+broker: \|deal_price - quote_at_request_lado_correto\|. NULL pra MODIFY (sem deal_price) ou stale (>60s). |
pipeline_slip_pts |
Float | Slippage rota inteira: \|deal_price - signal.price\|. Independe de bid/ask. |
Lados da cotacao (R2):
- OPEN BUY -> ASK (compra ao ASK)
- OPEN SELL -> BID (vende ao BID)
- CLOSE BUY -> BID (vende pra fechar)
- CLOSE SELL-> ASK (compra pra fechar)
- MODIFY -> guarda bid+ask pra audit (saber se preco passou pelo SL/TP em transito), exec_slip = NULL
Tag close_reason="ROLLOVER" (Layer 1 EA anti-swap): Rollover_TryClose chama Rollover_TagTicket antes do GUI close; SnapshotEngine override close_reason -> "ROLLOVER" em vez de "MANUAL" (default DEAL_REASON_CLIENT). Distingue de Layer 2 servidor (close_reason="ROLLOVER_FALLBACK" via close_pair_positions).
Spec completa: specs/exec-slippage-telemetria.md.
Tabela: trade_links
| Campo | Master | Slave |
|---|---|---|
trade_group_id |
UUID (gerado no OPEN) | mesmo UUID |
is_origin |
True |
False |
local_ticket |
ticket MT5 master | ticket MT5 slave |
account_id |
ID conta master | ID conta slave |
is_closed |
false → true | false → true |
Todo MODIFY e CLOSE usa o
trade_group_idpara encontrar quais slaves precisam ser notificados. Sem TradeLink = sinal nao propaga.
| Situacao | Comportamento |
|---|---|
| WS desconecta | Poll HTTP assume (1-3s). WS tenta reconectar com backoff 10s → 300s |
| WS morto >30s sem atividade | g_ws_connected resetado, volta ao HTTP |
| WS reconecta | Servidor envia PendingSignals pendentes |
Mecanismo A — Background task (ativo):
| Aspecto | Detalhe |
|---|---|
| Intervalo | 15 minutos (main.py:24) |
| Logica | Busca TradeLink WHERE is_closed=False cujo local_ticket nao aparece em open_positions |
| Acao | Marca TradeLink como fechado. Propaga CLOSE para peers abertos (Signal com origin="server", TTL 1min) |
Mecanismo B — Heartbeat passivo (v2.0):
| Aspecto | Detalhe |
|---|---|
| Trigger | Cada heartbeat POST do EA |
| Logica | _detect_missing_positions(): compara posicoes reportadas pelo EA vs TradeLinks abertos |
| Threshold | Posicao ausente por >30s |
| Acao | Cria CLOSE signal com origin="server", propaga para peers |
Mecanismo B detecta closes mais rapido que A (dentro de 1 heartbeat vs 15min).
Problema: EA slave executa um trade → SnapshotEngine detecta como "novo" → reenvia ao servidor → loop infinito.
Nivel 1 — EA (SE_SuppressTicket):
| Acao executada | TTL de supressao |
|---|---|
| OPEN | 5 segundos |
| MODIFY | 10 segundos |
| CLOSE | 10 segundos |
Buffer: g_se_suppressTickets[] (max 20). Antes de enfileirar MODIFY ou CLOSE, verifica SE_IsTicketSuppressed().
Nota:
SE_MAX_RETRIESdefine 5 em SnapshotEngine.mqh:43, masFlushQueueWebusa hardcoded>= 10. O valor efetivo e 10 retries.
Nivel 2 — Servidor (suppress_markers):
| Origem do sinal | TTL do marker |
|---|---|
| MODIFY/CLOSE (qualquer origem) | 10 segundos |
Nota (v2.0): Implementacao atual usa 10s para TODAS as origens (EA e dashboard). A diferenciacao 10s/15s documentada na v1.0 nao foi implementada.
Quando master envia MODIFY/CLOSE, servidor verifica se existe marker ativo → ignora (evita loop).
abs(curr.volume - prev.volume) > 0.0001close_reason = "PARTIAL_CLOSE"PositionSelectByTicket() retorna falseFILLED direto (posicao ja nao existe — nada a fazer)if acct.invert (sem comparar com outra conta, diferente do EA-to-EA)NAO altera as regras R1-R10 acima. Adiciona 3o estado de conta entre
aliveeoffline+ 2 guardas no dispatch (dedup #330 + back-pressure SKIP). Invariante S2-zero-sum de hedge preservado por construcao.Nota: as referencias a "invariante S2-zero-sum" abaixo apontam pra regra interna do modulo
saturation.py— NAO confundir com a R2 deste documento ("CLOSE tem prioridade maxima").
Problema: o EA executa ordens via cliques de GUI Win32 (GUIExecution.mqh), seriais, no MESMO OnTimer que envia batimento. Quando a tela ocupa o laco, o batimento estica e o servidor fica cego — sem saber distinguir "EA morreu" de "EA esta ocupado executando". Pior: o servidor reemitia CLOSEs do mesmo ticket em loop (classe do #330) achando que o EA nao recebeu.
Solucao: 3o estado saturated + dedup in-flight + back-pressure SKIP. Modulo central: server/saturation.py (3 funcoes puras + store in-memory).
| Estado | Quando | Efeito no dispatch |
|---|---|---|
alive |
gap < SATURATED_HB_GAP_SEC (15s) OU gap < 60s sem trabalho pendente | Round novo entra, dispatch normal |
saturated |
beacon ea_busy ativo dentro de BEACON_HARD_CAP_SEC (90s) OU gap > 15s com trabalho pendente/sem-ACK |
Conta NAO entra em round novo; OPEN novo independente eh SKIPADO (S2-zero-sum preservado — perna completante + CLOSE/MODIFY passam) |
offline |
gap > 60s E sem beacon ativo | Round novo nao entra; pares ja abertos seguem para fechamento normal |
Backstop: mesmo sem beacon (EA velho/travou), inferencia por ritmo marca saturated se gap > 15s com pendente.
ea_busy (EA → Servidor, dispara-e-esquece)EA dispara frame WS {"type":"ea_busy","op":<OPEN|MODIFY|CLOSE>,"ticket":<n>,"expected_ms":<n>} IMEDIATAMENTE APOS pegar o lock de GUI (GUI_AcquireGUILock em GUIExecution.mqh), antes do trabalho lento de tela (GUI_EnsureVisible, GUI_ExecuteOpenInternal). Se o lock falhar, o beacon NAO eh enviado. Dispara-e-esquece — sem retry, sem ACK queue, timeout <=200ms. Handler servidor: routes/ea_ws.py::_handle_ws_ea_busy -> saturation.set_busy(account_id, op, ticket). Beacon explica silencio longo (gap > 60s mas conta esta saturated, nao offline).
Antes de criar PendingSignal pra (conta, ticket, action=CLOSE), signal_dispatch.create_signal_canonical checa se ja ha pendente NAO-resolvido (sem ACK, dentro do TTL) pro mesmo (conta, ticket) -> pula + emite evento DISPATCH_DEDUP + retorna skipped_duplicate=True, skip_reason="dedup_inflight_close". Excecao: ACK FAILED do EA resolve o pendente legado MAS cria retry-PS novo (+70s) — dedup AINDA suprime re-CLOSE server-originated enquanto retry-PS estiver em-voo (correto, anti-#330: nao empilhar enquanto o retry do EA esta pendente). Liberacao real: FILLED, 3 retries exauridos, OU TTL expirou. Escopo: SO CLOSE (idempotente). MODIFY fica com a supressao anti-echo do SuppressMarker (descrita acima nas regras de processamento server-side) + dedup da fila do EA.
Gating primario em af/signals.py::check_all_online_in_pool (sync+async): conta saturated NAO entra em round novo, mesmo com HB fresco. Reason: "saturated (afogada — GUI gargalo, fora de round novo)". Filtra antes do generate_signals_for_pair/scheduler.
Defesa em profundidade em signal_dispatch.create_signal_canonical: antes do PendingSignal create, consulta should_hold_dispatch(state, action, is_completing_hedge_leg, is_existing_position_op). Se True -> skip + evento DISPATCH_HELD + retorna skipped_duplicate=True, skip_reason="held_saturated".
Invariante S2-zero-sum (CRITICO, regra do modulo saturation.py): should_hold_dispatch retorna False (passa direto) se:
- is_completing_hedge_leg=True (perna que completa par ja aberto — naked leg = quebra zero-sum)
- action in (CLOSE, MODIFY) ou is_existing_position_op=True (mexe posicao broker existente)
Sobra apenas OPEN novo independente em conta saturated como alvo do skip.
Abort do par (HR-iter2-01, race protection): em generate_signals_for_pair (af/signals.py), apos master canonical: se master_result.skip_reason == "held_saturated" (race entre check_all_online e dispatch) -> db.rollback() + raise ValueError. Caller main.py af_scheduler tem except ValueError que loga warning + nao marca pair.status='failed' (proxima rodada reavalia). Slave nunca abre sozinho.
Trade-off honesto: Hold = SKIP, nao ADIA. Signal record fica pra audit, mas sem PendingSignal e sem retry server-side. Quando conta sai de saturated, proximo signal natural (proxima rodada AF / novo broadcast) reentra. Retry temporizado dedicado fica como TODO S386-FU2 (sweetspot inicial confia no fluxo upstream — gating primario cobre 99%).
GET /api/accounts retorna account_state na carga inicial (evita janela ~10s F5 sem badge).alert_ea_saturated edge-trigger + market-hours + dedup (50s) descreve as 4 salvaguardas (distingue, nao reemite CLOSE, fora de round novo, PULA OPEN novo).DISPATCH_DEDUP, DISPATCH_HELD em trade_events/signal_events. ea_saturated no canal de alertas.server/saturation.py — 3 funcoes puras (classify_state, should_dedup_dispatch, should_hold_dispatch) + store in-memory + helpers ingest_pulse/set_busy/clear_busy/get_state/reset_state.server/heartbeat_helpers.py — ingestao do pulso enriquecido + broadcast account_state no WS.server/signal_dispatch.py — wire dedup + hold + skip_reason no canonical.server/af/signals.py — check_all_online_in_pool (sync+async) filtro saturated + 8 callsites com flags is_completing_hedge_leg / is_existing_position_op + abort do generate_signals_for_pair se master held.server/routes/ea_ws.py — handler frame ea_busy.server/routes/accounts.py — REST initial state + reset_state em hard delete.Include/CopyTrade/WinHttpWS.mqh — WS_SendHeartbeat enriquecido (queue_depth, busy_*, oldest_age_ms) + WS_SendBusyBeacon dispara-e-esquece.static/js/{overview,positions,websocket}.js — selo AFOGADO + sync WS.specs/saturacao-visibilidade-gui-S386.md — spec autocontida (Tier L, sweetspot 3 pilares + Fase 4 hedge gating)..planning/PLAN-S386-saturacao-visibilidade-gui.md — plano atomico (5 fases).O sistema AF gera sinais que usam a mesma infraestrutura (Signal, PendingSignal, TradeLink) mas com isolacao total do copy-trade normal.
| Canal | Origem | Distribuicao |
|---|---|---|
ws |
EA (copy-trade) | Broadcast para peers do grupo |
af |
AF engine (hedge) | SEM broadcast — direto para contas do par AF |
http |
EA fallback | Broadcast normal |
| Tipo | Origin | detection_method | Detalhe |
|---|---|---|---|
| AF OPEN | af |
af_engine |
sl_distance=0, tp_distance=0 (SL/TP absolutos, EA aplica buffer) |
| AF CLOSE | af_close |
af_engine |
trade_group_id = "AF_CLOSE_{pair_id}" |
| AF MODIFY | af_modify |
af_push_modify |
Push zone precision (ver §6d AF whitepaper). F2/F3 cap preserva margem ($2-10), F7 desconta sl_buffer do SL. |
Mecanismo A — AF Pending Linkage: Quando conta re-detecta sua propria execucao AF (via SE/OTT), servidor reconhece signal_channel='af' no PendingSignal, vincula TradeLink sem criar broadcast novo, auto-ACK como FILLED.
Mecanismo B — Pool Active Suppression: Se conta esta em pool AF ativo/pausado, OPEN cria signal + auto-ACK com status='ORIGIN', define signal_channel='af', retorna 'af_pool_suppressed'. Peers NAO recebem.
P19 (CRITICO): Sem esta isolacao, sinais AF propagariam para TODAS as contas do grupo. Bug causou 63 posicoes e -$46k.
Mecanismo retroativo que recupera profit/close_price quando o EA volta de
janela offline durante a qual o broker fechou posicoes (SL hit, manual close,
margin call). Causa raiz coberta: profit_source=none em _detect_missing_positions
ao detectar ticket sumido (3 fallbacks retornavam None → profit gravado=0 →
AfPair classificada erroneamente como pnl_suspect).
| # | Trigger | Quando dispara | Cobre |
|---|---|---|---|
| 1 | OnInit |
Apos boot do EA (recompile, restart MT5) | Janela offline + crash recovery |
| 2 | WS reconnect callback | Apos WS_Reconnect() retornar true |
Disconnect transiente (proxy, rede) |
| 3 | OnTimer |
A cada 60s (debounce 30s interno) | Cinturao+suspensorio: OTT fila 1024, erros transientes |
Include/CopyTrade/ReconcileEngine.mqh — modulo:
- RE_FetchOpenTradeLinks: GET /api/ea/open-tradelinks retorna lista de tickets que o servidor ainda considera abertos.
- RE_ScanHistory: HistorySelect(from_time, now) + filtra DEAL_ENTRY_OUT cujo DEAL_POSITION_ID ∈ open_tickets.
- RE_EnqueueRetroactive: POST /api/signals com is_retroactive=true + profit consolidado (R4: DEAL_PROFIT + DEAL_SWAP + DEAL_COMMISSION) + deal_price.
- Mutex g_reconcile_running (R10) impede multi-trigger concorrente.
- Checkpoint local MQL5/Files/reconcile_checkpoint_<account>.txt (R11) reduz custo de scan na N-esima execucao. Primeiro run varre ate 7d atras.
signals.is_retroactive BOOLEAN NOT NULL DEFAULT FALSE + index parcial:
CREATE UNIQUE INDEX ix_signals_retroactive_unique
ON signals (source_account, ticket, action)
WHERE is_retroactive = TRUE;
Garante: 1 retroactive por (conta, ticket, action). Multi-trigger seguro — segunda chamada bate no IntegrityError ou no SELECT prefix-check, retorna skipped_idempotent. Signals normais (is_retroactive=false) NAO sao afetados pelo index.
_process_retroactive_close em routes/signals.py:
1. Idempotency check (R6): SELECT existing → skip silent.
2. Sanity check (R28): validate_retroactive_profit(profit, balance) → |profit| > 2*balance rejeita 422 + Telegram alert.
3. Tolerance check (R5): |old_profit - new_profit| < $0.01 → skip silent (sem audit churn).
4. Override absoluto: link.profit = data.profit; link.close_price = data.deal_price. Broker eh fonte canonica.
5. Audit: log_trade_event(origin='reconcile_retroactive') com before/after profit + delta.
6. Re-validate: atualiza AfTrade.profit do lado; se ambos lados tem profit, chama process_trade_result(pair_id, profit_a, profit_b).
validate_retroactive_profit(profit, balance) em af/engine.py:
- balance <= 0 → reject balance_invalid
- |profit| > 2 * balance → reject absurd_value
- caso contrario → ok
Tests cobrem 11 cenarios (5 ex + 3 inv + 3 property-based via Hypothesis em test_reconcile_retroactive.py).
Coroutine async em main.py:_watchdog_offline_with_trade():
- Tick 60s. Lista accounts com TradeLink open.
- Compara max(Heartbeat.created_at) com NOW(). age > 300s E acc ∈ TradeLink open → alert candidato.
- Edge-trigger via dict _watchdog_alerted — alerta dispara 1x na transicao online→offline. Reset com heartbeat fresco.
- R9 market hours: is_market_open(symbol) cruzado antes de alertar (suprime weekend/rollover).
- Telegram via send_watchdog_offline_with_trade com debounce 60s.
Reconcile cobre apenas CLOSE retroativo. Se EA estava offline durante OPEN, pair vira failed/timeout e nao eh recuperavel via reconcile. Exemplo: Pair 81 (Round 21) — af_trades.status='timeout' sem ACK de OPEN, fix via SQL manual em S299 (CA-8).
specs/reconciliacao-pos-offline.md — 14 casos de borda, 11 regras, 27 riscos, prior art externo (MQL5 Article #11248, HistorySelect docs). Status: APROVADO (S299) → IMPLEMENTADO (S300).
Mecanismo server-side que detecta e corrige divergencias de SL/TP entre peers.
| Aspecto | Detalhe |
|---|---|
| Trigger | Cada heartbeat POST do EA (routes/heartbeat.py) |
| Logica | _reconcile_sl_tp(): compara SL/TP das posicoes do EA vs valores esperados (TradeLink) |
| Threshold | Mismatch detectado |
| Acao | Cria MODIFY signal com origin="server_reconcile" — direto pro peer divergido (sem broadcast) |
| Cooldown | Skip se MODIFY recente (<300s) para o mesmo ticket |
| Log | "[SL/TP-RECONCILE] Mismatch conta X ticket Y: SL=A->B, TP=C->D" |
Todo evento significativo eh registrado na tabela TradeEvent para auditoria completa.
log_trade_event(db, event_type, ...)| Campo | Tipo | Descricao |
|---|---|---|
event_type |
str | OPEN, MODIFY, CLOSE, PROPAGATE, ACK |
account_id |
int | Conta envolvida |
ticket |
int | Ticket do broker |
trade_group_id |
str | UUID do grupo |
signal_id |
int | FK para signals |
origin |
str | ea, server_reconcile, af, af_close, af_modify |
close_reason |
str | SL, TP, STOP_OUT, MANUAL (so CLOSE) |
price |
float | Preco do evento |
profit |
float | P&L consolidado (so CLOSE) |
volume |
float | Volume |
context |
JSON | Metadados extras (distributed_to, applied_sl/tp, etc.) |
| Momento | event_type | Contexto extra |
|---|---|---|
| Signal OPEN criado | OPEN |
distributed_to (numero de peers) |
| Signal MODIFY criado | MODIFY |
SL/TP alvos |
| Signal CLOSE criado | CLOSE |
close_reason, profit, close_price |
| CLOSE propagado pra peers | PROPAGATE |
Peers notificados |
| ACK recebido (OPEN) | ACK |
status, channel, latency_ms |
| ACK recebido (MODIFY) | ACK |
applied_sl vs target_sl, applied_tp vs target_tp |
Analogia: O trace_id e como o numero de rastreio de uma encomenda. Cada sinal recebe um codigo unico no momento em que nasce, e esse codigo acompanha TODOS os passos — da deteccao no EA master ate o ACK do EA slave e a timeline no dashboard. Se algo deu errado, basta buscar o trace_id pra ver exatamente onde parou.
| Aspecto | Detalhe |
|---|---|
| Formato | 32 caracteres hexadecimais (padrao W3C Trace Context) |
| Onde nasce | Servidor, no momento do INSERT INTO signals (routes/signals.py) |
| Header HTTP | traceparent (preparado pra integracao futura com OTel/Grafana) |
| EA fallback | Telemetry_GenTraceId() (Telemetry.mqh) gera trace_id local se servidor nao fornecer — composto de account_id + timestamp + tick_counter |
| Enriquecimento | Middleware _ensure_trace_id() (telemetry.py) gera automaticamente se ausente no request |
| Etapa | Componente | Como trace_id chega |
|---|---|---|
| 1. Deteccao (EA master) | SnapshotEngine.mqh |
Ainda sem trace_id — sinal detectado localmente |
| 3. Envio (EA -> servidor) | WebBridge_PostSignal() |
Sem trace_id no payload (servidor gera) |
| 4. Processamento (servidor) | signals.py INSERT |
trace_id gerado aqui — salvo em signals.trace_id |
| 5. PendingSignal | pending_signals |
Herdado via signal_id FK (JOIN com signals) |
| 6. Distribuicao (WS push) | ea_ws_manager.push_signal() |
trace_id incluido no payload WS |
| 7. Execucao (EA slave) | GUIExecution.mqh hooks |
EL_SetCurrentContext(signalId, traceId) — todos os EL_Emit() herdam |
| 8. ACK | signal_acks.trace_id |
Salvo na tabela signal_acks via handler HTTP/WS |
| 9. TradeLink | trade_links |
Correlacionado via trade_group_id (mesmo grupo) |
| 10. EventLog | signal_events |
Cada evento carrega trace_id — UNIQUE(signal_id, event_type, seq_num) |
| 11. Telemetria | event_stream |
Eventos telemetricos vinculados pelo mesmo trace_id |
O EventLog (EventLog.mqh) emite eventos em cada passo da execucao GUI:
| Grupo | Events | Descricao |
|---|---|---|
| OPEN | gui_s1..gui_s11 |
11 passos: F9 dialog -> symbol -> volume -> SL/TP -> click -> confirmacao |
| MODIFY | gui_m1..gui_m5 |
5 passos: dialog -> combo -> painel -> click -> resultado |
| CLOSE | gui_c1..gui_c3 |
3 passos: dialog -> botao -> resultado |
| ACK | ack_sent, ack_failed |
Confirmacao de execucao enviada/falhada |
| Recepcao | ea_received |
EA slave recebeu o sinal |
Cada event type tem variante _ok e _fail (ex: gui_s5_ok, gui_s5_fail).
| Cenario | Comportamento |
|---|---|
| Signal antigo (pre-S224, sem trace_id) | trace_id = NULL — queries usam LEFT JOIN, timeline funciona sem ele |
| EA versao antiga sem Telemetry.mqh | Servidor gera trace_id normalmente — EA so nao emite eventos de telemetria |
| Mix de EA builds no deploy gradual | Campos novos sao opcionais com default no Pydantic schema |
| Acao | Endpoint | O que mostra |
|---|---|---|
| Timeline por signal | GET /api/signals/{id}/timeline |
Todos eventos ordenados por ts_ea ASC com payload expandivel |
| Timeline por trace | GET /api/telemetry/trace/{trace_id} |
Eventos de telemetria vinculados ao trace |
| Anomalias recentes | GET /api/telemetry/anomalies |
Eventos com late=true (gap > 10s entre broker e servidor) |
specs/ea-observability.md — Event sourcing hibrido, buffer persistente, idempotencia (12/12 CAs)specs/telemetria-sweetspot.md — Gap detection, anomaly detector, Telegram alerts (9/11 CAs)| Componente | Arquivo | Responsabilidade |
|---|---|---|
| EA principal | Experts/LinniuC.mq5 |
Orquestrador, OnTimer, OnTradeTransaction |
| Deteccao | Include/CopyTrade/SnapshotEngine.mqh |
Snapshot, fila, anti-echo EA |
| HTTP | Include/CopyTrade/WebBridge.mqh |
POST signals, GET pending, ACK, heartbeat |
| WebSocket | Include/CopyTrade/WinHttpWS.mqh |
WS connect, read, send |
| GUI | Include/CopyTrade/GUIExecution.mqh |
Win32 automation: OPEN/MODIFY/CLOSE |
| Settings | Include/CopyTrade/SettingsManager.mqh |
Sync servidor, invert |
| Servidor sinais | routes/signals.py |
POST signals, GET pending, ACK |
| Servidor WS | routes/ea_ws.py |
WS /api/ws/ea, push sinais |
| Servidor cleanup | main.py |
Orphan checker, TTL cleanup |
| Servidor heartbeat | routes/heartbeat.py |
POST heartbeat, posicoes, sync |
O EA envia logs pro servidor via heartbeat. Pipeline completo:
EA (MQL5) Servidor (Python)
───────── ─────────────────
LNC_LogRemote(msg)
→ g_logBuffer[100] (circular)
→ LNC_BuildLogJSON()
→ heartbeat body: "last_logs": [...]
POST /api/heartbeat
→ heartbeat.py:385-418
→ dedup (5min window por account)
→ INSERT ea_remote_logs
→ level auto-detect:
"ERROR"/"FAIL"/"FALHOU" → ERROR
"WARN"/"SKIP" → WARN
else → INFO
GET /api/heartbeat/diagnostics
→ recent_logs (20 ultimas)
→ recent_errors (10 ultimas, level=ERROR)
Cleanup: TTL 7 dias (main.py startup)
| Evento | Formato | Level |
|---|---|---|
| EA iniciado | EA iniciado BUILD X.Y.Z |
INFO |
| OPEN OK | OPEN OK #ticket symbol dir vol=X Xms [gui_trace] |
INFO |
| OPEN FAIL (no ticket) | OPEN FAIL no-ticket symbol [gui_trace] |
ERROR |
| OPEN FAIL (GUI) | OPEN FAIL gui symbol [gui_trace] |
ERROR |
| MODIFY OK | MODIFY OK #ticket Xms [gui_trace] DIAG{...} |
INFO |
| MODIFY FAIL | MODIFY FAIL #ticket [gui_trace] |
ERROR |
| MODIFY SKIP | MODIFY SKIP #ticket (already correct) |
WARN |
| MODIFY READBACK FAIL | MODIFY READBACK FAIL #ticket msg DIAG{...} |
ERROR |
| CLOSE OK | CLOSE OK #ticket Xms [gui_trace] |
INFO |
| CLOSE FAIL | CLOSE FAIL #ticket [gui_trace] |
ERROR |
| CLOSE already closed | CLOSE already closed #ticket |
INFO |
| CLOSE race-closed | CLOSE race-closed #ticket [gui_trace] |
INFO |
| SKIP (signal old) | SKIP #signalId old Xs |
WARN |
| WS debug | [WS-DBG] type=X len=Y |
INFO |
| WS price request | [WS] Price request recebido: symbol id=X |
INFO |
| OTT event | [OTT] Deal #X ENTRY_IN/OUT REASON |
INFO |
| AutoUpdate diag | [AU-DIAG] ... |
INFO |
TAB:0|C1d:rows=1|SEL:0|F9:ok|DLG:found|SYM:set|DIR:set|VOL:set|SLTP:set|PLACE:ok
Cada passo da execucao GUI separado por |. O ultimo passo indica onde parou se falhou.
-- Ultimos 30 logs de uma conta
SELECT message, level, created_at FROM ea_remote_logs
WHERE account_id = X ORDER BY created_at DESC LIMIT 30;
-- Erros das ultimas 24h
SELECT account_id, message, created_at FROM ea_remote_logs
WHERE level = 'ERROR' AND created_at > NOW() - INTERVAL '24 hours'
ORDER BY created_at DESC;
-- GUI traces (buscar passos que falharam)
SELECT message FROM ea_remote_logs
WHERE account_id = X AND message LIKE '%gui_trace%'
ORDER BY created_at DESC LIMIT 10;
| Funcao | Linha | O que faz |
|---|---|---|
LNC_LogRemote(msg) |
134 | Buffer circular de logs (50 msgs) |
LNC_BuildLogJSON() |
141 | JSON array dos logs pra heartbeat |
LNC_BuildDiagnosticsJSON() |
162 | JSON completo de diagnosticos |
LNC_IsSignalProcessed(id) |
222 | Anti-retry: checa se sinal ja foi processado |
LNC_MarkSignalProcessed(id) |
229 | Marca sinal como processado |
ExecuteSingleSignal(json) |
523 | Executa 1 sinal (OPEN/MODIFY/CLOSE via GUI) |
PollAndExecuteSignals() |
968 | Poll /api/signals/pending + executa |
EquityFloor_Check() |
1560 | Circuit breaker: fecha tudo se equity < floor |
OnTimer() |
1597 | Ciclo 200ms: SE + poll + WS + ACK queue |
OnTradeTransaction() |
1739 | Deteccao instantanea (OTT) de deals |
| Funcao | Endpoint | Descricao |
|---|---|---|
WebBridge_Heartbeat() |
POST /api/heartbeat |
Status: balance, equity, positions, logs |
WebBridge_PostSignal() |
POST /api/signals |
Master reporta trade detectado |
WebBridge_PollSignals() |
GET /api/signals/pending |
Slave busca sinais pendentes |
WebBridge_SendAck() |
POST /api/signals/{id}/ack |
Reporta resultado (FILLED/FAILED) |
| Funcao | Linha | Descricao |
|---|---|---|
GUIExecution_Open() |
1183 | Abre posicao via F9 dialog |
GUIExecution_Modify() |
1401 | Modifica SL/TP via Position dialog |
GUIExecution_Close() |
1488 | Fecha posicao via Position dialog |
GUI_Trace(step) |
165 | Acumula passo no trace buffer |
GUI_EnsureTradeTabActive() |
472 | Garante Trade tab ativa |
GUI_AcquireGUILock() |
393 | Lock exclusivo (1 EA por vez) |
| Metodo | Endpoint | Arquivo:Linha | Descricao |
|---|---|---|---|
| POST | /api/signals |
signals.py:96 | Master reporta trade (anti-loop + broadcast) |
| GET | /api/signals/pending |
signals.py:722 | Slave busca sinais pendentes |
| POST | /api/signals/{id}/ack |
signals.py:811 | ACK com validacao V1-V4 |
| POST | /api/heartbeat |
heartbeat.py:201 | Status + logs + positions |
| POST | /api/signals/broadcast |
signals.py:1094 | Dashboard broadcast manual |
| POST | /api/signals/broadcast-close |
signals.py:1532 | Fechar todas posicoes de grupo |
| GET | /api/diagnostics |
heartbeat.py:785 | Diagnosticos de todos EAs |
Checks automaticos no handler de ACK (HTTP + WS):
| Check | Condicao | Acao |
|---|---|---|
| V1: Volume zero | actual_volume < 0.01 em OPEN |
Rejeitar como FAILED + Telegram |
| V2: Open price zero | open_price <= 0 em OPEN |
Rejeitar como FAILED + Telegram |
| V3: Ticket reutilizado | ticket ja existe em trade_links (outro trade_group) |
Rejeitar como FAILED + Telegram |
Implementado em validate_ack_data() (signals.py), chamado por ambos handlers (HTTP e WS).
Apos ambos ACKs de um par AF (_af_ack_count >= 2):
- reconcile_af_pair() verifica: 2 trades, tickets diferentes, direcoes opostas, volumes iguais, open_price > 0
- Se falha: alerta Telegram + bloqueia MODIFY scheduling
Endpoint: POST /api/test/inject (so disponivel quando TEST_INJECTION_ENABLED=true)
signal_source: "test" (vs "ea" no fluxo normal)
O lifecycle normal comeca na Etapa 1 (deteccao de trade no EA master). A injecao de teste entra direto na Etapa 4 (processamento no servidor), pulando as etapas 1-3:
| Aspecto | Fluxo normal | Injecao de teste |
|---|---|---|
| Origem | EA master (OTT ou SnapshotEngine) | POST /api/test/inject (servidor) |
| Auth | X-API-Key (ea_api_key) | Bearer JWT (dash auth) |
signal_source |
"ea" |
"test" |
| Conta target | peers do mesmo group_id (broadcast) |
account_id especifico (obrigatorio is_test=true) |
| Filtro dashboard | Incluido por default | Excluido por default (include_test=false) |
| ACK path | EA Slave → /api/ack |
EA Slave (conta 52) → /api/ack (mesmo handler) |
R10: Sinal
signal_source='test'NUNCA distribui para contais_test=false.
- O handler valida queaccount.is_test=Trueantes de criar qualquerPendingSignal.
- Nao ha broadcast degroup_id— so a conta alvo recebe.
- Invariant CA-7/R1:COUNT(*) FROM pending_signals JOIN signals JOIN accounts WHERE signal_source='test' AND is_test=false = 0SEMPRE.
- Conta 52 (Teste Sandbox) esta emblocked_account_idsdo pairing AF — nunca entra em round real.
A partir da distribuicao (Etapa 5), o fluxo e identico ao normal:
- PendingSignal criado → EA da conta 52 polls/recebe WS → executa via GUI automation → ACK volta com ticket real
- signal_source='test' preservado em todos os registros (Signal, SignalAck, TradeLink)
- Dashboard filtra sinais test por default (include_test=false em /api/signals/history e /api/accounts)
O endpoint so existe quando TEST_INJECTION_ENABLED=true (env). Em producao fica false — qualquer chamada retorna 404.
Isolamento via porta: o runner de testes sobe uvicorn :8001 --lifespan off separado do prod :8000.
Status: ACTIVE | Ultima revisao: S396+6 (2026-06-05) — entra o Entregador (camada de entrega unificada, Onda 6). Antes: S342 trio carteiro fechado.
3 carteiros canonicos cobrem o ciclo de vida de cada sinal; o Entregador (Onda 6) e'
a camada de ENTREGA que LEVA o sinal ja criado ate o EA por 1 molde unico, varias rotas.
Cada um eh um helper unico ("1 cabeca, varias bocas") — wrappers HTTP e WS finos delegam
toda a logica pro mesmo helper, eliminando drift por construcao.
| # | Personagem | Quando dispara | Helper | Spec |
|---|---|---|---|---|
| 1 | Carteiro Canonical (IDA) — CRIA o sinal | Dashboard ou EA cria sinal de trade | create_signal_canonical em server/signal_dispatch.py |
specs/signal-dispatch-canonical.md |
| 2 | Carteiro Conferente (VOLTA) — confere recibo | EA responde se executou (FILLED/FAILED/REJECTED) | apply_ack_canonical em server/ack_helpers.py |
specs/carteiro-conferente-ack.md |
| 3 | Carteiro Plantonista (PLANTAO) — ronda telemetria | EA bate ponto a cada 10s (WS) ou 30s (HTTP) | apply_heartbeat_canonical em server/heartbeat_helpers.py |
specs/carteiro-plantonista-hb.md |
| 4 | Entregador (Onda 6) — LEVA o sinal/avisos ao EA | Apos o Carteiro criar; tambem no poll HTTP e no flush da reconexao | _build_ws_payload (sinal) + build_ea_update_payload (aviso de update) em signal_dispatch.py/ea_ws.py |
specs/signal-dispatch-canonical.md secao "## Onda 6" |
Entregador NAO e' um 4o carteiro do ciclo (IDA/VOLTA/PLANTAO sao fases — criar, conferir,
rondar). Ele e' a CAMADA DE ENTREGA transversal: 1 envelope padrao + N transportes (push WS /
poll HTTP / flush na reconexao). O Carteiro IDA o usa pra empurrar logo apos criar; o HTTP poll
e o flush o usam pra reentregar minutos depois sem passar pela criacao. Foi a entrega divergir
entre rotas (target_login/originso num caminho) que motivou a Onda 6.
[Dashboard/Master EA]
|
| (1) cria sinal
v
+---------+ sinal +-----+
| IDA | ---- via WS ---> | EA |
+---------+ ou HTTP +-----+
^ |
| | (2) executou (ou falhou)
| broadcast WS v
| +---------+
+---------------------- | VOLTA |
+---------+
[PLANTAO roda em paralelo, sempre — 10s ou 30s]
+-----------+ HB telemetria +-----+
| PLANTAO | <---------------- | EA |
+-----------+ (vivo? saudo?) +-----+
create_signal_canonicalArquivo: server/signal_dispatch.py:create_signal_canonical
Quem chama: 20 callsites em routes/signals.py, af/signals.py, routes/heartbeat.py:_reconcile_sl_tp, etc. (python scripts/show_canonical_api.py --callsites)
Origem: S317 Onda 4a (2026-05-02) — substituiu Signal() raw nas rotas e AF engine.
signals (a "encomenda")pending_signals (a "lista de espera ate alguem entregar")trade_links se origem eh originsignal_ack com status=SUCCESS (server-initiated routes)trade_events para auditoriadispatch_pending_to_eas| Tabela | Colunas-chave | Observacao |
|---|---|---|
signals |
id, action, symbol, direction, volume, sl, tp, trade_group_id, origin, signal_source, trace_id |
origin eh OriginType enum (20 valores); signal_source eh SignalSource enum (4 valores) |
pending_signals |
signal_id, account_id, trade_group_id, expires_at, resolved |
TTL via expires_at (PENDING_SIGNAL_TTL_SECONDS) |
trade_links |
signal_id, account_id, local_ticket, is_origin, trade_group_id |
local_ticket so chega no ACK FILLED — IDA cria com NULL e VOLTA preenche |
signal_acks |
(opcional) status=SUCCESS pra server-initiated |
Bypass de aguardo do EA |
trade_events |
event_type=CREATE, origin, context |
Audit trail JSONB |
(account_id, trade_group_id, action) — race UNIQUE constraint detecta duplicata e retorna skipped_duplicate=Truedb.commit() (commit_then_publish pattern, evita idle in transaction)flag_modified() em qualquer mutacao JSONB (pattern P12)OriginType.SERVER_RECONCILE, MASTER_ENTRY, AF_PAIR_ENTRY respeitam invert da conta destinoapply_ack_canonicalArquivo: server/ack_helpers.py:apply_ack_canonical
Quem chama:
- server/routes/signals.py:ack_signal (HTTP POST /api/ack)
- server/routes/ea_ws.py:_handle_ws_ack (WS msg_type=ack)
Origem: S341 (2026-05-12) — refactor extraiu 600 linhas duplicadas entre HTTP e WS handlers.
signal_acks (race-safe via IntegrityError retry com _apply_fields interno)compute_exec_slippage ANTES do commit (1 commit so — D1 canonical)validate_ack_data (V1-V4) e marca status=FAILED se aplicaveltrade_links quando status=FILLED (preenche local_ticket)pending_signals.resolved=True pra acabar o ciclotrade_events (ACK + MODIFY-BUFFER mismatch + CLOSE P&L cascade)reconcile_af_pair + check_and_schedule_modify) quando 2 ACKs FILLED chegam no mesmo trade_group_id AFcommit_then_publish("ack", ...) no final pro dashboard| Tabela | Colunas-chave | Observacao |
|---|---|---|
signal_acks |
signal_id, account_id, status, local_ticket, executed_at, exec_slip_pts, pipeline_slip_pts, bid_at_request, ask_at_request, ea_received_at_ms, trace_id |
Status: FILLED, SUCCESS, FAILED, REJECTED, PARTIAL |
pending_signals |
resolved=True apos terminal status |
Fecha o ciclo da IDA |
trade_links |
local_ticket, close_price, profit, is_closed |
Preenchido aqui (IDA criou com NULL) |
trade_events |
event_type=ACK, context.profit_source |
Cascade deal_ack > broker > cache > unknown |
signals |
so leitura (busca por signal_id) |
Nao escreve |
Auditoria completa em specs/scratch/ack-drift-S341.md. 2 bugs latentes corrigidos:
- SE-1: HTTP nao usava commit_then_publish (B5/P224 nao propagado)
- SE-2: WS nao tinha CLOSE FAILED auto-retry (90% dos ACKs vem por WS)
db aberto, NAO chama db.close() (lifecycle do FastAPI Depends)_apply_fields (race-safe upsert)flag_modified() em qualquer mutacao JSONB (pattern P12)SignalAck.trace_id em INSERT e UPDATE (race recovery cobre branch UPDATE manualmente)apply_heartbeat_canonicalArquivo: server/heartbeat_helpers.py:apply_heartbeat_canonical
Quem chama:
- server/routes/heartbeat.py:receive_heartbeat (HTTP POST /api/heartbeat)
- server/routes/ea_ws.py:_handle_ws_heartbeat (WS msg_type=heartbeat)
Origem: S342 (2026-05-12) — refactor extraiu 1130 linhas duplicadas entre HTTP e WS handlers.
_sanitize_nan recursive (NaN/Inf em qualquer profundidade)broker_name, mt5_server (com guard 2+ chars apos strip)terminal_build change alert via Telegram (SE-3 cure)account.last_heartbeat_at SEMPRE (mesmo no throttle WS — S314.3 Bug 3 fix)_offline_accounts)_reconcile_sl_tp — MODIFY pra peer divergente)_detect_missing_positions com advisory lock)ea_version + sl/tp/open_price)| Tabela | Colunas-chave | Observacao |
|---|---|---|
accounts |
last_heartbeat_at, broker, mt5_server, terminal_build, desired_version, update_attempts, settings_dirty |
UPDATE sempre, throttle so afeta heartbeats row |
heartbeats |
balance, equity, margin, free_margin, positions, ea_version, applied_settings, created_at |
INSERT throttled: WS=30s; HTTP=sempre (polling natural ja 30s) |
open_positions |
account_id, ticket, symbol, direction, volume, profit, sl, tp, open_price, current_price, pips, swap, magic, commission, spread, tick_value, tick_size, profit_at_sl, profit_at_tp |
Upsert por diff incoming vs existing |
equity_snapshots |
balance, equity, margin, positions |
Snapshot permanente cada 5min |
symbol_info |
account_id, symbol, tick_value, tick_size, etc. |
Bulk upsert ON CONFLICT |
trade_events |
event_type=CLOSE, origin=heartbeat_gone, context.profit_source |
Audit trail das posicoes que sumiram |
trade_links |
is_closed=True, closed_at, close_price, profit |
Quando ticket some entre HBs |
Auditoria completa em specs/scratch/hb-drift-S342.md. 6 bugs latentes corrigidos:
- SE-1: WS NaN/Inf so cobria 5+8 campos
- SE-2: WS auto-reg criava broker=""
- SE-3: WS nao detectava terminal_build change
- SE-4: HTTP settings_verify_fail nao usava commit_then_publish (B5/P224)
- SE-5: broadcast HTTP nao incluia ea_version
- SE-6: broadcast HTTP open_positions sem sl/tp/open_price
db aberto, NAO chama db.close() (lifecycle FastAPI/SessionLocal)force_save: bool — HTTP sempre True; WS = should_save_db decide_sanitize_nan recursive cobre dict/list/tuple/set/frozenset (SE-1 cure)commit_then_publish (SE-4 cure)heartbeats table pra 30s/conta — sem perder telemetria (broadcast + last_heartbeat_at continuam em cada HB)Cada carteiro recebe um payload JSON e responde/broadcasta outro. Aqui o que efetivamente trafega entre EA, servidor e dashboard — diferente das tabelas DB acima que mostram o que persiste.
Entrada: chamada Python interna (nao tem JSON IN — eh callee de 20 callsites).
Saida 1 — push WS pro EA (WS_PAYLOAD_FIELDS, 18 campos exatos — schema estrito, contrato com EA):
| Campo | Tipo | Origem | Notas |
|---|---|---|---|
id |
int | Signal.id |
Chave de correlacao com SignalAck |
action |
str | OPEN/MODIFY/CLOSE |
Discriminador do tipo |
ticket |
int | 0 em OPEN, MT5 ticket em MODIFY/CLOSE | |
symbol |
str | XAUUSD, BTCUSD, etc | |
direction |
str | BUY/SELL |
NUNCA invertido aqui (EA aplica invert) |
volume |
float | Lotes | |
price |
float | Preco entrada (0 em CLOSE) | |
sl |
float | Stop Loss (preco absoluto) | |
tp |
float | Take Profit (preco absoluto) | |
sl_distance |
float | Distancia em pontos (alternativa) | |
tp_distance |
float | Distancia em pontos (alternativa) | |
created_at |
str ISO8601+Z | Server time UTC | |
trade_group_id |
str | AF_P5, AF_R9_P3, etc | |
signal_channel |
str|null | Canal origem | Audit trail |
detection_method |
str|null | OTT, SE, manual, AF | Audit trail |
close_reason |
str|null | SL/TP/STOP_OUT/MANUAL | So em CLOSE |
target_login |
int | Account.account_num do alvo |
S396 selo P4: EA confere == ACCOUNT_LOGIN (anti-vazamento entre contas) |
origin |
str | Signal.origin ("" se NULL) |
Onda 6: EXECUTAVEL — EA usa origin=="server_reconcile" pra skipBuffer no MODIFY (LinniuC.mq5:1418/2044) |
Saida 2 — broadcast "new_signal" pro dashboard (dashboard ouve via WS, atualiza aba Sinais).
Entrada AckIn (EA -> servidor, schema Pydantic em server/schemas.py:120):
| Campo | Tipo | Obrigatorio | Notas |
|---|---|---|---|
status |
str | SIM | FILLED, SUCCESS, FAILED, REJECTED, PARTIAL |
local_ticket |
int | NAO (0 default) | Ticket MT5 que o EA gerou |
error_msg |
str | NAO ("" default) | Mensagem de erro se FAILED |
open_price |
float | NAO (0 default) | Preco que o EA conseguiu |
applied_sl |
float | NAO (0 default) | SL que o EA conseguiu setar |
applied_tp |
float | NAO (0 default) | TP idem |
actual_volume |
float | NAO (0 default) | Volume efetivo (partial fill) |
receive_channel |
str|null | NAO | http_poll, ws_push (telemetria latencia) |
gui_duration_ms |
int|null | NAO | Tempo dialog F9 |
ea_received_at_ms |
int|null | NAO (build 3.85+) | T4 epoch ms UTC pra calc latencia exata |
deal_price |
float|null | NAO (v3.63+) | Preco exato do MT5 deal (CLOSE enrichment) |
deal_profit |
float|null | NAO (v3.63+) | P&L do MT5 deal |
bid_at_request |
float|null | NAO (3.85+) | BID via SymbolInfoTick no click GUI (slippage) |
ask_at_request |
float|null | NAO (3.85+) | ASK idem |
Saida 1 — response HTTP (apenas via path HTTP, WS path retorna None): {ok: bool, ack_id: int|null, status: str, errors: list[str]}.
Saida 2 — broadcast "ack" pro dashboard (via commit_then_publish):
| Campo | Tipo | Notas |
|---|---|---|
signal_id |
int | Correlaciona com Signal IDA |
account_id |
int | |
status |
str | Espelha AckIn.status |
action |
str | Vindo do Signal: OPEN/MODIFY/CLOSE |
local_ticket |
int | |
account_name |
str | (so HTTP path enriquece) |
open_price |
float | (so HTTP path enriquece) |
applied_sl |
float | (so HTTP path enriquece) |
applied_tp |
float | (so HTTP path enriquece) |
actual_volume |
float | (so HTTP path enriquece) |
Entrada HeartbeatIn (EA -> servidor, schema Pydantic em server/schemas.py:46):
| Campo | Tipo | Obrigatorio | Notas |
|---|---|---|---|
account_num |
int | SIM | Numero MT5 (chave de auth) |
balance |
float | SIM | Saldo do broker |
equity |
float | SIM | Equity (saldo + P&L flutuante) |
margin |
float | NAO (0 default) | Margem usada |
free_margin |
float | NAO (0 default) | Margem livre |
positions |
int | NAO (0 default) | Quantas posicoes abertas |
server_time |
str (max 64) | NAO | Server time do MT5 |
ea_version |
str (max 32) | NAO | LinniuC_b3.85.5 (broker|build) |
open_positions |
list[PositionItem] | NAO | Array de posicoes abertas (snapshot) |
applied_settings |
dict|null | NAO | Snapshot das configs aplicadas pelo EA |
broker_name |
str | NAO | "Exness", "FTMO", etc |
mt5_server |
str | NAO | Servidor MT5 reportado |
diagnostics |
dict|null | NAO | timer_tick, last_logs, etc (zombie detection) |
symbol_info |
list|null | NAO | tick_value, tick_size, contract_size por simbolo |
chart_symbol |
str|null | NAO | Simbolo atual no chart EA |
chart_bid |
float|null | NAO | BID fresh do MT5 (Olheiro consome) |
chart_ask |
float|null | NAO | ASK fresh idem |
terminal_build |
int|null | NAO (v3.85+) | Build do MT5 — mudanca dispara alerta |
Cada item de open_positions: {ticket, symbol, direction, volume, profit, sl, tp, open_price, current_price, pips, swap, magic, open_time, spread, commission, tick_value, tick_size, profit_at_sl, profit_at_tp} (19 campos).
Saida 1 — response HTTP HeartbeatOut (HTTP-only, enriquecido com auto-update):
| Campo | Tipo | Notas |
|---|---|---|
status |
str | "ok" |
account_id |
int | |
next_heartbeat |
str | Slot de 30s alinhado ao minuto |
wait_seconds |
int | Quanto esperar ate proximo HB |
group_peers |
str | "1:ON:2pos:5s:Exness:12345,2:OFF:0pos:never:..." |
update_available |
bool? | (so se desired_version pendente) |
update_version, update_hash, update_size, update_url, update_force |
str/int/bool | Dados do EaVersion pra EA baixar |
Saida 2 — push WS update_push (WS-only, EA recebe via send_text):
{ "type": "update", "version": "3.85.6", "hash": "...", "size": 102400, "url": "/api/ea/download/3.85.6" }
Saida 3 — broadcast "heartbeat" pro dashboard (super-set unificado, SE-5/SE-6 cures):
| Campo | Tipo | Notas |
|---|---|---|
account_id, name |
int, str | |
balance, equity, positions |
float, float, int | |
ea_version |
str | (SE-5 cure: sempre incluido) |
open_positions |
array | Schema super-set: {ticket, symbol, direction, volume, profit, pips, sl, tp, open_price} (SE-6 cure) |
settings_dirty |
bool | UI mostra badge "sync pendente" |
applied_settings |
dict | (so se truthy) |
diagnostics |
dict | (so se truthy) |
Saida 4 — alerta drawdown (so se dd_pct > max_risk_pct * 2): broadcast {type: drawdown, account_id, account_name, account_num, drawdown_pct, threshold, balance, equity}.
Prometheus REMOVIDO em S367. O endpoint
/metricse os counters
copytrade_*existiam mas NADA os consumia (sem Prometheus/Grafana rodando na
VPS). A observabilidade dos carteiros hoje eh via aba de saude customizada
(/api/health,/api/debug/health-full), logs e alertas Telegram. Se um dia
precisar de tendencia historica/grafico, considerar Netdata (1 binario,
zero-config) em vez de reerguer Prometheus+Grafana.
OPEN/MODIFY/CLOSE (IDA cria, VOLTA confirma)equity, last_heartbeat (PLANTAO atualiza)open_positions (PLANTAO mantem snapshot)mt5_build_change, modify_buffer_mismatch, af_modify_blocked, close_retry_exhausted)/api/debug/health-full (auth JWT) — health consolidado de cada conta com ack_stats_1h, recent_logs, diagnosticscreate_signal_canonical criado, migrou 4 callsites primarioscanonical-carteiro + script show_canonical_api.py (cheatsheet auto-fresh)apply_ack_canonical extraido (5 iters review)apply_heartbeat_canonical extraido (10 iters validate+review — pattern P228 capturado: zero MED/LOW e bar absoluta, nao apenas zero CRITICAL/HIGH)Status: ACTIVE | Adicionado S336+ (2026-05-10, deploy 02:20 UTC) — explica em linguagem leiga + tabela tecnica o que cada um dos 4 campos novos em
signal_ackssignifica.
Imagina que voce pediu uma pizza por delivery por R$50, mas quando chegou cobraram R$52. A diferenca (R$2) é o slippage — o quanto voce foi "pisado" entre o pedido e o recibo. No copy trade, cada operacao real tem 3 momentos de preco:
bid_at_request / ask_at_request)deal_price)signal.price)A telemetria S336+ mede duas distancias:
Ambos sao modulo (sempre positivo) e em pontos (a unidade nativa do simbolo: 1 ponto BTCUSD = 0.01, 1 ponto XAUUSD = 0.01).
Quando a posicao fecha sozinha porque bateu o stop loss ou o take profit, nao houve clique — foi o broker que disparou. Logo nao existe "foto da vitrine" (bid/ask no clique) pra comparar. Antes, esse fechamento ficava sem medicao ("—" no painel) — e esse era o caminho MAIS comum (a maioria das batalhas morre batendo TP/SL).
A correcao S388: pra esse caso a regua certa nao e a cotacao, e o NIVEL pedido. O EA passou a guardar o nivel do stop/alvo que disparou (lendo do historico do broker) junto do preco real do fechamento, e o servidor calcula:
|preco real do fechamento − nivel do stop/alvo| = o quanto o broker honrou MAL o stop que voce pediu.Exemplo real (sandbox, fechamento por SL): voce pediu stop em 73640, o broker fechou exatamente em 73640 → Exec Slip = 0.0 (honrou perfeito). Se tivesse fechado em 73638, seria 0.02 pra baixo (broker escorregou 2 pontos contra voce).
Isso vale pros 3 caminhos de fechamento: ao vivo (deteccao instantanea), por varredura, e ate offline — se o EA estava fora do ar quando o stop bateu, ao voltar ele le do historico e atualiza a medicao. So fechamento MANUAL (sem stop disparado) fica "—" honesto: nao ha "nivel pedido" pra comparar.
Resumo das 3 reguas: abertura/fechamento-por-comando usam a cotacao (foto no clique); fechamento-por-stop usa o nivel; quem nao tem nem clique nem stop (manual) fica "—".
| Campo (signal_acks) | OPEN | MODIFY | CLOSE |
|---|---|---|---|
bid_at_request |
sim — foto do BID 1 tick antes do click | sim — foto antes de aplicar SL/TP novo | sim — foto antes do click |
ask_at_request |
sim — foto do ASK no mesmo instante | sim | sim |
exec_slip_pts |
sim — \|deal_price − ASK\| (BUY) ou \|deal_price − BID\| (SELL) |
NULL — MT5 nao devolve deal_price em MODIFY (sem fill novo) | sim — \|deal_price − BID\| (fechar BUY) ou \|deal_price − ASK\| (fechar SELL) |
pipeline_slip_pts |
sim — \|deal_price − signal.price\| (se signal.price > 0) |
NULL — idem acima | sim |
open_price (deal_price) |
sim — broker reporta | NULL | sim — broker reporta |
applied_sl / applied_tp |
sim | sim — valor novo aplicado | n/a |
actual_volume |
sim | NULL — MODIFY nao altera volume | sim |
gui_duration_ms |
sim | sim | sim |
Os NULLs em MODIFY nao sao bugs — sao consequencias fisicas: MT5 MODIFY so altera SL/TP sem criar deal novo, entao nao ha deal_price pra comparar. A foto bid/ask e capturada mesmo assim pra auditar "o preco passou pelo SL em transito?".
A foto bid/ask vale por 60 segundos (stale_threshold_ms = 60000). Se o tempo entre captura e fill no broker passar disso, o servidor descarta o calculo de exec_slip e seta NULL — mas mantem bid/ask pra audit trail. Assim sempre se sabe "qual era a vitrine quando clicaram".
close_reason="ROLLOVER" (Layer 1 anti-swap)Quando o EA fecha uma posicao perto da meia-noite pra fugir do juro overnight (swap), o Rollover_TryClose tagueia o ticket num buffer em memoria (g_rolloverTagged). O SnapshotEngine consulta esse buffer ao montar o CLOSE signal e:
close_reason → "ROLLOVER" em vez de "MANUAL" (default DEAL_REASON_CLIENT)bid_at_request/ask_at_request no payloadDistingue de Layer 2 servidor (close_reason="ROLLOVER_FALLBACK" via close_pair_positions — fallback se EA falhar). Spec completa: rollover-dupla-camada.md.
bid_at_request ≤ ask_at_request sempre que ambos NOT NULLexec_slip_pts ≥ 0 sempre (modulo)pipeline_slip_pts ≥ exec_slip_pts em 99% dos casos (transporte adiciona ruido, raro subtrair)bid_at_request IS NULL ⇔ ask_at_request IS NULL (par sempre junto)| Coluna | Valor |
|---|---|
| signal_id | 13123 |
| action | OPEN |
| symbol | BTCUSD |
| direction | BUY |
| bid_at_request | 81460.86 |
| ask_at_request | 81465.61 |
| deal_price (open_price) | 81465.82 |
| exec_slip_pts | 0.21 |
| pipeline_slip_pts | NULL (signal.price=0, signal manual) |
Calculo: BUY pega ASK como referencia. \|81465.82 − 81465.61\| = 0.21 pts. Significa que o GUI levou ~0.21 ponto a mais que a vitrine mostrava no momento do click.
No painel /af (modulo af_hedge.js), cada card de signal completo mostra:
close_reason="ROLLOVER" ou "ROLLOVER_FALLBACK"Se ambos slips forem NULL, as linhas sao omitidas (nao aparecem como "—" pra evitar poluicao).
specs/exec-slippage-telemetria.mdspecs/plans/exec-slippage-telemetria-PLAN.mdserver/signal_dispatch.py::compute_exec_slippageInclude/CopyTrade/WebBridge.mqh::SRolloverTagsignal-lifecycle.md linhas 401-413specs/reviews/S336-exec-slippage-REVIEW.mdStatus: ACTIVE | Ultima revisao: S233 (2026-04-14) — addendum S231 invert audit (ver
specs/invert-log.md)
Versao: 3.2
Data: 2026-03-29
Addendum S231 (invert audit): o step "Slave signal via invert pipeline" em
server/af/signals.py::generate_signals_for_pairagora usa_invert_for_peer()(helper centralizado) que alem de aplicar invert grava audit row emsignal_inversionsvia savepoint fire-and-forget. Zero mudanca de regra — so observabilidade. Detalhes:specs/invert-log.md. [drift-flush S233]
Status: CONFIRMADO — Regras core (S67), R-SAFE v1 (S95), Plano Diario + R-SAFE v2 (S96), Invert Dinamico + Pipeline (S99), Push Zone + MODIFY Post-Fill + Anti-Rollover + Post-Round Validation + Price Freshness + DLQ (S150).
Enforced em:server/af/(6 modulos) + o miolo puroserver/af/core/. (Fase 5 do sim-rapido:scripts/hedge/rules.py+scripts/hedge/test_rules.py— o "cerebro paralelo" — foram REMOVIDOS; a regra vive numa fonte unica.)
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| R1 | Pool fixo = N cadeiras | Sempre N contas ativas. Se morre (F1/F2) ou chega funded: compra nova F1 no slot. Nunca mais, nunca menos. Hoje N=12. | Perde throughput (menos) ou gasta atoa (mais) |
| R7 | Nunca same-prop (= mesma Empresa, S362) | NUNCA colocar 2 contas da MESMA Empresa (prop_firms.company) uma contra a outra. S362: compara Empresa, nao o nome completo — 2 Programas diferentes da mesma Empresa (ex: "FTMO Swing" + "FTMO Aggressive", ambas company "FTMO") contam como same-prop e NAO pareiam. Se nao tem par de Empresa diferente, conta espera. SEM FALLBACK. |
Prop detecta hedge = ban |
| R8 | So mesma fase | F1 vs F1, F2 vs F2, Funded vs Funded. Nunca misturar fases. | Risco inconsistente entre fases |
| R12 | Max 6 contas/prop | No maximo 6 contas por pessoa na mesma prop firm. S362: conceitualmente conta por Empresa (a firma ve todos os Programas dela). Limite operacional (nao enforced no pareador). | Regra da prop |
| D6 | Parear por distancia | Ordenar contas por distancia ao target. Parear vizinhos (adjacentes). | Desperdicar trades pareando longe |
| D7 | Impar = 1 ociosa | Se N eh impar, 1 conta descansa no dia. Normal. | N/A |
Roteamento dentro da pool (S392 — nota): a ordem e' entregue pelo NUMERO da conta de cada lado do par (
AfPair.account_a_id/account_b_id->create_signal_canonical(target_account_id=...)), NAO pelo nome do grupo (group_id). Ogroup_ide' so RoTULO de isolamento da dupla; a copia legada que rotearia por nome fica SUPRIMIDA pra conta em pool AF.invertsegue sendo a oposicao do hedge (motor forca o invert do parceiro). Provado em producao (7 batalhas -> exatamente os 2 do par). Detalhe + codigo (arquivo:funcao):specs/af-roteamento-conta-vs-grupo.md.
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| R2 | F1 max = $2.500 | Na fase 1 (Challenge), risco maximo = $2.500 por trade (2.5% de $100k). | Ban pela prop |
| R3 | F2 max depende da prop | Props COM profit days (5ers, City): max $2.000 (2%). Props SEM profit days: max $2.500 (2.5%). | Ban ou trades insuficientes pra profit days |
| R4 | Smart risk | Nao apostar mais que precisa pra fechar. Se falta $800: aposta = ceil($800 / (1 - 0.02)) = $816. Minimo entre teto e distancia. | Overshoot = dinheiro jogado fora |
| R5 | Look-ahead (zona morta) | Antes de apostar, simula resultado. Se deixaria distancia OU colchao entre $0 e $500: reduz o risco. | Conta fica "quase la" com micro-trades inuteis |
| R5b | Spread compensation | No ULTIMO trade (distance <= max_risk + $150): permite risk = ceil(distance/(1-spread)), mesmo que ultrapasse max_risk interno. Safety cap = max_risk + $150: Funding Pips cap=$2,650 (2.65%, margem $350 pro hard breach 3%). Props com max_risk=$5k: cap=$5,150 (nunca atinge). Seguro com qualquer spread. Look-ahead do OPONENTE continua ativo normalmente. S362 #291: o cap deriva de max_risk_pct/100 * prop_size + $150 (_spread_comp_cap, escala por tamanho da conta; em 100k = identico). O cap e mascarado pelo hard-cap final (push_limit), entao a migracao de prop.max_risk USD pra max_risk_pct e zero mudanca de comportamento — so escala correto p/ contas != 100k. |
1 dia a mais por fase |
| R6 | Death trade | Colchao < max risk efetivo = modo death trade. Risk = colchao (aposta o que tem, SL/TP cabe na vida restante). Look-ahead nao protege. Ainda limitado pelo oponente (smart risk). Motivo: apostar mais que o colchao = SL ultrapassa piso DD = conta morre intraday mesmo "ganhando". | Sangrar devagar numa conta moribunda |
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| L1 | Zona morta = $0 a $500 inclusivo | Resultado entre $0 e $500 (inclusive) = zona morta. Precisa ser > $500 pra ser OK. Inclui rem <= 0 (morte por spread): contas com colchao em [max_risk, max_risk*(1+spread)) morreriam sem protecao. LA reduz risk pra manter rem > $500. | Micro-trades inuteis / morte evitavel |
| L2 | Threshold dinamico = max risk | Look-ahead so protege quando distancia OU colchao >= max risk efetivo da conta. Abaixo = 1 trade resolve, ignora look-ahead. F1: threshold $2.500. F2 profit-day (5ers/City): $2.000. F2 normal: $2.500. | Deadlock (S65) se threshold muito baixo |
| L2b | Push Zone = sem LA | Se distancia OU colchao estao na push zone (dentro de push_limit = max_risk + buffer), look-ahead NAO ativa. Conta vai passar ou morrer em 1 trade — LA seria contraproducente. Ver secao "Push Zone" abaixo. |
LA bloqueia fechamento natural |
| L3 | Convergencia 3x | Look-ahead roda ate 3 iteracoes (ajuste num check pode afetar outro). | Resultado sub-otimo |
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| D1 | Morte = breach real | Conta so morre quando balance cai ABAIXO do piso de DD ($90k com 10% DD). Nao tem "eutanasia antecipada". | Reciclar conta que ainda tem chance |
| D2 | Reciclar = nova F1 | Conta morta: comprar nova prop, voltar pra F1 ($100k). Slot preenchido. | Slot desperdicado |
| D3 | Funded = reciclar | Conta que passa F2: FUNDED! Comprar nova F1 pro slot. | Slot parado |
| D4 | F1 → F2 = CONTA NOVA | Passou target F1: status=passed. Prop firma DESATIVA conta F1 e emite NOVA conta com login/senha diferentes pra F2. Usuario cadastra nova conta manualmente no pool como F2. NUNCA promover mesma conta automaticamente — sao contas fisicamente diferentes no broker. |
Falso funded, equity errada |
| D4b | Cada fase = conta separada | Equity da conta F1 eh IRRELEVANTE pra F2 (conta F1 ja desativada). _has_passed() avalia o balance efetivo via _bal(pa): em modo live/demo usa _real_balance (sincronizado do heartbeat, transiente — nunca persistido), em simulacao usa virtual_balance. Virtual balance rastreia APENAS P&L do AF (recalc via recalc_virtual_balance()). P27: mundos separados — NUNCA misturar virtual com real. |
Avaliar com equity errada |
| D5 | Spread = 2% | XAUUSD: ganha = risk * (1 - 0.02), perde = risk * (1 + 0.02). Custo real por trade. | Simulacao nao reflete realidade |
| D6 | Transicao = dia seguinte | TODA transicao (passed, morte, funded) tem 1 dia de gap. Conta nova entra no pool no dia SEGUINTE. Slot fica vazio no dia da transicao. | Na realidade leva dias pra comprar prop |
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| P1 | Profit days | 5ers e City: 3 dias lucrativos (> $500 cada). Demais: sem exigencia. Se ja bateu target mas falta dias: atrasa 1 dia (micro-lote, sem hedge). Maioria bate naturalmente. | Rejeicao pela prop |
| P2 | Min dias trading | Varia por prop (3 a 5 dias F1/F2). Completar com 0.01 lote se atingiu target antes. Na simulacao: +1 dia, zero impacto no balance. | Rejeicao pela prop |
| P3 | Tipo DD diario | Equity (5ers, FTMO, Bright, FundingPips) = queda do pico do dia. Balance (City, FundedNext) = perda liquida. Com 1 trade/dia: nao faz diferenca. So importa com 2+ trades/dia. | Daily DD breach |
Filtro: steps == '2-step'
S389-cont5 (2026-05-30): removido o termo
max_risk_pct >= 2.5%do filtro de
elegibilidade — era a ULTIMA curadoria legada (mesma classe domax_dd >= 10
removido no S382: herdada da era do "10% fixo", NAO-validada). Risco minimo NAO
eh requisito de pool. Os requisitos REAIS sao: mesmo TAMANHO (prop_size, nivel da
conta), mesma FASE (R8, agrupada no pareamento) e mesmos STEPS (2-step, unico
requisito de prop firm — mantido). Mesa apertada (ex: Alpha 1.5%) opera SEGURA: o
motor cap no teto dela via a FKprop_firm_id -> prop_firms.max_risk_pct(S389
_ROUND_PROP_CACHE). ANTES do S389 ela furava (operava a 2.5% da pool), por isso a
curadoria fazia sentido defensivo; com o S389 virou restricao indevida. Decisao do
usuario.is_af_eligible = (steps == '2-step')— ver_compute_af_eligible.S382b (2026-05-27):
is_af_eligiblevirou TRAVA DURA de entrada (antes era so
filtro de catalogo, com fallback que deixava mesa nao-elegivel entrar). Agora mesa com
is_af_eligible=falseNAO entra em pool nenhuma — bloqueada em
_create_chairs_from_accounts(pula com motivo) eassign_account(400), via helper
_resolve_eligible_prop. Contas JA sentadas nao sao afetadas (trava so vale pra ENTRADA
de conta nova). Decisao do usuario S382.S382 (2026-05-27): o portao
max_dd >= 10%foi REMOVIDO do filtro de
elegibilidade. Era curadoria preventiva NAO-validada (herdada da era do "10% fixo"),
nao uma trava de seguranca real — cada mesa usa seu PROPRIO piso de morte
(_max_dd_val = prop_size*(1-max_dd/100), lido por-mesa). Mesa de 8% validada no
simulador E2E: morre no piso correto (92k em 100k) e o lado perdedor sobrevive ate o
par fechar. Verspecs/prop-firm-dd-form-redesign-S382.md.max_riskmigrado de USD
($2.500) pramax_risk_pct(2.5%) em S358/S362.
| # | Prop | Preco | F1% | F2% | Max Risk | Profit Days |
|---|---|---|---|---|---|---|
| 1 | The 5%ers | $545 | 8 | 5 | $5.000 | 3 lucrativos |
| 2 | FTMO Swing | $635 | 10 | 5 | $5.000 | nao |
| 3 | BrightFunded | $582 | 8 | 5 | $5.000 | nao |
| 4 | City Traders | $689 | 10 | 5 | $5.000 | 3 lucrativos |
| 5 | FundedNext | $549 | 8 | 5 | $5.000 | nao |
| 6 | Funding Pips | $529 | 8 | 5 | $2.500 | nao |
12 cadeiras = CLEAN_PROPS x 2 (2 contas por prop)
Excluidas:
- ~~Alpha Pro: max_risk $1.500 < $2.500 base~~ — S389-cont5: NAO mais excluida (portao max_risk>=2.5% removido; opera capada em 1.5% via S389 _ROUND_PROP_CACHE, validado no sim: par Alpha cap em $1.500 vs $2.500 das demais)
- ~~FTP Classic: max_dd 8% < 10%~~ — S382: NAO mais excluida (portao max_dd removido; 8% e' valido, validado no sim)
- (hoje so mesas NAO-2-step ficam de fora — unico requisito estrutural)
Pares podem ser same-person (5ers-Linniu vs FTMO-Linniu). Pipeline copy-trade usa invert pra hedge. Se ambos tem mesmo invert → mesma direcao → nao eh hedge.
Regra: AF engine garante master.invert != slave.invert antes de criar signal. Se iguais, flipa o slave.
| Passo | Acao |
|---|---|
| 1 | Sortear master/slave (aleatorio) |
| 2 | Group_id unico: AF_P{pool}_R{round}_P{pair} |
| 3 | Se master.invert == slave.invert → flip slave no DB |
| 4 | Sortear direcao (BUY/SELL) + anti-OSB check |
| 5 | Criar signal do master via pipeline copy-trade |
| 6 | Servidor aplica invert → slave recebe oposto |
Pre-condicao flip: 0 open positions na conta (Rule 6 invert-rules.md). Se tem posicao → skip par.
Pares same-person validos. Unica restricao: R7 (nunca same-prop).
Por que pipeline copy-trade: Trade_links automaticos, CLOSE propagacao, anti-echo, 70+ sessoes testado.
Quando uma conta esta muito perto de passar ou morrer, o risco normal + look-ahead podem ser contraproducentes (reduzir risco = mais trades = mais spread = mais chances de nao fechar). A push zone permite risco levemente acima do max_risk pra fechar em 1 trade.
| Aspecto | Detalhe |
|---|---|
| Constante | PUSH_BUFFER_PCT = 0.001 (0.1% de $100k = $100 para XAUUSD) |
| Limite | push_limit = max_risk + buffer ($2.600 para F1 XAUUSD) |
| Ativa quando | 0 < distance <= push_limit OU 0 < cushion <= push_limit |
| Efeito no risco | Permite risk > max_risk (ate push_limit) para fechar em 1 trade |
| Efeito no LA | Look-ahead NAO ativa na push zone (L2b) — conta resolve em 1 trade |
| Uso em MODIFY | Detecta near-pass/near-death para agendar MODIFY pos-fill |
| Config por simbolo | presets.py: BTCUSD usa buffer diferente de XAUUSD |
Exemplo: Conta F1 com distance=$2.550 (dentro de push_limit=$2.600). Risco normal seria limitado a $2.500 (max_risk). Na push zone: risk = ceil($2.550 / 0.98) = $2.602. Fecha em 1 trade ao inves de 2 (economia de 1 dia + 1 spread).
Apos AMBOS os lados de um par serem FILLED (executados), o servidor pode agendar um MODIFY (ajuste de SL/TP) para otimizar o fechamento — especialmente quando o par esta na push zone.
| Aspecto | Detalhe |
|---|---|
| Trigger | Ambos sides FILLED + par na push zone (near-pass ou near-death) |
| Delay | 30-120s apos fill (configuravel) — parece humano |
| Resultado | Novos sinais MODIFY com SL/TP ajustados |
| Log | Decisao salva em risk_detail do par |
| Regra | Protecao |
|---|---|
| F1: Anti-duplicata | Flag modify_scheduled impede MODIFY duplicado no mesmo par |
| F2/F3: Cap pelo PERDEDOR (S258) | desired_loss/desired_gain limitados a min(cushion_ou_dist, risk_max(perdedor) + push_buffer_usd) + margin. No hedge, ganho_vencedor == perda_perdedor: quem paga (perdedor) define o cap. Push buffer autoriza zona push near-target/near-death (+$100 default), coerente com get_push_limit usado pra ATIVAR. Regra antiga min(max_a, max_b) era aplicacao indevida da regra de abertura no pos-fill — punia injustamente vencedor fraco. Margin ($2-10) preservada. Em near_death o bug antigo era dormente (c pequeno raramente ativa cap); em near_target materializou no Pool 20 R3 BrightFunded ficando a $23 de passar F1. |
| F4: Sanidade | Shift maximo de SL/TP = 2x distancia original. Rejeita ajustes extremos |
| F5: Limite original | Risco pos-MODIFY nao pode exceder risco original do par |
| F6: Margem minima | Skip MODIFY se par ja esta dentro de margin_max do target |
| F7: Buffer SL | MODIFY SL desconta sl_buffer ($1.00 XAUUSD) da distancia — EA aplica buffer ao SL inclusive no MODIFY, entao sl_dist = desired/vol - buffer. Sem isso, perda real = desired + buffer_cost. So aplica pra SL (death), nao TP (target). |
Fluxo:
Ambos FILLED → Detecta push zone → Delay 30-120s → Calcula novo SL/TP
→ F1 (duplicata?) → F2/F3 (mais fraco) → F4 (sanidade) → F5 (limite)
→ F6 (ja proximo?) → Cria sinais MODIFY → EA aplica via GUI
| # | Regra | Detalhe |
|---|---|---|
| E1 | 1 trade/dia/conta | Maximo 1 operacao por conta por dia |
| E2 | SL = TP simetrico | Valor aleatorio $25-$50. Buffer $1 no SL |
| E3 | Anti-padrao | Variar SL/TP aleatoriamente a cada trade |
| E4 | Janela de abertura | 22:30 UTC (30min apos Sydney) ate 12:00 UTC (NY forex open). = 13.5h. Ordens abertas fecham por SL/TP a qualquer hora |
| E5 | Plano diario | Servidor gera plano completo no rollover (~22:00 UTC). Usuario aprova via Telegram antes de executar |
| E6 | Pre-check heartbeat | ANTES de enviar signal a um par, checar heartbeat de AMBAS as contas (< 90s). Se uma offline: notificar, esperar, skip se timeout |
| E7 | Retry com price gate | Se uma conta do par falha: retry via TV WS streaming. So abre se |
| E8 | EA = executor (excecao time-critical) | Regra geral: EA executa ordens via GUI e reporta status, servidor decide. Excecao: operacoes time-critical onde latencia de rede eh intoleravel (ex: E9 rollover close) — EA eh PRIMARY, servidor eh fallback+alerta |
| E9 | Rollover Dupla Camada (dual-layer swap close) | Camada 1 (EA primary, v3.72.0+): EA fecha posicoes dentro da janela [swap - min_before, swap - max_before] (default [21:00, 21:30] UTC). Horario sorteado POR POSICAO via DJB2 hash deterministico (account+ticket+UTC_date) % window_sec — reproducivel, sem persistencia, dispersa fechamentos entre posicoes pra evitar timing correlacionado. Camada 2 (servidor fallback): se qualquer pair AF ainda estiver executing em T-min_before (default T-30min), servidor forca close emergencial via check_rollover_fallback + Telegram CRITICAL rollover_fallback_fired + safecheck log. EA offline/drift/GUI fail: camada 1 emite WS rollover_close_failed ou rollover_drift e servidor cobre. SSoT detalhado: specs/rollover-dupla-camada.md. |
| E10 | Anti-self-hedge | Antes de gerar sinais, verifica se NENHUMA das contas do par tem posicao aberta. Se alguma tem: skip o par. Tambem filtrado no pareamento (contas com posicoes removidas do pool). Previne hedge contra si mesmo |
22:00 UTC (rollover):
1. Servidor coleta estado atualizado de todas as contas (balance, fase, distancia, colchao)
2. Gera plano:
a. Quais contas estao ativas (nao em transicao, nao mortas)
b. Pareamento (fase, distancia, never same-prop)
c. Risco por par (smart risk, LA, death trade, R-SAFE4)
d. Horarios de execucao (R-SAFE6: espacamento entre siblings)
e. Ordem aleatoria entre siblings da mesma prop
3. Envia resumo compacto via Telegram
4. Aguarda aprovacao do usuario
APROVACAO:
[Aprovar] -> execucao automatica nos horarios planejados
[Skip dia] -> nenhum trade executa
[Replanejar] -> recalcula plano com janela restante
(bloqueado se ha trades abertos da rodada)
Sem timeout — plano fica pendente ate resposta.
Se demorou: [Replanejar] gera novo plano a partir do horario atual.
PLANO 20/Mar — 6 pares, 12 contas
P1: 5ers-A vs FTMO-B | F1 | $2,500 | ~22:40
P2: City-C vs Bright-D | F2 | $1,920 | ~01:15
P3: 5ers-E vs FNext-F | F1 | $2,500 | ~05:45
P4: FTMO-G vs FPips-H | F1 | $2,500 | ~08:00
P5: 5ers-I vs City-J | F1 | $1,200 | ~10:30
P6: Bright-K vs FNext-L | F2 | $2,500 | ~11:45
Same-prop: 5ers(3): 7h+5h | FTMO(2): 9.3h
[Aprovar] [Skip dia] [Replanejar]
ANTES de enviar signal ao par:
Conta A: heartbeat < 90s? SIM
Conta B: heartbeat < 90s? SIM
-> Ambas online: prosseguir
Conta B: heartbeat > 90s? -> OFFLINE
-> Notificar Telegram: "Conta B offline, esperando..."
-> Esperar ate ficar online (sem timeout fixo, janela de abertura limita)
-> Se nao voltar antes do proximo par: skip este par
Signal enviado pra A e B (trade_group_id compartilhado)
A executa, ACK recebido (entry_price salvo)
B falha (GUI timeout, ACK fail)
Retry via TradingView WS (streaming, tempo real):
A cada tick: B online (HB < 90s) E |tv_price - entry_A| <= $2?
SIM -> re-envia signal pra B com MESMO trade_group_id
NAO -> continua esperando
Timer random(60, 90)s expira sem B abrir:
-> Fecha A via auto-close (origin="server_auto_close")
-> Notifica Telegram
Cenarios:
A ok B ok -> hedge OK
A ok B falha preco OK -> retry abre B, hedge OK
A ok B falha preco >$2 -> timeout, fecha A
A falha B falha -> nada abriu, notifica, skip
Price gate $2 (XAUUSD): Conservador. Cobre ~15-30s de movimento em NY. Garante hedge simetrico.
Futuro (BTCUSD, USDJPY): Threshold em % do preco em vez de USD fixo.
| # | Regra | Detalhe |
|---|---|---|
| S1 | 2 fases | Simulador DEVE modelar F1 E F2 separadamente |
| S2 | Same-phase pareamento | Respeitar R8 |
| S3 | Spread > 0 | Min 1%, padrao 2%. Spread 0% so pra sanity check |
| S4 | Min 500 seeds | Monte Carlo com >= 500 seeds |
| S5 | Float (decimal) | Balances e riscos em float. Sem arredondamento. |
| S6 | Custo reciclagem | Cada morte ou funded = custo da prop (preco normal, sem desconto). ROI desconta TODOS os custos. |
| S7 | Profit days tracking | Props com min_profit_days: contar dias lucrativos separadamente. Conta so passa quando balance >= target E profit_days >= exigencia. Na pratica, com risk $2k, matematicamente garantido bater 3 profit days antes do target. |
Principio: VARIACAO MAXIMA — Entre quaisquer contas same-prop (pessoas diferentes, pares diferentes), nenhuma dimensao observavel pode ter correlacao estatistica. O foco: nunca parecer copy trade.
Aplica-se quando: 2+ contas da mesma prop (pessoas diferentes) estao ativas no mesmo dia — INDEPENDENTE DA FASE. F1 vs F2 da mesma prop TAMBEM precisa divergir. A prop nao sabe o que eh "F1" ou "F2" — ela ve TODAS as contas.
Setup: PCs locais separados por pessoa (IP diferente, KYC diferente). Servidor coordena, EA so executa (E8).
Implementacao: Range Valido. Calcular o conjunto de valores validos ANTES de sortear — garantido encontrar se existir, zero tentativa-e-erro.
| # | Regra | Detalhe | Threshold | Se violar |
|---|---|---|---|---|
| R-SAFE1 | Espacamento minimo | Minimo random(1h, 2h) entre execucao de same-prop siblings. Planejado no rollover como parte do plano diario (E5). Sempre tenta agendar todos os siblings. | >= 1h (randomizado) | Prop ve abertura correlacionada |
| R-SAFE2 | Price gate | Preco atual deve diferir do preco de entrada do sibling em >= rsafe2_price_gate_pct * price / 100 (XAUUSD: 0.35%, BTCUSD: 0.08%, _default: 0.30%). Se nao moveu: retry a cada 5min, ate 60x (rsafe2_max_retries). Se fim da janela (12:00 no timezone do pool, via trading_tz_offset) OU max retries: skip (terminal). S255: threshold em % (era USD) — auto-escala cross-symbol. |
>= 0.35% (XAUUSD), 0.08% (BTCUSD) | Prop ve mesmo preco de entrada |
| R-SAFE3 | Lote != sibling | Lote resultante DEVE diferir do lote do sibling. Se igual: re-rolar SL/TP distance ate lote divergir. Hard check APOS todo calculo de risco. Lote = risk / (SL_distance * point_value) — SL/TP distance eh o motor principal de divergencia de lote. | >= 0.05 lots | Prop ve mesmo volume |
| R-SAFE4 | Risco variavel | DESATIVADO (S172). Reduzia risco em 20% quando siblings tinham risco < $100 diferenca. Removido: SL distance aleatorio ja cria variacao suficiente. | ~~< $100~~ | ~~P&L~~ |
| R-SAFE5 | SL/TP nivel absoluto != sibling | O NIVEL DE PRECO do SL/TP (nao a distancia) deve diferir >= $10 do sibling. Garante que closes acontecem em precos diferentes. Se nao cabe: skip a conta (sibling que ja executou fica OK). | >= $10 USD nivel absoluto | Prop ve close correlacionado |
| R-SAFE6 | Horario variavel (graph coloring) | Usa graph coloring pra atribuir slots a pares same-prop. rsafe_gap_min (1h), rsafe_gap_max (2h) — gap randomizado por par. Jitter 5-55s por par dentro do slot (anti-robotico). Ordem dos slots randomizada a cada rodada. Janela definida por trading_start/trading_end e trading_tz_offset do pool. |
Janela configuravel, min ~1-2h | Prop detecta padrao temporal |
Problema da versao anterior (S95): Checar distancia SL/TP (>= $3) nao garante closes em precos diferentes. Entry diferente + distancia diferente podem se cancelar:
Conta A: entry $3,050, SL dist $28 -> SL em $3,022
Conta B: entry $3,063, SL dist $41 -> SL em $3,022 <- MESMO nivel!
Distancia diferiu $13 (OK), mas nivel absoluto IDENTICO (PROBLEMA).
Solucao (S96): Checar o nivel absoluto do SL/TP (onde a ordem realmente fecha):
SL_level_A = entry_A - SL_dist_A (se BUY)
SL_level_B = entry_B - SL_dist_B (se BUY)
|SL_level_A - SL_level_B| >= $10 <- o que a prop realmente ve
TP_level_A = entry_A + TP_dist_A (se BUY)
TP_level_B = entry_B + TP_dist_B (se BUY)
|TP_level_A - TP_level_B| >= $10
Se nao cabe (range valido de SL/TP nao satisfaz >= $10 de nivel absoluto): skip a conta.
O sibling que ja executou continua normal — apenas o que nao conseguiu divergir fica ocioso.
Integrado com o plano diario (E5). Nao eh por sessao (robotico). Nao eh proporcional exato (robotico).
Regras:
- Janela: 22:30 UTC (30min apos Sydney open) ate 12:00 UTC (NY forex open)
- Espacamento entre siblings: minimo random(1h, 2h) por par de siblings, nao fixo
- Gaps irregulares: as vezes 1.5h, as vezes 8h. Sem padrao
- Ordem aleatoria cada dia (quem opera primeiro eh sorteado)
- Todos os siblings sao SEMPRE agendados. Skip so acontece se preco nao coopera no polling
Exemplo com 3 siblings (janela 13.5h):
Dia 1: 22:40, 01:15, 09:50 (gaps: 2.5h, 8.5h) ordem: C, A, B
Dia 2: 23:55, 06:20, 07:45 (gaps: 6.4h, 1.4h) ordem: B, C, A
Dia 3: 00:10, 04:30, 11:50 (gaps: 4.3h, 7.3h) ordem: A, B, C
Quando chega o horario de um sibling e o preco nao moveu >= gate do sibling anterior:
Timer encerra (ex: 05:45)
-> Polling a cada 5 min (rsafe2_retry_min, configurable)
-> A cada check: |preco_atual - entry_sibling| >= rsafe2_price_gate_pct * price / 100?
SIM -> executa (com jitter R-SAFE6)
NAO -> re-agenda +5min, incrementa retry counter
Fim da janela (12:00 no timezone do pool, via trading_tz_offset)
OU max retries (60, configurable):
-> Skip terminal (status = skipped_rsafe)
Gates por simbolo (presets.py, S255 em %): XAUUSD: 0.35% | BTCUSD: 0.08% | _default: 0.30%
Configuravel por pool: rsafe2_price_gate_pct no config JSONB (override via API ou dashboard). S255: migrado USD -> % pra auto-escalar cross-symbol.
A ordem entre same-prop siblings eh ALEATORIA. Nao eh sempre "Pessoa A primeiro, Pessoa B depois".
Cada dia, o servidor sorteia quem opera primeiro. Isso impede o padrao "Account X sempre precede Account Y".
Com 3 pessoas: ordem A-B-C, B-C-A, C-A-B, etc. Tudo aleatorio.
PARA CADA conta same-prop que vai operar apos siblings (ordem aleatoria):
1. Esperar horario planejado (R-SAFE6, definido no plano diario)
2. Checar preco vs TODOS os siblings ja executados via TradingView WS
(R-SAFE2: |preco_atual - preco_sibling_N| >= $10 pra cada N?)
- NAO: esperar ticks ate mover. Delay random(30s, 180s) apos gate.
- Fim da janela sem mover: skip
3. Calcular risco normalmente (smart risk, LA, death trade, etc.)
4. R-SAFE4: checar risco vs TODOS os siblings
- Se |risco - risco_sibling_N| < $100 pra algum N:
risco *= random(0.80, 0.90)
5. Calcular range valido de SL/TP:
- Range base: $25-50
- Pra cada sibling: calcular nivel absoluto e excluir SL/TP que ficaria
a menos de $10 do nivel absoluto de qualquer sibling
- Range valido = base - todas exclusoes
- Se range vazio: skip a conta
6. Sortear SL/TP do range valido
7. Calcular lote = risco / (SL/TP_distance * valor_ponto)
8. Checar lote vs TODOS os siblings:
- Se |lote - lote_sibling_N| < 0.05 pra algum N:
re-sortear SL/TP do range valido (novo valor muda lote)
Se persistir: aceitar (risco/preco ja divergiram)
9. Pre-check heartbeat (E6): ambas contas do par online?
10. Executar ordem. Se falha: retry com price gate $2 (E7)
R-SAFE2 (entry >= $10 diferente) + R-SAFE5 (SL/TP nivel absoluto >= $10 diferente) = closes em precos diferentes = closes em momentos diferentes. Garantido, nao por consequencia.
Funciona com N pessoas. Cada adicional = +1 slot de tempo, +1 check pairwise.
3 pessoas: todos checks sao pairwise (A vs B, A vs C, B vs C).
Coincidencia de direcao (ambos BUY no mesmo dia) NAO eh problema. Cada sibling esta num par diferente — direcao determinada pelo par (anti-OSB). Traders independentes podem comprar no mesmo dia naturalmente.
Padroes que nao sao de correlacao same-prop, mas de "parecer humano". Baixa prioridade — implementar quando R-SAFE1-6 estiver rodando em producao. TODOs #105-107.
No Monte Carlo (50/50, sem preco real), R-SAFE eh modelado como:
- Skip rate: ~10% dos trades second-pair (R-SAFE1+R-SAFE2 combinados)
- Risk reduction: Quando risco proximo do sibling (< $100), multiplicador 0.80-0.90
- Impacto medido (500 seeds): -8% funded, -6% ROI vs baseline. Aceito como custo de seguranca.
- Horario, SL/TP absoluto e lote sao detalhes de execucao sem impacto no 50/50
Apos cada rodada completar, o servidor roda 3 camadas de validacao automatica (run_post_round_validation()):
| Camada | Nome | Checks | Detalhe |
|---|---|---|---|
| L3 | Invariantes | I1-I11 | Contas, pares, sanidade: contas ativas no pool, pares validos, sem orfaos |
| L4 | Audit R-SAFE | I12-I17 | Compliance R-SAFE: espacamento (R-SAFE1), preco (R-SAFE2), volume (R-SAFE3), risco (R-SAFE4), niveis SL/TP (R-SAFE5) |
| L5 | Health | H1-H5 | Dead letters, timeouts, precos stale, falhas de margem, heartbeats ausentes |
post_round_validation no audit logSinais que falharam sao registrados na tabela AfDeadLetter para auditoria:
| Campo | Valores |
|---|---|
| failure_type | timeout, margin, rsafe, invert, gui_fail, e7_exhausted |
| Contexto | pair_id, account_id, prop_name, reason, attempts, context (JSON) |
volume = risk_usd / ((sl_distance + sl_buffer) / tick_size * tick_value)
sl_buffer = $1 — garante que TP trigger antes do SL no par (safety margin)Cada simbolo pode ter defaults diferentes via presets.py. Valores customizados por simbolo sao mergeados com config do banco: defaults ← preset ← database.
| Parametro | XAUUSD (default) | BTCUSD |
|---|---|---|
sl_min_pct / sl_max_pct (S255) |
0.85% / 1.70% | 0.65% / 1.60% |
dz_min / dz_max |
$500 / $1000 | $500 / $1000 |
rsafe2_price_gate_pct (S255) |
0.35% | 0.08% |
rsafe5_min_gap_pct (S255) |
0.35% | 0.30% |
push_buffer_usd |
$100 | $100 |
spread_pct |
2% | 3% |
S255 USD -> %: 4 thresholds price-facing migrados pra % do preco (auto-escala cross-symbol).
dz_min/dz_maxpermanecem USD (equity-facing). Verusd-to-pct-migration.md.
O codigo AF esta modularizado em 6 arquivos no diretorio server/af/:
| Camada | Modulo | Responsabilidade |
|---|---|---|
| Layer 0 | constants.py |
Constantes puras, sem imports internos |
| Layer 1 | risk.py |
Calculo de risco, R-SAFE |
| Layer 1 | pairing.py |
Pareamento de contas |
| Layer 1 | audit.py |
Invariantes, validacao pos-rodada |
| Layer 2 | lifecycle.py |
Orquestracao de rodadas, lifecycle |
| Layer 2 | signals.py |
Comunicacao com broker (OPEN/MODIFY/CLOSE) |
| Helper | presets.py |
Defaults por simbolo |
INICIO DO DIA:
1. Adicionar novas F1 (slots reciclados ontem)
2. Checar passes (balance >= target + profit days ok) → promover F1→F2 ou FUNDED
3. Checar mortes (balance < piso DD) → marcar pra reciclar amanha
4. Parear contas restantes (por fase, distancia, same-prop)
5. Calcular risco (smart risk + look-ahead)
6. Executar trades (50/50)
7. Atualizar balances (float)
FIM DO DIA
| Evento | Sistema faz | Usuario faz |
|---|---|---|
| Conta bate meta F1 | Sai do pool (stand-by), Telegram avisa | Esperar prop aprovar, cadastrar nova conta F2 |
| Conta bate meta F2 | Sai do pool (funded), Telegram avisa | Solicitar payout, comprar nova F1 pro slot |
| Conta morre | Sai do pool (dead), Telegram avisa | Comprar novo teste (~$500+) |
Transicoes NAO sao automaticas. Toda mudanca de conta/fase depende de acao humana.
| Momento | Funcao | O que checa | Arquivo |
|---|---|---|---|
| Inicio da rodada | process_round() |
sync_real_balances → check_passes → check_deaths → pair_accounts |
lifecycle.py:200-211 |
| Apos CADA trade | process_trade_result() |
Atualiza P&L → check_deaths → check_passes |
lifecycle.py:607-608 |
| Regenerar | regenerate_round() |
sync_real_balances → check_passes → check_deaths → re-pair |
lifecycle.py:407-408 |
| Round completo | process_trade_result() |
Se todos pares completaram → round_completed → run_post_round_validation → auto-gera proxima rodada |
lifecycle.py:617-650 |
Fluxo automatico: Rodada completa → nova rodada gera automaticamente → checks rodam no inicio → contas mortas/passadas sao excluidas antes de parear.
Gap: Se pool esta inativo (sem rodadas), mortes entre rodadas NAO sao detectadas ate proxima rodada.
Props flaggam contas que operam sempre na mesma direcao. Sistema sorteia direcao aleatoria com peso crescente:
- 1 seguido = ok | 2 seguidos = alerta | 3+ = critico (forca inversao)
- Se ambas contas do par tem peso alto: prioriza a mais critica
chair_owners no JSONB do pool (chave = # cadeira, valor = nome)owner_colors no JSONB do pool (chave = nome, valor = cor hex)| Evento | Conteudo |
|---|---|
| Par iniciado | Props, direcao, preco, volume, risco |
| Par concluido | Resultado por lado, custo spread |
| Rodada concluida | Resumo W/L, spread total |
| Transicao | Morte/Funded/Passed + detalhes |
| Plano diario | Resumo com pares + horarios (E5) |
Regra (I1): Sistema NUNCA envia OPEN signal pra symbol em mercado fechado.
4 camadas de defesa:
| Camada | Onde | O que faz |
|---|---|---|
| 1. Per-pair guard | main.py:1252 (scheduler) |
Checa is_market_open(pair.symbol) por par (NAO pool.symbol — bug S287) |
| 2. Dashboard badge | routes/af.py + static/js/af_hedge.js |
Endpoint expoe market_status no par; UI mostra badge amarelo "Mercado fechado" |
| 3. Cleanup periodico | _run_af_scheduler a cada 30s |
Pairs scheduled em symbol fechado >30min viram failed_market_closed (ou failed_swap_window em rollover) |
| 4. TV defensivo | is_market_open Camada 4 |
TV WS stale >5min + hardcoded=open = ambiguidade -> circuit breaker 3 strikes -> alerta HIGH |
Por que pair.symbol (nao pool.symbol): AfPair.symbol eh snapshot imutavel no momento de criar par. Pool symbol pode mudar (PUT /api/af/pool/X) mas pares ja criados com symbol antigo precisam ser checados pelo SEU symbol.
Estados terminais novos (TERMINAL_PAIR_STATES):
- failed_market_closed — pair scheduled em symbol fechado >cutoff
- failed_swap_window — pair scheduled em rollover ativo
Categoria separada no daily report (R7): failed_market_closed NAO conta como falha de execucao (sistema funcionou — mercado estava fechado). Renderiza segregado pra nao inflar taxa de erro.
Telegram dedup: f"market_closed:{pool_id}:{symbol}:{YYYYMMDD}" — 1 alerta/pool/symbol/dia.
Bug exposto (S287): Apos PUT /api/af/pool/20 trocando symbol XAU->BTC, par 81 (scheduled XAU) disparou OPEN em sexta 23:25 UTC (XAU fechado). Causa: guard checava pool.symbol (BTC, aberto). Fix: pair.symbol.
O sistema NAO diferencia — logica identica. Demo (Exness): morte = resetar gratis. Real (prop firms): morte = comprar novo teste.
Fluxo de reset (S207, teste only): reset-classification → acha cadeira passed/dead → restaura group_id → seta account_id=NULL → cadeira vira registro historico (prop_config tem snapshot: nome, dono, tipo). Conta fica livre pra re-classificar (#2 se mesmo combo prop/fase/dono). Re-adicionar na pool cria cadeira nova. Dashboard mostra cards historicos com saldo congelado (virtual_balance) e label "Saldo final (historico)". Timer da pool usa paused_at como referencia quando pausada (congela countdowns).
PAREAMENTO (por fase, separado):
F1 pool ──┬── F1 vs F1 (prop diferente, adjacente por distancia)
└── F1 ociosa (se sem par valido)
F2 pool ──┬── F2 vs F2 (prop diferente, adjacente por distancia)
└── F2 ociosa (se sem par valido)
RISCO (hierarquia, primeiro que limita ganha):
1. Teto da fase (F1: $2.500 | F2: $2.000 ou $2.500 conforme prop)
2. Smart risk: min(teto, ceil(dist / (1-spread)))
3. Look-ahead: evitar zona morta $0-$500 (so se >= max risk)
- Distancia < max_risk = reta final (ignora LA distancia)
- Colchao < max_risk = death trade (ignora LA colchao, risk = colchao)
4. Risco do PAR = min(risco_A, risco_B)
CICLO DE VIDA:
Comprar prop → F1 → [passa] → F2 → [passa] → FUNDED ($8k)
↓ ↓
[morre] [morre]
↓ ↓
Nova F1 Nova F1
| Data | Regra | Mudanca | Sessao |
|---|---|---|---|
| 2026-03-04 | R7 | Same-prop proibido, sem fallback | S64 |
| 2026-03-05 | L2 | Reta final fix (dist < 2*DZ skip check) | S65 |
| 2026-03-05 | R8 | Same-phase only (F1vsF1, F2vsF2) | S66 |
| 2026-03-05 | S1 | Simulador DEVE usar 2 fases | S66 |
| 2026-03-05 | R3 | F2 risk depende da prop (profit days: 2%, senao: 2.5%) | S67 |
| 2026-03-05 | R6 | Death trade: colchao < $2.500 = all-in, sem LA | S67 |
| 2026-03-05 | L2 | Threshold unificado $2.500 (distancia E colchao) | S67 |
| 2026-03-05 | D1 | Morte = breach real (balance < piso DD), nao threshold fixo | S67 |
| 2026-03-05 | R13 | REMOVIDA (piso $100 considerado inutil) | S67 |
| 2026-03-05 | R5 boost | REMOVIDA (boost reta final, coberta por L2) | S67 |
| 2026-03-05 | L2 | Threshold dinamico = max risk efetivo (nao fixo $2.500) | S67 |
| 2026-03-05 | R6 | Death trade threshold = max risk efetivo (consistente com L2) | S67 |
| 2026-03-05 | R2 | F1 max = $2.500 universal confirmado (todas props) | S67 |
| 2026-03-05 | — | Funded vs funded: futuro, nao simulado agora | S67 |
| 2026-03-05 | R5b | Spread compensation no fechamento (evita 3o trade em F2) | S67 |
| 2026-03-05 | L1 | Zona morta inclusiva: $0 a $500 (inclusive, precisa > $500) | S67 |
| 2026-03-05 | S5-S7 | Float, custo reciclagem, profit days tracking | S67 |
| 2026-03-05 | §9 | Ordem do dia definida (7 passos) | S67 |
| 2026-03-05 | R5b | Clarificado: seguro pra Funding Pips (hard breach 3%). LA do oponente ativo. | S68 |
| 2026-03-05 | D6 | Clarificado: F1→F2 TAMBEM tem 1 dia de gap (toda transicao) | S68 |
| 2026-03-05 | L1 | Expandido: inclui rem <= 0 (morte por spread). Contas com colchao em [max_risk, max_risk*(1+spread)) agora protegidas. +3.2 LA/seed. | S68 |
| 2026-03-05 | R5b | Safety cap: R5b limitado a max_risk + BUFFER ($150). FP: cap $2,650 (2.65%), margem $350 pro hard breach 3%. Seguro com qualquer spread. | S68 |
| 2026-03-07 | R6 | Death trade: risk = colchao (nao max_risk). Apostar mais que colchao = SL ultrapassa piso DD = morte intraday. Simulacao: 46.4 funded/yr (vs 48.8), 300% ROI (vs 324%), mas numeros realistas. | S69 |
| 2026-03-19 | R-SAFE1-6 | Regras anti-deteccao same-prop: timer minimo 1h, price gate $10-15, lote/risco/SL-TP divergentes, horario variavel Sydney-NY. Check & Re-roll. Principio: variacao maxima. | S95 |
| 2026-03-19 | — | advancing-front-v2.md arquivado. SSoT reduzido pra 2 arquivos: whitepaper + rules.py | S95 |
| 2026-03-19 | R-SAFE4 | Threshold $100 mantido. Sweep (0/50/100/200/500) mostrou custo ~8% funded constante. Seguranca > ROI. | S96 |
| 2026-03-19 | R-SAFE5 | Evoluido: distancia SL/TP (>=$3) -> nivel absoluto SL/TP (>=$10). Garante closes em precos diferentes. | S96 |
| 2026-03-19 | R-SAFE6 | Evoluido: horario generico -> plano diario. Janela 22:30-12:00 UTC (13.5h). Espacamento random min 1-2h. | S96 |
| 2026-03-19 | E4-E8 | Novas regras execucao: janela, plano diario, pre-check HB, retry price gate $2 via TV WS, EA executor. | S96 |
| 2026-03-19 | — | Versao whitepaper 2.0 -> 3.0 | S96 |
| 2026-03-19 | §6b | Invert dinamico por rodada: flip slave se master.invert == slave.invert. Pares same-person validos. Pipeline copy-trade pra signals. | S99 |
| 2026-03-19 | — | Versao whitepaper 3.0 -> 3.1 | S99 |
| 2026-03-29 | §6c | Push Zone: permite risk > max_risk na zona de fechamento (push_limit = max_risk + buffer) | S150 |
| 2026-03-29 | §6d | MODIFY Post-Fill: ajuste SL/TP pos-execucao com framework F1-F6 (mais fraco manda, P57) | S150 |
| 2026-03-29 | L2b | Push Zone Exemption: LA nao ativa se dist/cushion na push zone | S150 |
| 2026-03-29 | E7 | Price freshness (5s max age, 10 retries) + vol mismatch handling | S150 |
| 2026-03-29 | E9 | Anti-rollover: fecha pares antes do swap (janela configurable) | S150 |
| 2026-03-29 | E10 | Anti-self-hedge: skip par se conta tem posicao aberta | S150 |
| 2026-03-29 | D4b | P27: virtual vs real balance — _bal() usa real em live, virtual em sim | S150 |
| 2026-03-29 | R-SAFE2 | Retries (60x max) + price gate por simbolo (BTCUSD: $200) | S150 |
| 2026-03-29 | R-SAFE6 | Graph coloring + jitter 5-55s por par | S150 |
| 2026-03-29 | §10b | Validacao pos-rodada L3-L5 + DLQ + volume formula + presets + arquitetura | S150 |
| 2026-03-29 | — | Versao whitepaper 3.1 -> 3.2 | S150 |
| 2026-04-02 | §11 | Health endpoint: GET /pool/{id}/health — detecta missing snapshots, gap anomalies (>3x gap_max), stuck pairs (>2h executing), balance divergence (>15% virtual vs real). Severidade medium/high. | S173 |
| 2026-04-02 | — | Versao whitepaper 3.2 -> 3.3 | S174 |
| 2026-04-06 | §6d F2/F3 | F2/F3 cap preserva margem: desired = min(cushion, risk_usd) + margin — garante cruzamento floor/target |
S193 |
| 2026-04-06 | §6d F7 | Novo filtro F7: MODIFY SL desconta sl_buffer ($1.00 XAUUSD) — EA aplica buffer no MODIFY | S193 |
| 2026-04-06 | — | Versao whitepaper 3.3 -> 3.4 | S194 |
| 2026-04-19 | §6d F2/F3 | Cap usa risk_max(perdedor) + push_buffer, nao MIN das duas contas. Corrige case Pool 20 R3 BrightFunded F1 ficar a $23 de passar F1 (vencedor fraco com perdedor forte) | S258 |
| 2026-04-19 | — | Versao whitepaper 3.4 -> 3.5 | S258 |
| 2026-05-21 | R7 + R-SAFE | Empresa: R7 e sibling key R-SAFE comparam prop_firms.company (Empresa), nao o nome completo. 2 Programas da mesma Empresa (FTMO Swing + FTMO Aggressive) contam como same-prop. Coluna company obrigatoria. Preventivo (8 firmas distintas hoje = company=name, zero regressao). |
S362 |
| 2026-05-22 | R5b | Cap do R5b + fallback de get_max_risk migrados de prop.max_risk (USD fixo em 100k) pra max_risk_pct/100 * prop_size (_spread_comp_cap). Escala por tamanho da conta; em 100k identico. Cap mascarado pelo push_limit → zero mudanca de comportamento. Pre-requisito do drop da coluna max_risk (USD). |
S362 #291 |
Status: ACTIVE | Ultima revisao: S255 (2026-04-18)
Versao: 1.5
Data: 2026-04-18
Status: Familia R-SAFE completa (R-SAFE1..R-SAFE6), todas ATIVAS. R-SAFE4 foi reativado em v3 (rank-based, multiplicativo) apos rework S172->S2xx. R-SAFE5 e R-SAFE3 unificados em uma passada (R-SAFE5+3). Presets por simbolo (XAUUSD + BTCUSD) introduzidos em server/af/presets.py. S255: 4 thresholds price-facing migrados de USD para % do preco (auto-escala cross-symbol, corrige bug BTC latente).
SSoT para: Regras de anti-deteccao entre contas same-prop no sistema AF
O AF (Advancing Front) roda hedges pareados entre contas de prop firms. Em producao, cada conta esta em uma prop firm/broker DIFERENTE — nenhum broker individual ve multiplas contas.
Porem: a mesma prop firm pode ter 2+ contas (ex: Alpha Pro Linniu + Alpha Pro Lucas). A PROP FIRM consegue ver ambas.
Objetivo: Trades de contas same-prop devem parecer naturais (nao coordenados/roboticos).
┌─────────────────────────────────────────────────────────────────┐
│ O QUE A PROP FIRM VE │
│ │
│ Conta Alpha Pro Linniu: │
│ XAUUSD BUY 1.12 lot SL=2884.50 TP=2898.50 @14:32:05 │
│ │
│ Conta Alpha Pro Lucas: │
│ XAUUSD SELL 0.94 lot SL=2909.20 TP=2895.20 @14:32:08 │
│ │
│ Campos visiveis: Symbol, Direction, Volume, SL, TP, Entry, Time│
│ NAO visiveis: risk em USD, pair ID, trade group, metadata AF │
└─────────────────────────────────────────────────────────────────┘
SL distance randomization eh o mecanismo PRIMARIO de anti-deteccao. Um unico SL aleatorio (faixa XAUUSD $25-$50, BTCUSD $400-$1000) cria variacao em cascata em volume, nivel de SL e nivel de TP. As outras R-SAFE sao redes de seguranca para edge cases — timing, price, volume, niveis e sanidade.
SL distance aleatorio (faixa por simbolo)
│
├──> Volume (lots) varia ~50% ← cascata direta (risk fixo / SL variavel)
├──> SL level varia ate metade do range ← cascata direta (entry + SL distance)
└──> TP level varia ate metade do range ← cascata direta (entry + TP = f(SL))
Resumo em tabela antes do detalhe:
| Regra | Faz | Onde | Quando |
|---|---|---|---|
| R-SAFE1 | Espacar entrada de siblings no tempo (1-2h) | engine.py::_schedule_pairs_rsafe |
Na criacao da rodada (scheduling) — enxerga o DIA (semente) |
| R-SAFE2 | Bloquear entrada se preco muito proximo do sibling (retry ate window end) | signals.py linha ~607 |
Na hora de gerar o sinal — enxerga o DIA |
| R-SAFE3 | Garantir volumes distintos entre siblings (>= 0.05 lot) | signals.py linha ~772 (junto com R-SAFE5) |
Selecao do SL distance |
| R-SAFE4 v3 | Reduzir risk dos siblings subsequentes (rank-based, 0.90-0.95^rank) | signals.py linha ~671 |
Apos calcular risk_usd |
| R-SAFE5 | Garantir SL/TP levels distintos (>= $10 apart) | signals.py linha ~725 (unificado com R-SAFE3) |
Selecao do SL distance |
| R-SAFE6 | Sanity check de volume (detectar erro de unidade pips vs price) | signals.py linha ~837 |
Depois de calcular volume |
Nota: O comentario do codigo em
engine.py::_schedule_pairs_rsafechama a funcao de "R-SAFE1/R-SAFE6". Isso eh legacy —R-SAFE6naquele contexto refere-se ao jitter aleatorio de 5-55s aplicado junto do gap. Nosignals.pyo nomeR-SAFE6foi re-usado para o sanity check de volume. Duas coisas diferentes com o mesmo numero — convivem hoje, renomear eh trabalho futuro.
Pra decidir o que um simulador RAPIDO (moeda 50/50, sem preco real) precisa rodar, as R-SAFE
separam em duas camadas pelo EFEITO no resultado (por FUNCAO, nao pelo numero):
| Camada | Regras | Muda quem ganha/perde? | Papel |
|---|---|---|---|
| DECISAO (com quem parear + quanto arriscar) | R7 (nunca mesma Empresa) · R-SAFE4 v3 (reducao de risco) · R5b (teto) | ✅ sim | decidido ANTES da ordem sair. E' o que uma busca de estrategia otimiza |
| EXECUCAO (como/quando a ordem sai) | R-SAFE1 (espacamento) · R-SAFE2 (price gate) · R-SAFE3 (lote≠) · R-SAFE5 (nivel SL/TP≠) · R-SAFE6 (jitter/sanity de volume) | ❌ nao (no 50/50) | so faz o trade PARECER nao-coordenado pra prop. Nao muda o resultado |
Uso: um sim rapido/matematico roda so a camada de DECISAO (identico ao real no que importa) e
ignora a EXECUCAO (disfarce visual, so relevante no broker real). Excecao: R-SAFE2 tem efeito
colateral no $ (skip ~10% -> ~8% menos funded, whitepaper §10) — mas parelho entre estrategias, nao
muda o ranking. R7 nem e R-SAFE (e' pareamento) mas entra na DECISAO por ser absoluta.
R-SAFE6 e' ambiguo (nota acima): jitter de horario (engine) E sanity de volume (signals) — ambas
em EXECUCAO. A categorizacao e' por FUNCAO, entao a ambiguidade nao atrapalha.
Spec da aplicacao:sim-rapido-deterministico-paridade.md§2b.
| Propriedade | Valor |
|---|---|
| O que faz | Espaca o scheduled_at de pares com propriedade em comum por gap de 1-2h (configuravel) |
| Mecanismo | Graph coloring: constroi grafo de conflito (siblings = props compartilhadas), aloca em slots; primeiro aparecimento = stagger normal, reapareceu = last_time + gap + jitter |
| Configuracoes | rsafe_gap_min=3600, rsafe_gap_max=7200 (segundos); rsafe6_jitter_min=5, rsafe6_jitter_max=55 |
| Localizacao | server/af/engine.py funcao _schedule_pairs_rsafe (linha 347) |
| Quando ativa | Na criacao da rodada (lifecycle.py linha 268 chama o scheduler) |
Por que existe: Se 3 contas same-prop entram em XAUUSD em 60s, a prop firm ve um cluster — obvio que e EA. Espacando 1-2h, o padrao some. Jitter de 5-55s evita ate mesmo granularidade "redonda" (ex: gap exatamente 3600s, 7200s).
Exemplo concreto:
Pool com 3 contas Alpha Pro + 6 outras contas (3 pares total)
Base time = 12:00 UTC
Sem R-SAFE1: 3 pares entrariam em ~12:00, 12:05, 12:10 (stagger normal)
Com R-SAFE1:
Par 1 (contem Alpha Pro) -> 12:03:24
Par 2 (sem Alpha Pro) -> 12:08:11
Par 3 (contem Alpha Pro) -> 13:15:47 (sibling, gap = 4703s, jitter = +22s)
| Propriedade | Valor |
|---|---|
| O que faz | Se o preco atual esta a menos de rsafe2_price_gate_pct * price / 100 do open_price de um sibling, re-agenda o par. Se a janela de trading fechar (ou exceder rsafe2_max_retries), termina com skipped_rsafe (nao-terminal: tenta varias vezes ANTES de desistir). S255: threshold em pct (era USD) — auto-escala por simbolo |
| Mecanismo | Loop sobre irmas_da_janela (a lista do DIA, round_id=None + desde; a lista por rodada sibling_trades segue alimentando R-SAFE3/4/5); se abs(price - sib.open_price) < gate, empurra scheduled_at em rsafe2_retry_min minutos e levanta ValueError (re-agendamento). Quando utcnow() > window_end OU retries >= max_retries, marca skipped_rsafe + dead letter + WS push |
| Window end | trading_end do pool (HH:MM em timezone do pool), convertido pra UTC via trading_tz_offset. Default: 12:00 timezone local. Se ja passou, rola pro proximo dia |
| Configuracoes | rsafe2_price_gate_pct (XAUUSD: 0.35%, BTCUSD: 0.08%, _default: 0.30%), rsafe2_retry_min=5, rsafe2_max_retries=60 |
| Localizacao | server/af/signals.py linhas 607-669 |
| Quando ativa | Inicio do fluxo de geracao de sinal, antes de calcular SL/TP |
Exemplo concreto:
Sibling (Alpha Pro Linniu) abriu XAUUSD @ 2884.50 hoje as 10:00
Agora eh 11:00, preco atual 2886.30
diff = $1.80 < $10 gate -> RE-AGENDA pra 11:05
Em 11:05, preco 2889.10. diff = $4.60 < $10 -> RE-AGENDA pra 11:10
Em 11:10, preco 2895.30. diff = $10.80 >= $10 -> OK, continua.
Se a janela fechar antes do gate abrir: par vira skipped_rsafe (terminal).
| Propriedade | Valor |
|---|---|
| O que faz | Garante |volume_A - volume_B| >= rsafe3_min_lot_diff entre siblings same-prop |
| Mecanismo | Unificado com R-SAFE5 hoje — ambos rodam numa unica enumeracao de candidatos de SL distance (veja "R-SAFE5+3 Unificado" abaixo). Se nao ha candidato que satisfaz ambos, R-SAFE3 eh RELAXADO (prioriza divergencia de niveis) |
| Configuracoes | rsafe3_min_lot_diff=0.05 |
| Localizacao | server/af/signals.py linhas 725-822 (dentro do bloco R-SAFE5+3 unified) |
Mudanca importante: Esta regra foi DESABILITADA em S172 (v1.1 do spec) e REATIVADA em v3 com mecanica diferente. A versao antiga (fator fixo 0.80-0.90 contra o sibling "mais fraco") gerava false positives. A v3 eh rank-based e cumulativa.
| Propriedade | Valor |
|---|---|
| O que faz | Aplica um fator de reducao multiplicativo no risk_usd do par atual, proporcional a quantos siblings same-prop ja abriram trade na mesma rodada com risk similar |
| Mecanismo | Rank = count de siblings na MESMA rodada com |risk_atual - risk_sib| < 100 USD. Se rank > 0, risk_usd *= random.uniform(min, max) ** rank |
| Configuracoes | rsafe4_factor_min=0.90, rsafe4_factor_max=0.95 (5-10% de reducao por nivel de rank) |
| Localizacao | server/af/signals.py linhas 671-694 |
| Ordem natural | Como R-SAFE1 embaralha a ordem de entrada (random slot), o rank nao fica sempre na mesma conta — varia por rodada |
Exemplo concreto (3 contas Alpha Pro na mesma rodada, risk base $1500 cada):
Primeiro sibling abre: risk = $1500 (rank 0, sem reducao)
Segundo sibling abre: rank = 1, factor = 0.93^1 = 0.93 -> $1500 * 0.93 = $1395
Terceiro sibling abre: rank = 2, factor = 0.91^2 = 0.828 -> $1500 * 0.828 = $1242
Por que funciona em hedge (critica antiga): A critica original era "prop firma nao ve risk em USD". Correto — mas a prop ve VOLUME, e volume = risk / SL_distance. Se os 3 siblings usam SL distances aleatorios E risks diferentes, a dispersao de volume fica consideravelmente maior do que a cascata natural do SL sozinho. Custa ~5-10% de risk por nivel (aceitavel), mas diminui acoplamento visivel.
S396 — Excecao decisive (prioridade pra death/win trade): o corte R-SAFE4 NUNCA cai numa conta decisiva (vai morrer: colchao < teto; OU passar: distancia <= teto). Ela precisa do risco cheio pra resolver — cortar transformaria a death em zumbi (sobra um trocado, nao morre) e a win em quase-passou (fica aquem do alvo), desperdicando o round. O corte cai nas irmas NAO-decisivas (ja contadas no rank das que processam depois). Helper puro rsafe4_decisive_exempt(a, b, cfg) (engine.py) + isencao no bloco R-SAFE4. Gateado por rsafe4_decisive_priority (default on, reversivel). A ordem de abertura NAO muda (continua aleatoria via R-SAFE1) — so o ALVO do corte muda, entao nao introduz padrao de ordem detectavel. Residual aceito: como a ordem e aleatoria, as vezes 2 irmas ficam no risco cheio (decisiva + uma nao-decisiva rank 0) — a randomizacao do SL (disfarce PRIMARIO) ainda da volumes distintos. Spec: specs/rsafe4-decisive-priority.md.
| Propriedade | Valor |
|---|---|
| O que faz | Enumera TODOS os candidatos de SL distance dentro do range efetivo (step=0.01% do preco, S255). Pra cada candidato, testa: (a) levels (SL/TP) ficam a pelo menos rsafe5_min_gap_pct * price / 100 de qualquer sibling; (b) volume resultante fica a pelo menos rsafe3_min_lot_diff de qualquer sibling. Pega aleatoriamente entre os que passam em AMBOS. S255: thresholds em pct (era USD) — corrige bug BTC onde $10 = 0.016% (quase inexistente) |
| Fallback degradado | Se nenhum candidato passa nos 2 criterios, relaxa R-SAFE3 (soh exige levels OK). Se mesmo assim nao achar, emite warning e usa o SL inicial (sorteado antes) |
| Configuracoes | rsafe5_min_gap_pct (XAUUSD: 0.35%, BTCUSD: 0.30%, _default: 0.30%), rsafe3_min_lot_diff=0.05 |
| Localizacao | server/af/signals.py linhas 725-822 |
| Note | sl_buffer=1.0 hardcoded no server (pool config armazena pips, EA converte). Range efetivo: [sl_min_usd, sl_max_usd] normal (derivados de sl_min_pct*price/100 e sl_max_pct*price/100); [sl_min_usd, midpoint] quando F1 near-death/near-target (deixa espaco pra MODIFY expandir) |
Exemplo concreto (XAUUSD, price=2900.00, sl_min_pct=0.85, sl_max_pct=1.70):
sl_min_usd=24.65, sl_max_usd=49.30, step=0.29 (=0.01%*price) -> ~85 candidatos
Sibling A: SL=2875.50, TP=2925.50, vol=1.04
Sibling B: SL=2922.00, TP=2878.00, vol=0.88
Candidato d=25.0:
test_levels = [2874.00, 2926.00, 2925.00, 2875.00]
2874.00 vs 2875.50 -> diff=$1.50 < $10 -> REJEITA
Candidato d=40.0:
test_levels = [2859.00, 2941.00, 2940.00, 2860.00]
Todos >= $10 de cada sibling level -> level_ok
test_vol = risk / 40 = diff vs 1.04 e 0.88 -> OK
ACEITA
...
Candidatos validos: 47 SL distances
Escolhe random.choice -> d=34.2 (por exemplo)
| Propriedade | Valor |
|---|---|
| O que faz | Apos calcular volume, se risk_usd > 500 E volume <= volume_min (0.01 lot), levanta erro e manda alerta Telegram |
| Motivo | Detectar bug historico: alguem passou sl_buffer em pips (100) quando esperava preco ($1) e volume veio absurdamente baixo (quase zero) |
| Localizacao | server/af/signals.py linhas 837-850 |
| Nota | Mesmo nome do "R-SAFE6 jitter" do scheduling; aqui eh o sanity check de volume. Sao dois usos distintos do nome no codigo atual |
Exemplo concreto:
risk_usd = $1500, volume = 0.01 lot (lot_min)
sl_distance = 45.0, sl_buffer = 1.0 (OK)
Mas: alguem chamou calculate_volume com sl=4500 (pips ao inves de $) -> volume vem 0.01
R-SAFE6: volume=0.01 + risk=$1500 = BLOCK + Telegram alert
Hoje o sistema tem presets para 2 simbolos (XAUUSD e BTCUSD). A pool 28 usa BTCUSD em S254 (antes usava XAUUSD). Presets ficam em server/af/presets.py::SYMBOL_DEFAULTS.
Config (S255): sl_min_pct=0.85%, sl_max_pct=1.70%, sl_buffer=$1.0, contract_size=100, rsafe2_price_gate_pct=0.35% (= $24.65-$49.30 SL / $10.15 gate @ $2900)
| Metrica | Min | Max | Variacao Maxima |
|---|---|---|---|
| SL distance | $25 | $50 | 50% (100% range) |
| Volume (risk $1500) | 0.30 lot | 0.60 lot | 50% |
| Volume (risk $2500) | 0.50 lot | 1.00 lot | 50% |
| SL level from entry | $26 | $51 | $25 de diferenca |
| TP level from entry | $25 | $50 | $25 de diferenca |
Formula:
volume = risk / (sl_distance * contract_size)
Com risk $1500 e XAUUSD (contract=100):
SL $25 -> 1500 / (25 * 100) = 0.60 lot
SL $50 -> 1500 / (50 * 100) = 0.30 lot
Variacao = (0.60 - 0.30) / 0.60 = 50%
Config (S255): sl_min_pct=0.65%, sl_max_pct=1.60%, sl_buffer=$1.0, rsafe2_price_gate_pct=0.08%, spread_pct=3.0 (= $403-$992 SL / $49.60 gate @ $62k)
| Metrica | Min | Max | Variacao Maxima |
|---|---|---|---|
| SL distance | $400 | $1000 | 60% |
| SL level from entry | ~$401 | ~$1001 | $600 de diferenca |
| TP level from entry | ~$400 | ~$1000 | $600 de diferenca |
| Volume | (depende de tick_value BTCUSD, varia por broker) | — | 60% |
Nota: BTCUSD tem ADR historica ~$1500/dia, entao o gate R-SAFE2 de $50 abre ~30 pares/dia potenciais (bem mais permissivo proporcionalmente que XAUUSD, que tem gate $10 vs ADR $60 -> 6 pares/dia).
presets.py tem defaults para XAGUSD, ETHUSD, US30, US500, USTEC, EURUSD, GBPUSD, USDJPY no dicionario _ADR_ESTIMATES (so ADR, nao preset completo). Hoje nenhum desses roda em producao no AF — soh XAUUSD e BTCUSD. Se forem ativados, cairao no fallback _default (sl_min_pct=0.85%, sl_max_pct=1.70%, rsafe2_gate_pct=0.30%, rsafe5_gap_pct=0.30%) e devem ser calibrados especificamente antes.
| Propriedade | Valor |
|---|---|
| Funcao | _get_sibling_trades_today() |
| Localizacao | server/af/risk.py linha 27 |
| Filtros | Same pool + same Empresa + status NOT IN (failed, timeout). O round_id e' CONDICIONAL (risk.py: if round_id is not None) e ha o filtro alternativo desde (por TEMPO). Quem chama decide: R-SAFE3/4/5 passam round_id; o R-SAFE2 passa round_id=None + desde |
| Cross-round | DEPENDE DA REGRA (2026-08-26). R-SAFE1 e R-SAFE2 comparam o dia de negociacao inteiro (ancorado no swap, via _get_trading_day_start); R-SAFE3, R-SAFE4 e R-SAFE5 seguem por rodada. Ver "Escopo por regra" abaixo |
| Cross-phase | SIM — compara F1 vs F2 vs Funded se compartilham prop (a prop nao ve fase) |
Logica:
siblings = trades WHERE
pool_account.pool_id = current_pool
AND pool_account.prop_name = current_account.prop_name
AND (pair.round_id = current_round -- quando o chamador passa round_id (R-SAFE3/4/5)
OR trade.created_at >= desde) -- quando o chamador passa desde (R-SAFE2, o DIA)
AND trade.status NOT IN ('failed', 'timeout')
AND pair_id != exclude_pair_id
Retorna: pair_id, account_id, pool_account_id, direction, volume, sl_price, tp_price, open_price, risk_usd, created_at (lista de dicts).
Gerado por
scripts/meta_da_obra.py, com o histórico até 2026-08-26 19:37 (GMT-4). Não edite este bloco à mão — a próxima geração sobrescreve. Uma meta muda de estado quando um commit dizfecha,mudaoudescarta meta N.⚠️ O bloco COMMITADO fica um commit atrás, e isso é da natureza da coisa: a meta fecha pelo commit, e um commit não pode conter o registro de si mesmo. O arquivo em disco fica em dia (o gancho regenera logo depois), e o commit seguinte leva a correção junto. Quem lê pelo site do git pode ver um estado defasado.
| # | meta | custo | quando (GMT-4) | commit | |
|---|---|---|---|---|---|
| ✅ | 1 | A prova que FALHA hoje: a semente nao sobrevive ao aprovar — implementada: a prova que falha antes do conserto (8 casos) | — | 2026-08-26 18:13 | 0bc2b9153 |
| ✅ | 2 | A semente atravessa o botao de aprovar — implementada: a porta unica de agendamento | — | 2026-08-26 18:13 | 0bc2b9153 |
| ✅ | 3 | Mutacao dirigida na semente e no aprovar — implementada: mutacao dirigida: 4 de 4 sabotagens mortas | — | 2026-08-26 18:13 | 0bc2b9153 |
| ✅ | 4 | A spec das regras R-SAFE entra na pagina /guide — implementada: a spec das regras publicada no /guide | — | 2026-08-26 18:13 | 0bc2b9153 |
| ✅ | 5 | Tooltip na tela: por rodada x por dia — implementada: o escopo de cada regra no tooltip da tela | — | 2026-08-26 18:13 | 0bc2b9153 |
| ✅ | 6 | A simulacao E2E nao quebrou — implementada: simulador local: 877 verdes, zero falha, zero regressao | — | 2026-08-26 18:23 | eeb6f2690 |
| ✅ | 7 | Subir pra producao — implementada: promovido em eeb6f2690, prova de vida rodada dentro da VPS | — | 2026-08-26 18:32 | 36bacc114 |
Histórico — 26 commit(s):
| quando | commit | o quê |
|---|---|---|
| 2026-08-26 19:37 | 7f122d51c |
fix(replay): a ultima vermelha da varredora — subprocess.run cru virou a porta unica |
| 2026-08-26 19:33 | 119667d9f |
fix(varredoras): mais DUAS catracas aprendem a TERCEIRA categoria — e a 3a mordeu a si m |
| 2026-08-26 19:23 | 7a011f630 |
docs(doutrina): a blindagem de import entra na tabela — inclusive a decisao CONTRARIA a |
| 2026-08-26 19:21 | e8de9fab4 |
fix(p846): o contrapeso que faltava — 10 provas de 'tem que barrar', ZERO de 'nao pode' |
| 2026-08-26 19:19 | 115b8df72 |
docs(licoes): a metrica que nao pode ver o proprio ganho — cetico derrubou minha conclus |
| 2026-08-26 19:12 | 060cdac5d |
feat(guardas): blindagem de import nos 116 — de 53 mortes para ZERO, medido ao vivo |
| 2026-08-26 18:48 | 82bfbbbe4 |
feat(rede-torta): a sonda do contrapeso ausente — FERRAMENTA, e o motivo esta escrito ne |
| 2026-08-26 18:41 | f620dc57b |
feat(saude): backup de mutacao orfao e' UMA MORTE do motor — e agora alguem olha |
| 2026-08-26 18:32 | 36bacc114 |
chore(deploy): o espacamento entre rodadas esta em producao — com a prova de vida e o li |
| 2026-08-26 18:30 | 3bee2c8e8 |
fix(portao da prova): para de mandar cacar base quando a prova E' o conserto |
| 2026-08-26 18:23 | eeb6f2690 |
fix(metas): o quadro nao declarava obra, e por isso NENHUM trailer enderecado fechava |
| 2026-08-26 18:19 | 89cb0c632 |
chore(de-quem-e-este-item): fecha a obra — 12 de 12, laco de revisao encerrado |
| 2026-08-26 18:15 | 5ec34de02 |
docs(degradado): o residuo escrito — 62% da casa tem o mesmo defeito, e NAO vamos varrer |
| 2026-08-26 18:13 | 0bc2b9153 |
docs(guide): as regras anti-deteccao entram na pagina, e cada campo diz se vale por RODA |
| 2026-08-26 18:10 | b03e54cfd |
fix(degradado): a OUTRA lista tambem era minha — 9 guardas na prova, 13 no disco |
| 2026-08-26 18:04 | 3d84bf525 |
feat(sinal-9): a contagem de licoes para de sair sem dizer quantas viraram parede |
| 2026-08-26 18:01 | f32594a40 |
fix(af): o espacamento entre rodadas agora ATRAVESSA o botao "Aprovar Rodada" |
| 2026-08-26 18:01 | fb080b3b4 |
test(varredura): a regua do socorro ganha rede propria — e mata o mutante que sobrevivia |
| 2026-08-26 14:22 | 68bcdc567 |
feat(catracas): a TERCEIRA categoria — socorro de import tolerante nao e' divida nem cop |
| 2026-08-26 14:13 | 77ee539f1 |
feat(sinal-8): placar de mutacao sem dizer o recorte para de sair calado |
| 2026-08-26 14:11 | d5a9c20df |
fix(guardas): fecha as 6 mortes + a prova ganha PROVA DE VIDA do proprio instrumento |
| 2026-08-26 14:01 | 7e507ee6e |
feat(parede): assert X or True deixa de ser gravado — a 1a licao de hoje que virou 100 |
| 2026-08-26 13:57 | 95f78294f |
fix(guardas): a prova parou de usar a MINHA lista e achou 6 mortes que ela nao via |
| 2026-08-26 13:53 | b10aaf88c |
docs(bancada): a lista de dependencias NUNCA descreveu a VPS — ela nasceu como foto de o |
| 2026-08-26 13:53 | 96af157de |
docs(rsafe): a obra nasce porque a semente que eu entreguei ontem e' NO-OP em producao |
| … | mais 1 commit(s) — veja git log |
A regua, em 1 linha (decisao do dono): regra de RISCO e' por RODADA; regra de
ANTI-DETECCAO e' por DIA de negociacao. A prop firm nao sabe que rodada existe — ela le'
o extrato da conta, e a linha "entrou 00:26" continua la' depois de a posicao fechar.
O dia comeca no SWAP, nao a meia-noite (_get_trading_day_start + _safe_swap_hour, em
af/constants.py) — a MESMA porta que decide se a rodada e' a 1a do dia pro reset de PnL.
| Regra | O que compara | Escopo | Onde |
|---|---|---|---|
| R-SAFE1 | horario de entrada | dia | a porta agenda_com_memoria_do_dia (af/risk.py), usada pelos quatro caminhos que agendam: nascimento e recall (af/lifecycle.py), aprovar e reset de timers (routes/af.py). Ha prova por AST cobrando — chamador novo REPROVA |
| R-SAFE2 | preco de entrada | dia | irmas_da_janela (af/signals.py), com round_id=None + desde |
| R-SAFE3 | lote | rodada | sibling_trades |
| R-SAFE4 | risco (rank) | rodada | sibling_trades — e' regra de RISCO |
| R-SAFE5 | nivel de SL/TP | rodada | sibling_trades |
| R-SAFE6 | dois usos do mesmo nome — (a) jitter de 5-55s no agendamento, somado ao gap; (b) sanidade de volume | (a) acompanha o R-SAFE1; (b) o proprio trade | (a) engine.py, dentro do agendador; (b) signals.py. Nenhum dos dois COMPARA irmas |
⚠️ has_siblings (que governa o sorteio de risco) e o rank do R-SAFE4 continuam lendo
sibling_trades, por RODADA. A lista do dia (irmas_da_janela) alimenta so' o laco do
R-SAFE2 — misturar as duas mudaria o risco sorteado, que nao e' o que a mudanca queria.
⚠️ AS DUAS PONTAS LEEM A MESMA REGUA, e isso e' obrigatorio (licao P1269): se o
agendamento (R-SAFE1) espacar por dia-de-swap e a porta (R-SAFE2) barrar por outra janela, o
agendamento marca um horario que a porta recusa.
O que NAO foi estendido, e o numero: medido nas duplas mesma-Empresa que atravessam rodadas
em 24h — 16 lotes colados (R-SAFE3), 10 niveis de SL e 8 de TP (R-SAFE5), contra
3 de preco (R-SAFE2). ~10x mais adiamentos, sem evidencia de dano. Estender seria construir
por simetria, nao por evidencia.
⚠️ ESTE NUMERO NAO TEM FORMA DE RECONTAR, e isso vai dito (achado de cetico, 26/08). Ele
saiu de uma consulta ad-hoc ao banco de producao no dia da obra e nao ficou em nenhum script
— nem em scripts/, nem em .claude/medicoes/. Pela regua desta casa, numero em doutrina so'
vale escrito junto da forma de reconferir; este nao esta'. Some-se a isso que ele foi medido na
janela de 24h rolante, e a regra que entrou ancora no swap — mediu-se na janela que o
dono mandou trocar. Trate-o como ordem de grandeza, nao como fato, e remeça antes de usa-lo
para reabrir a decisao.
O caso que motivou: round 7671 -> 7672 (06/07, pool 448, 21 min de intervalo) produziu duas
entradas coladas — BrightFunded a $1,88 e City Traders a $1,99, contra os $5,43 exigidos.
Card #1067.
| Componente | Relacao |
|---|---|
server/af/signals.py |
Implementa R-SAFE2, R-SAFE3, R-SAFE4 v3, R-SAFE5, R-SAFE6 (sanity) na generate_signals_for_pair |
server/af/engine.py |
Implementa R-SAFE1 (+ jitter R-SAFE6 scheduling) em _schedule_pairs_rsafe |
server/af/risk.py |
Re-exporta _schedule_pairs_rsafe e expoe _get_sibling_trades_today |
server/af/presets.py |
Presets por simbolo (sl_min_pct, sl_max_pct, dz_min, dz_max, rsafe2_price_gate_pct, rsafe5_min_gap_pct), gate context report (S255) |
server/af/lifecycle.py |
Chama _schedule_pairs_rsafe na criacao da rodada (linha 268) |
server/routes/af.py |
Expoe parametros R-SAFE na API de update de pool (rsafe_gap_min/max, rsafe2_price_gate_pct, rsafe3_min_lot_diff, rsafe4_factor_min/max, rsafe5_min_gap_pct, rsafe6_jitter_min/max) (S255 pct rename) |
af-rules-whitepaper.md |
Regras-pai (R-SAFE eh subset das regras de pool/risco) |
O sistema AF tem varios mecanismos anti-pattern ATIVOS que nao usam o prefixo R-SAFE. Sao menos visiveis porque estao espalhados pelo codigo, mas compoem o cinturao de defesa anti-robotico. Auditados em S254 (2026-04-18).
| Mecanismo | Localizacao | O que faz | Motivacao |
|---|---|---|---|
| Direction tie-breaker random | server/af/engine.py:157 |
Quando weight(BUY) == weight(SELL), escolhe random.choice(['BUY','SELL']). Evita vies deterministico |
Anti-bias |
| Invert flag por conta | accounts.invert (DB) |
Metade da pool tem invert=true. Quando master manda BUY, 6 EAs abrem BUY e 6 abrem SELL |
Espalha direcao por conta |
| MODIFY margin random ($2-10) | server/af/engine.py:200 + signals.py:1934 |
_margin = random.uniform(2.0, 10.0) somado a calculos de R4/R5b/push e MODIFY. Evita landing exato em limites |
Anti-robotic (TODO #125) |
| Dead zone target random | server/af/engine.py:259 |
dz_target = random.uniform(dz_min, dz_max). Alvo da DZ varia por trade |
Anti-robotic H7 |
| ~~CLOSE delay random (61-90s)~~ REMOVIDO (S-piso-unificado) | ~~server/af/signals.py~~ |
Timer paralelo eliminado. O fecho por Vol mismatch agora respeita SO o piso unico min_hold (precedencia mesa>pool>global) via can_close_orphan. Conta prop (135-150s) fecha idêntico; conta sem firma cai no piso global (10-20s). Spec: piso-fechamento-unificado.md |
Prop firm safety (agora via min_hold) |
| MODIFY delay random (30-120s) | server/af/signals.py:1866 |
delay = random.uniform(modify_delay_min_s=30, modify_delay_max_s=120). Comment explicito: "looks human to prop firms" |
Anti-pattern human-like |
| Firing order shuffle | server/routes/af.py:2032 |
random.shuffle(pairs) antes de definir scheduled_at em reset de timers |
Evita ordem previsivel |
| Pairs base order shuffle | server/af/lifecycle.py:251, 420 |
random.shuffle(pairs) na criacao/reshuffle da rodada |
Evita pareamento previsivel |
| Vetor detectavel por prop firm | Mecanismo atuando |
|---|---|
| Direcao uniforme em todas contas | Direction tie-breaker + invert flag 50/50 |
| Trades sempre em niveis "redondos" | MODIFY margin random + DZ target random |
| CLOSEs muito rapidos apos erro | Piso min_hold da mesa (135-150s prop; 10-20s global) — S-piso-unificado |
| MODIFYs instantaneos apos OPEN | MODIFY delay random 30-120s |
| Ordem deterministica de entrada | Firing shuffle + pairs base shuffle |
R-SAFE1 lida com gap entre siblings same-prop (1-2h). R-SAFE6 scheduling eh jitter 5-55s no timing. Os mecanismos desta secao sao de outra natureza: variacao dentro do trade (margin, direction, delays especificos). Complementares, nao substitutos.
| Gap | Risco | Mitigacao proposta |
|---|---|---|
| Horario de news economicas (NFP, FOMC, CPI) | Medio (XAUUSD) / Baixo (BTCUSD 24/7) | Nao mitigado. Considerar integracao com calendario economico |
| Symbol rotation intra-pool | Baixo (cada prop ve 1 simbolo historico na conta) | Non-goal consciente. Revisitar se volume crescer |
| Dia da semana previsivel | Baixo (BlossomBlend varia scheduled_at por equity virtual) | Monitorar, nao mitigado explicitamente |
| Risco | Probabilidade | Impacto | Mitigacao |
|---|---|---|---|
| SL aleatorio cai identico para 2 siblings | Baixa (range $25, step $0.1 = 250 valores) | Medio | R-SAFE5+3 re-rolls ate esgotar candidatos |
| Volume identico apos R-SAFE5 | Muito baixa (enumera candidatos ambos-validos) | Baixo | R-SAFE3 relaxado quando impossivel (levels > volume) |
| Prop firma correlaciona timing de entrada | Baixa | Medio | R-SAFE1 (gap 1-2h) + jitter 5-55s |
| Preco entrada identico entre siblings | Medio (mercado pode rondar) | Alto (obvio que eh bot) | R-SAFE2 gate pct (XAUUSD 0.35%, BTCUSD 0.08%) com retry ate window_end |
| Mesmo simbolo + direcao oposta simultaneo | N/A | N/A | Contas same-prop NUNCA pareadas (R7 do whitepaper) |
| Volume absurdamente baixo por unit mismatch | Baixa (bug historico) | Alto (trade nao executa) | R-SAFE6 sanity + Telegram alert |
| Clusterizacao de risk entre siblings | Media | Medio (prop ve volume similar) | R-SAFE4 v3 (cumulative rank factor) |
Todas configuravaveis via PoolUpdate (server/routes/af.py). Defaults:
| Campo | Default | Descricao |
|---|---|---|
rsafe_gap_min |
3600 s | R-SAFE1 gap minimo (1h) |
rsafe_gap_max |
7200 s | R-SAFE1 gap maximo (2h) |
rsafe6_jitter_min |
5 s | Jitter no scheduling |
rsafe6_jitter_max |
55 s | Jitter no scheduling |
rsafe2_price_gate_pct |
0.35% (XAUUSD) / 0.08% (BTCUSD) / 0.30% (_default) | S255 — % minimo do preco entre entry e sibling. Auto-escala cross-symbol (era USD) |
rsafe2_retry_min |
5 min | Intervalo entre retries de R-SAFE2 |
rsafe2_max_retries |
60 | Maximo de retries antes de terminal skip |
rsafe3_min_lot_diff |
0.05 lot | Divergencia minima de volume |
rsafe4_factor_min |
0.90 | Reducao min de risk (por rank) |
rsafe4_factor_max |
0.95 | Reducao max de risk (por rank) |
rsafe4_similar_risk_pct |
10.0 % | S254 — Gate: % do risk pra considerar siblings 'similar' (rank++). Adaptativo a R4-smart/DD (substitui threshold USD absoluto) |
rsafe4_decisive_priority |
true |
S396 — Conta decisiva (death/win) NUNCA leva o corte R-SAFE4 (precisa do risco cheio pra resolver). Corte cai nas irmas nao-decisivas. Reversivel (false = legado). Spec: rsafe4-decisive-priority.md |
rsafe5_min_gap_pct |
0.35% (XAUUSD) / 0.30% (BTCUSD, _default) | S255 — % minimo de SL/TP entre siblings. Corrige bug BTC onde $10 era 0.016% |
sl_min_pct |
0.85% (XAUUSD, _default) / 0.65% (BTCUSD) | S255 — SL distance minima em % do preco (era USD) |
sl_max_pct |
1.70% (XAUUSD, _default) / 1.60% (BTCUSD) | S255 — SL distance maxima em % do preco (era USD) |
~~close_delay_min_s~~ |
— | REMOVIDO (S-piso-unificado) — colapsado no piso min_hold. Knob e leitura eliminados |
~~close_delay_max_s~~ |
— | REMOVIDO (S-piso-unificado) — idem |
trading_end |
"12:00" (default) | Fim da janela de trading (HH:MM local) |
trading_tz_offset |
-4 (default) | GMT offset do pool em horas |
| Versao | Data | Mudancas |
|---|---|---|
| 1.0 | 2026-02-xx | Documentacao inicial — R-SAFE3 + R-SAFE5 ativos, R-SAFE4 desabilitado (S172) |
| 1.1 | 2026-04-04 | R-SAFE2 gate BTCUSD $200->$50 (S181), window end usa trading_tz_offset do pool |
| 1.2 | 2026-04-18 | Atualizacao completa S254: familia R-SAFE1..R-SAFE6 documentada. R-SAFE4 REATIVADO em v3 (rank-based cumulative). R-SAFE5+R-SAFE3 unificados em uma passada. Presets por simbolo (XAUUSD + BTCUSD) documentados. Non-goals reavaliados |
| 1.3 | 2026-04-18 | Segunda passada S254: adicionada secao "Outros Mecanismos de Variacao (fora da familia R-SAFE)" — 8 mecanismos anti-pattern ativos mas previamente nao documentados (direction tie-breaker, invert flag, MODIFY margin random, DZ target random, CLOSE/MODIFY delays, 2 shuffles). Adicionada tabela de cobertura por vetor de deteccao + gaps residuais conhecidos (news timing, symbol rotation, dia da semana) |
| 1.4 | 2026-04-18 | Terceira passada S254: expostos 5 campos hardcoded/ausentes na UI de config do pool — rsafe2_retry_min (5 min), rsafe2_max_retries (60), rsafe4_similar_risk_pct (10%, trocado de threshold USD absoluto pra % — adaptativo a R4-smart/DD cortes), close_delay_min_s (61), close_delay_max_s (90). Patterns P132 (drift spec vs codigo), P133 (3 conceitos de risco em props: prop.max_risk teorico vs phase_ceiling 2.5%/2% vs daily_DD 5%). |
| 1.7 | 2026-08-26 | Escopo por regra (card #1067): R-SAFE1 e R-SAFE2 passam a comparar o dia de negociacao (ancorado no swap_hour_utc), nao so' a rodada. R-SAFE3/4/5 seguem por rodada — medido: 16 lotes / 10 SL / 8 TP colidiriam, contra 3 de preco. Nova secao "Escopo por regra". |
| 1.6 | 2026-07-06 | S-piso-unificado: close_delay (timer paralelo 61-90s do vol_mismatch) REMOVIDO de vez — código do timer, campo da UI (static/index.html/af_hedge.js), schema/validação/writer (routes/af.py), leitura + whitelist de espelho da sim (af_tick_real.py). Piso ÚNICO de fecho = min_hold (precedência mesa>pool>global) via can_close_orphan. Conta prop: zero mudança (min_hold 135-150s já engolia o close_delay); conta sem firma cai no piso global (10-20s). Spec: piso-fechamento-unificado.md. |
| 1.5 | 2026-04-18 | S255 USD -> % migration: 4 thresholds price-facing migrados de USD para % do preco — sl_min_pct (0.85%/0.65% XAU/BTC), sl_max_pct (1.70%/1.60%), rsafe2_price_gate_pct (0.35%/0.08%), rsafe5_min_gap_pct (0.35%/0.30%). Auto-escala cross-symbol sem ATR. Fix bug BTC latente: rsafe5 era 0.016% ($10/$62k), agora 0.30%. dz_min/dz_max permanecem USD (equity-facing, fora de escopo). Migration destrutiva: backend + frontend + DB SQL em uma fase (R17). UI mostra conversao live pct → pips + USD usando preco vivo do symbol_info. Spec: usd-to-pct-migration.md. |
Referência rápida pra consultar sempre. Explica cada valor que aparece na
tabela de regras das mesas (prop firms) e nos ajustes da pool — em linguagem
leiga. Última revisão: S362 (2026-05-21).
Muita gente mistura "risco" com "drawdown". São três coisas separadas:
| Limite | Pergunta que ele responde | Quem define | Exemplo em $100k |
|---|---|---|---|
| Risco por trade | Quanto arrisco em UMA operação? | A pool (max_risk_f1/f2_pct), com teto da mesa |
2,5% = $2.500/trade |
| Daily DD (drawdown diário) | Quanto posso perder SOMANDO o dia inteiro? | A prop firm (daily_dd_op_pct) |
4,5% = $4.500/dia |
| Max DD (drawdown total) | A partir de quanto a conta MORRE de vez? | A prop firm (max_dd) |
10% = morre em $90.000 |
Como eles conversam no motor: o risco por trade é o tamanho normal de cada
aposta. O Daily DD é um teto do dia — se você já perdeu muito hoje, ele
aperta o risco do próximo trade pra não estourar o dia (risco = min(risco_do_trade,
quanto_ainda_posso_perder_hoje)). O Max DD é o piso de morte: cruzou, a
conta acabou.
Hoje rodamos ~1 round/dia em demo, então o Daily DD quase nunca "morde" (1
trade de $2.500 < $4.500/dia). Quando operarmos vários trades/dia no real, ele
passa a limitar de verdade.
prop_firms)São as regras da mesa. Cada programa (ex: "FTMO Swing") tem as suas. Viram dólar
multiplicando o % pelo tamanho da conta.
| Campo | O que é (leigo) | Unidade | Usado hoje? |
|---|---|---|---|
name |
Nome do programa (ex "FTMO Swing") | texto | sim |
company |
Empresa dona do programa (ex "FTMO") — usada pra nunca parear 2 contas da mesma Empresa no hedge (R7) | texto | sim (S362) |
price |
Custo do desafio | USD | sim (relatório de custo) |
max_dd |
Piso de morte total: perda máxima desde o início. 10% = conta de $100k morre em $90k | % | SIM (motor) |
daily_dd |
Perda máxima do dia nominal (a regra oficial da mesa) | % | rótulo (futuro) |
daily_dd_op_pct |
Perda máxima do dia operacional (com folga: ~0,5% abaixo do nominal pra não chegar no limite). É o que o motor usa | % | SIM (motor, mas inerte com 1 round/dia) |
daily_dd_type |
Mede o DD diário por equity (valor vivo) ou balance (saldo fechado) |
enum | rótulo (futuro) |
target_f1 |
Alvo de lucro pra passar a Fase 1 | % | SIM (motor) |
target_f2 |
Alvo de lucro pra passar a Fase 2 | % | SIM (motor) |
min_days_f1 |
Dias mínimos de trade na Fase 1 | dias | rótulo (futuro) |
min_days_f2 |
Dias mínimos de trade na Fase 2 | dias | rótulo (futuro) |
min_profit_days |
Dias mínimos com lucro | dias | rótulo (futuro) |
max_risk_pct |
Teto da mesa: risco máximo por trade que a prop permite (o "mesa cap") | % | SIM (motor) |
limitation |
Observação textual (ex "SL obrigatório") | texto | rótulo |
steps |
Quantas fases o desafio tem (1 step / 2 steps) | número | sim |
max_risk |
LEGADO USD (risco/trade calibrado em $100k) — sendo aposentado | USD | em remoção |
daily_dd_op |
LEGADO USD (daily DD calibrado em $100k) — aposentado: motor não lê mais | USD | não (S362) |
af_pools.config)São os ajustes operacionais da pool, não da mesa.
| Campo | O que é (leigo) | Unidade |
|---|---|---|
max_risk_f1_pct |
Risco por trade na Fase 1 (o "risco por round") | % |
max_risk_f2_pct |
Risco por trade na Fase 2 | % |
dead_zone_min_pct / dead_zone_max_pct |
Zona morta (folga aleatória anti-detecção) | % |
equity_floor_enabled |
Liga/desliga o piso de segurança (airbag) | bool |
equity_floor_pct |
Piso de segurança em %: se o equity cair abaixo desse % do tamanho, EA fecha tudo. 8% = fecha em $92k (numa conta de $100k) | % |
spread_pct |
Spread assumido no cálculo do hedge | % |
sl_buffer_* / tp_buffer_* |
Folga de SL/TP por grupo de ativo (metais/forex/default) | pips ou preço |
buffer_mode |
Como o buffer é medido: PIPS ou PRICE |
enum |
swap_hour_utc / janela de trading |
Horários de rollover e janela operacional | hora UTC |
max_rounds_per_day |
Limite de rounds por dia (0 = ilimitado) | número |
pairing_strategy |
Algoritmo de pareamento (default greedy) |
texto |
Legado:
equity_floor_value(piso em USD) foi trocado porequity_floor_pct
(%) no S362. O servidor ainda lê o USD como fallback pra pools não-migrados.
A tela de preview das regras (e o motor) pegam o % × tamanho da conta:
target_f1 % × tamanho → 8% × $100k = $108.000(1 − max_dd%) × tamanho → (1 − 10%) × $100k = $90.000max_risk_f1_pct % × tamanho (limitado pelo max_risk_pct da mesa)(1 − equity_floor_pct%) × tamanho → (1 − 8%) × $100k = $92.000Por isso o S362 conectou o tamanho real da conta ao motor: antes ele assumia
$100k fixo (funcionava por coincidência, porque tudo é $100k). Agora escala
sozinho — uma conta de $50k calcula alvo $54k e piso $45k.
daily_dd, daily_dd_op_pct, daily_dd_type): só "morde"min_days_f1/f2, min_profit_days): só preenchem a tabela,max_risk, daily_dd_op): em remoção. daily_dd_op já estádaily_dd_op_pct). max_risk USD ainda tem 1 uso vivo naO botão Config Settings de cada pool abre um painel pra ajustar os buffers
(margens de segurança) e conferir como as contas estão. O que cada parte mostra:
O buffer é uma folga que você dá no preço de saída (stop/alvo) pra não ser
tirado por um detalhe do mercado. Ao digitar o buffer de cada tipo (Metais /
Forex / Cripto), o painel mostra, pra cada par real:
Tabela com o spread típico de cada par, juntado das contas. É uma média que
vai acumulando (o spread "normal", não o de um instante isolado). Dá pra ver
por par, por mesa (empresa) ou por conta.
Um interruptor "Só contas demo (simulando mesa)". Hoje todas as 12 contas
são demo (treino). Quando entrarem contas reais de prop firm, desmarque pra
ver só elas. Essa marca é definida num checkbox na hora de classificar a
conta — conta nova entra como real por padrão. No painel inicial, cada
conta demo ganha uma etiqueta azul "demo" no card, pra diferenciar de bate-
pronto.
Onde antes aparecia só o nome do desafio (ex: "Standard 2 Steps"), agora aparece
a empresa junto (ex: "FundedNext — Standard 2 Steps"), pra não confundir qual
mesa é qual.
O robô (EA) que roda em cada conta recebe os buffers e confirma de volta o
que aplicou. Esse painel compara o que você salvou com o que o robô
confirmou:
Por que isto existe: o hedge cruzado assume que cada prop firm é uma empresa independente — dono diferente, time de risco diferente, e elas não comparam contas entre si. Se duas "marcas diferentes" forem do mesmo grupo-mãe, o hedge entre elas vira uma independência ilusória (a empresa percebe que as duas contas são a mesma jogada e pode eliminar as duas). O setor consolida rápido — compras e fusões mês a mês — então esta lista envelhece e precisa de revisão periódica.
Última auditoria: 27 de Maio de 2026 · Próxima revisão recomendada: ~Agosto/2026 (ou assim que sair notícia de nova aquisição).
As 11 marcas cadastradas são, hoje, 11 grupos donos distintos — nenhum par da pool compartilha dono, então a premissa do hedge se sustenta. A única mudança recente: a Funded Trading Plus foi comprada pela Instant Funding (grupo Acello, Reino Unido) em 26/05/2026. Como a Instant Funding não está na nossa lista, não cria conflito interno hoje — mas a Funded Trading Plus deixou de ser independente.
| Marca | Grupo / dono real | País (sede) | Independente? |
|---|---|---|---|
| FTMO | FTMO s.r.o. (holding OMHC) | 🇨🇿 Chéquia | Sim |
| The 5%ers | Five Percent Online Ltd | 🇬🇧 Reino Unido | Sim |
| FundedNext | NEXT Ventures | 🇦🇪 Emirados | Sim |
| BrightFunded | BrightFunded B.V. / Bright Global FZCO | 🇳🇱 Holanda (opera de Dubai) | Sim |
| City Traders Imperium | CTI FZCO | 🇦🇪 Emirados | Sim |
| Funding Pips | ANKH PROP FZCO | 🇦🇪 Emirados | Sim |
| Alpha Capital (Alpha Pro 10%) | Alpha Capital Group Ltd (Kohler / AMGP) | 🇬🇧 Reino Unido | Sim |
| Funded Trading Plus | Acello Ltd / Instant Funding | 🇬🇧 Reino Unido | Trocou de dono 26/05 |
| For Traders | FT Trading Ltd + BLN Tech Club DMCC | 🇦🇪 Emirados (registro em St. Lucia) | Sim |
| Maven | MAVEN LLC | 🇱🇨 Saint Lucia (operação em Dubai) | Sim |
| Fintokei | Purple Group / Purple Trading | 🇨🇿 Chéquia | Sim |
Estas marcas não estão na nossa lista hoje, mas pertencem ao MESMO grupo de uma marca que já está. Se qualquer uma entrar na pool, não pode parear (hedge) com a marca-mãe — seria a mesma empresa disfarçada. A regra R7 do sistema hoje compara o nome da marca, não o grupo-mãe, então este radar é manual até a regra olhar o grupo.
| Marca-irmã | Mesmo grupo de | Tipo |
|---|---|---|
| Instant Funding | Funded Trading Plus | prop firm (a compradora) |
| IF Crypto / IF Pro | Funded Trading Plus | sub-marca / broker do grupo Acello |
| Trade The Pool | The 5%ers | prop firm de ações |
| TSG ("Trade Set Go") | The 5%ers | broker dos fundadores |
| FundYourFX | The 5%ers | prop firm (co-fundador virou CEO) |
| FNmarkets | FundedNext | broker próprio do grupo |
| OANDA | FTMO | broker (comprado pela FTMO) |
| Quantlane | FTMO | tech (comprada pela FTMO) |
| Alpha Futures / Alpha Prime | Alpha Capital | sub-marcas internas |
| Purple Trading | Fintokei | broker que respalda a Fintokei |
Inspirado no mapa "Locations of Prop Firms" do PropFirmMatch. Repare na concentração: a maioria fica em Emirados e Reino Unido. Atenção importante — ficar no mesmo país NÃO significa mesmo dono; é só onde a empresa se registrou (muitas escolhem Emirados ou paraísos fiscais por imposto e regulação mais leve).
| País | Marcas | Quantas |
|---|---|---|
| 🇦🇪 Emirados | FundedNext · City Traders Imperium · Funding Pips · For Traders | 4 |
| 🇬🇧 Reino Unido | The 5%ers · Alpha Capital · Funded Trading Plus | 3 |
| 🇨🇿 Chéquia | FTMO · Fintokei | 2 |
| 🇳🇱 Holanda | BrightFunded (opera de Dubai) | 1 |
| 🇱🇨 Saint Lucia | Maven (operação em Dubai) | 1 |
O que o "mapa" ensina pro hedge: várias firmas independentes dividem o mesmo endereço (Emirados, principalmente). Isso é normal e não compromete o hedge — o que importa é o dono, não o CEP. O risco de verdade aparece quando duas marcas têm o mesmo grupo-mãe (como Funded Trading Plus e Instant Funding agora) ou usam o mesmo provedor de liquidez nos bastidores — aí a execução pode ficar correlacionada mesmo com donos diferentes.
| Data | Evento | Toca a lista? |
|---|---|---|
| 2026-05-26 | Instant Funding (Acello Ltd) adquire Funded Trading Plus (+70% receita do grupo) | SIM — FTP é a #8. Comprador fora da lista (sem par-problema hoje) |
| 2025-12-01 | FTMO fecha compra da OANDA da CVC Asia Fund IV (~US$250M, holding OMHC) | Parcial — OANDA é broker, não prop. Risco de correlação de backend |
| 2025-mid | Alpha Capital reestrutura controle (Kohler Investment Group + AMGP) | Não — entidades fora da lista |
| 2025-05 | FundedNext lança FNmarkets (broker próprio, licença Comores) | Não — verticalização própria |
| 2023-10 | FTMO compra Quantlane | Não — tech, fora da lista |
(Estas duas seções vieram de
.claude/knowledge/prop-firm-ownership.md, apagado em 2026-08-12. Aquele arquivo era gêmeo deste, sem nenhum apontador vivo e com zero consulta medida em 520 transcritos — mas estas duas seções só existiam lá. Auditoria dos 22 documentos deknowledge/.)
Toda conta precisa de uma etiqueta pra operar, e o sistema te protege de
transformar sem querer uma conta de verdade numa conta de teste. Esta página
explica as três proteções que trabalham juntas nos bastidores — você quase
nunca vê, mas elas evitam acidentes sérios.
Analogia: crachás numa empresa. Cada pessoa tem um crachá (Visitante,
Funcionário, Diretor). Sem crachá, ninguém entra na área de produção. E é
impossível colar o crachá de "Visitante/teste" numa pessoa que é funcionária
de verdade — o segurança não deixa. Aqui é igual: cada conta tem uma etiqueta,
e não dá pra rebaixar uma conta de mesa a "conta de teste".
Cada conta carrega uma etiqueta que diz de que mundo ela é:
A regra é simples: conta sem etiqueta não recebe sinal e não opera. É de
propósito — uma conta "solta", sem classificação, fica de fora por segurança, em
vez de operar por engano. Quando você cadastra uma conta na pool, ela ganha a
etiqueta Demo automaticamente.
Você não consegue transformar uma conta de produção (de mesa/pool) numa conta
de teste. Se tentar, aparece um recado claro:
"Essa conta é uma conta de produção (mesa/prop firm) e está (ou esteve)
vinculada a uma pool — por segurança, não dá pra transformá-la em conta de
teste. Se ela é de verdade uma conta aposentada, use 'Aposentar/Reclassificar'
primeiro."
Por que isso existe: as ferramentas de teste conseguem disparar ordens de
mentira. Se alguém pudesse disfarçar uma conta de verdade como "teste" e depois
apontar uma ferramenta dessas nela, dispararia ordem numa conta que não devia. A
trava fecha esse caminho. Pra reaproveitar uma conta de verdade, o caminho certo é
Aposentar/Reclassificar primeiro (isso solta a classificação de produção com
segurança).
As ferramentas que forçam situações de teste (forçar uma trava, forçar uma troca
de versão do robô, etc.) só funcionam na conta-cobaia (Sandbox). Mesmo que
alguém vire a etiqueta de uma conta de verdade pra "teste", essas ferramentas
recusam — elas não confiam só na etiqueta, elas conferem qual conta é.
É um segundo cadeado por cima do primeiro: cinto e suspensório.
Quando você aposenta uma conta por desfecho terminal (ela morreu ou passou de
vez), a chave de acesso dela é trocada. Assim, o robô que estava naquela conta
perde o acesso e não fica pendurado operando algo que já acabou.
E se você reaproveitar a conta depois? Sem problema — quando você reclassifica
a conta pra usar de novo, o robô reconecta sozinho com a chave nova, sem você
precisar fazer nada. Se em vez de aposentar você só está reclassificando (reuso
imediato na mesma conta), a chave é preservada — o mesmo robô continua sem
interrupção.
Você quase nunca interage com isso diretamente — são redes de segurança que
trabalham em silêncio pra que um clique errado não vire um trade indevido.
Status: ACTIVE | Ultima revisao: S326 (2026-05-04) — bump EA v3.82.0 -> v3.84.2 (atual). Drift coberto desde S294: S300 RECONCILE retroativo pos-offline (ReconcileEngine.mqh + 3 hooks LinniuC.mq5), S301 fix log spam ReconcileEngine (gate 60s OnTimer), S313 remove MODIFY precheck por tolerancia (formula 5point10^(digits-1) sempre dava 0.5 USD constante), S315.2 popup_text Unicode escape \uXXXX (preserva PT-BR no JSON), S319 cleanup WebBridge_PostReverse codigo morto. S326 Sessao A+B (NOVO): telemetria T4 EXATA via campo
ea_received_at_msno ACK payload. Buffer circularg_signalReceivedAtMs[64]em WebBridge.mqh + helperRecordSignalReceivedchamado dentro deLNC_BeginSignalObs(cobre WS via ExecuteSingleSignal:728 + HTTP via PollAndExecuteSignals:1221). Helper_S326_NowEpochMs()usa TimeGMT()*1000 com fallback TimeCurrent() se TimeGMT()=0. Sandboxaccount_id=3validado live em 4 signals (WS+HTTP+OPEN/MODIFY/CLOSE). Modulos e responsabilidades inalterados. Tabela WebBridge.mqh "v3.69.0" e nota historica preservada. [allow-spec]SSoT para: Modulos EA, compilacao, versionamento, erros MT5, armadilhas
Versao: 2.4 (EA v3.84.2, atualizado S326 — 2026-05-04). Desde 2.3: S300 ReconcileEngine retroativo, S301 fix log spam gate 60s, S313 remove MODIFY precheck constante, S315.2 popup_text Unicode escape, S319 cleanup PostReverse, S326 telemetria T4 ea_received_at_ms (Sessao A backend+UI + Sessao B EA exato).
Consolida:memory/ea-modules.md+.claude/knowledge/copytrade-ea.md
Relacionados:specs/invert-rules.md(inversao),specs/auto-update-flow.md(auto-update),specs/settings-sync-guardian.md(sync settings),memory/business-constants.md(constantes)
g_floorBreached=true → EA para TUDO (one-way, sem recovery)gui_lock_<HWND>.txt | 5. SettingsManager_InitPoll -> parse JSON -> para cada signal: resolve symbol -> apply buffers -> GUI execute -> detect local ticket -> ACK (FILLED/FAILED)
OnTradeTransaction — evento instantaneo do MT5. Chama SE_FindRecentOpenDeal que detecta o ticket no momento da transacaoDetectNewTicket — polling (antes era primario). Usado se OnTradeTransaction nao capturar| Modulo | Linhas | Prefixo | Responsabilidade |
|---|---|---|---|
| WebBridge.mqh | 809 | g_wb_ | HTTP via WinInet. Heartbeat, poll, ACK, signal post. Headers: X-API-Key + X-Account-Num. v3.69.0: ACK JSON precos usam Digits() do simbolo (antes: hardcoded 5). v3.84.0+ (S326 Sessao B): novo bloco no inicio do arquivo com struct SSignalRcvTs, buffer circular g_signalReceivedAtMs[64] (TTL implicito por overwrite, suficiente pra multi-ordem N=20 stress) + 3 helpers (_S326_NowEpochMs com fallback TimeCurrent quando TimeGMT()=0, RecordSignalReceived(signalId), GetSignalReceivedMs(signalId)). WebBridge_SendAck agora chama GetSignalReceivedMs(signalId) e adiciona campo "ea_received_at_ms":<long> no JSON do ACK quando valor > 0 (omitido quando 0 = entrada nao encontrada, preserva backward compat). Helpers movidos de LinniuC.mq5 em v3.84.1 (forward-dep cross-file MQL5 falhava silente). |
| GUIExecution.mqh | 1185 | g_gui_ | GUI automation Win32. OPEN=F9, MODIFY=ListView+DBLCLK, CLOSE=ListView+Fechar. Lock file anti-conflito. v3.83.8 (S315.2): GUI_TryConfirmBrokerPopup(hDlg, &popupText) retorna texto do popup via out param. GUI_JsonEscapeStr faz Unicode escape canonico \uXXXX (BMP) em chars >= 0x7F pra preservar acentos PT-BR no JSON HTTP body sem quebrar FastAPI parser. 3 emits gui_*_manual_close (OPEN s12 / MODIFY m6 / CLOSE) incluem field popup_text no payload signal_events. Spec: specs/decisions/0014-popup-text-mql5-unicode-escape.md |
| SnapshotEngine.mqh | 417 | g_se_ | Detecta OPEN/MODIFY/CLOSE. Compare snapshots -> enfileira. Cooldowns (CLOSE 0.5s, MODIFY 1s, OPEN 0.5s — OPEN guard via SE_SuppressTicket(newTicket, 5s)). SE_MAX_RETRIES=5: sinal descartado apos 5 falhas. SE_MAX_QUEUE=200 |
| SymbolResolver.mqh | 401 | g_sr_ | 3 camadas: Override -> Cache normalizado -> Alias (30 hardcoded). Buffers por classe |
| SettingsManager.mqh | 541 | g_sm_ | Cascata: SERVER -> LOCAL+JSON -> LOCAL -> OFFLINE. Persiste em Common/Files/. Init nunca falha. v3.66.0: Settings Sync Guardian — WS push notifica EA de mudancas, EA verifica e aplica (ver specs/settings-sync-guardian.md). v3.75.0: Config Freshness — AgeSec(), IsFresh(), IsStale(), CheckFreshness(). Config >5min sem sync = stale. Correcao doc (G7/G18): em stale o Rollover camada 1 NAO pausa — o EA age sozinho com selfSwap DST-aware (Rollover_Check em Experts/LinniuC.mq5:3171, lógica stale :3186-3190: stale=IsStale() -> operativeSwap=selfSwap) e pula o drift-check contra o site caido. BuildAppliedJSON reporta config_age_sec + config_fresh |
| PanelDisplay.mqh | 440 | g_pnl_ | Painel Apple Finance Dark. Rounded corners, semi-transparent. Throttle 2s |
| AutoUpdate.mqh | 662 | g_au_ | States: IDLE->WAITING->DOWNLOADING->VERIFYING->LAUNCHING->DONE. Lock file anti-race. PS1 profile-aware |
| EventLog.mqh | 294 | g_el_ / EL_ | v3.73.2 (Fase C obs): Buffer per-signal de eventos de observabilidade (struct EL_Event, max 500 com FIFO overflow + WARN). EL_Emit() persiste cada evento em MQL5/Files/signal_events_pending.jsonl (append) — R3 zero-perda em crash. EL_LoadPersistent() em OnInit recupera buffer pos-crash. EL_FlushBulk() em OnTimer envia via POST /api/events (throttle 5s, batch 50, idempotente no servidor via UNIQUE). EL_RewritePersistFile + EL_BuildBulkJson auxiliares. Auto seq_num per-signal via scan linear. Sem call sites de emit ainda — integracao em WebBridge_SendAck/GUIExecution S1-S11/Poll fica em C10-C14. Spec: specs/ea-observability.md |
| WinInetHTTP.mqh | 325 | -- | Conexao persistente, request por chamada, leitura em chunks 8KB. Sem whitelist MT5 |
| WinHttpWS.mqh | 391 | -- | WebSocket async via DLL (MQL5 tem sockets TCP nativos mas sincrono/bloqueante; DLL da async + WS pronto + sem whitelist). Auth via 1a mensagem JSON. Fallback: HTTP polling sempre ativo |
| asyncwebsocket.mqh | 502 | -- | DLL wrapper para WS async |
| asyncwinhttp.mqh | 233 | -- | DLL wrapper para WinHTTP async |
| Caderneta.mqh | ~ | Cad_ / g_cad_ | v3.154.0+ (Fatia 2 restart resiliente): diario em disco MQL5\Files\linniuc_caderneta_<login>.jsonl — 1 linha JSONL por evento com digito verificador (FNV-1a hex8, Cad_Checksum) e hora de parede UTC (nunca tick-count, que zera no reboot). Eventos: intent_armed (ANTES do clique F9), intent_resolved, idw_arm/disarm, trap_arm/cancel, executed. Cad_Compact no boot reescreve so o vivo. Linha com checksum ruim -> descarta + g_cad_needsFullReconcile. Instrumentacao SO nos call-sites nao-selados (LinniuC.mq5, GUIExecution.mqh). Isolamento de teste via g_cad_fileOverride. Selo S5: Caderneta OBSERVA, nunca altera logica congelada. |
| BootGate.mqh | ~ | (puro) + LNC_BootGate* |
v3.154.2+ (Fatia 2 E3): portao de boot — apos restart, portas FECHADAS pra OPEN novo ate: ler caderneta -> cruzar broker (posicoes + HistorySelect por position_id) -> resolver intents pendurados. intent_armed sem evidencia no broker -> FAILED_UNKNOWN (R1: NUNCA re-executa; politica F6 existente decide). Teto 60s -> modo degradado (CLOSE/MODIFY de posicao existente PODE, OPEN novo NAO). g_bootWatermark (menor intent nao-resolvido -1) enviado como last_signal_id no poll (E4). Heartbeat manda boot_gate/boot_degraded -> servidor alerta crash-loop se boot_gate >3min (E6, health_guard.boot_crashloop). Decisor PURO testavel (TestBootGate.mqh). |
| ReconcileEngine.mqh | ~210 | RE_ / g_reconcile_ | v3.83.3 (S301): Reconcile retroativo pos-offline. 3 trigger points: OnInit + OnTimer (gate 60s no LinniuC.mq5 + debounce interno 30s) + WS reconnect. RE_FetchOpenTradeLinks (GET /api/ea/open-tradelinks) + RE_ScanHistory (HistorySelect filtra DEAL_ENTRY_OUT por position_id) + RE_EnqueueRetroactive (POST /api/signals com is_retroactive=true). Profit canonico R4: DEAL_PROFIT + DEAL_SWAP + DEAL_COMMISSION. Mutex g_reconcile_running (R10) + checkpoint local (R11). Servidor garante idempotency (R6). NAO substitui _detect_missing_positions — eh fallback adicional. Decisao S302 (specs/decisions/0009-reconcile-architecture-simples-vs-esperto.md): manter gate 60s simples. Tentativa de smart short-circuit (v3.83.4) revertida — ganho irrelevante (~0.6s CPU/min total na pool) em troca de cobertura degradada nos cenarios 4 (OTT missado) e 5 (Snapshot bug). Specs: specs/reconciliacao-pos-offline.md + ADR 0009. |
Restart: Posicoes existentes viram snapshot inicial. NAO gera falsos OPEN ao reiniciar.
Anti-Echo (Pattern #34):
- SE_SuppressTicket(ticket, TTL): impede re-deteccao de ticket recem-operado
- OPEN: 5s TTL (chamado em LinniuC.mq5 apos GUI execute com sucesso)
- MODIFY/CLOSE: 10s TTL (chamado dentro do SnapshotEngine)
SettingsManager — Invert:
- ONLINE: Invert eh informacional. Servidor ja entrega sinais invertidos
- OFFLINE/LOCAL: EA faz inversao localmente via ApplyInversion
- SyncFromServer: So sobrescreve campo se servidor envia valor nao-nulo
Quando um deal fecha (SL/TP/STOP_OUT/MANUAL), o EA detecta no callback OnTradeTransaction e envia CLOSE direto via WebSocket — sem esperar o polling do SnapshotEngine (3s).
| Aspecto | Detalhe |
|---|---|
| Latencia | 10-50ms (vs 3s via SE polling) |
| Payload | deal_price (fill exato) + deal_profit (DEAL_PROFIT + COMMISSION + SWAP) |
| Fallback | Se WS fail → SnapshotEngine envia via HTTP normalmente |
| Guard | SE_MarkHintSent(posId) evita duplicata na fila SE |
| Close reasons | SL, TP, STOP_OUT, MANUAL |
EA manda HB cada 10s (WS) ou 30s (HTTP fallback). Servidor tem 2 lugares pra rastrear:
| Lugar | Campo | Throttle | Lag | Uso |
|---|---|---|---|---|
accounts.last_heartbeat_at (NOVO S314.3) |
UPDATED em CADA HB | NENHUM | <=10s | API canonica is_account_alive(), EA Tester badge |
heartbeats table (legacy) |
INSERT row + created_at |
WS_HB_DB_INTERVAL=30s (R2) |
ate 30s | Historico, balance/equity audit, callsites legacy (lazy migration) |
Helper canonico (server/account_filters.py:is_account_alive):
from app.account_filters import is_account_alive
if not is_account_alive(account, threshold_sec=30):
return jsonify({"error": "EA offline"}), 503
NULL-safe: retorna False se last_heartbeat_at IS NULL (conta nova/pre-migration).
Adopcao lazy — 6 callsites legacy (ea_update.py:529, monitoring.py:64, daily_report.py,
settings.py, heartbeat.py:1184, accounts.py:565) continuam usando
heartbeats.created_at. Cada PR que tocar arquivo migra pro helper. (S320: tournament.py removido.)
Spec completo: heartbeat-last-seen.md.
A partir de S386, o batimento (WS_SendHeartbeat em Include/CopyTrade/WinHttpWS.mqh) virou pulso leve — enriquecido com campos novos pra observabilidade da saturacao SEM tocar caminhos pesados:
| Campo | De onde vem | Custo |
|---|---|---|
balance, equity, positions_count |
cache do OnTradeTransaction (ja calculados) |
zero |
queue_depth |
SnapshotEngine_QueueCount() |
O(1) |
oldest_queued_age_ms |
SnapshotEngine_OldestQueuedAgeMs() — MAX de now - enqueuedTick per-item, GetTickCount monotonico |
O(N) na fila ate teto pequeno |
busy, busy_op, busy_ticket |
globais em GUIExecution.mqh (g_gui_busyOp, g_gui_busyTicket, g_gui_busySince), setados pelo beacon. NOTA: o pulso transmite apenas busy/busy_op/busy_ticket; o servidor calcula busy_since server-side via set_busy() no momento que recebe o beacon (nao depende do pulso transmitir). |
zero |
last_gui_ms |
GUIExecution.mqh::GUIExecution_LastGuiMs() (retorna g_gui_lastAnyMs — duracao da ULTIMA op de GUI, qualquer tipo) |
zero |
ea_version, timer_tick |
constantes/contador | zero |
Invariante R1 (S386): WS_SendHeartbeat NUNCA chama HistorySelect* nem GUI_*. Censo pesado (WB_BuildPositionsJSON) fica no ciclo separado de ~30s, inalterado.
ea_busy (S386 Fase 2)GUIExecution.mqh em OPEN/MODIFY/CLOSE dispara WS_SendBusyBeacon(op, ticket, expected_ms) IMEDIATAMENTE APOS pegar o lock de GUI (GUI_AcquireGUILock), antes do trabalho lento de tela. Se o lock falhar, o beacon NAO eh enviado. Frame WS {"type":"ea_busy",...}, dispara-e-esquece (timeout <=200ms, sem retry, sem ACK queue). Globais g_gui_busyOp + g_gui_busyTicket + g_gui_busySince setados sincronos pro pulso reportar status atual. Limpeza ocorre via GUI_ClearBusy() (em GUIExecution.mqh:204, chamado pelo GUI_ReleaseGUILock em :694) ao final de cada op. O servidor reflete o estado via pulso quando g_gui_busyOp="". Em paralelo, o ACK transporta gui_duration_ms (montado em Include/CopyTrade/WebBridge.mqh:1249) pra calcular latencia.
Spec completo: saturacao-visibilidade-gui-S386.md + signal-lifecycle.md secao "Estado Afogado".
Apos GUIExecution_Modify retornar sucesso, o EA le de volta os valores do broker e so envia FILLED se realmente aplicou:
max(5.0 * point * 10^(digits-1), 0.01) — acomoda precisao do simboloFAILED com diagnostico completo (g_gui_modifyDiag)O EA usa 3 estrategias em cascata pra preencher campos do dialog F9:
| Prioridade | Metodo | Como funciona | Quando falha |
|---|---|---|---|
| 1 (primario) | GUI_TypeText (WM_CHAR) |
Envia caractere por caractere — unico que atualiza o valor INTERNO do MT5 | Terminal extremamente lento |
| 2 (fallback) | SlowType |
Mesmo que #1 mas com 20ms delay por caractere | Raro |
| 3 (ultimo) | WM_SETTEXT |
Muda display mas NAO valor interno — broker pode rejeitar | Campo "validado" pelo MT5 |
Sync Barrier (WaitForEditValue):
1. SendMessageW(WM_NULL) — flush da fila de mensagens do OS (barreira de sincronizacao)
2. Check imediato — funciona 95%+ das vezes (sem polling)
3. Fallback poll 500ms — captura edge cases (MT5 reformatando valor)
Descoberta (S124): WM_SETTEXT sozinho causa "Invalid Volume" — MT5 precisa de WM_CHAR real pra validar.
| Guard | Threshold | Acao se violar |
|---|---|---|
| Volume Guard | \|actual - expected\| / expected > 10% |
ACK = FAILED, servidor fecha ambos lados (delay 61-90s) |
| SL/TP Readback | Tolerancia dinamica por simbolo | ACK = FAILED com diagnostico |
Buffer circular das ultimas 15 linhas de log significativas. Enviado no heartbeat campo last_logs.
Exemplos:
"OPEN OK #123456 XAUUSD BUY vol=0.10 250ms [S1-S10]"
"MODIFY READBACK FAIL #123456 SL=2049.5(tgt=2048.5)"
"OTT Deal ENTRY_OUT SL posId=123456 P&L=150.50 reason=SL"
"EQUITY FLOOR BREACHED: 89850.00"
Usado pra diagnostico remoto sem acesso ao terminal MT5.
Cada passo da execucao GUI eh logado com timing:
| Step | Acao | Timing tipico |
|---|---|---|
| S1 | CloseAllDialogs | 5ms |
| S2 | F9 sent | 1ms |
| S3 | Dialog appears (WaitForDialog) | 50-250ms |
| S4 | SelectSymbol | 50-150ms |
| S5 | SubDialog visible | 10-50ms |
| S6 | Controls found (hVol, hSL, hTP) | 5ms |
| S6b | QuickFlush vol | 10ms |
| S7 | Volume set (TypeText) | 100-200ms |
| S8 | SL set | 100-200ms |
| S9 | TP set | 100-200ms |
| S10 | VOL validation (final check) | 5ms |
Trace acumulado em g_gui_traceLog (pipe-separated), enviado nos remote logs.
Servidor envia SL/TP raw (precos absolutos). EA aplica buffer localmente:
| Input | Default | Descricao |
|---|---|---|
InpSLBuf_Metals |
100 pips ($1.00 XAUUSD) | Buffer SL metais |
InpTPBuf_Metals |
0 | Buffer TP metais (sem buffer) |
InpSLBuf_Forex |
0 | Buffer SL forex |
InpTPBuf_Forex |
0 | Buffer TP forex |
InpSLBuf_Default |
0 | Buffer outros |
InpBufferMode |
PIPS | PIPS (pontos fixos) ou PRICE (absoluto) |
Logica: Buffer adicionado ao SL (margem extra contra slippage). TP geralmente sem buffer.
| Aspecto | Detalhe |
|---|---|
| Input | InpEquityFloor (double, 0=desabilitado, ex: 90000) |
| Check | OnTimer + PollAndExecuteSignals |
| Trigger | AccountInfoDouble(ACCOUNT_EQUITY) < equity_floor |
| Efeito | g_floorBreached = true → EA para TUDO (OPEN/MODIFY/CLOSE rejeitados) |
| Recovery | NAO tem — one-way (feature de seguranca, nao bug) |
| Complementa | Server-side equity floor check (redundancia) |
| Aspecto | Detalhe |
|---|---|
| Inputs | InpRolloverEnabled (bool), InpRolloverSwapHourUTC (int, default 22), InpRolloverMinBefore (60), InpRolloverMaxBefore (30), InpRolloverClockDriftMax (60 seg) |
| Check | Rollover_Check() chamado no OnTimer apos EquityFloor_Check() |
| Janela ativa | [swap - min_before, swap - max_before] — default [21:00, 21:30] UTC |
| Sorteio | DJB2 hash (account_login + ticket + UTC_date) % window_sec via Rollover_ComputeOffsetSec() — deterministico, sem persistencia. Mesma posicao mesmo dia = mesmo horario |
| Execucao | Rollover_TryClose() usa GUI Win32 (F9) — mesmo padrao de EquityFloor_Check |
| Retry | Rollover_ScheduleRetry() + Rollover_ProcessRetries() — jitter 15s/30s/45s, max 3 tentativas |
| Deduplicacao | Rollover_AlreadyDone() + Rollover_MarkDone() via g_rolloverDoneTickets[] — evita reabrir dialog no mesmo ticket |
| Clock drift | Se |TimeGMT() - rollover_server_time| > drift_max: desabilita camada 1 + WS rollover_drift — servidor cobre em T-30 (camada 2) |
| Eventos WS | rollover_close_ok, rollover_close_failed (apos 3 fails), rollover_drift |
| Fallback | Servidor (check_rollover_fallback em server/af/signals.py) acorda em T-30 se pair ainda executing + envia Telegram CRITICAL |
| SSoT | specs/rollover-dupla-camada.md (desenho), specs/af-rules-whitepaper.md §E9 (regra) |
Anti-pattern nota: sorteio eh POR POSICAO (nao por par/pool) — evita 5 fechamentos correlatos que prop firm identifica como carimbo de robo.
Compilar: bash compile.sh [LinniuC|RandomTrader|RiskCalculator]
Deploy completo: Usar bash deploy_ea.sh (NUNCA copiar .ex5 manualmente). O script: compila, copia .ex5 Terminal→Dropbox, sincroniza pra VPS.
compile.sh: Detecta worktree via git rev-parse (S301), define DBX apropriado, sincroniza .mqh do $DBX/Include/CopyTrade/ -> $MQL5/Include/CopyTrade/ antes de compilar, chama MetaEditor CLI, copia .ex5 de volta pro $DBX.
Terminal MT5: C:\Users\mrodr\AppData\Roaming\MetaQuotes\Terminal\53785E099C927DB68A545C249CDBCE06\MQL5
Include/CopyTrade: Pasta REAL (S301+) — antes era junction apontando pro Dropbox/main, mas isso vazava arquivos da worktree pro main local quando alguem editava .mqh fora do main. Agora cada bash compile.sh sincroniza explicitamente do $DBX (worktree-aware) pro Terminal.
O sistema tem dois fluxos de deploy distintos com riscos diferentes:
deploy-canonico.sh (servidor) |
deploy_ea.sh (EA) |
|
|---|---|---|
| O que sobe | Codigo Python do servidor | Binario .ex5 do EA |
| Origem do codigo | Commits do git (git push) |
Arquivo .mq5 no disco (working dir) |
| Pra onde vai | VPS (FastAPI restart automatico) | VPS + auto-update na pool de N EAs |
| Automatico? | SIM, via post-commit hook a cada commit | NAO, precisa rodar manualmente |
| Precisa guard contra dirty? | NAO — git push so envia commits, dirty no working dir fica pra tras | SIM — deploy_ea.sh compila o .mq5 no disco, dirty vira binario na pool |
Por que a assimetria eh deliberada:
- Compilar EA exige MetaEditor (Windows GUI), nao roda em CI
- Pool baixa .ex5 em coordenacao — erro propaga pra todas as N contas de uma vez
- Quality gate proposital: passo manual forca validacao no sandbox antes de propagar
- Server restart eh trivial (segundos), reverter eh facil; EA deploy mexe com ordens reais em 12 mesas prop
Implicacao operacional: depois de git commit no main, o servidor ja foi atualizado em background. Mas o EA so atualiza quando voce explicitamente roda bash deploy_ea.sh (com guard que verifica commit limpo + avisa se rodando de worktree).
Quando trabalhar numa worktree paralela (criada via Claude Desktop ou bash scripts/paralelo.sh create <nome>):
| Passo | Comando | Efeito |
|---|---|---|
| 1. Editar .mq5/.mqh | direto na worktree | edits ficam isolados na worktree |
| 2. Compilar | bash compile.sh (de dentro da worktree) |
detecta worktree, sincroniza .mqh da worktree -> Terminal MT5, compila |
| 3. Testar | restart EA no MT5 | EA roda codigo da worktree, isolado do main |
| 4. Deploy pra VPS | bash deploy_ea.sh (de dentro da worktree) |
guard recusa se ha mudancas nao-commitadas; senao compila + SCP + EaVersion + auto-update na pool |
| 5. Commit | git commit na worktree |
fica em claude/<nome> ou paralelo/<nome> |
| 6. Merge pro main (OBRIGATORIO) | git merge claude/<nome> no main local |
post-commit auto-empurra pra VPS |
Risco se pular passo 6: pool roda binario da worktree, mas codigo fonte fica SO na branch. Proximo bash deploy_ea.sh rodado do main compila codigo DIFERENTE/ANTIGO -> regressao silenciosa na pool.
Mitigacao S301++ (commit 125da3f): deploy_ea.sh agora tem guard:
- Aborta se ha mudancas tracked nao-commitadas (forca commit antes de deploy)
- Avisa (nao bloqueia) se rodar de worktree, lembrando do merge pos-validacao
- Override emergencia: DEPLOY_DIRTY_OK=1 bash deploy_ea.sh
Regra: SEMPRE incrementar EA_BUILD ao modificar EA ou qualquer .mqh. Formato: MAJOR.MINOR.PATCH.
CUIDADO: "3.9" > "3.10" em string compare! Sempre usar 2+ digitos: "3.09"
Verificar versao: Log inicializacao | Heartbeat ea_version | Dashboard aba Contas | API GET /accounts
Mixed versions no mesmo grupo: Heartbeat intervals diferentes (WS vs polling), bugs corrigidos em versoes diferentes, ghost WS connections. Recomendacao: Manter TODAS as contas do mesmo grupo na MESMA versao.
CRITICO: Auto-update NUNCA deve rodar durante trade ativo — EA verifica g_se_hasOpenPositions.
common.ini campo ProfileLast (UTF-16LE) pra detectar qual profile esta ativo. Injeta EA em 1 chart do profile ativo, remove dos extrasupdate_attempts <= 5, depois paraUPDATE accounts SET update_attempts=0 WHERE id=Xlstrip('b') no servidorWS Reconnect (v3.77.0+, S253):
- Backoff exponencial: 3s → 6s → 12s → 24s → 48s → 60s (cap 1min). Antes v3.76-: 10-300s (cap 5min) — WS raramente vencia HTTP em reconnect
- Jitter por conta: account_num % 15 (0-14s). Antes v3.76-: % 7 (0-6s). Spread maior anti-storm com 13 EAs simultaneos
- Flush parcial: _flush_pending_signals usa continue em erro (S253) — antes break deixava pendings 4o/5o orfaos ate proximo reconnect
- Health check 60s: se connected=true mas sem msgs -> reset
- Fallback: 3x WS fail -> HTTP polling (2-5s adaptive). WS retenta cada 120s
- Constantes em WinHttpWS.mqh: WS_RECONNECT_BASE_SEC=3, WS_RECONNECT_MAX_SEC=60, WS_RECONNECT_JITTER_MAX_SEC=15
Retomada apos reconexao (server-side, ea_ws.py):
- _flush_pending_signals (linha 600): ao EA reconectar + autenticar, servidor busca PendingSignal da conta com resolved=False e expires_at nao vencido -> re-envia todos via WS. Log [WS-FLUSH] Sent N pending signals to account X. TTL = PENDING_SIGNAL_TTL_SECONDS=60s (sinal velho = pending_expired)
- _detect_missing_positions (linha 647): compara posicoes reportadas no heartbeat vs TradeLink is_closed=False. Ticket sumiu -> cria CLOSE signals pros pares (sibling accounts) automaticamente. Grace 30s pos-abertura pra evitar falso positivo
- Ghost connection (ea_ws.py:59-66): EA reconecta -> servidor fecha WS antiga com code 1000 reason "replaced". Evita 2 WS abertas mesma conta disputando sinais
- Auth: 2 modos — query params ou 1a mensagem JSON {"type":"auth"}. Timeout 10s. Version gate MIN_WS_VERSION=3.0.4
- Flush ACK (v3.76.0+, "tick cinza"): EA envia {"type":"flush_ack","signal_id":N} imediatamente ao receber signal via WS (ANTES de executar). Servidor handler _handle_ws_flush_ack marca pending_signals.ws_received_at = now. Separado de resolved=True (tick azul = ACK de execucao). Permite distinguir "nao chegou" de "chegou mas nao executou". EAs <3.76.0 nao emitem flush_ack — coluna fica NULL e polling HTTP continua sendo rede de seguranca
| Codigo | Nome | Causa | Fix |
|---|---|---|---|
| 10004 | REQUOTE | Preco mudou | Retry com slippage maior |
| 10006 | REQUEST_REJECTED | Broker rejeitou | Checar restricoes da conta |
| 10014 | INVALID_VOLUME | Lote invalido | Checar min/max/step do simbolo |
| 10016 | INVALID_STOPS | SL/TP invalido | Distancia minima, checar invert |
| 10017 | TRADE_DISABLED | Trading off | Horario, permissoes, AlgoTrading |
| 10018 | MARKET_CLOSED | Mercado fechado | Esperar abertura |
| 10019 | NOT_ENOUGH_MONEY | Sem margem | Balance/leverage insuficiente |
"EA nao faz nada": Checar aba Experts no MT5. Se nenhum log -> EA pode estar em loop ou OnTimer nao esta sendo chamado.
new/delete/nullptr/templates/STLStringLen/StringFind/StringSubstr (nao .length())#ifndef/#define/#endif obrigatorio em todo .mqhProp firms detectam ordens de EA (magic number, filling flags, deal comment). GUI automation cria ordens que parecem MANUAIS (F9 dialog).
CTrade, OrderSend(), PositionClose() ou OrderModify()<Trade\Trade.mqh> no GUIExecution.mqhuser32.dll, FindWindowExW, SendMessageW, PostMessageWDescoberta (S73): EA usava OrderSend desde v3.12.0 disfarçado como "fill mode fix" sem usuario saber.
| Campo | OPEN | MODIFY | CLOSE | Descricao |
|---|---|---|---|---|
status |
X | X | X | FILLED, FAILED, SKIPPED |
ticket |
X | X | X | Ticket local do broker |
error_msg |
X | X | X | Mensagem se FAILED |
open_price |
X | X | — | Preco de abertura/modificacao |
sl |
X | X | — | SL aplicado |
tp |
X | X | — | TP aplicado |
volume |
X | — | — | Volume executado |
channel |
X | X | X | ws_push ou http_poll |
latency_ms |
X | X | X | Tempo de execucao GUI (ms) |
deal_price |
— | — | X | Preco de fill exato do deal |
profit |
— | — | X | DEAL_PROFIT + COMMISSION + SWAP |
Quando OrderCalcProfit() falha (XAUUSD durante rollover, exoticos), EA usa formula manual:
profit = (price_diff) * volume * pip_value. Garante que AF tem dados pra calculo de risco mesmo em periodos com bugs do broker.
EA le key de MQL5/Files/copytrade_key.txt no OnInit. Prioridade: FILE (se len >= 10) > INPUT. Whitespace trimmed.
| Versao | Feature principal |
|---|---|
| 3.25.0 | WS Reconnect backoff (5s→15s→30s) |
| 3.29.3 | OnTradeTransaction como primario pra ticket detection |
| 3.32.0 | Volume/SL/TP safety guards (abort on mismatch) |
| 3.34.0 | GUI-TRACE 50 logs + LNC_LogRemote buffer |
| 3.39.0 | OTT Fast Close via WS |
| 3.44.0 | Sync barrier (WM_NULL) em SetEditText |
| 3.49.0 | TypeText (WM_CHAR) como primario pra GUI input |
| 3.48.0 | MODIFY diagnostic (g_gui_modifyDiag) + readback validation |
| 3.58.0 | Buffer SL no EA only, servidor envia SL/TP raw |
| 3.65.0 | Equity Floor Circuit Breaker |
| 3.72.0 | Rollover Dual-Layer — camada 1 EA primary (TODO #152) |
| 3.76.0 | WS flush_ack "tick cinza" (S251/S252) — EA confirma recepcao via WS |
| 3.77.0 | WS backoff tuning (S253 Onda 1): 10-300s → 3-60s, jitter 7→15, break→continue |
| 3.78.0 | WS resume semantics (S253 Onda 2): EA envia last_signal_id no auth, servidor filtra flush. _detect_missing_positions com paridade HTTP/WS + advisory lock |
| 3.79.0 | Onda 3: zombie detector (g_timerTickCount no HB + alerta Telegram) + ACK queue 20→100 + dump persistente em disco |
wss:// nao funciona em WinHttpCrackUrl -> usar https:// + secure=trueStatus: ACTIVE | Ultima revisao: S221 (2026-04-13) — drift fixes: --host 127.0.0.1 (nao 0.0.0.0), PG 16 (nao 15), js_error_reporter movido pra endpoints [allow-spec] | Drift S294 (2026-04-25): apos S288-S289 main.py absorveu 3 features novas (Market Hours Guard Camadas 1/4 — ver specs/market-hours-guard.md, Correlation ID end-to-end HTTP middleware, Exception Alerter Telegram com dedup). telegram_alerts.py absorveu Telegram dry-run + AfAuditLog (sim-e2e-real F2). Stack/nginx/systemd/PG/cron/rollback inalterados. Cron VPS atual (S293 D''): backup_daily 03h, healthcheck_v2 /2, monitor /5, error-monitor /30 (sem LLM, S293), build-context, quality_monitor, audit-export. daily-audit + weekly-research REMOVIDOS S293. Drift S354 (2026-05-19): post-receive agora sincroniza
scripts/(causa-raiz drift Abr-25 — crons server-path rodavam código congelado); 7 crons versionados ganharam liveness marker (trap EXIT→/var/log/copytrade-cron-*-last-success.txt) que torna E23 executável. Cron VPS atual = 16 jobs (S433: +ea_rollover reinstalado, -watchdog.py aposentado; ver seção Cron + índice único emmalha-de-saude.md). Cache-bust cap 3 (2026-07-07): o passo 0/2 dodeploy-canonico.sh(re-taguear?v=dos assets pra furar cache do browser) generalizou de "sóindex.html" pra por-container*: agora varre TODO arquivo com refs?v=— incluistatic/js/index-inline.js, que faz_loadScript('/static/js/XXX.js?v=gHASH')LAZY com o?v=hardcoded (ex:ea_tester.js,simulacao_e2e.js,sim_e2e_battery.js,saude.js). Antes esses lazy NUNCA eram bumpados → deploy servia JS velho até bump manual (mordeu 2x na frente do EA Tester). Ordem: inline primeiro (avança o próprio hash),index.htmldepois. Motor: fncachebust_container()emscripts/deploy-canonico.sh. [allow-spec]SSoT para: Stack VPS, deploy, nginx, systemd, backup, rollback, Telegram, PostgreSQL
Consolida:memory/server-infra.md+.claude/knowledge/copytrade-ops.md+.claude/knowledge/copytrade-server.md+.claude/knowledge/copytrade-rollback.md
Internet -> Cloudflare (CDN/WAF) -> nginx (SSL linniuc.com) -> uvicorn 127.0.0.1:8000 (1 worker) -> FastAPI
|
PostgreSQL 16
ssh [email protected]/opt/copytrade-server/app/curl localhost:8000/api/...https://linniuc.com/api/ (porta 8000 NAO exposta)[Service]
User=copytrade
WorkingDirectory=/opt/copytrade-server
ExecStart=/opt/copytrade-server/venv/bin/gunicorn app.main:app \
--bind 127.0.0.1:8000 --workers 1 \
--worker-class uvicorn.workers.UvicornWorker \
--timeout 120 --graceful-timeout 30 --keep-alive 5 \
--access-logfile - --error-logfile - --log-level info
Restart=always | RestartSec=5
1 worker (single-process). Restart limit: 5 em 10s, depois "failed".
Drift fix S373 (2026-05-23): ExecStart real usa gunicorn (gerenciador de processos que supervisiona o worker — reinicia no crash, mata se travar >120s) com worker uvicorn (
UvicornWorker), naouvicorndireto como dizia antes. 1 worker, bind so em localhost. Verificado viasystemctl cat copytrade. [allow-spec]
systemctl restart copytrade # restart
systemctl reset-failed copytrade # limpar failed
journalctl -u copytrade -f # logs real-time
journalctl -u copytrade --since '5m ago' --no-pager # ultimos 5min
Config: /etc/nginx/sites-enabled/copytrade. HTTP->HTTPS redirect. www->non-www redirect.
SSL: /etc/ssl/linniuc.com.pem + .key. Certbot auto-renew.
WS: proxy_http_version 1.1, upgrade headers, timeout 600s.
| Erro | Causa | Fix |
|---|---|---|
| 502 | Uvicorn down | systemctl restart copytrade |
| 504 | Request >60s | Query lenta ou endpoint travado |
nginx -t && systemctl reload nginx # testar + reload sem downtime
certbot certificates # validade SSL
| Task | Intervalo | Funcao |
|---|---|---|
| offline_checker | 30s | Detecta EAs offline >90s, Telegram alert |
| drawdown_checker | 5min | Alerta drawdown por tipo conta |
| daily_report | 23:55 UTC | Relatorio diario Telegram |
| periodic_cleanup | 15min | Limpa heartbeats antigos, reconcilia trade_links orfaos, resolve pending expirados |
| auto_close_pendings | 15s | Auto-close stale pending signals (sinais nao consumidos) |
| initial_cleanup | 30s apos start | Resolve pending expirados na inicializacao |
| tv_price_ws | continuo | WebSocket TradingView precos (watchdog 45s) |
| telegram_bot | continuo | Polling comandos Telegram |
| af_scheduler | continuo | Orquestracao AutoFund — plano diario, execucao de pares |
| af_scheduler Block 7 | ~5s (dentro do scheduler) | Swap close checker — fecha pares antes do rollover |
| af_scheduler Block 8 | ~5s (dentro do scheduler) | MODIFY fallback — re-triggers stale modify_scheduled >3min (S154) |
| af_scheduler Block 9 | ~5s (dentro do scheduler) | Alert latch timeout — publica msg Telegram parcial se 2o ACK nao chegou em 10s (S258) |
| af_scheduler DST check | ~1h (dentro do scheduler) | Auto-adjust swap_hour_utc: 21 (verao) / 22 (inverno) (S154) |
| af_daily_report | 22:00 UTC | Relatorio AF diario via Telegram |
Endpoints auxiliares (nao background task):
- POST /api/js-error (main.py:835-854) — erros JS do dashboard vao pro Telegram (max 50/uptime via contador _JS_ERROR_MAX, dedup). Nao eh background task; eh endpoint HTTP normal chamado pelo window.onerror do dashboard.
Anti-spam Telegram (S389): o agrupador de rajada (burst aggregator em telegram_alerts.py) agora age SO em erros/riscos — alertas de status (EA online/offline/atualizado/rollout/restart/login ok/reconcile ok) sempre chegam individuais. Erros comprimidos sao resgataveis via comando /erros no bot. SSoT: specs/telegram-antispam-so-erros.md.
Índice único de TODOS os auditores/vigias (cron + in-server + Telegram + quem-vigia-quem):
specs/malha-de-saude.md(SSoT da malha de saúde). A lista abaixo é o crontab; a semântica de
auditoria (o que checa, sobreposições, drifts) vive lá.
0 3 * * * backup_daily.sh # Backup diario 3h UTC (VPS-only)
*/2 * * * * scripts/vps/healthcheck_v2.sh --watch # Vigia UNIFICADO modo leve: restart conservador (S407 #409)
*/5 * * * * scripts/vps/healthcheck_v2.sh --full # Vigia UNIFICADO modo cheio: exames de banco + JSON + Telegram dedup (S407 #409)
# monitor.sh APOSENTADO (S407 #409): fundido no healthcheck_v2 --full. Vigia mutuo: server vigia frescor do JSON (schedule_watchdog_freshness_check).
0 * * * * build-context.sh # Contexto pre-compilado do bot
*/30 * * * * error-monitor.sh # Erros journalctl -> Telegram. Julga por TEXTO, nao por prioridade (2026-08-01: filtrava -p err/warning e via ZERO — o journald estampa o stream do gunicorn como info). Detalhe: malha-de-saude.md
*/15 * * * * quality_monitor.py # Quality monitor unificado
# watchdog.py APOSENTADO S433 — dobrado no vigia-mutuo in-server (health_checks.py, 5min)
30 3 * * * cron/purge_signal_events.sh # Retention signal_events 90d
0 * * * * cron/health_audit.sh # Traffic por endpoint -> alerta
0 4 * * * cleanup_login_attempts.sh # Purga login_attempts >30d
45 3 * * * cron/audit-export.sh # Export audit .csv.gz (6 anos)
30 4 * * * cron/hunter_nightly.sh # Caca-Bugs pente fino 12 cacadores -> Telegram so se quebrar
0 12 * * * vps/drift-detect.sh # Detecta edicao direta VPS
0 10 * * 0 spec-freshness-alert.sh # Freshness specs (VPS-only)
Fonte da verdade do crontab (S417): scripts/vps/crontab.canonical (versionado) +
scripts/vps/install-crontab.sh (instalador idempotente, roda na VPS). NÃO editar o crontab na mão
— foi a edição VPS-only que fez o error-monitor sumir sem rastro. Mudou cron? Edita o .canonical,
push, e roda o instalador.
Deploy de scripts/ (S354): post-receive sincroniza scripts/ →
/opt/copytrade-server/scripts/ (rsync SEM --delete — preserva VPS-only
como spec-freshness-alert.sh). Antes (até Abr-25) só server/static/docs
eram rsync'd → crons server-path rodavam código congelado. Crons chamados de
/opt/copytrade-code/scripts/ (build-context, error-monitor, drift-detect)
sempre foram frescos (checkout direto do hook).
Liveness markers E23 (#257): os 9 crons versionados gravam epoch UTC em
/var/log/copytrade-cron-<name>-last-success.txt via trap EXIT (só em exit
0). GET /api/health/cron-status lê isso → estação E23 da bateria EA Tester
sai de WARN permanente (available=false) para executável.
Alerta proativo de cron stale (#321, S378+): antes, a estação E23 só pegava
um cron caído manualmente (rodar a bateria) — drift-detect ficou DOWN 6 dias
sem ninguém ver. Agora uma task de fundo no próprio servidor (schedule_cron_stale_check
em server/routes/health_checks.py, disparada no lifespan do main.py) checa
o cron-status a cada CRON_STALE_CHECK_INTERVAL_SEC (6h) e manda Telegram por
cron stale, com dedup por nome via send_telegram_alert(category="cron_stale",
dedup_key=<nome>, cooldown=24h) (≤1 alerta/cron/dia). Decisão de design: mora
no servidor (sempre-vivo), NÃO num cron — um cron vigiando crons teria o MESMO
ponto único de falha que isto quer eliminar ("quem vigia o vigia"); se o servidor
cair, tudo já alerta. Função pura testável stale_crons_for_alert(status, expected=None)
(filtra stale=True E dentro da allowlist; available=false → [], degradação graciosa).
Limitação conhecida: um cron que nunca gravou marcador é invisível (sem marcador = fora do
mapa); pega o caso real "rodou antes, parou agora" (marcador envelhece > 24h).
S417 — 3 endurecimentos (após o error-monitor cair do crontab e o alerta repetir a cada deploy):
- PROB-A (cron caiu sem rastro): o crontab agora tem fonte da verdade versionada em
scripts/vps/crontab.canonical + instalador idempotente scripts/vps/install-crontab.sh.
Editar crontab na mão é o anti-pattern que fez o error-monitor sumir (VPS-only, sem rastro).
- PROB-B (órfão alarma pra sempre): o caminho de alerta (stale_crons_for_alert) filtra
por _CRON_EXPECTED_DEFAULT (9 crons que escrevem marcador). Marcador órfão de cron
decomissionado segue visível no painel E23 (_read_cron_status NÃO filtra — diagnóstico) mas
nunca dispara Telegram. env CRON_EXPECTED (csv) sobrescreve; vazia → usa a constante (não
fail-open total). Stale fora da allowlist → logger.info, nunca Telegram.
- PROB-C (re-spam no restart): dedup persistido em disco (CRON_STALE_DEDUP_PATH, default
ao lado do monitor_status.json em /opt/copytrade-server/), map nome→dia-UTC. Antes era só
in-memory: cada deploy zerava e re-alertava 180s depois. Agora ≤1 alerta/cron/dia mesmo com N
deploys. Fail-open: erro de IO → alerta (nunca silencia).
Pre-deploy:
1. Syntax check: python -c "import ast; ast.parse(open('file.py').read())"
2. scp arquivo [email protected]:/opt/copytrade-server/app/
3. Import test: ssh root@... 'cd /opt/copytrade-server && python -c "from app.modulo import funcao"'
Deploy:
4. Upload TODOS os arquivos ANTES de restart (deploy atomico)
5. ssh root@... 'systemctl restart copytrade'
Post-deploy:
6. systemctl is-active copytrade -> "active"
7. journalctl -u copytrade --since '30s ago' -> sem erros
8. curl -s https://linniuc.com/api/health -> {"status":"ok"}
9. EAs reconectaram? (ct_status)
REGRA: Restart causa ~2-3s downtime com 4-6 HTTP 502 nos heartbeats. ESPERADO.
Horarios a EVITAR: 23:50-00:05 (daily report), Sun 22:00 UTC (abertura mercado), durante trade ativo.
A resposta curta: ~70s do disparo ao "concluido", e a maior fatia (53s) e a bateria
bloqueante pytest -m trap_gate, que roda contra staging ANTES de tocar o runtime.
Esta secao existe porque a pergunta "da pra deixar o deploy mais rapido?" ja foi feita e
respondida com medicao — sem ela, a proxima pessoa remede tudo ou, pior, remove a bateria
achando que sao 50s de gordura. Nao sao.
| pedaco | tempo | % | o que acontece |
|---|---|---|---|
| preparo | 26,6s | 53% | _ensure_sqlite_schema (autouse em server/tests/conftest.py) roda create_all ANTES DE CADA uma das 353 provas |
| coleta | 14,3s | 28% | o pytest IMPORTA os 806 arquivos de server/tests/ antes de filtrar pela marca |
| execucao real | 7,1s | 14% | o trabalho que voce quer pagar |
| limpeza | 5,1s | 10% | teardown |
Para cada 1 segundo testando, gastam-se ~6 preparando e escolhendo. E os dois maiores
NAO sao desperdicio:
conftest.py como ja tentada e"Aponte os 50 arquivos em vez da pasta inteira" — parece resolver os 14,3s. Medido:
| forma | tempo | provas |
|---|---|---|
| pasta inteira (como e' hoje) | 54s | 433 |
| apontando os arquivos | 68s (26% PIOR) | 422 |
E perde 2 arquivos, porque a bateria tem DUAS portas de entrada, nao uma:
| porta | como entra | arquivos |
|---|---|---|
| por NOME | casa um dos 15 fragmentos de _TRAP_GATE_SUBSTR (server/tests/conftest.py) |
50 |
| por MARCA explicita | o arquivo declara pytestmark = pytest.mark.trap_gate |
2 |
Total: 52 arquivos. Qualquer lista montada lendo NOMES e' cega pra segunda porta — e a
cegueira e' silenciosa (a bateria roda, fica verde, e 2 arquivos ficaram de fora). Ironia
registrada: um dos dois e' test_g3_post_receive_trap_enforcement.py, que vigia se a
bateria esta mesmo sendo chamada no deploy, e se marcou de proposito pra se proteger.
Os 52 arquivos sao de UM tema so': posicao aberta que pode ficar largada (armadilha 21,
perna orfa 13, solo/ronda/persist 8, corridas/forcador/vigia 8+2). O criterio da lista
curada e' "tema sem segunda rede".
Os 12 cacadores do hunter_nightly.sh (cron 04:30 UTC) nao cobrem armadilha nem perna
orfa — sao limbo, modify, dd_breach, dd_recog, invert, rollover, pairing, rsafe,
carteiro, conferencia, zerosum, faxina. Remover a bateria deixaria 34 dos 52 sem rede
automatica nenhuma. E os metodos sao diferentes: o cacador varre combinacoes num banco
descartavel; a bateria roda os testes contra o staging, com o codigo que vai publicar.
Criar a estrutura do banco uma vez por arquivo em vez de por prova (potencial ~20s).
Descartada pelo RISCO, nao por medicao — mexe exatamente na peca que impede falha falsa.
Se um dia os 53s incomodarem de verdade, e' ali que se deve olhar.
⚠️ Os numeros VARIAM ~5% entre execucoes — e' medicao de relogio numa maquina com outras
conversas rodando, nao constante. Segunda passada conferida no mesmo dia: preparo 25,94s
(documentado 26,6), execucao 6,75s (7,1), limpeza 4,32s (5,1) — a contagem de arquivos (52)
e as PROPORCOES sao estaveis; os decimais nao. Se voce recontar e der diferente na 2a casa,
a doutrina nao esta errada: use a SUA medicao e nao reescreva esta secao por causa disso.
Reescreva se a PROPORCAO mudar (ex: execucao real passar de 14% pra 40%) — ai algo mudou de
verdade.
# tempo total + quantas provas
time python -m pytest server/tests/ -m trap_gate -q -p no:randomly
# onde vao os segundos (preparo vs execucao)
python -m pytest server/tests/ -m trap_gate -q -p no:randomly --durations=0 2>&1 | grep -E "^[0-9]+\.[0-9]+s (call|setup|teardown)" | awk '{gsub("s","",$1); soma[$2]+=$1; n[$2]++} END {for (k in soma) printf "%-9s %7.2fs em %4d
", k, soma[k], n[k]}'
# quantos arquivos a bateria pega HOJE (as duas portas)
python -m pytest server/tests/ -m trap_gate --collect-only -q -p no:randomly 2>/dev/null | grep "::" | sed 's/::.*//' | sort -u | wc -l
Deploy/mudanca + sistema quebrou?
|
+-- Servidor 502? -> §1 Rollback servidor
+-- DB corrompido? -> §2 Restore DB
+-- EA crasha? -> §3 Rollback EA (ver specs/ea-architecture.md)
+-- Config errada? -> §4 Revert config
+-- Tela branca? -> §5 Rollback frontend (ver specs/dashboard.md)
§1 Servidor: cp arquivo.py.bak arquivo.py && systemctl restart copytrade. Sem .bak: git show HEAD~1:scripts/server/ARQUIVO.py.
§2 DB: systemctl stop copytrade && psql < BACKUP.sql && systemctl start copytrade. Parcial: pg_restore -t TABELA.
§4 Config: ct_update_account(account_id=ID, field="group_id", value="GRUPO") ou DB direto.
Checklist pos-rollback: Servidor ativo? Logs sem erros? EAs reconectaram? Posicoes intactas? Dashboard funcional?
Pool (S361/S406, conferido S-atual): prod 25 fixas + 25 overflow = 50 (DB_POOL_SIZE/DB_MAX_OVERFLOW, database.py:36-37); sim 6 + 4 = 10 (sim_engine bulkhead S406, database.py:74-75); pool_timeout=5s, recycle 30min. Teto: 50+10=60 < ~70 (RAM-seguro 2GB) << 97 (copytrade_user; superuser reserva 3). Escalar além de ~25 contas: PgBouncer ou +RAM, NÃO esticar o teto do PG. (Drift corrigido: dizia "5+10=15", era estimativa antiga.)
pg_stat_statements: Habilitado (PG 16). MCP Postgres Pro disponivel.
psql -U copytrade_user -d copytrade -c "VACUUM ANALYZE;"
Tabelas que crescem: heartbeats (cleanup 6h >24h), signal_acks (monitorar), trade_links (ok se <100k).
Migrations: Sem Alembic. ALTER TABLE manual. SEMPRE backup antes. Deploy codigo DEPOIS do ALTER.
2 sistemas (mesmo bot @linniuc_vps_bot):
- telegram_alerts.py — Alertas automaticos (thread sync, non-blocking). Debounce por chave+cooldown
- telegram_bot.py — Bot interativo (asyncio polling). Comandos /s, /p, /help
Anti-spam: _should_send(key, cooldown). Chave ACK: ack_failed_{account_name}_{action} (5min).
Cooldown reseta no restart do servidor.
Severidade: _send(text, level="INFO") — prefix emoji por nivel: CRITICAL 🚨, ERROR ❌, WARNING ⚠️, INFO (sem prefix), DEBUG 🔍.
Todo envio com parse_mode=HTML passa por _sanitize_html_for_telegram. O Telegram le
< como abertura de etiqueta: um entry diff $8.97 < $14.00 derruba a mensagem INTEIRA com
400 "can't parse entities: Unsupported start tag". Nao e' truncamento — some tudo, calado.
Escape cego nao serve: o servidor usa ~1.490 etiquetas HTML de proposito. O portao e' um
filtro com lista de permissao — preserva o que o Telegram documenta e neutraliza o resto.
Idempotente; preserva < > & " ja escapadas (sem escape duplo) e converte
entidade NUMERICA (') pro caractere, que o Telegram nao aceita.
| Grupo | Etiquetas | Atributo |
|---|---|---|
| Sem atributo | b strong i em u ins s strike del pre tg-spoiler |
nenhum aceito |
| Atributo OPCIONAL | code (class="language-*") · blockquote (expandable) |
vale nua |
| Atributo OBRIGATORIO | a (href) · span (class="tg-spoiler") · tg-emoji (emoji-id em 1..int64) |
nua NAO passa |
Conferido contra a fonte (Bot API 10.2 + parser parse_html do tdlib): emoji-id tem
FAIXA, nao so forma — 0 e valores fora do int64 dao 400. tg-time existe e nos filtramos
de proposito (cosmetico; nao emitimos). Bloquear <a> nu e' conservadorismo nosso, nao
protecao — a sem href nao gera 400. Entidade numerica e' aceita pelo Telegram; nos
convertemos pro caractere por outro motivo (html.escape gera ').
A coluna "obrigatorio" nao e' detalhe: <span> nu virava 400 ("Tag span must have class
tg-spoiler") — o filtro deixava passar 3 familias que CAUSAM o erro que ele existe pra impedir.
Pareamento por pilha (2a passada): etiqueta so sobrevive se o PAR dela sobrevive. Sem isso,
<b class="x">X</b> saia com a abertura escapada e o </b> intacto -> 400 "Unmatched end tag":
o filtro FABRICAVA o erro. Cobre tambem "Unclosed tag". Saida sempre equilibrada.
| Caminho | Onde entra o filtro |
|---|---|
| Funil de alertas | telegram_alerts.py _do_telegram_post |
| Bot interativo (respostas a comando) | telegram_bot.py _send_message |
| Relatorio diario AF | af_daily_report.py _send_telegram |
| Relatorio diario geral | daily_report.py _send_telegram |
| Envio manual do dashboard | routes/admin.py send_telegram_message |
Rede: 400 com "parse entities" no corpo reenvia sem parse_mode
(_reenvia_sem_formatacao + _strip_html_for_plain_text). Aviso feio ainda avisa.
So existe no funil de alertas — nos outros 4 caminhos a garantia e' o filtro sozinho.
Tamanho: cortar_para_telegram corta o texto CRU e filtra depois (o pareamento cuida da
etiqueta que o corte deixou orfa). Cortar DEPOIS partia o <pre> ao meio. Mede em unidades
UTF-16, que e' como o Telegram conta — emoji fora do plano basico vale 1 em len() e 2 pra API.
Formatacao e' do CANAL, nao da mensagem: o sino do site (_mirror_to_dashboard) recebe o
texto SEM etiqueta, porque o dashboard desenha com escapeHtml (static/js/websocket.js:653)
e mostrava <b> literal. Medido: 424 de 3.371 avisos espelhados carregavam etiqueta.
Ao criar caminho NOVO de Telegram: passe pelo filtro. Cron/shell tem remendo proprio
(scripts/vps/_deploy_changelog.py, healthcheck_v2.sh, drift-detect.sh) — foram 3
remendos isolados do mesmo defeito antes de alguem consertar na raiz (licao P805).
Testes: server/tests/test_telegram_html_sanitize.py.
/api/sim/* e /api/test/* piso interno 1500/min. Login 30/min/IP (50 localhost). Toda resposta /api/* traz RateLimit-Limit/Remaining/Reset; o 429 traz Retry-After (>=1s) — cliente se auto-regula (dashboard apiFetch re-tenta leitura honrando Retry-After+jitter, nunca escrita; bateria idem). Relogio monotonic (imune a NTP). Estouro sustentado de uma chave (>=20/300s) -> alerta Telegram debounced. in-memory janela-deslizante (CF-Connecting-IP ou TCP source — anti-spoof). Spec: specs/api-rate-limit-sweetspot.md. TODO #322: token bucket futuro gated em metrica.Claude Code (Win11) --stdio--> copytrade_mcp.py (FastMCP 3.1, Python 3.14) --HTTPS--> linniuc.com/api/*
Auth JWT auto-renovacao. Cache TTL 15s/30s. 15 tools read-only.
| Script | Funcao |
|---|---|
deploy.sh |
Deploy seguro: backup -> syntax -> import -> restart -> health -> auto-rollback |
healthcheck_v2.sh |
Watchdog: servico, HTTP, disco, heartbeat age, orphan links |
monitor.sh |
Proativo: failed ACKs, pending, stale, orphans, disco, DB size |
backup.sh / backup_daily.sh |
Backup manual / diario |
restore.sh |
Restaura backup |
Adicionar: DB insert ou Dashboard Admin > Contas > Adicionar. Instalar EA, configurar key.
Remover: Fechar posicoes -> SET is_active=false -> remover EA do chart. NAO deletar do DB.
| Preciso de... | Comando |
|---|---|
| Disco | df -h / |
| RAM | free -h |
| CPU | top -bn1 | head -5 |
| Logs | journalctl -u copytrade --since '5m ago' |
| SSL | certbot certificates |
| Nginx | nginx -t && systemctl status nginx |
| Firewall | ufw status (portas: 22, 80, 443) |
| Clock | timedatectl status |
| DB size | SELECT pg_size_pretty(pg_database_size('copytrade')); |
http://5.161.104.126:8000 -> porta NAO expostaStatus: ACTIVE | Ultima revisao: S233 (2026-04-14) — drift flush: S11 closePair #160 fix, test accounts toggle, signal timeline modal, pool-paused timer freeze, historical chairs P53-safe [allow-spec] | Drift S294 (2026-04-25): apos S256-S289 — Tools tab ganhou Simulacao E2E sub-pill (S277-S280, ver specs/sim-e2e-real-S281.md), market_status badge S288 em af_hedge.js, swap_after_buffer_minutes input no form AF Pool, error feedback closePair/closeAllPool, Health Audit widget Sistema>Saude. Estrutura JS de namespace CT. + wsEvents inalterada. [allow-spec] | S311 (2026-04-29): classificacao PASSOU/BREACH em overview.js + af_hedge.js agora consome
prop_rules.target_pct/max_ddda API (era hardcode 8%/10%/100k duplicado). Ver secao "Classificacao de Contas Prop". [allow-spec] | S344 (2026-05-12): 3 bugs descobertos em wsEvents handlers via validacao visual Playwright. Fixes deployados: (a) handler 1 nao re-renderiza em modify_scheduled/modify_done (preserva badge cronometro), (b) setInterval do countdown agora tem ref pra clearInterval em modify_done, (c) catalogo de armadilhas em.claude/knowledge/dashboard-handler-traps.md. Ver secao "Bugs S344 — MODIFY badge handlers". [allow-spec] | S391 (2026-05-30): secao "Ultima Rodada" (rounds encerrados) em af_hedge.js agora agrupa cards por tipo (Concluidas x Nao executadas), espelhando o S388 da secao ativa — cada sub-grid com altura uniforme, mata o buraco branco entre card alto (CONCLUIDO) e baixo (TIMEOUT/PAUSA ROLLOVER). Helper purosplitLastRoundByTypeem af_hedge_logic.js (testado em vitest). Cabecalho de grupo so quando ha 2+ classes. Verspecs/af-last-round-group-by-type.md. [allow-spec] | S396+7 (2026-06-05): mini-box de precos (Entry/SL/TP/Exit embuildFighterHtml, af_hedge.js) reformado pra corrigir desalinhamento. SL e TP migraram pra um grid de 2 colunas (.af-sltp-gridno index.html: rotulo a direita | valor a esquerda) pra ficarem ALINHADOS ENTRE SI e imunes as decoracoes (✓ de MODIFY, * de colchao, destaque). Antes cada linha era um inline-block centralizado sozinho -> SL/TP saltavam ate ~10px na horizontal e o "SL ajustado" (✓ + caixinha + valor + ) VAZAVA pra 2 linhas em card estreito, deixando aquele card mais alto e quebrando a harmonia dos concluidos. Entry e Exit seguem centralizados cada um. Destaque de MODIFY virou outline (nao ocupa espaco no layout) no lugar de fundo+borda+padding. Lucro-projetado do live preservado. Validado por medicao Playwright no proprio buildFighterHtml: delta SL-TP=0, altura dos concluidos identica (86px). [allow-spec] | S396+8 (2026-06-05): Entry e Exit TAMBEM entraram no grid (mesma.af-sltp-grid, agora com.af-price-gridjunto) pra os 4 numeros (Entry/SL/TP/Exit) encostarem na MESMA coluna — corrige o desalinhamento residual que o S396+7 deixou (Entry/Exit ainda eram frases centralizadas soltas, e o numero caia em X diferente por linha: Entry ~+14px, Exit ~+10px, SL/TP no centro). Ordem (Entry topo / Exit base) e bloco centralizado preservados; o respiro Entry->SL e TP->Exit vira padding.sep-b/.sep-tnessas celulas. Validado por medicao Playwright na funcao real: spread dos 4 numeros = 0px (coluna perfeita) em todos os cards. HOTFIX (mesma sessao): mover Entry/Exit pra fora dasdiv.af-fighter-detailtirou ofont-size:11pxque vinha herdado -> os numeros do grid passaram a herdar 16px do card (GIGANTES, destoando do resto do card que esta em 11px) -> "cagou tudo, ficou gigante". Fix:font-size:11px+line-height:1.5EXPLICITOS no proprio.af-sltp-grid. Confirmado por medicao (16px->11px) e screenshot antes/depois. Licao: o harness mascarou porque o card de teste nao tinha font-size base (tudo herdava igual e parecia coeso); reproduzir SEMPRE com o mesmo contexto de font-size do site. [allow-spec]SSoT para: Frontend SPA, tabs, deploy, armadilhas, XSS, temas
Consolida:memory/dashboard-features.md+.claude/knowledge/copytrade-dashboard.md+memory/dashboard-sidebar-notes.md
/opt/copytrade-server/static/ (subpastas: css/, js/)https://linniuc.comlocalStorage.ct_token (NAO jwt!)access_token (NAO token)| Arquivo | Responsabilidade |
|---|---|
index.html |
Layout principal, sidebar, modais (~500 linhas) |
api.js |
Estado global (window.CT), API wrappers, helpers |
app.js |
Init, auth, tabs, toasts (~1475 linhas) |
websocket.js |
WS client, reconnect, wsEvents event emitter |
overview.js |
Tab Overview (cards equity, grupos) |
accounts.js |
Tab Contas (CRUD, remote settings) |
positions.js |
Tab Posicoes (close/modify individual) |
admin.js |
Tab Admin (cleanup, debug) (~800 linhas) |
remote_settings.js |
Modal Remote Settings (6 buffers) |
delay.js |
Delay Analysis (stats + tabela) |
ea_update.js |
Auto-update UI (upload, release, rollback) |
health.js |
Tab Saude (diagnostics, logs) |
roadmap.js |
Kanban roadmap |
af_hedge.js |
AF Hedge tab (pair battles, pool config, force execute, history, debug mode, swap countdown, compact timeline) (~3860 linhas) |
af_hedge_logic.js |
Helper functions para operacoes AF hedge (~150 linhas) |
journal.js |
Tab Sistema > Journal (diario de trades, CSV export UTF-8, filtros) (~360 linhas) |
prop_firms.js |
Tab Cadastros > Prop Firms (tabela comparativa + CRUD + arquivar/desarquivar) (~350 linhas) |
prop_firm_logic.js |
Helper PURO de mesa: fase de entrada, visibilidade por fase, rotulo "(encerrada)", contador (~90 linhas). Testado em vitest (tests/unit/prop_firm_arquivar_logic.test.js). Spec: specs/prop-firm-arquivar-desarquivar.md |
risk_calc_v2.js |
Risk Calculator V2 (3 modos: %Bal, %Eq, USD) (~980 linhas) |
~~simulator.js~~ |
REMOVIDO na Fase 5 (sim-rapido): a aba Monte Carlo antigo + o endpoint /api/simulator/run + o pacote scripts/hedge/ foram aposentados (endpoint ja retornava 500 em prod desde S280). O sucessor eh o simulador E2E/rapido (simulacao_e2e.js). |
styles.css |
Estilos globais + componentes extraidos (~3100 linhas) |
themes.css |
Overrides de temas (OBRIGATORIO) |
CT.*Todo estado compartilhado vive em window.CT = {} (definido em api.js). Variáveis: CT.token, CT.accounts, CT.allSignals, CT.equityChart, CT.chartHours, CT.refreshInterval, CT.notifSound, CT.ws, CT.wsHealthy, CT.syncingAccounts, CT.syncConfirmedAt, CT.afCurrentPool, CT.afPools, etc. Código novo DEVE usar CT.* — nunca criar variáveis globais soltas.
wsEventsWebSocket handlers registram-se via wsEvents.on('event', fn) em vez de monkey-patching handleWS. Definido em websocket.js. Eventos: heartbeat, signal, ack, reverse, login_failed, settings_synced, alert, account_new, af_pair_update. Módulos que escutam: af_hedge.js (af_pair_update), remote_settings.js (heartbeat), websocket.js (signal, ack, account_new, login_failed, heartbeat).
Todo CSS vive em styles.css e themes.css. NUNCA injetar CSS via JavaScript (document.createElement('style')). Seções em styles.css: base + Simulator + Delay Analysis + Remote Settings + Modify Modal + ACK Spinner + AF Hedge (battle cards, fighter panels, insight cards) + Journal + Prop Firms.
af_hedge.jsgetAccountDisplay() — busca nome completo + cor do dono de CT.accounts (alinhado com Visao Geral)cadastros.js. Hash legacy /tools/propfirms/cadastros/propfirms (window 30d).prop-firm-arquivar-desarquivar.md): botao por linha; a listaphases[0], nao "F1" chumbado) e aparece nas seguintes; a mesa que a conta ja usaGET /api/prop-firms filtra is_active por padrao?include_archived=true traz tudo); o servidor recusa a classificacao com?permitir_mesa_encerrada=true como escape explicito.Atalhos: Alt+1..5 (tabs), R (refresh), Esc (fechar modal), ? (ajuda)
Backwards compat: switchTab('signals') redireciona automaticamente para Trading > Sinais
Browser MCP (unico — S160):
| Ferramenta | Melhor para | Config Brave |
|---|---|---|
| Playwright MCP | Automacao, snapshots, screenshots, debug | --executable-path pro Brave, --isolated, --image-responses omit |
Login: Usar conta test_runner (NAO admin). Credenciais em .secrets.local e tests/test-helpers.js.
- Campos: #loginUser + #loginPass (submit com \n no campo senha)
- Apos login: #loginPage some e #dashboard aparece
Exemplo @playwright/cli:
playwright-cli open https://linniuc.com --headed # abre Brave
TEST_PASS=$(grep TEST_PASS .secrets.local | cut -d= -f2) # le a senha viva do arquivo gitignored
playwright-cli fill e8 "test_runner" # usuario (ref do snapshot)
playwright-cli fill e11 "$TEST_PASS" # senha
playwright-cli click e12 # botao Entrar
playwright-cli screenshot # salva PNG em .playwright-cli/
playwright-cli close # fecha browser
Navegacao — SEMPRE usar eval (NAO clicar):
// Tabs principais (div.nav-tab, NAO sao buttons)
switchTab('overview') // Visao Geral
switchTab('tournament') // AF Hedge (S320: data-tab="tournament" eh naming legacy AF — backend tournament removido, frontend ID mantido por compat com af_hedge.js + app.js + overview.js)
switchTab('trading') // Trading
switchTab('system') // Sistema
switchTab('tools') // Tools
// Sub-tabs (div.sub-pill)
switchSub('tools', 'roadmap') // Tools > Roadmap
switchSub('tools', 'propfirms') // Tools > Prop Firms
switchSub('tools', 'simulator') // Tools > Simulador
switchSub('trading', 'signals') // Trading > Sinais
switchSub('trading', 'orders') // Trading > Ordens
switchSub('trading', 'positions') // Trading > Posicoes
Regras browser:
- Browser eh Brave, nao Chrome. SEMPRE rodar Brave-Debug.bat antes do DevTools MCP
- new_tab com URL no payload (NUNCA vazio + navigate separado)
- list_tabs ANTES de close_tab
- NAO usar tab principal do usuario — sempre nova tab
- Usar test_runner pra login automatizado, NUNCA admin
// Fetch com auth
const token = localStorage.getItem('ct_token');
const resp = await fetch('/api/endpoint', {
headers: { 'Authorization': `Bearer ${token}` }
});
// WS
const ws = new WebSocket('wss://linniuc.com/api/ws/dashboard');
ws.onopen = () => ws.send(JSON.stringify({ token: localStorage.getItem('ct_token') }));
// setInterval (LIMPAR antes de criar novo — evita memory leak)
if (window._refreshInterval) clearInterval(window._refreshInterval);
window._refreshInterval = setInterval(refreshData, 5000);
Nenhuma hora do painel pode sair do relogio do sistema de quem esta olhando. O
servidor escreve as frases no relogio de operacao da mesa; enquanto os dois coincidem
ninguem ve, e foi assim que o mesmo prazo apareceu como 16:00 e 20:00 no mesmo card
(licao P817). SSoT: relogio-unico-do-painel.md.
| Ordem | Degrau | Onde vive |
|---|---|---|
| 1o | Override da chamada — so afeta "por cima" | {tz} no CT.fmtHora(iso, {tz}) |
| 2o | Relogio universal do site | CT.SITE_TZ em static/js/api.js |
| 3o | UTC — rede, se o universal for invalido |
fixo |
// LER instante do servidor (ISO naive, sem 'Z') -> CT.utcDate (licao P537)
// ESCREVER instante na tela -> CT.fmt* (licao P817)
CT.fmtHora(iso) // 16:00 ({segundos:true} -> 16:00:00)
CT.fmtData(iso) // 06/08/2026 ({curto:true} -> 06/08)
CT.fmtDataHora(iso) // 06/08/2026 16:00
CT.fmt(iso, {weekday:'short'}) // cru: qualquer opcao do Intl, com o fuso ja injetado
CT.fmtHora(iso, {tz:'UTC'}) // override pontual, so naquela chamada
// PROIBIDO — projeta no fuso do PC de quem abriu a tela:
new Date(x).toLocaleTimeString('pt-BR')
DURACAO nao entra na cascata. "ha 5 min", "2h 30min" sao subtracao de milissegundos;
converter fuso ali quebra a conta. Formate duracao a mao, como sempre.
Mudar o relogio do site = mudar SITE_TIMEZONE em server/constants.py. E' o unico
lugar. O CT.SITE_TZ do JavaScript e' SEMENTE: ele pinta a tela nos milissegundos antes da
1a resposta chegar e vale de reserva se a rede cair — e um teste OBRIGA os dois a baterem.
A pool que nao declara fuso proprio HERDA esse mesmo relogio (decisao do dono,
2026-08-06). No formulario da pool isso aparece como a opcao "herda o relogio do site", com
o valor herdado a vista no rotulo (GMT-4 · herdado do Campo Grande) — "GMT-4" sozinho nao
distingue "eu escolhi" de "eu herdei", e e' essa duvida que faz mexer no que ja esta certo.
Pool que declara um fuso proprio continua mandando nele.
⚠️ Esta linha ja mentiu DUAS vezes. (1) Dizia que o fuso da pool decidia "quando cai o
swap" — nao decide, e nunca decidiu. (2) Dizia que mudar o relogio do site era mudar
CT.SITE_TZ— deixou de ser verdade quando o relogio mudou de casa pro servidor.O swap segue fora de tudo isso: guardado em
swap_hour_utce comparado em UTC
(is_near_swap), e' um momento do mundo, igual pra todo fuso. Nem o fuso da pool nem o
relogio do site movem a virada de juros um minuto — o que o fuso faz e' so' exibir ela
convertida no formulario. Isso e' trava, nao promessa:TestOSwapNaoSeMoveComOFusopercorre
5 fusos exigindo o mesmo veredito.
Rede: tests/unit/relogio_do_painel.test.js e tests/unit/pool_herda_relogio_do_site.test.js
(ambos sob Pacific/Kiritimati, +14) + server/tests/test_relogio_do_site.py.
3 modos: % Balance, % Equity, USD fixo. Formula: riskMoney / (slDist / tickSize * tickValue)
Lista completa (definida em websocket.js via wsEvents):
- heartbeat (balance/equity/positions)
- signal (novo signal)
- ack (confirmacao — MODIFY/CLOSE/OPEN)
- ea_status (online/offline)
- af_pair_update (critico AF — usado por af_hedge.js pra live updates de pares)
- reverse, login_failed, settings_synced, account_new, alert (auxiliares)
A regra, em 1 frase: o remendo barato cuida do que so' troca de NUMERO; qualquer coisa
que muda o DESENHO exige redesenho — e quem decide isso e' uma assinatura de conteudo,
nunca uma contagem nem umreturnseco.
Por que existe: uma varredura de 2026-08-04 achou 18 defeitos de "so' fica certo depois do
F5", e 7 deles eram a mesma familia: alguem escreveu um atualizador incremental pra evitar
piscar de tela e o pos num caminho que curto-circuitava o desenho completo. Tudo que o remendo
nao cobria congelava — e o que congelava eram justamente os campos derivados (custos, selos,
banners, botoes), que dependem de dado que so' vem de endpoint e nunca caberia no payload do
push. Licao P795.
| Onde | Quem decide | O que o remendo cobre | O que exige redesenho |
|---|---|---|---|
Card de batalha (af_hedge.js) |
o proprio ramo do handler | nada — status terminal SEMPRE recarrega | Exit Gap, Hedge Cost, Slippage, prateleira "Concluidas desta rodada" |
Cartao de conta (overview.js) |
_assinaturaDoCartao(a) |
saldo, equity, P&L, posicoes, batimento, selo online, AFOGADO | nome, classificacao, INVERT, versao do robo, chave de entrada, DESYNC, grupo |
As tres regras que nao se negociam:
last_positions.card.dataset.assinatura). Primeira passada so' carimba (o elementoouterHTML, re-carimbarapi.js compara quantos cartoesRajada vira UM redesenho: _afAgendarRecarga(atraso, chave) coalesce avisos que chegam
juntos (4 pares fechando em sequencia = 1 redesenho, nao 4 x 5 chamadas de API). E o redesenho
e' PULADO se o card nem esta na tela — batalha de outra pool nao remonta o seu painel.
Corolario do servidor: se o site reage ao aviso indo BUSCAR dado, o aviso sai depois do
commit. process_trade_result (server/af/lifecycle.py) publica o af_pair_update de
completed apos o db.commit(); o payload e' montado antes porque o commit expira os objetos
ORM. Mesmo principio ja registrado em server/config_gate.py. Rede:
server/tests/test_aviso_de_par_concluido_depois_do_commit.py.
Aba que se atualiza sozinha usa guarda de aba ativa (padrao de websocket.js:168-171):
Journal, Terminais e Saude so' recarregam se a aba estiver visivel. Excecao deliberada: o
contador de usuarios online da barra do topo roda SEM guarda — ele aparece em toda tela.
Redes: tests/unit/af_card_concluido_atualiza_sozinho.test.js,
cartao_de_conta_atualiza_sozinho.test.js, journal_filtro_e_atualizacao.test.js,
terminais_e_contador_online.test.js, delay_filtro_de_grupo.test.js,
ea_update_ouvintes_ws.test.js, cor_do_dono_invalida_cache.test.js,
abrir_pool_leva_pra_pool_certa.test.js, revisar_resultado_busca_no_servidor.test.js,
painel_config_remota_e_historico.test.js. Achados completos:
.planning/varredura-precisa-de-f5-achados.md.
SSoT: Tabela prop_firms no DB. Backend enriquece cada conta no payload /api/accounts com prop_rules (objeto com target_pct, max_dd, daily_dd, equity_floor, target_usd, etc) ja ajustado por phase (F1 vs F2). Codigo: server/routes/accounts.py funcao _get_prop_rules.
Regra (overview.js + af_hedge.js):
// Frontend NUNCA reimplementa lookup de prop firm. Usa o que vem da API:
var baseSize = parseInt((a.prop_size || '100k').replace('k','')) * 1000 || 100000;
var targetPct = (a.prop_rules && a.prop_rules.target_pct) || 8; // fallback so se prop_rules vier null
var maxDdPct = (a.prop_rules && a.prop_rules.max_dd) || 10;
var target = baseSize * (1 + targetPct / 100);
var ddFloor = baseSize * (1 - maxDdPct / 100);
var isDead = equity < ddFloor; // BREACH (perdeu mais que max_dd)
var isPassed = equity >= target; // PASSOU (alcancou target_pct)
// senao: em progresso (visivel em "Contas Prop", nao em "Inativas (N)")
Anti-pattern (S311 fix): dicionario hardcoded JS tipo _propTargets = {'FTMO Swing':10, ...} ou 100000 hardcoded como baseSize. Duplica info do DB e bugifica ao adicionar prop firm nova ou conta de tamanho diferente. Smoking gun original: card mostrava "Meta 10%" (correto, do prop_rules) mas tag "PASSOU" (errado, do hardcode 8%) na mesma tela.
Filtro "Inativas (N)": Conta com isDead || isPassed cai aqui. Default escondido (_ovShowInactive = false). Checkbox toggle revela.
data-theme no <html>: original, apple, cyberpunk, pinkpurple, figma, brutalistdata-mode: dark, lightthemes.css, overrides via [data-theme="xxx"]--bg-card: #1a1a2e, --accent: #00d4aa, botoes #7c4dffRegra: NUNCA inserir dados da API via innerHTML sem escapeHtml() de api.js.
Status S82: 11 pontos XSS encontrados/corrigidos. Pendentes: positions.js (data-ticket), remote_settings.js (rsModalAccountId)
# 1. Copiar
scp app.js admin.js styles.css [email protected]:/opt/copytrade-server/app/static/
# 2. Cache bust (OBRIGATORIO)
ssh [email protected] "sed -i 's/v=[0-9]*/v=$(date +%Y%m%d%H)/' /opt/copytrade-server/app/static/index.html"
# 3. NAO precisa restart (nginx serve static files)
# 4. Verificar em aba anonima
Tela branca: F12 -> Console -> erro JS | Network -> app.js (200 vs 404) | Se erro -> const TDZ ou fetch sem try/catch
Dados nao atualizam: Network -> requests sendo feitas? | setInterval limpo? | WS desconectou?
Layout quebrado: Elements -> CSS | styles.css carregou? | media queries
| Armadilha | Fix |
|---|---|
const TDZ |
Declarar ANTES de usar em template literals |
| Fetch sem try/catch | SEMPRE envolver fetch |
| Login redirect loop | Limpar ct_token, recarregar |
| innerHTML XSS | SEMPRE usar escapeHtml() |
apiPost() retorna {ok, data} |
Acessar resp.data, checar resp.ok |
apiFetch timeout 8s |
S255: retry silencioso 1x em 1s em GET/HEAD se 502/503/504/AbortError (absorve restart do backend em deploys). POST/PUT/DELETE nao retry (nao-idempotente). Endpoints CPU-bound reais (simulador/export) ainda devem usar fetch() direto com timeout custom |
| CSS var inexistente | Consultar themes.css. Validos: --bg-card, --text-primary, --text-muted, --border |
Tabs fora do .main |
TODA tab-content DEVE estar dentro de div.main |
| Cache bust no deploy | Sem ?v= atualizado, browser serve versao antiga |
| themes.css removido | Quebra todos os temas. NUNCA remover |
| Chart.js resize infinito | Canvas DEVE estar em div com position:relative;height:Xpx |
~~showToast vs toast~~ |
CORRIGIDO S92. ea_update.js agora usa toast() de api.js |
| Validacao em browser separado | NUNCA usar browser principal do usuario |
| Mudanca | Arquivo | Nota |
|---|---|---|
| S11 #160 closePair | af_hedge.js:480-535 |
afConfirm refatorado: Promise com handlers escopados (sem singleton _afConfirmCb). Cada chamada tem settled local + cleanup(val), listener Escape/Enter, resolve direto. Corrige trava em double-click ou re-entrada por WS refresh. closePair(pairId, btn) agora recebe btn explicito (nao mais window.event). Teste Playwright: tests/af_confirm_modal.spec.js (regressao reentrante) |
| Test accounts toggle | index.html:688-700, accounts.js |
Checkbox "Mostrar contas de teste" + banner amarelo quando ligado. Separa visao normal de sandbox |
| Signal timeline modal | signal_timeline_modal.js, af_hedge.js botao showTradeTimeline |
Novo JS carregado em index.html, botao "Timeline" nos cards live/completed |
| Pool-paused timer freeze | af_hedge.js:1298-1360 |
Cronometros usam refMs = paused_at || Date.now(). Quando pool pausada, nenhum timer avanca. Inclui getStatusInfo(p, refMs) e isTimerExpired com mesma referencia |
| sl_buffer em cards live | af_hedge.js:1403-1410 |
rd.sl_buffer_offset injetado em posA/posB pra mostrar indicador "+" no side P&L ao vivo (nao so historico) |
| Historical chairs P53-safe | af_hedge.js:2638-2720 |
Chairs com status in ('passed','dead') e sem account_id ficam como _isHistorical, renderizam "Saldo final (historico)" em vez de "Aguardando sync MT5" |
| Add chair modal prefill | af_hedge.js:3148-3160 |
_populateAddChairFilters agora le require_type/firm/phase/size do pool config e pre-seleciona filtros |
| getAccountDisplay 3o arg | af_hedge.js:268-290 |
Novo parametro fallbackOwner pra color lookup quando Account tem nome generico (Conta #N). Usa prop_name + "F<phase>" + owner como fallback de display |
.af-flagstrip (S394)Problema: a 1a linha do topo do card amontoava ate ~8 carimbos (overflow:hidden cortava) e a explicacao deles vivia em tooltip de hover (title nativo / .af-tip:hover) — que nao funciona em toque (celular). P388.
Solucao: buildFlagStrip(p, rd, ctx) (af_hedge.js, antes de renderPairCard) re-deriva os carimbos CONDICIONAIS como tags por extenso sempre visiveis numa faixa .af-flagstrip logo abaixo da linha de identidade. Cobre: close_reason (Fechado no alvo/stop, anti-swap robo/site, fecho anormal), MODIFY (SL ajustado/abortado/agendado), rechamado, mercado fechado, motivo de espera (rsafe2/stale/swap_window/cotacao), risco (ultima chance/otimizado/ajustado) e colchao do SL.
onclick="afShowFlags(pairId)" -> abre afConfirm (modal #afConfirmModal) com a explicacao longa. _afFlagDetails[pairId] guarda o HTML. NAO depende de hover._afEqualizeCardRow(root, selector, collapse) roda apos el.innerHTML em renderAfBattles (e ao abrir a Ultima Rodada), via _afEqualizeFlagStrips(root). Mede a altura REAL (piso 0, ignora min-height do CSS) DENTRO de cada grupo (sub-grid por estado = parentElement) e poe todos no maximo do grupo. Aplicado em 10 seletores (eram 2 ate 2026-08-10 — esta linha dizia "2 lugares" e virou mentira no dia em que os outros 8 entraram): (1) .af-flagstrip com collapse=true -> grupo 100% sem tag (max<=1) some (display:none, mata espaco morto em NAO-EXECUTADAS/agendado-limpo/preview); (2) .af-fighter-name com collapse=false -> antes fixo em 3 linhas (CSS), agora encolhe pra altura real do grupo (2 linhas quando ninguem tem nome de 3); (3) .af-riskline, a linha "Risco desta batalha / segurou: <limite>"; (4-10) as faixas DE DENTRO do painel dos lutadores: .af-fighter-dir, .af-row-risk, .af-row-prices, .af-row-pnl, .af-row-bal, .af-row-progress, .af-row-cushion. Basta o helper mexer no lado A: os 2 lados dividem a mesma grade (subgrid), entao a faixa cresce pros dois. CSS min-height do nome = piso 2 linhas (so pre-JS, evita flash). "Dinamico por rodada".div ANONIMA — sem classe, o mecanismo nao a enxergava. Quando o rotulo do limite nao cabia numa linha ("mesa Alpha Pro 10% 2 Steps"), aquele card ganhava 12px e TODO o resto dele descia. A sonda achou um 2o caso da mesma familia, .af-row-progress ("F1 46.1% — falta $10.787,40"), com 47px num card contra 30,5px no vizinho a 1280px. Numero: 14 de 20 combinacoes (5 larguras x 4 grupos) desalinhadas antes, 0 de 20 depois. Rede: tests/af_card_alinhamento.spec.js — carrega o site REAL, troca o af_hedge.js publicado pelo local e mede; falha NOMEANDO a faixa culpada.align-content:flex-start na .af-riskline NAO e' enfeite: ela e' display:flex com align-items:center, entao sem isso o texto ficaria CENTRALIZADO na altura reservada e a frase do card curto desceria ~6px — alturas iguais e texto torto, o oposto do objetivo. Mesma razao pela qual a .af-flagstrip ja tinha align-content:flex-start.display quando collapse=true. O reset style.display='' existe pra UM caso: desfazer o display:none que a propria funcao pos no grupo vazio da passada anterior. Fora dele, ele APAGA display declarado no atributo inline. Regressao real, chegou a producao em 2026-08-10: a .af-riskline declara display:flex so' inline (nao ha regra CSS pra ela), entao o igualador a virava bloco e o par "SL | TP" parava de ser empurrado pra borda direita, colando no texto da esquerda em todo card com rd.sl_usd. Achada por cetico independente, nao pelo teste — as 2 primeiras versoes da rede so' mediam ALTURA e TOPO, e display/posicao horizontal ficavam fora da conta. Conferido: dos 10 seletores igualados, .af-riskline e' o UNICO com display inline. Antes de acrescentar um seletor novo a esta lista, confira se ele tem display no atributo style.tests/af_card_alinhamento.spec.js verifica altura, topo E display, no codigo local e no JS publicado; roda na varredura da casa (scripts/testar-tudo.sh, entrada 4c) — antes disso ela nao rodava em projeto, script nem CI, e ainda assim era chamada de "rede permanente" em commit e spec. Aceita PW_JS_SOB_TESTE=<caminho> pra rodar contra uma copia MUTADA sem tocar no repo. Provado com 4 mutantes (tirar a igualacao do risco / tirar a classe / tirar as 7 faixas / voltar a apagar o display): os 4 deixam a bateria vermelha, o codigo bom passa._afAbaAberta, 2026-08-10). Havia DUAS copias da pergunta (auto-refresh e conferencia pos-save), as duas lendo _aft.style.display !== 'none' — e a aba e' escondida por CLASSE (app.js usa classList; styles.css:492-496 define .tab-content{display:none} / .tab-content.active{display:block}). MEDIDO com a aba fechada: style.display vale "" e classList.contains('active') vale false — a condicao era SEMPRE verdadeira. O estrago nao era custo (medido: 11,2/15,0/18,6 ms com a aba visivel, 0,5 e 0,1 ms escondida): num container oculto offsetHeight e' 0, entao o igualador zerava as alturas e a faixa de avisos caia no max <= 1 e sumia — 8 de 8 faixas apagadas, 8 de 8 alturas zeradas, e o card ficava degradado ate' o proximo desenho. _afEqualizeFlagStrips agora desiste com container de altura 0 e devolve se igualou._afScheduleEqualize gravava _afEqualizeLastWidth na hora e o trabalho rodava 150ms depois, as cegas. Com a guarda de tela escondida isso viraria armadilha permanente: o resize marca a largura, o trabalho desiste porque a aba esta fechada, e nenhum resize naquela largura re-executa nunca mais. As duas mudancas so' funcionam JUNTAS — consertar o portao sozinho troca um defeito passageiro por um definitivo.resize — arrastar a janela desalinhava ate' chegar dado novo do servidor (medido: nome da conta com 46,8px contra 31,2px a 1680px). _afScheduleEqualize re-mede quando a LARGURA muda, com freio de 150ms. Ignora mudanca so' de ALTURA de proposito: no celular a barra do navegador some/aparece e dispara resize o tempo todo.MANUAL -> "fechado manualmente" (de proposito, neutro) ≠ POSITION_GONE -> "⚠ fecho anormal" (perna sumiu). Abort do MODIFY mapeado por codigo (so 2 reais: F4_sanity->"Ajuste cancelado" = trava de seguranca, o ajuste calculado moveria o stop/alvo >2x a distancia original; F6_in_range->"Ajuste não necessário" = ja estava perto do alvo/piso; sao motivos DISTINTOS, nao confundir; near_death/no_shift eram lixo legado, removidos dos 3 mapas). Rotulos das tags com inicial MAIUSCULA. Colchao do SL deixou de ser tag -> asteriscozinho * na cotacao do SL (buildFighterHtml _bufStar), valor no hover/toque.marketStatusBadge/priceGuardBadge foram REMOVIDOS (eram codigo morto pos-S394). Mantido so var _wr (o cronometro o usa em _isSwapWait pro chip "Pausado")._modExtraCssOn) usa padding:0 4px (vertical 0) — e so fundo+barra, nao pode aumentar a altura, senao card com MODIFY ficava 2px mais alto que os irmaos (quebra "mesmo tipo = mesma altura"). Mesma regra do P388: efeito visual nao pode custar layout.title (hover) do selo — sumia no celular (P388). Agora, quando si.title existe, o selo e tappavel (onclick=afShowStatusReason(p.id), cursor:pointer) e abre o motivo leigo no overlay (afConfirm); _afStatusReason[p.id] guarda o texto. Hover segue no desktop. TIMEOUT/FALHOU nao tem si.title (sem texto de motivo no getStatusInfo) -> selo nao-tappavel, nada escondido.Problema (incidente 2026-06-09): relogio vazado matou 3 batalhas no papel e a tela escondeu o estrago: gaveta "Ultima Rodada" lembrava o recolhido PRA SEMPRE (chave global), card de limbo nao dizia que as ordens seguiam VIVAS no broker, contador "1 concluido" sem card visivel, e o reconcile fazia o card SUMIR da secao roxa pra gaveta fechada. Spec: af-batalhas-legibilidade-pos-vida-S402.md.
Invariante de ouro (testado — fuzz 100 seeds em af_hedge_logic.test.js): todo par visivel aparece em EXATAMENTE 1 secao — nem some, nem duplica.
af_lastround_collapsed_round guarda O ID da rodada recolhida (lastRoundStartsCollapsed, logica pura). Rodada nova nasce ABERTA; expandir esquece. Chave legada af_lastround_collapsed removida no render. toggleAfLastRound le data-round-id da secao.offline_held nao-reconciliado: quem apagou (perna balance_age_s>600, ou "ja voltou"), quando encerrou no papel (manual_offline_close_at + auto/operador), ultimo retrato POR PERNA (daily_pnl_estimated_a/b congelado do risk_detail — NUNCA inventa dado vivo de conta muda; perna fresca rotulada "● ao vivo") + texto fixo "ordens ABERTAS no broker fecham sozinhas". clampAgeMin trava idade no zero (skew). fetchAfLiveData coleta par em limbo (perna parceira fresca).splitLimboPairs (logica pura) separa limbo (roxo, nao-reconciliado, persistente entre rodadas) de resolved (reconciliado SEM rodada mais nova iniciada — fronteira por EVENTO via maxRoundId: "so sai do resultado atual quando inicio outra rodada"). getStatusInfo reconciliado = selo VERDE "RESOLVIDO ✓" (nao mais roxo). Footer mostra "(estimado era ~$X)" — a troca papel→recibo real visivel. Baldes marcados em shownInActive ANTES do activeDisplay (anti-duplicata). Secao 2b = "Limbo / acertos" com 2 sub-headers.afSummaryChipClick(targetId) — scrolla ate a secao; gaveta recolhida expande antes; secao ausente = no-op.// Login (NAO tem #loginBtn!)
document.querySelector('.login-box .btn-primary')
// Sidebar
document.querySelector('.sidebar')
// Tabelas: #positionsTable, #signalsTable, #accountsTable
// Modais: #editModal
// Sidebar mode: localStorage "layoutSidebar" (NAO "dashboardLayout")
// Theme: #themeSelector > #themePanel
Status: ACTIVE | Ultima revisao: S151 (2026-03-29)
SSoT para: Metodologia debug, checklists, ferramentas, race conditions
Consolida:memory/debugging-playbook.md+.claude/knowledge/copytrade-debug.md+.claude/knowledge/debug-sistematico.md
Relacionados:specs/signal-lifecycle.md,specs/invert-rules.md,memory/business-constants.md
Sem root cause = fix aleatorio. Debugging sistematico: ~95% 1st-time fix. Random fixes: ~40%.
Isolar ANTES de consertar. Entender o que acontece, NAO "mudar e ver se melhora".
1. REPRODUZIR -> Confirmar que o problema existe agora (nao assumir)
2. LOCALIZAR -> Qual camada? EA / Servidor / Dashboard / Banco / Rede
3. INSTRUMENTAR -> Coletar dados: logs, queries, screenshots
4. ISOLAR -> 1 hipotese testavel por vez
5. CORRIGIR -> Fix cirurgico, minimo necessario
6. VALIDAR -> Confirmar fix funciona E nao quebrou mais nada
7. DOCUMENTAR -> Atualizar known-issues.md e changelog.md
1. QUE DIA/HORA? -> FDS? Feriado? Fora de sessao?
- Forex: seg 00:00 -> sex 22:00 UTC. FECHADO sab/dom
- Metals (XAUUSD): similar Forex
- Crypto: 24/7 mas brokers podem ter manutencao
- Indices: sessoes especificas, nao 24h
2. QUAL BROKER? -> Exness (streaming parcial) vs Capital Point (corta tudo)
3. QUAL SIMBOLO? -> Cada simbolo tem sua sessao
4. CONTROLAR VARIAVEIS -> Se 2 contas diferem: versao EA? Broker? Grupo? PC?
Correlacao != Causalidade
5. SO DEPOIS -> Se contexto nao explica, AI SIM investigar codigo
Regra de ouro: Contexto simples (mercado fechado, broker offline) eh MAIS provavel que bug.
1. IMPACTO PRIMEIRO -> open_positions: quem tem posicao aberta?
-> trade_links is_closed=false orfaos?
-> Posicao SEM par = DESYNC ATIVO
2. DEPOIS SINTOMA -> debounce, logs, spam
3. NUNCA reportar "zero posicoes" sem conferir TODAS as contas
git log -5 <arquivo> — commits recentes?CopyTrade-specific:
- [ ] Broker market hours? Terminal MT5 conectado? VPS SSH reachavel? DB connection?
1. Conta abre trade -> SnapshotEngine detecta -> POST /api/signals
2. Servidor cria Signal + TradeLink(origin) + PendingSignal
3. Destino poll -> GUIExecution abre -> POST ack (FILLED)
4. Servidor cria TradeLink(slave) -> resolve PendingSignal
Timing: Poll 1-3s, GUI 3-7s, ACK imediato (retry 3x). Total: ~5-10s.
g_processedSignalIds[] 100 IDs circularRoda a cada 15min na VPS (scripts/quality/quality_monitor.py). Market-aware (weekday/weekend thresholds). Dedup 1h.
| Check | Severidade | O que detecta |
|---|---|---|
| same_sign_pnl | CRITICAL | Ambos lados do par AF lucraram (impossivel em hedge) |
| duplicate_af_trades | CRITICAL | Trade duplicado no mesmo par AF |
| net_exceeds_risk | HIGH | Custo liquido do par excede risco planejado |
| stale_heartbeat | HIGH | EA sem heartbeat > threshold (15min dia util, 30min FDS) |
| phantom_positions | HIGH | Posicao no broker sem TradeLink correspondente (>30min) |
| signal_without_ack | HIGH | Signal distribuido sem ACK em >5min |
| ghost_positions | MEDIUM | TradeLink aberto mas posicao nao existe no broker (>10min) |
| orphan_trade_links | MEDIUM | TradeLink orfao (>24h sem par) |
| pnl_suspect_unresolved | MEDIUM | Par P&L suspeito sem resolucao |
Reconcile (reconcile.py): Detecta phantoms, ghosts, e P&L drift. Roda standalone ou via cron.
Alertas: CRITICAL/HIGH → Telegram imediato. MEDIUM → log only.
CLOSE antes de OPEN ACK: PendingSignal bloqueia CLOSE enquanto OPEN pendente. Se expirou (>60s) -> orphan checker pega.
MODIFY antes de FILL: EA ignora MODIFY (posicao nao existe). SL/TP reconciliation no heartbeat corrige (debounce 30s).
Duplicate ACK: Servidor usa idempotency — segundo ACK com mesmo signal_id = ignora.
| Ferramenta | Quando usar |
|---|---|
ct_trace(trade_group_id) |
Rastrear um trade especifico (timeline completa) |
ct_signals(hours, limit) |
Ver sinais recentes |
ct_positions |
Posicoes abertas de todas contas (detectar desync) |
ct_delay |
Latencia entre pares |
ct_diagnostics |
Telemetria EA (uptime, erros/h, ultimo erro) |
ct_errors(hours) |
Erros recentes |
Trace completo: Identificar trade_group_id (dashboard/DB) -> ct_trace -> procurar gaps (signal sem ACK, ACK FAILED).
tasklist | grep terminal64SELECT * FROM heartbeats WHERE account_id=X ORDER BY created_at DESC LIMIT 3powershell Get-Content ...Logs/YYYYMMDD.log -Encoding Unicode -Tail 50Orfaos: WHERE is_closed=false AND created_at < NOW() - INTERVAL '1h'
Duplicados: GROUP BY trade_group_id HAVING COUNT(*) > 4
signal_acks: error_msg (NAO error_message, NAO error)heartbeats: created_at (NAO last_seen). Ultimo HB: ORDER BY created_at DESC LIMIT 1trade_links: is_origin (NAO role)access_token (NAO token)| Dado | Dashboard | Diferenca possivel |
|---|---|---|
| Posicoes | open_positions (heartbeat snapshot) | Cache browser (refresh) |
| Equity | Ultimo heartbeat | Se stale, mostra dado antigo |
| EAs online | last_seen < 90s | Poll interval |
Arquivos P0 (afeta TUDO): signals.py, ea_ws.py, heartbeat.py, settings.py, main.py.
Mudanca -> backup + teste local + deploy horario seguro + monitorar 5min.
| Ferramenta | Comando |
|---|---|
| Logs EA | powershell Get-Content ...Logs/YYYYMMDD.log -Encoding Unicode -Tail N |
| Logs servidor | ssh root@... "journalctl -u copytrade --since '5 min ago' --no-pager" |
| DB query | ssh root@... 'PGPASSWORD=... psql -U copytrade_user -d copytrade -c "..."' |
| Health | curl -s https://linniuc.com/api/health |
Status: ACTIVE | Ultima revisao: S230 (2026-04-14) — revisado, mudancas em
signals.pyeGUIExecution.mqhdesde S194 sao de observabilidade (EventLog S228 C10-C14, Fase F simetria Modify/Close 3.73.6, Telegram alerts ack_failed, drift sync). Nenhuma mudanca toca logica de inversao. Verspecs/ea-observability.md.
Versao: 1.2
Data: 2026-04-14
SSoT para: Logica exata de inversao de sinais no CopyTrade
Analogia: Invert e como um espelho. Quando o master abre BUY, o slave invertido abre SELL. O SL e TP tambem trocam de lugar (o que era protecao vira alvo, e vice-versa).
Por que existe: No hedge, um lado PRECISA ser o oposto do outro. Invert automatiza isso.
| Modo | Quem faz | Quando |
|---|---|---|
Online (InpOnlineMode=true) |
Servidor | Ao criar o Signal para o peer. EA recebe JA invertido. |
Local (InpOnlineMode=false) |
EA | Ao ler o sinal, antes de executar. |
No modo online, o EA slave NAO aplica inversao — o sinal ja chega invertido.
INVERTE se: peer.invert != master.invert
| Master invert | Peer invert | Resultado |
|---|---|---|
| false | false | Copia normal |
| false | true | Inverte |
| true | false | Inverte |
| true | true | Copia normal |
Dois EAs com
invert=trueno mesmo grupo copiam entre si SEM inverter. So inverte quando os flags sao DIFERENTES.
INVERTE se: account.invert == true
No broadcast nao ha conta de origem, entao a condicao e simplesmente
if acct.invert.
funcao inverter(direction, sl, tp):
direction = "SELL" se era "BUY", "BUY" se era "SELL"
sl, tp = tp, sl // swap simples
sl_distance, tp_distance = tp_distance, sl_distance // swap
MASTER abre: BUY XAUUSD, SL = 2000, TP = 2100
SLAVE recebe: SELL XAUUSD, SL = 2100, TP = 2000
(era TP) (era SL)
signals.py:78-86)def _invert_direction(direction: str) -> str:
return "SELL" if direction == "BUY" else "BUY" if direction == "SELL" else direction
def _invert_sl_tp(sl: float, tp: float, invert: bool):
if invert:
return tp, sl # swap simples
return sl, tp
GUIExecution.mqh:1527-1531)void GUIExecution_ApplyInversion(string &direction, string &sl, string &tp)
{
direction = (direction == "BUY") ? "SELL" : "BUY";
string tmp = sl; sl = tp; tp = tmp;
}
| Tipo | Direction inverte? | SL/TP inverte? | Distancias invertem? |
|---|---|---|---|
| OPEN | Sim | Sim | Sim |
| MODIFY (EA-to-EA) | Sim (por consistencia) | Sim | Nao (MODIFY nao tem distancias) |
| MODIFY (broadcast) | Nao (direction vazia) | Sim | Nao |
| CLOSE | Nao | Nao | Nao |
CLOSE nao inverte NADA. O servidor usa o
local_ticketdo TradeLink para saber qual posicao fechar. Direction/SL/TP sao zerados no CLOSE.
Quando InpOnlineMode=false e invert=true:
| Tipo | Comportamento |
|---|---|
| OPEN | GUIExecution_ApplyInversion(direction, sl, tp) — inverte tudo |
| MODIFY | Passa dummyDir = "" — so SL/TP sao swapados, direction nao muda |
| CLOSE | Nenhuma inversao |
# accounts.py:147-148
if data.invert != account.invert:
_check_open_positions(db, account_id, "invert")
# Lanca HTTP 409 se conta tem posicoes abertas
NAO e possivel mudar o flag
invertde uma conta que tem posicoes abertas. Fechar todas antes.
| Local | Campo | Tipo |
|---|---|---|
| PostgreSQL | accounts.invert |
Boolean, default False |
| EA input | InpInvertSignal |
bool, default false |
| EA runtime | g_sm_settings.invert |
Sobrescrito pelo servidor via SettingsManager_Sync() |
O servidor e a fonte da verdade. O EA sincroniza o valor no heartbeat.
| # | Armadilha | Detalhe |
|---|---|---|
| G1 | broadcast-modify: enviar valores originais | Dashboard deve enviar SL/TP para a posicao ORIGINAL (nao invertida). O servidor faz o swap. Enviar valores ja invertidos = dupla inversao = errado. |
| G2 | Validacao de SL/TP apos swap | BUY: SL < preco, TP > preco. SELL: SL > preco, TP < preco. Apos swap, os valores continuam validos para a direcao oposta porque o que era SL do BUY (abaixo) vira TP do SELL (tambem abaixo). |
| G3 | Partial close | Detectado como MODIFY com close_reason="PARTIAL_CLOSE". Inversao identica a MODIFY normal — sem tratamento especial. |
| G4 | Modo local sem protecao | Se EA offline e invert=true, inverte localmente sem validacao do servidor. Se flag estiver errado no input = trades invertidos errado. |
Status: ACTIVE | Ultima revisao: S230 (2026-04-14) — revisado, mudancas em
ea_ws.pydesde S173 sao de observabilidade (Telegram alerts ack_failed, Fase E S228) e NAO afetam o fluxo de auto-update. Logica inalterada. Verspecs/ea-observability.md.
Versao: 1.3
Data: 2026-04-14
SSoT para: Fluxo completo de auto-update do EA (upload → staged rollout → restart)
Relacionado: specs/settings-sync-guardian.md — Settings Sync Guardian (v3.66.0, WS push + verify) complementa o auto-update com sync de configuracoes em tempo real
Analogia: E como atualizar um app no celular. O operador faz upload da nova versao, o sistema distribui primeiro pros "beta testers" (contas early), e so depois libera pro resto (contas stable). O EA se auto-atualiza, reinicia o MT5, e volta a operar.
OPERADOR SERVIDOR EA
───────── ───────── ────
1. Upload .ex5 via Dashboard → 2. Salva + SHA256 → ea_versions(pending)
3. Clica "Release" → 4. stage=testing → desired_version nas early
5. Background monitor (30min)
6. Heartbeat HTTP → update_available=true
7. Posicoes=0? → Download + Verify + Launch
8. Gera PS1, mata MT5, copia .ex5, reinicia
9. Proximo HB: versao nova → limpa desired
10. Contas early atualizaram →
stage=stable → desired nas stable
11. Contas stable: mesmo fluxo (6-9)
AU_IDLE ──[update_available=true]──→ posicoes=0? ──sim──→ AU_DOWNLOADING
↑ | |
| nao [HTTP GET .ex5]
| AU_WAITING |
| (max 30min) AU_VERIFYING
| | [SHA256 + size]
+────[timeout 30min]─────────────────+ |
AU_LAUNCHING
[gera PS1, ShellExecute]
|
AU_DONE
[Sleep(2s) + ExpertRemove]
| Estado | Descricao |
|---|---|
AU_IDLE (0) |
Esperando update |
AU_WAITING (1) |
Update disponivel, aguardando 0 posicoes |
AU_DOWNLOADING (2) |
Baixando .ex5 |
AU_VERIFYING (3) |
Verificando SHA256 + tamanho |
AU_LAUNCHING (4) |
Gerando PS1 + lancando PowerShell |
AU_DONE (5) |
Script lancado, EA se mata |
AU_ERROR (-1) |
Erro — retry no proximo ciclo |
AU_LOCKED (-2) |
Outro EA no mesmo terminal ja esta atualizando |
Resposta do POST /api/heartbeat inclui:
{
"update_available": true,
"update_version": "3.25.0",
"update_hash": "1869006665f4eae46...",
"update_size": 337958,
"update_url": "/api/ea/download/3.25.0",
"update_force": false
}
Mensagem tipo "update" com version + hash + size + url. EA monta fakeHB e chama mesma funcao.
{"type": "update", "version": "3.65.0", "hash": "abc123...", "size": 337958, "url": "/api/ea/download/3.65.0"}
Nota: WS push NAO inclui
update_force(sempre assume force=false). Se force necessario, usar heartbeat HTTP.
Armadilha: Com WS ativo, HTTP roda so a cada 120s. Update pode demorar ate 2min pra ser detectado via HTTP.
Se conta NAO tem desired_version definido, o heartbeat promove automaticamente para a versao mais nova ELEGIVEL — sem acao do operador ("pegar o trem"). O conjunto de stages elegiveis depende do tipo da conta:
| Tipo de conta | Stages elegiveis | Pega build testing? |
|---|---|---|
Pool / real (is_test=false) |
stable (+is_stable=true) |
NAO |
Sandbox dogfood (is_test=true, nao-sim, nao-stress) |
testing, stable, completed |
SIM (S378) |
S378 — Dogfood ring: a bancada de dev (sandbox is_test) recebe automaticamente o build MAIS NOVO, mesmo em testing, igual ao "dogfood" da industria (no interno auto-instala o experimental, distinto do canary que vai pra subset de usuarios reais). Isso permite que a bancada e o teste "Validar tudo" exercitem o build novo sem o operador ter que promover pra stable. INV-LEAK: conta nao-dogfood NUNCA pega testing (property-tested em test_dogfood_catchup_s378.py).
Implementado em heartbeat_helpers.py (pick_catchup_version + is_dogfood_account + eligible_catchup_stages), chamado pelo _compute_update_info. Guard espelha can_force_downgrade (routes/ea_tester.py). Util quando contas novas sao criadas apos um release, e essencial pro vai-volta do teste restart+reconcile.
// MAJOR.MINOR.PATCH — componente por componente
AU_CompareVersions("3.25.0", "3.24.1") → positivo (3.25 > 3.24)
AU_CompareVersions("3.25.0", "3.25.0") → 0 (iguais = nao atualiza)
AU_CompareVersions("3.24.0", "3.25.0") → negativo (downgrade bloqueado)
Downgrade so permitido com force=true (rollback via dashboard).
| Etapa | Detalhe |
|---|---|
| Download | GET /api/ea/download/{version} via WinInet (bypassa restricoes MT5) |
| Arquivo staging | Common/Files/linniuc_update.dat |
| Sanity check | receivedSize >= 1000 bytes E receivedSize == expectedSize |
| SHA256 | CryptEncode(CRYPT_HASH_SHA256, ...) — compara com hash do servidor |
| Sem hash | Fallback: verificacao apenas por tamanho |
| Situacao | Comportamento |
|---|---|
| 0 posicoes | Adquire lock → AU_DOWNLOADING |
| >0 posicoes, force=false | AU_WAITING (sem lock, aguarda ate 30min) |
| >0 posicoes, force=true | Adquire lock → AU_DOWNLOADING (ignora posicoes) |
| Timeout 30min | Volta para AU_IDLE (desiste, tenta no proximo ciclo) |
Lock file so e adquirido quando pronto para baixar. Nao segura o lock durante a espera.
| Aspecto | Detalhe |
|---|---|
| Arquivo | Common/Files/linniuc_update.lock |
| Formato | PID|SYMBOL|TIMESTAMP |
| Lock stale | >5 minutos → removido automaticamente |
| Double-check | Apos criar lock: Sleep(200ms) + re-le pra confirmar que e nosso |
O EA gera dinamicamente Common/Files/linniuc_updater.ps1:
Stop-Process -Id $mt5pid -Force (aguarda ate 20s)LinniuC.ex5 → LinniuC.ex5.bak.mq5.disabled (evita recompilacao)linniuc_update.dat → MQL5/Experts/LinniuC.ex5common.ini (ProfileLast=).chr apenas no profile ativo.chrInpWebAPIKey no .chrStart-Process terminal64.exelinniuc_update.dat + linniuc_update.lockSe erro: Bloco catch restaura
.ex5.bak, remove lock, reinicia MT5.
pending → testing (contas early) → stable (contas stable) → completed
↘ rolled_back
| Fase | Contas | Quando avanca |
|---|---|---|
testing |
Early (ids 3, 4 — TESTE) | Ao clicar "Release" |
stable |
Stable (ids 1, 2, 22-26) | Automatico quando early adotam (monitor 30min) |
rolled_back |
Nenhuma | Manual via dashboard |
# heartbeat.py
if desired_clean == current_build:
# Update concluido! Limpa desired_version
account.desired_version = None
account.update_attempts = 0
elif attempts >= 10:
# Desistir: loop detectado
account.desired_version = None
alert_ea_update_failed(...)
else:
# So conta tentativa se pos_count == 0
# (Com posicoes = EA em AU_WAITING, nao e falha)
if pos_count == 0:
account.update_attempts += 1
| Constante | Valor | Onde |
|---|---|---|
AU_WAIT_TIMEOUT |
30 min | AutoUpdate.mqh:50 |
AU_MAX_RETRIES |
3 | AutoUpdate.mqh:51 |
| Lock expiry | 5 min | AutoUpdate.mqh |
| Check interval | 30s | AutoUpdate.mqh |
| Min file size | 1000 bytes | AutoUpdate.mqh |
| Max tentativas servidor | 10 | heartbeat.py |
| Rollout monitor | 30 min (30s x 60) | ea_update.py |
| Upload max size | 50 MB | ea_update.py |
| Double-check sleep | 200ms | AutoUpdate.mqh |
| Kill MT5 timeout | 20s (10x 2s) | PS1 script |
| Method | Endpoint | Acao |
|---|---|---|
| POST | /api/ea/upload |
Upload .ex5 + version + changelog |
| POST | /api/ea/release/{version}?force= |
Inicia rollout (testing → early) |
| POST | /api/ea/rollback/{version} |
Reverte (limpa desired de todas contas) |
| GET | /api/ea/versions |
Lista versoes com stage/downloads |
| GET | /api/ea/rollout-status |
Status por conta (current vs desired) |
| GET | /api/ea/download/{version} |
Serve .ex5 binario |
| DELETE | /api/ea/versions/{version} |
Remove versao (so pending) |
| Coluna | Tipo | Descricao |
|---|---|---|
| version | varchar | Ex: "3.25.0" |
| file_path | varchar | Caminho fisico no VPS |
| file_hash | varchar | SHA256 |
| file_size | int | Bytes |
| changelog | text | Descricao |
| is_stable | boolean | Promovido? |
| is_active | boolean | Disponivel? |
| rollout_stage | varchar | pending/testing/stable/rolled_back |
| download_count | int | Total downloads |
| uploaded_at | timestamp | Data upload |
Campos em accounts: update_group (early/stable), desired_version (ex: "b3.25.0"), update_attempts (0-10)
| # | Armadilha | Status |
|---|---|---|
| A1 | Prefixo "b" em desired_version causa loop infinito |
Corrigido (fix_update_loop.py) |
| A2 | SHA256 nunca verificado (antes 3.10.1) | Corrigido |
| A3 | Download parcial aceito | Corrigido |
| A4 | Loop cross-session (retryCount nao persiste) | Corrigido (AU_SaveFailedVersion + cooldown 1h) |
| A5 | Race condition .chr (multiplos EAs) | Corrigido (lock + profile ativo) |
| A6 | API key perdida no .chr apos update | Corrigido (PS1 corrige) |
| A7 | WS push nao inclui update_force |
Comportamento: assume force=false |
| A8 | Versao-sonda do gate EA Tester (0.0.0-eagate-probe) |
INERTE by design. _compute_update_info incrementa update_attempts pra ela (sinal de deteccao pro gate) mas retorna ANTES de emitir update_available — o EA nunca a baixa/aplica. Parse (0,0) => auto-catchup do pool nunca a oferece. So-sandbox is_test. Ver specs/ea-tester-gate-contrato-v2.md (Onda O4). |
| # | Mudanca | Estado atual | Desejado |
|---|---|---|---|
| M1 | Pular staged rollout | Release faz early→stable automatico | Release direto pra todos. Staged so quando solicitado |
| M2 | Atualizar com posicoes abertas | Espera 30min por 0 posicoes | Atualiza direto. Usuario avisa se update afeta ordens |
| M3 | Notificar falha do PS1 | Restaura backup silenciosamente | Enviar Telegram alert de falha |
| M4 | Lock por terminal | Lock global (bloqueia todos os terminais) | Lock por terminal (paralelo entre terminais diferentes) |
| M5 | Retry com backoff crescente | Retry a cada heartbeat (~30s) sem backoff | Backoff: 1min → 5min → 15min → 30min entre tentativas |
Ultima revisao: S372 (2026-05-23) — DROP COLUMN
accounts.prop_firm(era copia stale;Account.prop_firmagora@hybrid_propertyque derivaprop_firm_id->prop_firms.name). Anterior S294 (2026-04-25) — drift bookkeeping apos S288-S293 em server/models.py: schema base inalterado, snapshot prod emmemory/prod-schema.jsoncontinua autoritativo. Validate via hook schema-drift-check em todo commit. Mudancas absorvidas: senha plain auto-delete 30d (security S272), modelo S267 sim-e2e (AfRound/AfPair/AfTrade ja documentados em outras specs). PG 15 -> 16 ja drift-fixed em S221. [allow-spec]
PGPASSWORD=(ver .secrets.local) psql -U copytrade_user -h localhost -d copytrade
Contas MT5 registradas. Cada EA autentica via api_key.
| Coluna | Tipo | Default | Descricao |
|--------|------|---------|-----------|
| id | serial PK | auto | |
| name | varchar | | Nome amigavel |
| broker | varchar | | Nome da corretora |
| account_num | bigint UNIQUE | | Numero da conta MT5 |
| api_key | varchar | | Chave individual do EA |
| group_id | varchar | | Grupo de copy (ex: Grupo01, TESTE) |
| invert | boolean | | BUY<>SELL, SL<>TP |
| is_active | boolean | | EA ativo? |
| is_deleted | boolean | false | Soft delete |
| deleted_at | timestamp | | |
| created_at | timestamp | | |
| pool | varchar | '' | Pool de torneio |
| max_risk_pct | float | 2.0 | Risco maximo % |
| poll_interval | int | 3 | Segundos entre polls |
| sl_buffer | float | 0.0 | Buffer SL legado |
| tp_buffer | float | 0.0 | Buffer TP legado |
| sl_buffer_metals | float | 0.0 | Buffer SL metais |
| tp_buffer_metals | float | 0.0 | Buffer TP metais |
| sl_buffer_forex | float | 0.0 | Buffer SL forex |
| tp_buffer_forex | float | 0.0 | Buffer TP forex |
| sl_buffer_default | float | 0.0 | Buffer SL default |
| tp_buffer_default | float | 0.0 | Buffer TP default |
| buffer_mode | varchar | 'PIPS' | PIPS ou PRICE |
| settings_dirty | boolean | false | Pendente sync no EA |
| mt5_server | varchar | '' | Servidor MT5 reportado |
| account_type | varchar | 'undefined' | undefined/prop/normal/bonus (v2.71: default changed) |
| account_phase | varchar | '' | F1/F2/Funded/'' |
| bonus_pct | float | 0.0 | % bonus |
| prop_firm_id | int FK | NULL | S361 G.2 / S372: FK pra prop_firms.id (ON DELETE RESTRICT) — fonte UNICA do nome da mesa. Coluna copia prop_firm DROPADA em S372 (era stale apos rename); Account.prop_firm virou @hybrid_property que deriva prop_firm_obj.name (live). CHECK accounts_prop_firm_id_required_chk exige NOT NULL EXCETO se is_deleted=true OU is_test=true. Indice idx_accounts_prop_firm_id |
| prop_size | varchar | '' | 10k/25k/50k/100k/200k |
| prop_steps | varchar | '' | DEPRECATED S361: ainda existe pra compat legacy (af.py:608/3011/3035 + 5 callsites). Backfilled via FK lookup prop_firms.steps. DROP fisico pendente em TODO MED-04 (limpar refs Python primeiro) |
| update_group | varchar | 'stable' | early/stable (auto-update) |
| desired_version | varchar | | Versao desejada pra auto-update |
| update_attempts | int | 0 | Tentativas de update (max 5) |
| terminal_build | int | NULL | Build do MT5 (enviado 1x por sessao no HB). Mudanca dispara alerta Telegram (S265) |
| last_heartbeat_at | timestamp | NULL | Ultimo HB recebido (WS ou HTTP), atualizado SEM throttle (S314.3 Bug 3 fix Opcao C). Tabela heartbeats continua throttled em 30s. Helper is_account_alive() em account_filters.py. Spec: specs/heartbeat-last-seen.md |
| guard_lot_cap | real | NULL | S396 Parte 2: override do teto de lote da trava de seguranca POR CONTA. Precede a config da pool em get_guard_limits (override > pool > default); NULL = herda a pool. Vale ate sem pool (sandbox usa valor alto = frouxa). Migration S396_guard_overrides.sql. Spec: specs/blindagem-execucao-prop-firm.md |
| guard_max_concurrent | int | NULL | S396 Parte 2: override do max de posicoes simultaneas da trava POR CONTA. Precede a pool; NULL = herda |
| guard_max_rounds_per_day | int | NULL | S396 Parte 2: override do max de rodadas/dia da trava POR CONTA. Precede a pool; NULL = herda (0 = sem limite) |
Sinais de trade (OPEN/MODIFY/CLOSE). Criados pelo EA ou broadcast do dashboard.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| group_id | varchar | Grupo destino |
| action | varchar | OPEN/MODIFY/CLOSE |
| ticket | bigint | Ticket MT5 de referencia |
| symbol | varchar | BTCUSD, EURUSD etc |
| direction | varchar | BUY/SELL |
| volume | float | Lotes |
| price | float | Preco de abertura |
| sl | float | Stop Loss |
| tp | float | Take Profit |
| source_account | int FK | Conta que originou |
| created_at | timestamp | |
| expires_at | timestamp | TTL |
| origin | varchar | 'ea' ou 'dashboard' |
| trade_group_id | varchar | UUID agrupador de trades relacionados |
| sl_distance | float | Distancia SL em preco |
| tp_distance | float | Distancia TP em preco |
| signal_channel | varchar(20) | Canal de envio (ws/poll/af/group_copy) |
| detection_method | varchar(30) | Metodo de deteccao (ex: af_push_modify, af_engine, group_copy, OTT, SE) |
| close_reason | varchar(20) | Motivo do close |
| queued_duration_ms | int | Tempo na fila (ms) |
| server_received_at | timestamp | Quando servidor recebeu |
| server_processing_ms | int | Tempo de processamento servidor (ms) |
| signal_source | varchar(16) | S219: 'manual' (default) ou 'test' |
| trace_id | varchar(32) | S224: W3C-inspired trace identifier (nullable pre-migration) |
| is_retroactive | boolean | S300: TRUE = signal CLOSE retroativo via historico MT5 (reconcile pos-offline). Index parcial UNIQUE(source_account, ticket, action) WHERE is_retroactive=TRUE garante idempotency (R6 da spec) |
Confirmacao de execucao de sinais pelos EAs.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| signal_id | int FK | Sinal confirmado |
| account_id | int FK | Conta que executou |
| status | varchar | ORIGIN/FILLED/FAILED/SKIPPED |
| local_ticket | bigint | Ticket local criado |
| error_msg | text | Mensagem de erro se FAILED |
| executed_at | timestamp | |
| open_price | float | Preco real de abertura |
| applied_sl | float | SL aplicado |
| applied_tp | float | TP aplicado |
| actual_volume | float | Volume real executado |
| receive_channel | varchar(20) | Canal de recebimento (ws/poll) |
| gui_duration_ms | int | Tempo de execucao GUI (ms) |
| total_delay_ms | int | Delay total sinal->execucao (ms) |
| trace_id | varchar(32) | S224: carrega mesmo trace_id do signal pai (nullable pre-migration) |
| ea_received_at_ms | bigint | S326: epoch ms (relogio do EA) de quando o EA recebeu o signal. Buffer circular em WebBridge.mqh + helper RecordSignalReceived. Permite medir latencia EA-side vs server-side |
| bid_at_request | float | S336 exec-slippage: bid no momento que EA pediu cotacao (pre-OrderSend). Base pra exec_slip_pts |
| ask_at_request | float | S336 exec-slippage: ask no momento que EA pediu cotacao (pre-OrderSend) |
| exec_slip_pts | float | S336 exec-slippage: slippage de execucao em pontos (open_price vs bid/ask_at_request). Mede slippage do broker no fill |
| pipeline_slip_pts | float | S336 exec-slippage: slippage de pipeline em pontos (preco do signal vs bid/ask_at_request). Mede atraso sinal->EA pedir cotacao |
Event sourcing do ciclo de vida de cada signal (append-only, PARTITIONED BY RANGE(ts_server) daily, 90d retention).
Timeline 1-click: SELECT * FROM signal_events WHERE signal_id=X ORDER BY seq_num.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | bigserial PK composto | Parte do PK (id + ts_server exigido pela particao) |
| signal_id | int FK signals | Signal dono do evento |
| trace_id | varchar(32) | W3C-inspired 128-bit hex, nullable pra backward-compat |
| account_id | int | Conta (nao FK pra permitir conta deletada) |
| event_type | varchar(32) | signal_created, ea_received, gui_s1_ok..s11_ok, ack_sent, ack_failed |
| seq_num | smallint | Per-signal 1,2,3... ordem dentro do signal |
| ts_ea | timestamptz | Timestamp origem no EA (nullable) |
| ts_server | timestamptz PK composto | Quando chegou no servidor (chave de particao) |
| payload | jsonb | Contexto rico (error, duration, SL/TP aplicado, etc) |
Indexes (propagados automaticamente pras particoes):
- (signal_id, seq_num) — timeline query principal
- trace_id partial WHERE IS NOT NULL — correlacao cross-signal
- (event_type, ts_server) — stats por tipo no periodo
UNIQUE constraint (idempotencia R7): (signal_id, event_type, seq_num, ts_server) — permite retry ON CONFLICT DO NOTHING.
Particionamento: 7 particoes pre-criadas (CURRENT_DATE..+6d) + signal_events_default safety net. Cron scripts/cron/purge_signal_events.sh cria ahead + DROP >90d + VACUUM.
Vinculo entre trades copiados. Permite propagar CLOSE/MODIFY.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| trade_group_id | varchar | UUID do grupo |
| signal_id | int FK | Sinal que criou |
| account_id | int FK | Conta dona |
| local_ticket | bigint | Ticket MT5 |
| is_origin | boolean | true = quem originou |
| is_closed | boolean | false | Trade fechado? |
| created_at | timestamp | |
| closed_at | timestamp | |
| close_price | float | Preco de fechamento |
| profit | float | P&L |
| close_provisional | boolean NOT NULL default false | S267: fechamento via snapshot do ultimo HB (peer estava offline). Removido quando ACK WS traz deal_price/deal_profit reais (Fase B Caso 1). |
Indexes: idx_tl_tgid, idx_tl_account_ticket, idx_tl_close_provisional (parcial: WHERE close_provisional = true — S267)
Fila de sinais aguardando execucao. TTL default 60s (PENDING_SIGNAL_TTL_SECONDS). TTL 86400s (24h) quando awaiting_peer_return=True (S267 PENDING_TTL_PEER_OFFLINE).
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | Conta destino |
| symbol | varchar | |
| direction | varchar | BUY/SELL |
| volume | float NOT NULL | |
| signal_id | int FK | |
| trade_group_id | varchar | |
| expires_at | timestamp NOT NULL | TTL 60s default, 24h se awaiting_peer_return |
| resolved | boolean | false (true = ACK de execucao recebido) |
| ws_received_at | timestamp | EA flush_ack via WS (NULL = sem confirmacao, polling HTTP cobre) |
| awaiting_peer_return | boolean NOT NULL default false | S267: true no CLOSE origin='ea' quando peer stale. TTL 24h. Cleanup main.py Block 11. |
| created_at | timestamp | |
Indexes: idx_ps_account_symbol, idx_ps_tgid, idx_ps_ws_ack_missing (parcial: ws_received_at IS NULL AND resolved=false)
Anti-echo para MODIFY/CLOSE. TTL 10 segundos.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| trade_group_id | varchar | |
| action | varchar | MODIFY/CLOSE |
| expires_at | timestamp | TTL 10s |
| created_at | timestamp | |
Snapshot de posicoes abertas, atualizado via heartbeat.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | |
| ticket | bigint | |
| symbol | varchar | |
| direction | varchar | |
| volume | float | |
| open_price | float | |
| current_price | float | |
| sl | float | |
| tp | float | |
| profit | float | |
| pips | float | |
| swap | float | |
| magic | bigint | |
| open_time | varchar | |
| updated_at | timestamp | |
| tick_value | float | Valor do tick |
| tick_size | float | Tamanho do tick |
| spread | float | Spread em valor de preco |
| commission | float | Comissao total dos deals |
| profit_at_sl | float | P&L projetado se bater SL (OrderCalcProfit) |
| profit_at_tp | float | P&L projetado se bater TP (OrderCalcProfit) |
Heartbeats dos EAs (balance, equity, posicoes). Tabela grande (~20MB).
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | |
| balance | float | |
| equity | float | |
| margin | float | |
| free_margin | float | |
| positions | int | Quantidade |
| server_time | varchar | Hora do servidor MT5 |
| ea_version | varchar | Build do EA |
| created_at | timestamp | |
| applied_settings | jsonb | Settings aplicados pelo EA |
Logs persistidos dos EAs. TTL 7 dias.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | |
| level | varchar | 'INFO' |
| message | text | |
| created_at | timestamp | now() |
Versoes do EA para auto-update.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| version | varchar | Ex: 3.3.4 |
| file_path | varchar | Caminho do .ex5 |
| file_hash | varchar | SHA256 |
| file_size | int | |
| changelog | text | |
| is_stable | boolean | |
| is_active | boolean | |
| rollout_stage | varchar | pending/early/stable |
| download_count | int | |
| uploaded_at | timestamp | |
| stable_at | timestamp | |
| force_update | boolean | false | Forcar update mesmo com posicoes abertas |
| soak_passed | boolean | S422: bateria EA Tester PASS (gate _validate_soak_passed exige True pra stable) |
| soak_passed_hash | varchar(64) | Onda C: SHA256 do binario que estava verde; == file_hash pra valer |
| soak_passed_at | timestamp | Frente A furo #4: quando o soak foi carimbado (proveniencia) |
| soak_passed_by | varchar(50) | Frente A furo #4: quem carimbou (username do JWT) |
Snapshots periodicos de equity por conta.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | |
| balance | float | |
| equity | float | |
| margin | float | |
| free_margin | float | |
| positions | int | |
| snapshot_at | timestamp | now() |
Info de simbolos reportada pelos EAs (tick_value, tick_size, etc).
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | |
| symbol | varchar | |
| tick_value | float | |
| tick_size | float | |
| contract_size | float | |
| digits | int | |
| point | float | |
| volume_min | float | 0.01 |
| volume_max | float | 100.0 |
| volume_step | float | 0.01 |
| spread | float | Spread atual |
| bid | float | Preco bid |
| ask | float | Preco ask |
| swap_long | float | Swap compra |
| swap_short | float | Swap venda |
| updated_at | timestamp | now() |
Lista de pendencias do projeto (fonte unica de verdade). Kanban do site e todos.md sao "janelas" pra esta tabela.
| Coluna | Tipo | Default | Descricao |
|--------|------|---------|-----------|
| id | serial PK | auto | |
| title | varchar(200) | | Titulo da tarefa |
| description | text | '' | Descricao detalhada |
| priority | varchar(10) | 'media' | alta/media/baixa |
| status | varchar(10) | 'backlog' | backlog/next/doing/done |
| tags | text[] | '{}' | Tags (ex: bug, feature, infra) |
| created_at | timestamp | now() | |
| updated_at | timestamp | now() | |
| completed_at | timestamp | null | Preenchido quando status=done |
Prop firms com regras completas. SSoT para AF engine e classificacao de contas.
| Coluna | Tipo | Default | Descricao |
|--------|------|---------|-----------|
| id | serial PK | auto | |
| name | varchar(100) UNIQUE | | Nome Desafio (label UI S370): identidade unica da mesa (FTMO Swing, FundedNext). Chave de busca lower(trim(name)) + exibicao. So o rotulo da UI mudou (Nome -> Nome Desafio); coluna name inalterada |
| company | varchar(100) NOT NULL | '' | S362: Empresa (companhia, ex: "FTMO"). R7 pareia comparando company, nao name — 2 desafios da mesma Empresa nunca pareiam num hedge. Backfill company=name. CRUD obrigatorio (datalist) |
| steps | varchar(20) | '2-step' | S361 G.2: renomeada de default_steps. CHECK constraint prop_firms_steps_chk enforced em {'1-step','2-step','3-step','instant','sem-fase'} |
| phases | JSON | [] | Fases disponiveis (F1, F2, Funded) |
| sizes | JSON | [] | Tamanhos disponiveis (10k, 25k, etc) |
| is_builtin | boolean | false | Se veio pre-cadastrada (seed) |
| created_at | timestamp | now() | |
| price | int | 0 | Preco USD (referencia 100k) |
| leverage | varchar(20) | '' | Alavancagem (ex: 1:100) |
| max_dd | float | 10.0 | Max DD % (estatico, base saldo inicial) |
| daily_dd | float | 5.0 | Daily DD % |
| daily_dd_type | varchar(10) | 'equity' | 'equity' ou 'balance' |
| target_f1 | float | 8.0 | Meta F1 % |
| target_f2 | float | 5.0 | Meta F2 % |
| min_days_f1 | int | 0 | Dias minimos trading F1 |
| min_days_f2 | int | 0 | Dias minimos trading F2 |
| min_profit_days | int | 0 | Dias lucrativos obrigatorios |
| max_risk | int | 5000 | Risco operacional por trade USD (100k ref) |
| daily_dd_op | int | 4500 | DD operacional diario USD (100k ref) |
| news_eval | varchar(100) | '' | Restricoes noticias avaliacao |
| news_funded | varchar(100) | '' | Restricoes noticias funded |
| limitation | varchar(200) | '--' | Limitacoes especiais |
| has_200k | boolean | false | Tem conta 200k? |
| is_active | boolean | true | Ativa no sistema? |
| is_af_eligible | boolean | true | Elegivel pro AF engine? (auto: risk>=2500 AND dd>=10) |
| min_hold_min_s | int NOT NULL | 0 | S422 (perna-orfa-fecho-blindado): piso de min-hold da MESA em segundos (minimo do range). Tempo minimo que a posicao fica aberta antes de poder fechar (mesas rigidas tratam fecho <5min como breach). >0 vence a hierarquia mesa>pool>global. 0 = mesa nao impoe piso. Entra no config_snapshot do round |
| min_hold_max_s | int NOT NULL | 0 | S422: maximo do range de min-hold (sorteio [min,max] por par, persistido em risk_detail.min_hold_until) |
| updated_at | timestamp | null | Ultima atualizacao |
Historico de resets de classificacao de conta.
| Coluna | Tipo | Default | Descricao |
|--------|------|---------|-----------|
| id | serial PK | auto | |
| account_id | int FK | | Conta resetada |
| reset_at | timestamp | now() | Quando |
| pool_id | int | null | Pool de onde saiu |
| pool_name | varchar(100) | '' | Nome da pool (snapshot) |
| previous_type | varchar(20) | '' | Tipo anterior |
| previous_firm | varchar(100) | '' | Prop firm anterior |
| previous_phase | varchar(20) | '' | Fase anterior |
users — Usuarios do dashboard (username, password_hash, display_name, is_active, timezone)
- timezone varchar(64) NOT NULL DEFAULT 'America/Campo_Grande' — fuso de LEITURA (so exibicao).
Nunca entra na decisao do robo: janela de operacao e swap vem da cascata do servidor.
user_sessions — Sessoes ativas (username, ip, user_agent, login_at, last_activity, request_count)
login_attempts — Tentativas de login (username_tried, password_tried, ip, success, fail_reason)
page_visits — Visitas a paginas (ip, path, user_agent, username, visited_at)
audit_log — Log de acoes (user, action, detail, created_at)
ip_whitelist — IPs permitidos (ip_address, description, is_active)
af_pools — Pools AF v2 (name, mode varchar(20) default 'live' LEGACY, validated bool default false, symbol, status, spread_pct, trade_interval_sec, trade_timeout_sec, group_id, config JSONB, created_at, updated_at). S170: mode é legacy (código não lê mais). validated controla se pool pode operar.
af_pool_accounts — Contas no pool com prop atribuida (pool_id FK, account_id FK nullable UNIQUE (uq_account_one_pool — 1 conta = 1 pool), prop_name, prop_config JSONB, phase, virtual_balance, profitable_days, trading_days, status active/dead/funded/paused/passed, chair_number, transition_at, daily_pnl, daily_pnl_peak, direction_history JSONB default '[]' — ultimas 10 direcoes executadas pro anti-OSB, created_at, updated_at). S207: prop_config agora inclui snapshot: owner, account_name, account_type, original_group_id. Cadeiras passed/dead com account_id=NULL usam prop_config pra display historico (nome, cor, badge). virtual_balance serve como saldo congelado.
af_rounds — Rodadas de trading (pool_id FK, round_number, status pending/executing/completed/failed, pairs_count, started_at, completed_at, summary JSONB, config_snapshot JSONB nullable — S306 ADR-0011: snapshot imutavel do pool.config quando round foi criado; lida via app.af.config_helper.get_round_config(round, pool) durante execucao; NULL em rounds legacy → fallback pra pool.config; created_at)
af_pairs — Pares/batalhas dentro de rodada (round_id FK, pool_id FK, account_a_id FK, account_b_id FK CHECK(a != b — chk_pair_diff_accounts), risk_usd, risk_detail JSONB, symbol, direction_a, volume, sl_price/tp_price colunas SQL legadas — guardam half-width do colchao (distancia em points, NAO preco absoluto); Python/API/dashboard acessam como sl_distance/tp_distance via alias no model AfPair (S257); precos absolutos REAIS ficam em af_trades.sl_price/tp_price, scheduled_at, status, winner_pool_account_id FK, profit_a, profit_b, spread_cost, created_at, completed_at)
af_trades — Trades individuais (pair_id FK, pool_account_id FK, account_id FK, signal_id FK, direction, volume, sl/tp/open/close price, profit, status, local_ticket bigint)
af_audit_log — Log imutavel INSERT-only (pool_id FK, action, entity_type, entity_id, detail text)
af_dead_letters — Dead Letter Queue: sinais AF que falharam (pool_id FK, pair_id, account_id, prop_name, failure_type varchar(50), reason text, attempts int, context JSONB). Tipos: timeout, margin, rsafe, invert, e7_exhausted, gui_fail
symbol_presets — Presets de configuracao por simbolo (id PK, symbol varchar(20) UNIQUE, config JSONB, spread_pct float, created_at, updated_at). Campos preset: sl_min/max, dz_min/max, rsafe2_price_gate, modify_margin_min/max, push_buffer_usd. Defaults hardcoded em server/af/presets.py (XAUUSD, BTCUSD, _default).
Diario de bordo: cada evento na vida de uma posicao. Append-only, cleanup 90 dias.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | bigserial PK | |
| created_at | timestamptz | Quando o evento foi registrado |
| event_type | varchar(30) | OPEN, MODIFY, CLOSE, PROPAGATE, ACK |
| account_id | int FK accounts | Conta envolvida |
| ticket | bigint | Ticket MT5 |
| trade_group_id | varchar(64) | Trade group (ex: TG_xxx, AF_P123) |
| signal_id | int | Signal que gerou o evento |
| origin | varchar(40) | ea, server_cleanup, server_auto_close, ws_detect, heartbeat_gone, admin_reconcile, af_orphan_cleanup |
| close_reason | varchar(30) | Motivo do close (se aplicavel) |
| price | float | Preco da operacao |
| profit | float | P&L capturado |
| volume | float | Volume em lots |
| context | jsonb | Dados extras (symbol, direction, reason, etc) |
Indices: trade_group_id, (account_id, created_at), (event_type, created_at), created_at
Endpoints: GET /api/af/trade-events/pair/{pair_id}, GET /api/af/trade-events/account/{account_id}, GET /api/af/trade-events/{trade_group_id}
Append-only. Todo evento telemetrico do EA aterriza aqui. Dedupe via event_id UNIQUE. Retencao 30 dias (D5).
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| event_id | varchar(64) UNIQUE | ID deterministico gerado pelo EA (hash position_id+deal_time+symbol) |
| trace_id | varchar(32) | W3C Trace Context 32-hex. Auto-enriquecido pelo server se ausente (I12) |
| account_id | int FK accounts NULL | Conta relacionada (nullable pra eventos globais) |
| event_type | varchar(32) | signal_received, order_sent, order_rejected, gap_alert, etc |
| ts_broker | timestamp | Quando o EA observou o evento no broker |
| ts_received | timestamp | Quando o servidor recebeu (auto-preenchido) |
| gap_ms | int | ts_received - ts_broker em ms. late=true se > 5000 (I1) |
| payload | jsonb | Dados do evento (slip_pts, latency_ms, spread_pts, etc) |
| late | bool | gap_ms > 5000ms (R1) |
| clock_offset_ms | int NULL | Offset de clock reportado pelo EA |
| ea_instance_id | varchar(64) NULL | Identificador da instancia do EA |
| created_at | timestamp | Insert time |
Indices: trace_id, account_id, event_type, ts_received, (account_id, ts_received DESC)
Endpoints: POST /api/telemetry/events, GET /api/telemetry/trace/{trace_id}, GET /api/telemetry/anomalies, GET /admin/health
Worker queue simples via Postgres NOTIFY/LISTEN. Trigger trg_event_outbox_notify dispara pg_notify('event_outbox_new', id) em cada insert.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| event_stream_id | int FK event_stream | Evento original |
| published | bool | Worker marca true apos processar |
| attempts | int | Retries do worker |
| last_error | text NULL | Ultima msg de erro se worker falhou |
| created_at | timestamp | |
| published_at | timestamp NULL | Quando worker marcou published |
Indices: (published WHERE published=false) partial, event_stream_id
telemetry_ro (S243 — I7 enforcement mecanico)Role Postgres NOLOGIN com SELECT only em signals, signal_acks, open_positions, af_trades, event_stream, event_outbox. INSERT/UPDATE/DELETE revogados explicitamente. Garante que pipeline de telemetria nao corrompe tabelas de trading mesmo com bug de codigo (I7). Worker e dashboard de telemetria DEVEM usar essa role.
As 41 secoes abaixo foram GERADAS de
server/models.py, que e' a fonte da verdade do schema.
Ate 2026-08-08 este documento descrevia 13 tabelas e o cabecalho anunciava 24, enquanto
o codigo tinha 54 — 41 tabelas inteiras nunca chegaram aqui, incluindoaf_pools,
af_pool_accounts,prop_firms,terminalse as do simulador. A lacuna apareceu quando um
fiscal me cobrou provar quedesired_versionera coluna deaccounts: a coluna estava, mas as
tabelas de pool que eu ia citar em seguida, nao.A coluna
Descricaosai VAZIA quando o codigo nao tem comentario na linha. Documentacao
gerada que adivinha o proposito de um campo e' pior que documentacao ausente — ela tem cara de
autoridade. Quem souber o proposito, escreva; o resto fica em branco, honesto.Regenerar: o gerador le
server/models.pycomaste imprime as secoes que faltam. A rede
que impede a lacuna de voltar e'scripts/tests/test_db_schema_cobre_todas_as_tabelas.py.
| Coluna | Tipo | Default | Extras | Descricao |
|---|---|---|---|---|
| id | Integer | PK | ||
| signal_id | Integer | ForeignKey('signals.id'), index | ||
| account_id | Integer | ForeignKey('accounts.id'), NOT NULL | ||
| reason | String(30) | NOT NULL | ||
| signal_type | String(10) | NOT NULL | ||
| original_direction | String(10) | '' | ||
| inverted_direction | String(10) | '' | ||
| original_sl | Float | 0 | ||
| original_tp | Float | 0 | ||
| inverted_sl | Float | 0 | ||
| inverted_tp | Float | 0 | ||
| created_at | DateTime | datetime.utcnow | index |
Ficha do Guarda de Saude da Pool (spec: guarda-de-saude-pool.md).
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| account_id | Integer | | ForeignKey('accounts.id'), NOT NULL | |
| signal_type | String(20) | | NOT NULL | |
| severity | String(10) | | NOT NULL | weak | strong |
| status | String(16) | 'open' | NOT NULL | |
| channel_sent | Boolean | False | NOT NULL | |
| evidence | Text | | | JSON serializado (str) |
| first_seen | DateTime | datetime.utcnow | | |
| last_seen | DateTime | datetime.utcnow | | |
| snooze_until | DateTime | | | |
| created_at | DateTime | datetime.utcnow | | |
Usuario de login do dashboard (admin/zwrt/test_runner).
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK, index | |
| username | String(50) | | UNIQUE, NOT NULL | |
| password_hash | String(200) | | NOT NULL | |
| display_name | String(100) | | NOT NULL | |
| is_active | Boolean | True | | |
| created_at | DateTime | func.now() | | |
| timezone | String(64) | 'America/Campo_Grande' | NOT NULL | |
| Coluna | Tipo | Default | Extras | Descricao |
|---|---|---|---|---|
| id | Integer | PK, index | ||
| user | String(50) | NOT NULL | ||
| action | String(50) | NOT NULL | ||
| detail | String(500) | '' | ||
| created_at | DateTime | func.now() |
| Coluna | Tipo | Default | Extras | Descricao |
|---|---|---|---|---|
| id | Integer | PK | ||
| username | String(50) | NOT NULL | ||
| display_name | String(100) | '' | ||
| ip_address | String(50) | '' | ||
| user_agent | String(500) | '' | ||
| login_at | DateTime | datetime.utcnow | ||
| last_activity | DateTime | datetime.utcnow | ||
| request_count | Integer | 0 |
| Coluna | Tipo | Default | Extras | Descricao |
|---|---|---|---|---|
| id | Integer | PK | ||
| ip_address | String(50) | NOT NULL | ||
| path | String(200) | '/' | ||
| user_agent | String(500) | '' | ||
| username | String(50) | '' | ||
| visited_at | DateTime | datetime.utcnow |
Registra TODAS as tentativas de login (sucesso e falha).
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| username_tried | String(100) | | NOT NULL | |
| password_tried | String(100) | '' | | |
| ip_address | String(50) | '' | | |
| user_agent | String(500) | '' | | |
| success | Boolean | False | | |
| fail_reason | String(100) | '' | | |
| created_at | DateTime | datetime.utcnow | | |
IPs autorizados a acessar o dashboard. Whitelist vazia = permite todos.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK, index | |
| ip_address | String(45) | | NOT NULL | suporta IPv6 |
| description | String(200) | '' | | |
| created_at | DateTime | datetime.utcnow | | |
| is_active | Boolean | True | | |
Event log append-only do ciclo de vida de cada signal.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | BigInteger | | PK | |
| signal_id | Integer | | ForeignKey('signals.id'), NOT NULL | |
| trace_id | String(32) | | | |
| account_id | Integer | | NOT NULL | |
| event_type | String(32) | | NOT NULL | |
| seq_num | SmallInteger | | NOT NULL | |
| ts_ea | DateTime(timezone=True) | | | |
| ts_server | DateTime(timezone=True) | datetime.utcnow | NOT NULL | |
| payload | JSONB | dict | NOT NULL | |
Pool de contas para o sistema Advancing Front v2.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| name | String(100) | | NOT NULL | |
| mode | String(20) | 'live' | | Legacy — code no longer reads this |
| symbol | String(20) | 'XAUUSD' | | |
| status | String(20) | 'active' | | |
| spread_pct | Float | 2.0 | | |
| trade_interval_sec | Integer | 300 | | |
| trade_timeout_sec | Integer | 900 | | |
| group_id | String(50) | | NOT NULL | |
| config | JSONB | {} | | |
| max_risk_f1_pct | Float | 2.5 | NOT NULL | |
| max_risk_f2_pct | Float | 2.0 | NOT NULL | |
| max_risk_f3_pct | Float | 1.0 | NOT NULL | |
| dead_zone_min_pct | Float | 0.5 | NOT NULL | |
| dead_zone_max_pct | Float | 1.0 | NOT NULL | |
| created_at | DateTime | datetime.utcnow | | |
| updated_at | DateTime | datetime.utcnow | | |
| phase | SmallInteger | | | |
| model_type | String(20) | | | ex '2-step' — valida ligacao por tipo |
| account_size | Integer | | | USD — valida ligacao por tamanho |
| next_pool_id | Integer | | ForeignKey('af_pools.id', ondelete='SET NULL') | |
Preset de configuracao por simbolo (ex: XAUUSD, BTCUSD).
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| symbol | String(20) | | NOT NULL, UNIQUE | |
| config | JSONB | {} | | |
| spread_pct | Float | | | |
| created_at | DateTime | datetime.utcnow | | |
| updated_at | DateTime | datetime.utcnow | | |
Conta vinculada a um pool AF com prop firm atribuida.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| pool_id | Integer | | ForeignKey('af_pools.id'), NOT NULL | |
| account_id | Integer | | ForeignKey('accounts.id') | |
| prop_name | String(50) | | NOT NULL | |
| prop_config | JSONB | | NOT NULL | |
| phase | Integer | 1 | | |
| profitable_days | Integer | 0 | | |
| trading_days | Integer | 0 | | |
| status | String(20) | 'active' | | |
| chair_number | Integer | | NOT NULL | |
| transition_at | DateTime | | | |
| daily_pnl | Float | 0.0 | | |
| daily_pnl_peak | Float | 0.0 | | |
| direction_history | JSONB | [] | | |
| created_at | DateTime | datetime.utcnow | | |
| updated_at | DateTime | datetime.utcnow | | |
| created_from_zombie_id | Integer | | ForeignKey('af_pool_accounts.id', ondelete='SET NULL') | |
| recycle_reason | Text | | | S370: alinhado a prod (text) — drift S360 |
| journey_id | Integer | | ForeignKey('account_journeys.id', ondelete='SET NULL') | |
Rodada AF — 1 rodada = 1 dia em live, varias em demo.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| pool_id | Integer | | ForeignKey('af_pools.id'), NOT NULL | |
| round_number | Integer | | NOT NULL | |
| status | String(20) | 'pending' | | |
| pairs_count | Integer | 0 | | |
| started_at | DateTime | | | |
| completed_at | DateTime | | | |
| summary | JSONB | {} | | |
| config_snapshot | JSONB | | | |
| created_at | DateTime | datetime.utcnow | | |
Par de batalha dentro de uma rodada AF.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| round_id | Integer | | ForeignKey('af_rounds.id'), NOT NULL | |
| pool_id | Integer | | ForeignKey('af_pools.id'), NOT NULL | |
| account_a_id | Integer | | ForeignKey('af_pool_accounts.id') | |
| account_b_id | Integer | | ForeignKey('af_pool_accounts.id') | |
| risk_usd | Float | | NOT NULL | |
| risk_detail | JSONB | {} | | |
| symbol | String(20) | | NOT NULL | |
| direction_a | String(10) | | NOT NULL | |
| volume | Float | | | |
| sl_distance | Float | | | |
| tp_distance | Float | | | |
| scheduled_at | DateTime | | | |
| status | String(20) | 'scheduled' | | |
| winner_pool_account_id | Integer | | ForeignKey('af_pool_accounts.id') | |
| profit_a | Float | | | |
| profit_b | Float | | | |
| hedge_cost | Float | | | |
| created_at | DateTime | datetime.utcnow | | |
| completed_at | DateTime | | | |
| owner_snapshot_json | JSONB | | | |
| recovery_phase | String(20) | 'none' | NOT NULL | |
| closing_committed_at | DateTime | | | |
Trade individual AF — cada par gera 2 trades.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| pair_id | Integer | | ForeignKey('af_pairs.id'), NOT NULL | |
| pool_account_id | Integer | | ForeignKey('af_pool_accounts.id'), NOT NULL | |
| account_id | Integer | | ForeignKey('accounts.id'), NOT NULL | |
| signal_id | Integer | | ForeignKey('signals.id') | |
| direction | String(10) | | NOT NULL | |
| volume | Float | | | |
| sl_price | Float | | | |
| tp_price | Float | | | |
| open_price | Float | | | |
| close_price | Float | | | |
| profit | Float | | | |
| status | String(20) | 'pending' | | |
| local_ticket | BigInteger | | | |
| opened_at | DateTime | | | |
| closed_at | DateTime | | | |
| created_at | DateTime | datetime.utcnow | | |
Log imutavel de todas as acoes AF.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| pool_id | Integer | | ForeignKey('af_pools.id') | |
| action | String(50) | | NOT NULL | |
| entity_type | String(30) | | | |
| entity_id | Integer | | | |
| detail | Text | | | |
| created_at | DateTime | datetime.utcnow | | |
Etiqueta de jornada (spec metricas-fonte-unica-linhagem): correlation id que costura
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | = journey_id |
| prop_firm_id | Integer | | ForeignKey('prop_firms.id') | |
| owner_id | Integer | | | |
| state | String(24) | 'in_f1' | NOT NULL | prod=NOT NULL (add_account_journeys.sql) |
| is_simulator | Boolean | False | NOT NULL | P53: real e sim NUNCA se misturam |
| died_at_phase | SmallInteger | | | |
| opened_at | DateTime | datetime.utcnow | | |
| closed_at | DateTime | | | |
| updated_at | DateTime | datetime.utcnow | | |
Diario ESTRUTURADO da jornada (abordagem B do brainstorm): cada acontecimento com
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| journey_id | Integer | | ForeignKey('account_journeys.id', ondelete='SET NULL') | |
| account_id | Integer | | | |
| pool_id | Integer | | | |
| round_id | Integer | | | |
| event_type | String(20) | | NOT NULL | |
| phase | SmallInteger | | NOT NULL | |
| prop_name | String(50) | | | |
| chair_number | Integer | | | |
| amount | Float | | | |
| reason | String(20) | | | |
| is_simulator | Boolean | False | NOT NULL | LOW-03: NOT NULL (= migracao prod) |
| occurred_t | Float | | | |
| created_at | DateTime | datetime.utcnow | | |
Dead Letter Queue — sinais AF que falharam todas tentativas.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| pool_id | Integer | | ForeignKey('af_pools.id') | |
| pair_id | Integer | | | |
| account_id | Integer | | | |
| prop_name | String(50) | | | |
| failure_type | String(50) | | NOT NULL | timeout, margin, rsafe, invert, gui_fail |
| reason | Text | | | |
| attempts | Integer | 1 | | |
| context | JSONB | | | dados extras: preço, volume, drift, etc |
| created_at | DateTime | datetime.utcnow | | |
Diario de bordo: cada evento na vida de uma posicao (open, modify, close, propagate, ack).
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | BigInteger | | PK | |
| created_at | DateTime(timezone=True) | datetime.utcnow | NOT NULL | |
| event_type | String(30) | | NOT NULL | |
| account_id | Integer | | ForeignKey('accounts.id') | |
| ticket | BigInteger | | | |
| trade_group_id | String(64) | | | |
| signal_id | Integer | | | |
| origin | String(40) | | | |
| close_reason | String(30) | | | |
| price | Float | 0 | | |
| profit | Float | 0 | | |
| volume | Float | 0 | | |
| context | JSONB | {} | | |
Prop firms cadastradas (FTMO, FundedNext, etc). Builtins + custom.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| name | String(100) | | UNIQUE, NOT NULL | |
| company | String(100) | '' | NOT NULL | |
| steps | String(20) | '2-step' | | |
| phases | JSON | [] | | |
| sizes | JSON | [] | | |
| is_builtin | Boolean | False | | |
| created_at | DateTime | datetime.utcnow | | |
| price | Integer | 0 | | |
| leverage | String(20) | '' | | |
| max_dd | Float | 10.0 | | |
| payout | Float | 0.8 | NOT NULL | |
| daily_dd | Float | 5.0 | | |
| daily_dd_type | String(10) | 'equity' | | |
| target_f1 | Float | 8.0 | | |
| target_f2 | Float | 5.0 | | |
| min_days_f1 | Integer | 0 | | |
| min_days_f2 | Integer | 0 | | |
| min_profit_days | Integer | 0 | | |
| max_risk_pct | Float | 5.0 | NOT NULL | |
| daily_dd_op_pct | Float | 4.5 | NOT NULL | |
| min_hold_min_s | Integer | 0 | NOT NULL | |
| min_hold_max_s | Integer | 0 | NOT NULL | |
| limitation | String(200) | '--' | | |
| has_200k | Boolean | False | | |
| is_active | Boolean | True | | |
| is_af_eligible | Boolean | True | | |
| is_simulator | Boolean | False | NOT NULL | |
| max_dd_style | String(20) | 'static' | NOT NULL | |
| daily_dd_style | String(20) | 'static' | NOT NULL | |
| news_phases | JSON | list | | |
| news_minutes_before | Integer | 0 | NOT NULL | |
| news_minutes_after | Integer | 0 | NOT NULL | |
| news_penalty | String(300) | '' | NOT NULL | |
| news_notes | String(300) | '' | NOT NULL | |
| updated_at | DateTime | | | |
Dono real de contas (Linniu, Lucas, etc) — entidade de perfil rico.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| name | String(100) | | NOT NULL | |
| color | String(20) | '#58a6ff' | | 58a6ff") # hex color for UI |
| created_at | DateTime | datetime.utcnow | | |
| risk_mode | String(16) | 'inherit_pool' | NOT NULL | |
| max_risk_f1_pct | Float | | | |
| max_risk_f2_pct | Float | | | |
| max_risk_f3_pct | Float | | | |
Historico de resets de classificacao de conta.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| account_id | Integer | | ForeignKey('accounts.id'), NOT NULL | |
| reset_at | DateTime | datetime.utcnow | | |
| pool_id | Integer | | | |
| pool_name | String(100) | '' | | |
| previous_type | String(20) | '' | | |
| previous_firm | String(100) | '' | | |
| previous_phase | String(20) | '' | | |
Faixa 1 (specs/identidade-terminal-chave-vs-conta-login.md, decisão (e)): registro de
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| api_key | String(64) | | UNIQUE, NOT NULL | crachá do terminal |
| world | String(10) | 'real' | NOT NULL | |
| nickname | String(30) | | | |
| seq_no | Integer | | | |
| status | String(20) | 'active' | NOT NULL | active|revoked |
| last_ip | String(64) | | | último IP visto (do heartbeat) |
| last_login_served | BigInteger | | | login (account_num) reportado no último HB |
| last_mt5_server | String(100) | | | servidor do último login servido |
| first_seen_at | DateTime | datetime.utcnow | NOT NULL | primeira aparição |
| last_heartbeat_at | DateTime | | | último HB observado |
| revoked_at | DateTime | | | quando foi revogado (Faixa 4) |
| approved_at | DateTime | | | quando o dono abençoou o terminal |
| approved_by | String(50) | | | como: backfill-E1 | classify:
| hb_count | BigInteger | 0 | NOT NULL | nº de HBs observados |
| hdr_login | BigInteger | | | login reportado NO CABEÇALHO (X-MT5-Login) |
| hdr_mt5_server | String(100) | | | servidor reportado no cabeçalho (X-MT5-Server) |
| hdr_seen_at | DateTime | | | último HB/pedido que trouxe os 2 cabeçalhos |
| divergence_count | BigInteger | 0 | NOT NULL | chave!=rosto acumulado |
| last_divergence_at | DateTime | | | |
| serving_account_id | Integer | | ForeignKey('accounts.id', ondelete='SET NULL') | |
O3 · FASE 2 — O LIVRO DA SOMBRA INVERTIDA.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| key_tail | String(8) | | NOT NULL | 6 últimos da chave — NUNCA a chave inteira |
| door | String(16) | | NOT NULL | poll | nucleo | ws | hb — por onde entrou |
| shape | String(120) | | NOT NULL | |
| kind | String(12) | | NOT NULL | veredito | mundo |
| severity | String(10) | 'perigosa' | NOT NULL | |
| old_accepts | Boolean | | NOT NULL | |
| new_accepts | Boolean | | NOT NULL | |
| old_world | String(10) | | | |
| new_world | String(10) | | | |
| new_reason | String(20) | | | |
| terminal_id | Integer | | | sem FK: a linha sobrevive à faxina do crachá |
| hits | BigInteger | 0 | NOT NULL | |
| first_at | DateTime | datetime.utcnow | NOT NULL | |
| last_at | DateTime | datetime.utcnow | NOT NULL | |
O3 · FASE 2 — O DENOMINADOR da sombra (linha ÚNICA, id=1).
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| comparacoes | BigInteger | 0 | NOT NULL | |
| concordancias | BigInteger | 0 | NOT NULL | |
| por_porta | JSON | | | {"poll": N, "nucleo": M} — cobertura por porta |
| desde | DateTime | | | 1ª comparação registrada (o marco zero) |
| ultima_em | DateTime | | | |
O2 (§4 item 10) — o FILME: uma linha por PERÍODO em que uma máquina serviu uma conta.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| terminal_id | Integer | | ForeignKey('terminals.id', ondelete='CASCADE'), NOT NULL | |
| account_id | Integer | | ForeignKey('accounts.id', ondelete='SET NULL') | |
| started_at | DateTime | datetime.utcnow | NOT NULL | |
| ended_at | DateTime | | | NULL = ainda servindo |
F.1 — ESPELHO do plantão (quem é o EA titular por conta). NÃO é o relógio da eleição.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| account_id | Integer | | ForeignKey('accounts.id', ondelete='CASCADE'), PK | |
| leader_key | String(64) | | NOT NULL | crachá (api_key) do terminal titular |
| updated_at | DateTime(timezone=True) | func.now() | NOT NULL | |
S370: ledger de tentativas de desafio por conta (login).
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| account_id | Integer | | ForeignKey('accounts.id'), NOT NULL | |
| prop_firm_id | Integer | | | |
| owner_id | Integer | | | |
| account_phase | String(20) | | | |
| prop_size | String(20) | | | |
| combo_key | Text | | | S370b: chave do #N (empresa|passos|tamanho|fase|owner) |
| name | String(100) | | | |
| name_seq | Integer | | | |
| opened_at | DateTime | datetime.utcnow | | |
| closed_at | DateTime | | | |
| outcome | String(20) | 'active' | | active|passed|dead|funded|reset |
| final_balance | Float | | | |
| final_equity | Float | | | |
A CASA PRÓPRIA das chaves de bancada (#602, pré-requisito da obra O2 passo 4).
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| api_key | String(64) | | PK | |
| account_id | Integer | | ForeignKey('accounts.id', ondelete='CASCADE'), NOT NULL, index | |
| created_at | DateTime | datetime.utcnow | NOT NULL | |
| Coluna | Tipo | Default | Extras | Descricao |
|---|---|---|---|---|
| id | Integer | PK | ||
| event_id | String(64) | UNIQUE, NOT NULL | ||
| trace_id | String(32) | NOT NULL | ||
| account_id | Integer | ForeignKey('accounts.id') | ||
| event_type | String(32) | NOT NULL | ||
| ts_broker | DateTime | NOT NULL | ||
| ts_received | DateTime | datetime.utcnow | NOT NULL | |
| gap_ms | Integer | |||
| payload | JSONB | dict | NOT NULL | |
| late | Boolean | False | NOT NULL | |
| clock_offset_ms | Integer | |||
| ea_instance_id | String(64) | |||
| created_at | DateTime | datetime.utcnow | NOT NULL |
| Coluna | Tipo | Default | Extras | Descricao |
|---|---|---|---|---|
| id | Integer | PK | ||
| event_stream_id | Integer | ForeignKey('event_stream.id'), NOT NULL | ||
| published | Boolean | False | NOT NULL | |
| attempts | Integer | 0 | NOT NULL | |
| last_error | Text | |||
| created_at | DateTime | datetime.utcnow | NOT NULL | |
| published_at | DateTime |
S326-cont #3+#4: snapshot persistente de SimRunner pra sobreviver a
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | String(64) | | PK | RUN_xxx |
| session_id | String(64) | | NOT NULL, index | SIM_xxx |
| mode | String(20) | | NOT NULL | 'rapido' | 'completo' | 'real' |
| status | String(30) | 'running' | NOT NULL, index | |
| started_at | DateTime | datetime.utcnow | NOT NULL, index | |
| finished_at | DateTime | | | |
| abort_reason | Text | | | |
| config_json | JSONB | | | payload original do start (sync com prod-schema) |
| summary_json | JSONB | | | final summary (status_counts + post_validate) |
S358: 1 row por bateria EA Tester executada (rapida 4 estacoes ou
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| run_id | UUID(as_uuid=False) | | PK | |
| account_id | Integer | | ForeignKey('accounts.id'), NOT NULL | |
| started_at | DateTime(timezone=True) | | NOT NULL | |
| ended_at | DateTime(timezone=True) | | | |
| mode | String(20) | | NOT NULL | ver MODOS_DE_BATERIA |
| agg | String(10) | | | ver VEREDITOS ("PASS*" cabe: 5 de 10 chars) |
| ok_count | Integer | 0 | NOT NULL | |
| warn_count | Integer | 0 | NOT NULL | |
| fail_count | Integer | 0 | NOT NULL | |
| total | Integer | 0 | NOT NULL | |
| aborted | Boolean | False | NOT NULL | |
| orphans_remaining | Integer | 0 | NOT NULL | |
| cleanup_verify_failed | Boolean | False | NOT NULL | |
| invert_baseline | Boolean | | | |
| ea_version | String(50) | | | |
| terminal_build | Integer | | | |
| symbol | String(20) | | | par usado (BTCUSD weekend / EURUSD dia util) |
| weekend | Boolean | | | rodou sabado/domingo? |
| regime | String(20) | | | 'forex_aberto' | 'cripto_24_7' |
| running_hash | String(64) | | | |
| created_at | DateTime(timezone=True) | func.now() | NOT NULL | |
S358: 1 row por estacao da bateria (max 25). motivo_leigo + classifier_id
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | BigInteger | | PK | |
| run_id | UUID(as_uuid=False) | | ForeignKey('ea_tester_runs.run_id', ondelete='CASCADE'), NOT NULL | |
| station_id | String(50) | | NOT NULL | |
| station_name | String(120) | | NOT NULL | |
| station_order | Integer | | NOT NULL | |
| status | String(10) | | NOT NULL | 'ok' | 'warn' | 'failed' | 'skipped' | 'insight' (S430) |
| elapsed_ms | Integer | | | |
| attempts | Integer | | | |
| motivo_leigo | Text | | | |
| motivo_classifier_id | String(50) | | | |
| drill_json | JSONB | | | |
G1 (blindagem-final §3): ledger APPEND-ONLY de proveniencia — cada inject de
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | BigInteger | | PK | |
| run_id | String(36) | | NOT NULL | |
| account_id | Integer | | NOT NULL | |
| signal_id | Integer | | | |
| station_id | String(50) | | | futuro: correspondencia por-estacao |
| action | String(16) | | | |
| created_at | DateTime | datetime.utcnow | NOT NULL | |
Frente C Entrega 1: trava server-side da bancada (bateria EA Tester) por conta.
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| account_id | Integer | | PK | 1 lock por conta (a bancada e unica) |
| run_id | String(36) | | NOT NULL | |
| operator | String(80) | | | username do JWT que segurou |
| acquired_at | DateTime | datetime.utcnow | NOT NULL | |
| heartbeat_at | DateTime | datetime.utcnow | NOT NULL | |
| expires_at | DateTime | | NOT NULL | |
Sim E2E Battery (2026-05-19): 1 row por run do simulador Real Path
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| run_id | String(40) | | PK | |
| session_id | String(120) | | NOT NULL | |
| scenario_kind | String(40) | | | |
| subtype | String(40) | | | |
| mode | String(20) | | | |
| started_at | DateTime(timezone=True) | | NOT NULL | |
| ended_at | DateTime(timezone=True) | | | |
| status | String(20) | | NOT NULL | 'queued' | 'running' | 'done' | 'error' | 'stopped' |
| total_stations | Integer | 0 | NOT NULL | |
| ok_count | Integer | 0 | NOT NULL | |
| failed_count | Integer | 0 | NOT NULL | |
| elapsed_ms | Integer | | | |
| result_pass | Boolean | | | |
| summary_json | JSONB | | | |
| result_json | JSONB | | | |
| created_at | DateTime(timezone=True) | func.now() | NOT NULL | |
| favorite | Boolean | False | NOT NULL | |
| favorite_note | String(280) | | | |
Sim E2E Battery (2026-05-19): 1 row por estacao do run (~10-12 pra
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | BigInteger | | PK | |
| run_id | String(40) | | ForeignKey('sim_run_history.run_id', ondelete='CASCADE'), NOT NULL | |
| station_id | String(50) | | NOT NULL | |
| station_label | String(120) | | NOT NULL | |
| station_order | Integer | | NOT NULL | |
| status | String(10) | | NOT NULL | |
| started_at | DateTime(timezone=True) | | | |
| ended_at | DateTime(timezone=True) | | | |
| elapsed_ms | Integer | | | |
| live_msg | Text | | | |
| error_msg | Text | | | |
| drill_json | JSONB | | | |
Plano de rollout de UMA promoção de anel (pool/stable).
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| ring | String(20) | | NOT NULL | "pool" | "stable" |
| version | String(20) | | NOT NULL | versão-alvo do pin (sem prefixo 'b') |
| mode | String(20) | 'staggered' | NOT NULL | all_at_once | staggered |
| ceiling_min | Integer | 30 | NOT NULL | teto do jitter (minutos) |
| created_at | DateTime | datetime.utcnow | | |
| created_by | String(50) | | | |
| ungated | Boolean | False | NOT NULL | |
| active | Boolean | True | NOT NULL | |
Uma conta dentro de um plano de rollout, com o instante (release_at) em que o
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| plan_id | Integer | | ForeignKey('ring_rollout_plans.id'), NOT NULL | |
| account_id | Integer | | ForeignKey('accounts.id'), NOT NULL | |
| release_at | DateTime | | NOT NULL | |
| applied | Boolean | False | NOT NULL | |
Declaração de "pulei o anel de propósito", com prazo (padrão break-glass). Mover a frota sem
mover o alvo do anel é capacidade pedida (deploy_ea.sh com SEM_ANEL=1); o problema nunca foi
o pulo, foi ele não se declarar — sem isso o alarme de drift grita igual pra "eu pedi" e pra
"mexeram no robô por fora". Append-only: nunca se apaga uma declaração, só se cria outra; a
vigente é a mais recente do anel com expires_at no futuro.
Índice: ix_ring_bypasses_ring_expires (ring, expires_at).
| Coluna | Tipo | Default | Extras | Descricao |
|--------|------|---------|--------|-----------|
| id | Integer | | PK | |
| ring | String(20) | | NOT NULL | "pool" ou "stable" |
| version | String(20) | | NOT NULL | versão que a frota foi levada a rodar |
| declared_by | String(50) | | | quem pediu o pulo |
| declared_at | DateTime | tempo.agora_real | NOT NULL | relógio da APLICAÇÃO, não do banco — o mesmo que mede o prazo do anel |
| expires_at | DateTime | | NOT NULL | sem default em lugar nenhum, de propósito: break-glass sem prazo não deve existir |
| reason | String(300) | | | por que, pra quando o lembrete chegar dias depois |
O que eh: check rapido pra confirmar que o cenario
modify_dw_stress_real
esta funcionando ponta-a-ponta no servidor real (nao em mock).Quando rodar: depois de mexer em
signals.py(handler MODIFY),runner.py
(path do scenario),vea_client.py(simulador do EA), ou no caminho de
near_death/near_targetdaengine.py. Tambem antes de bater o branch
em prod.Tempo: ~20s pra rodar os 2 quadrantes (death + target).
Imagina que duas contas estao no hedge — uma comprando, outra vendendo
o mesmo par. Quando uma delas chega muito perto da morte (saldo prox.
do piso de DD) ou muito perto da meta (saldo perto do target), o
sistema deve ajustar o SL/TP dessa conta automaticamente, sem mexer
no lado oposto.
O cenario forca esse estado de propostito:
Em ambos os casos: o outro lado do hedge nao pode ser tocado (regra
P223 — invariante hedge zero-sum).
Pre-condicoes:
- VPS copytrade rodando + TEST_INJECTION_ENABLED=true no env
- JWT admin valido
- Nenhum outro run SIM em flight
Disparar os 2 quadrantes via Bash:
TOKEN=$(curl -s -X POST https://linniuc.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"..."}' | jq -r .access_token)
for SUB in death target; do
curl -sS -X POST https://linniuc.com/api/sim/scenarios/modify_dw_stress_real \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"subtype\":\"$SUB\",\"auto_cleanup\":true,\"timeout_sec\":180,\"seed\":0}" \
| jq '{status, pass: .result.pass, foco_USD: .result.foco_USD, elapsed_sec}'
done
Resposta esperada (cada chamada):
- HTTP 200 em ~9s
- status: "done", pass: true
- foco_USD entre 54 e 56
| O que checa (leigo) | Esperado | Death (S340) | Target (S340) |
|---|---|---|---|
| Veredicto unico — missao toda OK? | True | True ✅ | True ✅ |
| Perda/ganho do trade-alvo no range (cushion/dist $50 + margin random 4-6) | $54-$56 | $54.90 ✅ | $55.44 ✅ |
| Booleano alvo (cruza com outras regras) | True | True ✅ | True ✅ |
| Outro lado do hedge ficou parado (diff < $5) | True | diff $0.39 ✅ | diff $1.81 ✅ |
| Almofada aleatoria anti-robotica dentro do range pool config | $4-$6 | $5.11 ✅ | $5.53 ✅ |
| HB injection — empurrou saldo forcado nas 2 contas | 2/2 sucessos | 2/2 ✅ | 2/2 ✅ |
MODIFY dispara — gate near_* ativa e handler executa antes do timeout |
<60s | <1s ✅ | <1s ✅ |
| Resposta sincrona antes do nginx fechar (504) | <60s | 8.78s ✅ | 9.05s ✅ |
| Cleanup auto — contas/pool de teste apagadas | 0 residual | 0 ✅ | 0 ✅ |
| Telegram dry_run — alerts em modo ensaio | 2 por run | 2 ✅ | 2 ✅ |
| Sem quebra nos EAs reais da pool 20 pos-deploy | sem novos erros 2h | so 502s transient ✅ | idem ✅ |
pass: false com error: "MODIFY nao disparou em 60s"Significa que o gate near_death/near_target nao ativou. Causas comuns:
WS_HB_DB_INTERVAL=30s esta ativo. Solucao: usar force_http=True_send_hb_now (replica caminho que EA real usa em WS-down)._real_balance ficou stale — virtual_time avancou muito no SimClock_sim_tick apos HB inject nomax_dd diferente de 10% — cushion = balance - dd_floorprop_config da pool SIM.Run levou mais de 60s. Provavel timeout do poll MODIFY (60s wait). Roda em
~9s pos-fix S340. Se passar de 30s, investigar logs do servidor.
pair_id nao criadoEngine recusou o par. Olhar logs do _af_scheduler — provavel margin
validation, swap hour ou trading window (todos devem estar bypassados pela
config SIM).
heartbeat_interval divergente passam despercebidos.**result: se o dict de result ja temX=... explicito alem do **result. Exceptionforce_http=True): WS path temSpec tecnica completa: sim-e2e-real-S281.md (regras R1-R8 do real_path).
Pattern P224 (KnowledgeHub): "VEA deve replicar EA real — comparar
payload MQL5 + thresholds server-side antes de approval de review E2E".
[allow-spec]
O que este guia responde: "quando eu rodo o simulador no modo real, o que ele de fato faz,
passo a passo?" — em linguagem de quem opera, não de quem programa.
O real path é o simulador dirigindo o servidor de verdade (o mesmo agendador, o mesmo
motor de hedge, o mesmo ciclo de vida das contas que rodam em produção) — só que com contas
de mentira marcadas como simulação e um EA virtual (um programinha em Python, o "VEA")
fazendo o papel do EA: confirmar ordens e mandar batimento de saldo. Nada é mock degradado: é
linha real de banco com etiqueta is_simulator=true. Por isso o sim pega bug de servidor que
um teste matemático nunca pegaria — porque é o servidor REAL reagindo.
Analogia: é um ensaio geral de teatro no palco de verdade, com o elenco de verdade e a
peça de verdade — só os atores principais (os EAs) são dublês que seguem o roteiro.
| Peça | O que é |
|---|---|
| Gatilho | POST /api/sim/scenarios/... (só liga com TEST_INJECTION_ENABLED=true). Em produção a flag é false → os endpoints dormem (403/404). |
| Servidor local | python scripts/run_sim_local.py sobe um servidor próprio em :8001 com a flag ligada + Postgres local de teste (copytrade_test:5433). |
| EA virtual (VEA) | server/sim/vea_client.py — conecta no WebSocket real como ea_version="SIM_v1", escuta sinais e responde (ACK + heartbeat). |
| Mesas / contas / pool | linhas reais marcadas: contas is_simulator=true, pool com prefixo SIM_, mesas prop [SIM] .... |
Regra de ouro: a etiqueta is_simulator só serve pra (a) esconder do dashboard real e
(b) limpar depois. Nunca entra na lógica de comportamento — a mesa SIM resolve, pareia e
calcula risco IDÊNTICO a uma real.
[classificar]
│ conta nasce avulsa → escolhe mesa+fase+dono → ganha #N + ciclo de desafio
▼
[criar pool + sentar nas cadeiras]
│ N contas viram cadeiras (AfPoolAccount). Empresas distintas = pode parear (R7)
▼
[subir os VEAs] ── cada conta ganha 1 EA virtual que autentica no WebSocket
▼
┌────────────────── LOOP de rodadas ──────────────────┐
│ batimento de saldo (heartbeat) de todas as contas │
│ ▼ │
│ agendador REAL pareia + abre o par (sinais canônicos)│
│ ▼ │
│ VEA confirma a ordem (ACK) │
│ ▼ │
│ fecha o par zero-sum (um ganha o que o outro perde) │
│ ▼ │
│ ciclo de vida decide: passou de fase? morreu? ───────┼──► [transições]
│ ▼ │
│ recompra: morto/funded → nasce substituto T+1 │
└───────────────────────────────────────────────────────┘
▼
[resumo da run] ── as 22 métricas: passes/mortes/substitutos + taxa de sucesso por
fase, custo por aprovação, velocidade de graduação, comportamento
(limbo/zigzag/ociosidade) e ROI virtual
Mapeando cada estação pra função real do servidor (pra quem quiser cavar):
| Estação | Função real | O que prova | O que pode quebrar |
|---|---|---|---|
| Classificar | sync_classify_cycle (routes/accounts.py) |
#N temporal + account_challenges aberto + nome montado |
#N errado; ciclo órfão; vínculo conta-filha (S385) |
| Criar pool | create_sim_pool (sim/af_tick_real.py) |
pool SIM_* + N contas is_simulator + cadeiras |
capacidade; mesa não resolve por nome |
| Parear | agendador real + pairing (R7) |
nunca parear 2 da mesma Empresa | pareamento trava se poucas Empresas distintas |
| Abrir + ACK | sinais canônicos + VEA | sinal chega, EA virtual confirma | VEA não confirma; sinal perdido |
| Fechar zero-sum | process_trade_result |
P&L do par soma zero | assimetria SL/TP; slippage |
| Passar de fase | check_passes → create_opc3_f2_substitutes (sim/runner.py) |
F1 passa cria conta F2 NOVA — a velha fica passed, NUNCA muta |
promoção que muta conta velha (modelo errado de prop firm) |
| Morrer por DD | check_deaths (af/lifecycle.py) |
saldo < piso da mesa → morta + saldo congelado | morte não reconhecida; $NaN no card (S374) |
| Recompra | recycle intent + apply_recycle_batch |
morto/funded → substituto novo T+1 | substituto não nasce; substituto duplicado |
Em produção a conta morre por DD de duas formas — e desde S378 #8 o sim cobre as duas:
check_deaths): no fim da rodada, se o saldo fechado cruzou o pisopiso = tamanho × (1 − max_dd/100)), a conta é marcada morta e o saldo é congelado.check_deaths). É o que odd_recognition valida._handle_equity_breach, routes/ea_ws.py): o EAequity_floor_breach. É o gatilho REAL e mais rápido em produção (foi a origem do$NaN do S374). Desde S378 #8 o VEA cobre essa via também: novo métodoreport_equity_breach() emite o mesmo frame WS que o EA real. Cenário dedicado:dd_breach_intratrade — sobe VEA conectado, ele emite o frame, o handler real marcadead + congela saldo + grava AuditLog.Os DOIS pisos têm que bater: o que o EA recebe (get_equity_floor, via FK da mesa) ==
o que o motor usa pra declarar morte (_max_dd_val, via snapshot na cadeira). Se divergirem
(snapshot velho vs mesa viva), é exatamente a classe de bug "DD não-reconhecido".
| Endpoint | O que exercita |
|---|---|
POST /api/sim/scenarios/classify_cycle |
Classificação: nascer→#1, reset→#2, vínculo conta-filha S385 (consume-once) |
POST /api/sim/scenarios/dd_recognition |
Gatilho A (via SALDO): reconhecimento de DD por perfil de mesa (8%/10%/5%): pisos batem, mesmo saldo morre na apertada e sobrevive na larga, congela, daily cap por perfil |
POST /api/sim/scenarios/dd_breach_intratrade |
Gatilho B (via EQUITY ao vivo): VEA conectado emite frame equity_floor_breach → handler real marca dead + congela saldo + grava AuditLog (S378 #8) |
POST /api/sim/scenarios/modify_dw_stress_real |
MODIFY perto da morte/alvo (cenário focado de 1 par, E2E real com VEA + agendador) |
O Real Path principal (a run multi-rodada deste mapa, com as 22 métricas no resumo) é
disparado porPOST /api/sim/scenarios/start_real. Histórico e resultado:/real_path/history
e/real_path/report/{run_id}. Os cenários acima são provas focadas de um pedaço só.
Rede de segurança automatizada (rodam no sqlite rápido + Postgres real):
test_dd_recognition_S378.py, test_dd_breach_intratrade_S378.py,
test_classify_cycle_sim_S378.py, test_journey_2step_S378.py
(jornada inteira numa tacada + modos de falha).
# 1. sobe o servidor local com o sim ligado (Postgres copytrade_test migrado)
python scripts/run_sim_local.py # uvicorn :8001 + VEA aponta de volta pra :8001
# 2. dispara uma estação (token admin no servidor local)
curl -sS -X POST http://127.0.0.1:8001/api/sim/scenarios/dd_recognition \
-H "Authorization: Bearer $TOKEN" | jq '{all_passed, cross_profile}'
Pré-requisito do ambiente: o Postgres local copytrade_test precisa estar com as migrações
em dia (ex: S382 colunas de estilo de DD/news, S385 vínculo conta-filha) — senão qualquer query
de mesa quebra. Produção já tem; o local de dev pode ficar pra trás. Aplicar as migrações faltantes
de server/migrations/ antes de rodar.
source=test nunca toca a pool real (etiquetas + filtros).auto_cleanup=true (default) apaga pool/contas/sinais ao fim; manual viaDELETE /api/sim/cleanup. Cada cenário limpa suas fixtures por faixas de ID reservadas.SimRun.summary_json). A limpeza só remove o "paciente" (pool/contas/sinais de teste), nunca.git do projeto vive no Dropbox e embaralha sob sync de outragit worktree NÃO isola disso (só os arquivos). Pra trabalho paralelo de verdade, usargit clone fora do Dropbox.SSoT técnica: specs/sim-e2e-real-S281.md (endpoints + padrões anti-flake) +
specs/sim-cockpit-validacao-completa-S378.md (cockpit S378). Este guia é o mapa leigo do fluxo.
Status: ACTIVE | Ultima revisao: S348+1 (2026-05-15) | Documento vivo — atualizar conforme novas features entram.
O CopyTrade tem 2 camadas de logica:
Cada feature/funcao vive em uma das 3 categorias: so EA, so Site, ou dupla camada (defesa em profundidade — EA + Site cobrindo a mesma coisa, com responsabilidades complementares).
A regra geral: se um bug naquela camada pode causar PERDA REAL ou VAZAMENTO PRA PROD, criar dupla camada. Senao, single-layer eh aceitavel.
Total: 25 estacoes, ~115s tipico. Distribuicao por camada: EA-only 4, SITE-only 9, DUPLA 12.
| # | Estacao | Camada | Onde vive | Vale dupla camada? | Por que |
|---|---|---|---|---|---|
| 1 | HB shape | DUPLA | EA envia HB, Site recebe + valida campos | ja eh | EA reporta saude, Site consolida e alerta se stale |
| 2 | Sandbox limpa pre-flight | DUPLA | Site dispatch CLOSE, EA executa | ja eh | Limpeza so funciona com ambos |
| 3 | Olheiro SL/TP | SITE | Validacao SL/TP no servidor | nao | Dominio do servidor. EA so executa |
| 4 | Run-all OPEN/MODIFY/CLOSE | DUPLA | Site cria signal, EA executa via GUI | ja eh | Cadeia inteira do ciclo de vida |
| 5 | Invert (BUY→SELL + audit + SL/TP trocados) | DUPLA | Site insere signal_inversions, EA aplica GUIExecution_ApplyInversion |
ja eh | Audit no Site, execucao no EA |
| 6 | Symbol resolve | EA | SymbolResolver.mqh |
nao | Broker eh dominio do EA |
| 7 | Buffers SL/TP multi-asset | EA | _apply_buffers() no EA |
nao | Calculo proximo do tick |
| 8 | Mini-stress 3 contas | DUPLA | Site spawn_stress_pool, EAs executam paralelo |
ja eh | Pool + broadcast + execucao em massa |
| 9 | 6 cenarios de erro (M1 SWAP: antes de WS kill) | DUPLA | volume_inv valida no Site; margin/sym_fake erra no EA | ja eh | Cada preset testa camada diferente |
| 10 | WS fallback HTTP poll | DUPLA | EA reconecta canal, Site responde HTTP | ja eh | Canal eh propriedade compartilhada |
| 11 | Reconcile pos-restart EA (opt-in) | DUPLA | Site mantem fila, EA puxa pos-reboot | ja eh | Fila no Site, replay no EA |
| 12 | Equity floor breach + reset | DUPLA | _af_runtime_validator no Site + EquityFloor_Check no EA |
ja eh | EA defende mesmo offline; Site valida + reseta |
| 13 | Auto-update simulado | DUPLA | Site PATCH desired_version, EA detecta + reporta |
ja eh | Site agenda, EA executa |
| 14 | Telemetria 10 campos | DUPLA | Site coleta timeline, EA emite eventos | ja eh | Eventos no EA, agregacao no Site |
| 15 | Rollover diario antes do swap | DUPLA (ja aprovada em specs/rollover-dupla-camada.md) |
EA primary + Site fallback | ja eh | Critico: se Site cair entre T-60 e T-30, EA fecha sozinho |
| 16 | Isolamento de grupo | SITE | Filtragem target_group_id via API key da conta |
nao (recalibrado) | Single-layer forte. Dupla seria nice-to-have, nao urgente |
| 17 | Cap de lote (max_lots) | SITE | Servidor valida lots antes de despachar | nao (recalibrado, ver TODO) | Verificar se servidor ja valida. Se nao, dupla camada vale |
| 18 | Validacao payload (timestamp futuro, JSON quebrado, null bytes) | SITE | Pydantic + sanitizacao explicita | nao | EA recebe estrutura ja sanitizada |
| 19 | MODIFY queue overflow (>4) | EA | MOD_VERIFY_QUEUE_SIZE=4 no EA |
nao | Fila eh design interno do EA |
| 20 | Race OPEN+CLOSE 50ms | DUPLA | Lifecycle state machine no Site + g_busy no EA |
ja eh | Site sequencia, EA respeita flag |
| 21 | Replay ACK antigo | DUPLA | idempotency_unique index (server) + g_processedSignalIds[100] ring buffer (EA, LinniuC.mq5:162) |
ja eh (auditado S354) | Dedup signal_id 2 camadas. Replay >100 sinais atras: so server pega |
| 22 | Signal source=test em conta prod | SITE | R3 do sim-e2e: signal_source='test' NUNCA em conta is_test=false |
nao (recalibrado) | Single-layer suficiente. Dupla nice-to-have |
| 23 | Cron health pre-flight (NOVO) | SITE | 5 crons rodaram <24h? Disco OK? | nao | Pura infra servidor. Executável desde #257 (S354): 7 crons gravam liveness marker /var/log/copytrade-cron-*-last-success.txt; GET /api/health/cron-status lê. Antes era WARN permanente. |
| 24 | AF runtime pre-flight (NOVO) | SITE | Sem round stuck >30min, pending <50, par offline OK | nao | Estado AF eh servidor |
| 25 | OnTradeTransaction crash test (NOVO) | EA | positionId=0 fake: EA nao crasha, reporta erro |
nao | Robustez interna EA |
| Onde | O que valida | Camada |
|---|---|---|
| E1 HB shape | ea_version bate com server_version (/api/health) — detecta deploy mismatch |
DUPLA |
| E4 run-all | Ticket aparece em _ticketListPanel (capitulo C1 multi-ticket selector) |
EA |
| E4 run-all | signal_lifecycle transicao PENDING→DISPATCHED→ACKED→RESOLVED em ordem |
SITE |
| E4 run-all | ACK retornou com target_account_id=3 (defesa anti-vazamento) |
DUPLA |
| E4 run-all | Fila MODIFY verify 5s pos-MODIFY foi processada (g_modVerifyQueue) |
EA |
| E4 run-all | _af_runtime_validator confirma estado consistente pos-trade (detecta EA inert 15.6/15.9) |
DUPLA |
| E5 invert | SL e TP trocados de fato no broker (nao so direction) — pos-G11 auditoria EA | EA |
| E12 equity floor | Pos-breach + restore, EA volta a operar (floor_reset funcionou) | EA · forçar breach com segurança = spec ea-tester-e12-e15-force-breach-S354.md (F11, aguarda execução) |
| E14 telemetria | WS push reload UI tabela <2s | SITE |
| Pre-flight global | localStorage.ct_token valido + >5min restante; is_test=true na conta 3 |
SITE |
signal_dispatch.py passa volume direto;af/signals.py:76 calculate_volume tem piso max(lots,0.01) mas SEMrisk_usd jaaf/validator.py:333+ e af/audit.py:180+min(lots, MAX_LOT) em calculate_volume como cinto-e-risk_usd. Registrado, nao bloqueia.calculate_volume agoravolume_max opcional e aplica min(lots, MAX_LOT) — MAX_LOT =volume_max do broker quando valido, senao MAX_LOT_FALLBACK=100.0sym_info.get("volume_max"). Teste-primeiro: test_calculate_volume_cap.pyg_processedAcks mas o real (e correto) eh dedup deLinniuC.mq5:162-164 g_processedSignalIds[100] ring buffer +LNC_IsSignalProcessed/MarkSignalProcessed (:425-443), aplicado no:800-801) e no poll (:1305-1309). Server idempotency_uniqueNova feature precisa rodar:
├── So no MT5 (broker, GUI, simbolo)? → EA-only
├── So no servidor (orquestracao, AF, audit, lifecycle, telegram)? → Site-only
├── Ambos (signal, ACK, settings sync)? → Dupla camada NATURAL
└── Tem risco de PERDA REAL ou VAZAMENTO se 1 camada falhar? → Dupla camada OBRIGATORIA
Lista de criterios pra dupla camada obrigatoria:
Esta aba /guide#camadas-ea-vs-site eh fonte da verdade do mapa de camadas. Sempre que adicionar/remover feature da bateria de validacao, ou descobrir que algo precisa virar dupla camada, atualizar:
specs/camadas-ea-vs-site.md (este arquivo)bash scripts/guide-update-and-verify.sh camadas-ea-vs-site — script automatiza:/guide pra confirmar render OKspecs/rollover-dupla-camada.md — modelo de dupla camada ja implementadospecs/ea-tester-bateria-completa.md — bateria 22 estacoes "Validar completo"specs/ea-tester-adversarial-completo.md — 42 vetores adversariais que motivaram E16/E17/E22specs/sim-e2e-real-S281.md — Simulador E2E (camada irma que valida o cerebro do servidor)Derivado automaticamente das fases de cada
specs/*-PLAN.md. Sem marcacao manual. Estado vem de git + ledgers de review/validate + todos. Fingerprint de conteudo:5be4fe43.
Legenda: · Planejado · ~ Em obra · + Construido · R Revisado · ✓ Provado · 🔴 Bloqueado (flag ortogonal)
| Fase | Estado | Descricao |
|---|---|---|
| F1 | + Construido | Trace emitter (per-brainstorm jsonl + model_version) |
| F2 | + Construido | Emissores forte (0f/1/4/5/6/7b) + fraco (2/3) |
| F3 | + Construido | Conformance check PreToolUse /plan (tier-aware + model move) |
| F4 | + Construido | Retry cap + fail-safe + SessionStart cleanup |
| F5 | + Construido | Wiring settings.json + dogfood S349 + teste negativo |
| F6 | + Construido | Post-impl: code-review LOOP P228 + validate |
| Fase | Estado | Descricao |
|---|---|---|
| F1 | R Revisado | RED tests T1-T4 |
| F2 | R Revisado | E1-E4 (HB, cleanup, Olheiro, run-all) |
| F3 | R Revisado | E5-E7 (invert, symbol, buffers) + SA-3 |
| F4 | R Revisado | E9 6 cenarios erro + E10 WS fallback |
| F5 | R Revisado | E12 equity_floor + E13 auto-update |
| F6 | R Revisado | E14 + reconcile + cancel/beforeunload |
| F7 | R Revisado | toggles + Telegram opt-in + P228 LOOP |
| F8 | + Construido | Multi-aba lock BroadcastChannel + polish |
| F9 | + Construido | E15 Rollover + E22 prod + SA-1..SA-4 |
| F10 | ~ Em obra | E16-E21 + SA-5..SA-8 |
| F11 | ~ Em obra | E23-E25 + 3 endpoints servidor + SA-9/SA-10 |
| Fase | Estado | Descricao |
|---|---|---|
| F1 | ✓ Provado | Contract parser |
| F2 | ✓ Provado | Git-derived states |
| F3 | ✓ Provado | Review marker reader |
| F4 | ✓ Provado | Validate-ledger reader |
| F5 | ✓ Provado | Blocked flag from todos |
| F6 | ✓ Provado | Aggregator + cache + CLI |
| F7 | ✓ Provado | Contract migration + reality check |
| F8 | R Revisado | Publish /guide (P243-aware) |
| F9 | ✓ Provado | 4 hooks wiring |
| F10 | ~ Em obra | Post-impl: code-review LOOP + validate |
Status: ACTIVE | Criado: S362 (2026-05-22) | Atualizado: S364 (#E6 reset unificado + #297 piso diário) | Documento vivo — atualizar quando uma camada mudar de lado.
Rodar a pool tem 4 camadas. Pensa num restaurante:
O simulador eh uma cozinha de treino: usa o MESMO chef (receita identica),
mas NAO tem gerente (roda os pratos que voce pedir, sem horario nem limite) e usa
um cozinheiro de mentira (EA-fantasma / VEA). A "conta do dia" (reset) ja foi unificada
no treino canonico (S363 #E6) — ele agora le a mesma folha do restaurante; so os treinos
antigos (matematico / validator) ainda usam papelzinho separado.
Principio que decide compartilhar vs fingir:
- E uma DECISAO/REGRA de negocio (parear, risco, direcao, morte/pass, cap de DD,
quando zerar o contador)? -> logica unica, compartilhada.
- E uma ENTRADA DO MUNDO (heartbeat, saldo do broker, relogio, execucao no MT5)?
-> fingida no simulador de proposito (rapido + deterministico + sem efeito real).
| # | Camada | O que faz | Pool real (producao) | Simulador | Compartilha? |
|---|---|---|---|---|---|
| 1 | Gerente (agendador) | Decide QUANDO abrir batalha: mercado aberto? teto de rodadas/dia? intervalo passou? ja tem uma rolando? | _run_af_scheduler (main.py) + check_daily_round_limit (constants.py) |
NAO tem — roda N rodadas direto (rounds=N) |
NAO — so producao. Sim substitui pelo proprio loop |
| 2 | Chef (engine) | Quem joga contra quem, quanto apostar, pra que lado, quem morreu/passou, quanto ainda pode perder no dia + se a migalha do dia nao vale trade (piso #297) | server/af/engine.py (pair_accounts, calc_risk, _choose_direction, find_dead/passed, _daily_dd_remaining, piso min_daily_risk_pct em _individual_risk) |
usa os MESMOS (sim passa daily_floor_active) |
SIM — fonte unica |
| 3 | Cozinheiro (execucao) | Faz a ordem acontecer + espera "fiz" + atualiza saldo | EA real (GUI Win32 no MT5) | EA-fantasma (VEA) ou so anota o resultado (random walk) | NAO — fingido de proposito |
| 4 | Conta do dia (reset) | Zera a perda acumulada do dia no horario de virada (swap) | reset_daily_pnl (lifecycle.py) + helpers (constants.py) |
canonico (runner) consome o MESMO reset_daily_pnl (S363 #E6); math (harness.py) / validator.py / af_tick_real.py mantem reset proprio |
SIM no canonico (S363) — math/validator fora de escopo |
| Entrada | Pool real | Simulador finge como |
|---|---|---|
| "A conta esta viva agora?" (heartbeat) | pinga o EA e espera resposta (fail-closed) | conta-fantasma sempre responde / pula o ping |
| Saldo real no broker | le do que a conta reporta (sync_real_balances) |
saldo inventado do resultado da batalha |
| Abrir ordem no MetaTrader | manda o robo clicar (GUI Win32) | so registra "fiz" (VEA) ou calcula resultado direto |
| Que horas sao / mercado aberto | relogio real + is_market_open / janela de swap |
relogio virtual (SimClock) — roda quando quiser |
| Quando comecar a proxima batalha | espera intervalo + checa teto + se ja tem uma rolando | dispara em sequencia, sem esperar |
Por que o teto de rodadas/dia (
max_rounds_per_day) nao vale no sim: ele mora na
camada 1 (gerente), nao na camada 2 (chef). O simulador nao usa gerente, entao nunca
le esse teto — independente de ter valor ou nao na config. So producao enforca.
reset_daily_pnl (com o toggle do #290 — reseta 1x/dia no swaprunner._create_round_and_wait)reset_daily_pnl de producao, com o relogio virtual (SimClock) comonow — abordagem "consumo, nao copia". Fecha o gap E6: o sim canonico ja reproduz fielmentespecs/unify-daily-reset-rule-S363.md.harness.py matematico,validator.py fuzz, af_tick_real.py deprecated) mantem reset proprio — testam matematica_daily_dd_remaining) cair abaixo demin_daily_risk_pct (default 0.5% do tamanho da conta), a conta abstem (descansa) —_individual_risk (engine) → o sim consome igual (passa o booleanodaily_floor_active). Regra unica: reusa a decisao do reset_daily_pnlfloor_active = temporal_rules_on(pool) and not did_reset). Dormente ate ligar o toggle./guide#piso-risco-dia. Spec: specs/min-daily-risk-floor-S364.md.specs/temporal-rules-toggle-per-pool-S362.md (#290),specs/unify-daily-reset-rule-S363.md (#E6).Chef (engine) = compartilhado (inclui o piso de risco diario #297). Gerente (agendador +
teto de rodadas) e cozinheiro (execucao) = so producao, o sim substitui. Reset do dia: regra
unica ENTREGUE no sim canonico (S363 #E6) — math/validator ainda separados de proposito.
Esta aba /guide#camadas-pool-vs-simulador eh referencia rapida das camadas. Quando uma
peca mudar de lado (ex: a regra unica do reset for implementada), atualizar:
specs/camadas-pool-vs-simulador.md (este arquivo)bash scripts/guide-update-and-verify.sh camadas-pool-vs-simulador (commit + push + verify render)specs/temporal-rules-toggle-per-pool-S362.md — #290 toggle de regras temporais (camada 4)specs/unify-daily-reset-rule-S363.md — #E6/#296 regra unica do reset (sim canonico consome prod)specs/min-daily-risk-floor-S364.md + /guide#piso-risco-dia — #297 piso de risco minimo diario (camada 2)specs/camadas-ea-vs-site.md — outra divisao (EA vs Servidor)specs/af-rules-whitepaper.md — as regras do chef (engine)specs/sim-e2e-real-S281.md — o simulador E2E real (VEA)Status: ATIVO (dormente até o "Daily DD" ser ligado na pool) | Criado: S364 (2026-05-23) | #297
Cada conta tem um tanque de gasolina do dia: quanto ela ainda pode perder hoje antes de
estourar o limite diário da mesa (o "Daily DD"). Cada mesa enche esse tanque com um tamanho
diferente (umas 5%, outras 3%, outras 4% do valor da conta).
O Piso de Risco Diário é a marca de "reserva" no tanque: quando a gasolina do dia cai
abaixo dessa marca, a conta encosta o carro (não abre mais trade) e descansa até amanhã,
quando o tanque enche de novo.
Por que existe: sem o piso, quando sobra muito pouco no tanque, o sistema ainda tentaria
abrir o menor trade possível (0,01 lote) — e esse trade mínimo pode custar MAIS que o que
sobrou, furando o próprio limite diário que deveria respeitar. O piso para antes disso.
No modal Config da pool (aba AF Hedge), ao lado do toggle "Daily DD", tem
o campo "Piso Risco/Dia (%)":
"Não vale abrir o 2º trade do dia se o que a conta ainda pode arriscar hoje virou migalha
(< 0,5%) — ela descansa e tenta amanhã com o tanque cheio."
Status: ACTIVE | Criado S371 (2026-05-23) | Revisado S423 (2026-06-21) — blindagem das 3 ações destrutivas (verificar-antes-de-apertar) na frota, EA
3.123.1| Fonte: GUIExecution.mqh + GuardLimits.mqh + telemetria signal_events
O EA não usa API de trade (OrderSend/CTrade). Ele age como um humano clicando na
janela do MT5 — abre o diálogo de ordem (tecla F9), digita volume/SL/TP e clica
Comprar/Vender/Fechar, via automação Win32 (user32.dll). Motivo: mesas prop
detectam EA via API; cliques "parecem manuais". Toda ação passa por uma trava
única (GUI lock) — só uma ação por vez, nunca dois cliques concorrentes.
Antes a regra era "aperta e torce" — o EA clicava a ação destrutiva e esperava dar certo.
Agora toda ação que mexe em dinheiro CONFERE o estado da tela antes de apertar e DESISTE
com motivo claro se o que ele vê não bate com o pedido. Nunca mais "paga pra ver".
Vale pras 3 ações:
- Abrir: se a janela não está no estado certo de abrir (ex: o botão de 1-clique do Livro
de Ofertas, que abriria um lote pelado errado), recusa em vez de arriscar.
- Modificar (stop/alvo): lê o campo de volta ANTES de clicar "Modificar". Se o valor
reverteu, ficou vazio, ou diverge do pedido → aborta (não grava stop errado em silêncio).
Confere de novo depois do clique (tolerância por pip).
- Fechar 1 perna do hedge: casa o número EXATO do ticket no botão certo + confere de novo
antes do clique + exige que caia EXATAMENTE uma posição (delta −1). Se fecharia a perna
errada ou as duas (close-by), aborta; se caiu número errado, marca FALHA.
O motivo SEMPRE aparece: quando recusa, o EA manda o motivo nos 2 canais — o técnico (log) e
o leigo (traduzido pro seu Telegram, sino e modal do card). Você nunca fica sem saber POR QUE
uma ação foi barrada. Hoje há 6 motivos de recusa traduzidos (stop reverteu, campo vazio, perna
errada, fecharia as duas, número não bateu, Livro de Ofertas).
Os abortos não estão todos no mesmo ponto — formam 4 camadas, cada uma conferindo algo
diferente, da mais cedo pra mais tarde. A diferença-chave: as primeiras barram sem nem abrir
a janela de ordem; só a Camada 2 (a blindagem) precisa abrir a janela pra LER a tela.
| Camada | Quando | O que confere | Exemplos de recusa |
|---|---|---|---|
| 0 — Servidor | antes de chegar no robô | preço confiável + mercado aberto | preço velho/sem referência, mercado fechado |
| 1 — Pré-voo | robô recebeu o sinal, janela AINDA fechada | regra da mesa (sabe sem olhar a tela) | lote acima do teto, posições simultâneas demais, rodadas/dia demais, ordem duplicada → nem abre a janela (0 cliques) |
| 2 — Na tela (a blindagem) | janela F9 aberta, lendo o diálogo | a tela bate com o pedido? | botão do Livro de Ofertas, campo de stop vazio/revertido, botão de fechar da perna certa |
| 3 — Pós-clique | depois de apertar | o resultado bateu? | não abriu exatamente +1, não caiu exatamente −1, popup de erro do broker |
A diferença que importa: a Camada 1 barra pela regra — o robô já sabe o lote e quantas
posições tem sem olhar nada, então recusa antes de mexer na janela. A Camada 2 (a blindagem
nova) barra pela realidade da tela — só dá pra conferir DEPOIS que a janela abriu e ela lê os
campos. As duas se somam (defesa em profundidade): uma pega o que é proibido pela regra, a outra
pega o que a tela mostrar de errado na hora. A tabela detalhada de cada trava (com o motivo leigo)
vem logo abaixo, em "Travas de segurança".
| Modo | Input do EA | O que faz | Em produção (pool) |
|---|---|---|---|
| Visual | InpVisualMode |
Traz a janelinha pra frente + pausa 400ms antes de cada clique, pra um humano VER o EA operando ao vivo | OFF (ninguém olhando) → sem pausa, mais rápido |
| Log detalhado | InpDebugLog / coluna accounts.debug_log (server-push, S371) |
Grava o passo-a-passo [GUI-TRACE] de cada etapa |
OFF (senão enche o log) — liga por conta pelo painel só pra debug |
| Debug GUI | InpGUIDebug |
Mostra as tripas do diálogo (combo, leitura de campos, settle, readback) | OFF — só investigação pesada |
O debug_log agora é ligável por conta direto pelo painel (sem mexer no gráfico
do PC daquela conta): PUT /api/accounts/{id}/settings {"debug_log": true} → o EA
aplica em runtime no próximo sync (P310).
A Exness mostra um aviso de alavancagem antes de cada trade. O EA lê o texto do
aviso (varre os campos de texto da janelinha) e decide:
- Texto de aviso normal ("alavancagem", "risco", "confirma") → confirma e segue.
- Texto de ERRO ("saldo insuficiente", "rejeitado", "off quotes", "inválido") →
aborta e marca a ordem como FALHA — não finge que deu certo.
| Ação | Cliques de trade | Etapas principais | Tempo típico |
|---|---|---|---|
| Abrir | 1 (Comprar OU Vender) | F9 → selecionar símbolo → sub-diálogo → setar volume → SL → TP → clicar direção → confirmar popup | ~1,0–1,3s |
| Modificar | 2 (abrir painel "Modificar a Posição" + Modificar) | abrir diálogo da posição → painel modificar → setar SL/TP → Modificar → confirmar | ~0,5s |
| Fechar | 1 (Fechar #ticket… a mercado) | abrir diálogo da posição (duplo-clique na linha) → clicar Fechar do ticket certo → confirmar popup → detectar fechamento | ~0,7s |
As esperas são event-driven (sai assim que o resultado chega; marca 0ms quando
rápido). A seleção de símbolo dá 0ms quando já está no símbolo certo (não redigita à
toa). O maior pedaço do Abrir é confirmar o popup do broker + fechar a janelinha
(~400ms + ~250ms). Nenhum clique de trade redundante.
| Trava | Ação | Quando dispara | Resultado (+ motivo leigo) |
|---|---|---|---|
| Erro do broker | todas | Popup com texto de erro (rejeitado/insuficiente/off quotes) | Aborta + ACK FALHA (não finge sucesso) |
| Volume não entra | Abrir | Campo de volume diverge do alvo após retries | ABORTA com ZERO ordens (nunca abre com tamanho errado) |
| Livro de Ofertas (S423) | Abrir | O botão clicável seria o do Livro de Ofertas (1-clique, lote pelado) | Aborta — "abertura recusada (botão errado da janela)" |
| Delta de posição | Abrir | Após abrir, não apareceu EXATAMENTE +1 posição (veio 0 ou 2) | ACK FALHA (pega anomalia / posição manual concorrente) |
| Readback do stop (S423) | Modificar | O campo de SL/TP reverteu, ficou vazio, ou diverge do pedido ANTES de clicar | Aborta sem clicar — "stop reverteu" / "campo vazio"; posição segue com o stop anterior |
| Botão certo por ticket (S371+S423) | Fechar | Casa o botão Fechar pelo número EXATO do ticket (match ancorado, não pedaço) | Nunca fecha a perna oposta por engano |
| Fecharia as duas (S423) | Fechar | O botão clicável fecharia as DUAS pernas do hedge (close-by) | Aborta — "fecharia as duas pernas"; nada foi fechado |
| Delta −1 (S423) | Fechar | Após fechar, não caiu EXATAMENTE uma posição | ACK FALHA — "número de posições não caiu uma" (marca pra conferência) |
| GUI lock | todas | Sempre | Uma ação GUI por vez; auto-libera se travar >5min (sentinela) |
Os motivos viram texto leigo no seu Telegram + sino + modal do card (6 motivos
traduzidos). O motivo técnico cru NUNCA aparece pra você — sempre a versão em português claro.
Onde a correção do PAR mora: quem fica comprado vs vendido é decidido e blindado
no servidor (motor AF + hedge guards), NÃO no EA — o EA só executa o sinal que
chega. Não há trava EA-side de "par errado" porque o EA não conhece o par.
| Aspecto | Status | Nota |
|---|---|---|
| Blindagem 3 ações (verificar-antes-de-apertar) | ✅ Na frota S423 | Abrir/Modificar/Fechar conferem a tela e abortam com motivo leigo; EA 3.123.1 nas 13 contas |
| Precisão de cliques | ✅ Preciso | 1–2 cliques de trade por ação, sem redundância |
| Performance | ✅ Otimizado | Esperas event-driven; OPEN ~1s, MODIFY ~0,5s, CLOSE ~0,7s |
| Fechar ticket correto | ✅ Corrigido S371 | Bug "fechar 1 fechava as 2 opostas" resolvido (casar botão por ticket). Ver ea-gui-close-opposite-double-INVESTIGATION.md |
| Telemetria | ✅ Refinada | ms por etapa em signal_events (gui_s1..s12 / m1..m6 / c1..c3) + dump de controles (GUI_DumpButtons, debug-gated) |
| Detecção de erro do broker | ✅ Ativa | Lê popup, distingue erro de confirmação |
| Rede 2-opostas (server-side) | ✅ Ativa S371 | Alerta Telegram se uma conta tiver BUY+SELL mesmo símbolo abertos (pré-condição do bug raro) |
Validação ao vivo S423 (sandbox id=3, b3.123.1): E2E em 2 passadas — fechar 1 perna do
hedge deixou a outra de pé nas duas (pernas opostas: 1ª fechou a comprada, 2ª a vendida =
delta −1 confirmado); ajuste de stop aceito sem falso-aborto; 6/6 motivos de recusa
traduzidos pra leigo no servidor. Rollout fleet COMPLETO (deploy_ea.sh, 13/13 contas
em3.123.1, 0 erros). Antecedente: bug "fechar 1 fechava as 2 opostas" resolvido em S371,
agora endurecido (match ancorado por ticket) e validado na frota inteira.
Quando uma rodada começa, algumas contas podem ficar de fora — por exemplo o
computador de um dos PCs caiu e os EAs ficaram offline. A rodada abre com menos
duplas do que poderia. O Raio-X da rodada mostra quem ficou de fora e por quê;
o Rechamar Contas deixa você trazer de volta quem voltou, com 1 clique, sem
mexer nas batalhas que já estão rolando.
Analogia: o ônibus de alguns jogadores quebrou e a partida começou com poucas
duplas. Quando o ônibus chega, aparece um aviso "os atrasados chegaram — quer
incluir?". Você clica e o sistema monta duplas novas na mesma partida.
No card da rodada (aba AF Hedge) aparece a faixa "Raio-X da rodada · X de Y
em batalha · Z de fora". Abrindo, você vê cada conta de fora com o motivo em
linguagem clara e um ícone do "balde":
O servidor fica de olho: quando 2 ou mais contas transitórias voltam a dar
sinal de vida (ou fecham a posição que tinham aberta) e dá pra formar pelo menos
uma dupla de empresas diferentes, aparece a faixa "🔄 N conta(s) voltaram"
com o botão Rechamar N contas. Você também recebe o aviso na hora (notificação
instantânea no painel).
Só aparece enquanto a rodada está em andamento. Rodada encerrada não rechama.
Ao clicar, o sistema:
Toda dupla criada por rechamada ganha o selo 🔄 RECHAMADO no card. É
transparência: ela entrou depois do início da rodada, então o lucro/prejuízo
dela pode ser diferente das duplas que abriram no começo.
Status: ATIVO (no ar desde EA 3.101.0) | Criado: S392 (2026-05-30) | #352
Antes de abrir um par de hedge, o sistema precisa de um preço de referência pra colocar os
freios (stop loss e take profit) no lugar certo. Se esse preço estiver velho, travado ou
esquisito, abrir o par é como estacionar de olhos vendados — os freios saem tortos e a
corretora recusa calada. Foi o que aconteceu em 29/05: 6 contas tentaram abrir num preço
congelado e falharam todas.
Agora o sistema confere o preço antes e, se não confia, não abre e te avisa o motivo
num selo no card da batalha — em vez de abrir torto.
No card do par, quando ele está esperando em vez de abrir, aparece um selo com o motivo em
português (passe o mouse pra ler a explicação completa no balãozinho):
Nunca aparece código interno no selo — só português claro.
Se passar em tudo: abre normal, idêntico ao de sempre (zero diferença no dia bom). Só barra
quando o preço é ruim.
O selo só tira aberturas ruins — nunca adia uma boa sem motivo. Num hedge, abrir um lado com
preço errado quebra o espelho e gera perda que o outro lado não compensa. Recusar protege as
duas pernas igualmente.
"Se o preço pra abrir o par não é confiável, o sistema não abre, espera o próximo bom, e te diz
o porquê no card — em vez de abrir torto e a corretora recusar calada."
Status: ATIVO (no ar desde EA 3.103.0) | Criado: S392 (2026-05-30)
Toda madrugada a corretora "vira o dia" e cobra um juro de rolagem (swap) de quem ficou com
posição aberta. Pra evitar essa cobrança (e o risco de o preço dar um salto na virada), o
sistema fecha os pares de hedge um pouco antes dessa hora.
A novidade (build 3.103): o robô agora sabe sozinho que horas são essas — ele calcula pelo
padrão de Nova York, ajustando o horário de verão automaticamente. Então mesmo se o site
cair, o robô fecha na hora certa por conta própria.
Importante: isso não muda stop loss, take profit, direção ou tamanho da ordem. Só afeta
quando fechar pela virada de juros (e passa pela trava de hedge — fecha as duas pernas
juntas).
"O robô fecha o par antes da cobrança de juro da madrugada, sabendo a hora sozinho mesmo com o
site fora — e sem nunca matar um par recém-aberto por engano."
Status: ATIVO (no ar desde EA 3.99.0 / servidor S388) | Criado: S392 (2026-05-30) | Admin
O painel ao vivo só guarda as últimas 15 linhas do que o robô faz. Numa rajada (abrir um par
= ~12 cliques, fechar = ~4) isso estoura na hora e some antes de você ver. Quando um problema
já aconteceu, esse resumo curto não conta a história toda.
Agora dá pra pedir o diário completo do dia (o log inteiro do MT5) de qualquer conta —
até das máquinas remotas — pra investigar com calma depois.
Funciona pela mesma conexão robô↔servidor que já existe, então alcança as contas dos outros PCs
também.
Quando um par fez algo estranho e o painel ao vivo já perdeu as linhas — ex: um par que fechou em
segundos, uma ordem que falhou, um clique que não saiu. O diário completo tem tudo, sem janela e
sem perda por timing.
"Quando o painel ao vivo já apagou as linhas, peça o diário completo do dia daquela conta (até
das remotas) e investigue o que aconteceu com calma."
Quando uma conta para de dar sinal de vida (PC desligou, internet caiu, MetaTrader fechou), o sistema nunca fecha a ordem dela só por estar offline. As ordens de proteção (alvo e stop) ficam guardadas no broker e funcionam sozinhas, mesmo com o robô desligado. Esta página explica, em ordem, o que o sistema faz em cada momento.
Uma ordem só fecha pelos motivos normais do hedge:
Estar offline não está nessa lista. Conta no escuro = a ordem dela rola até o alvo/stop dela. Cruzar a virada de juros (swap) nesse estado é um custo já previsto e aceito.
Antes da batalha abrir, conta offline: o sistema confere o sinal de vida das duas contas antes de abrir. Sem sinal, não abre — adia a batalha e tenta de novo quando a conta voltar.
Batalha rodando, conta cai: nada fecha. A ordem segue protegida pelo alvo/stop no broker. No card aparece o aviso de "conta offline há X" e, se você quiser não esperar, um botão para encerrar a batalha no papel (isso não fecha a ordem — só destrava o sistema).
A batalha decidiu (um lado fechou) com a outra conta offline: o sistema guarda o recado de fechar por até 24 horas. Quando a conta voltar, ela fecha. O card mostra o resultado daquele lado como "≈ Último valor registrado (há X+)" em laranja — esse número é o último que a conta reportou ANTES de cair, não o fechamento real. O "+" quer dizer "pelo menos esse tempo" (o congelamento foi antes do fim da batalha).
Sempre que você ver o ≈ laranja num card, leia assim: "esse valor pode estar desatualizado — a conta estava offline e o número real do broker ainda não chegou". O Saldo e o Equity da conta no card também ganham um selo de "congelado há X" quando a conta está sem dar sinal.
Esses avisos somem sozinhos quando a conta volta: o sistema busca o valor verdadeiro do broker e troca o número estimado pelo real. Você não precisa fazer nada.
Duas formas, automáticas, nesta ordem:
Em qualquer dúvida (saldo velho, outra ordem mexeu na conta no meio, valor fora do esperado), o sistema NÃO chuta: mantém o ≈ laranja e o aviso, porque mostrar "ainda não sei o real" é mais honesto que inventar um número.
Conta offline muda como o sistema espera e registra — nunca muda se uma ordem fecha. Fechar só acontece pelos motivos normais do hedge, e o número estimado vira o real sozinho quando a conta volta.
Quando o robô coloca uma ordem pela tela do MT5 (pra parecer manual e não ser detectado por prop
firm), ele precisa saber com certeza o desfecho: a ordem abriu, foi recusada (e por quê),
ou ainda está no ar. Isso é vital pro hedge — se uma perna do par abre e a outra é recusada sem o
robô perceber, sobra uma perna nua (sozinha, sem proteção), o pior cenário numa conta de prop firm.
Este guia explica, sem jargão, como o robô descobre isso e por que o desenho é robusto.
Uma ordem pode ser recusada em dois lugares diferentes — e o lugar (não o motivo) decide quem
detecta a recusa:
Posto 1 — o terminal (seu PC): o MT5 confere a ordem localmente, antes de enviar. Se reprova
aqui, mostra o motivo no diálogo F9 na hora. São recusas que o terminal já sabe de cabeça:
| Recusa do Posto 1 (local) | Por que é local |
|---|---|
| Sem dinheiro (margem) | o terminal sabe seu saldo |
| Lote inválido (abaixo do mínimo) | o terminal sabe os limites do ativo |
| Stop inválido (SL/TP perto demais) | o terminal sabe a distância mínima |
| Negociação desabilitada | o terminal sabe se o ativo está liberado |
Posto 2 — o servidor do broker: se passou no Posto 1, a ordem é enviada e o servidor decide.
A recusa aqui pode ser na hora (broker rápido) ou depois (assíncrona — a prop firm aceita e
nega segundos depois). São recusas que só o servidor conhece:
| Recusa do Posto 2 (servidor) |
|---|
| Mercado fechado |
| Requote / preço mudou |
| Sem cotação |
| Prop firm aceita e nega depois (o perigo da perna nua) |
A pergunta que confunde — "o F9 pega TODAS as recusas?" — tem resposta simples: NÃO, e nem
precisa. O F9 pega todas as do Posto 1; o OnTradeTransaction pega todas as do Posto 2. Os
dois vigiam ao mesmo tempo, então juntos cobrem tudo. O Journal no disco é a rede pra qualquer
escape (mais lento).
Ao clicar a ordem, o robô arma 4 testemunhas ao mesmo tempo. A primeira que enxergar o
desfecho avisa o servidor e desarma as outras (não tem aviso duplicado):
| Testemunha | O que pega | Velocidade | Dá o motivo? |
|---|---|---|---|
| PositionsTotal | abriu (a posição apareceu) | +0,57s (o mais rápido) | — |
| OnTradeTransaction | abriu + recusa do servidor | +1,3s | ✅ código + texto do broker (universal) |
| Diálogo F9 | recusa local (Posto 1) | ~95ms (enquanto o diálogo está aberto) | ⚠️ texto do broker (nem sempre universal) |
| Journal no disco | TUDO (reserva) | 🐢 lento (~30s a minutos) | ✅ verbatim |
Por que +1,3s do OnTradeTransaction é aceitável: o fecho da perna órfã só acontece no piso de
tempo mínimo de vida (135s). Qualquer das 4 testemunhas (e até a rede de 55s) chega muito antes
disso. Não precisa ser instantâneo — precisa ser antes do piso.
O Journal do MT5 (a aba que lista "ordem enviada / recusada / executada") seria a fonte perfeita:
pega tudo. O problema: não dá pra lê-lo em tempo real. Provamos cada caminho:
| Tentativa de ler o Journal ao vivo | Resultado |
|---|---|
| Arquivo de log no disco | ❌ o MT5 grava em blocos, com atraso (a doc oficial confirma) |
| Ler a janela do Journal pela API do Windows | ❌ é uma lista "virtual" — não devolve o texto |
| UI Automation / acessibilidade | ❌ o MT5 desenha a janela na mão, sem expor o texto |
| Forçar o flush do log | ❌ só funciona pro log do próprio robô, não o do broker |
| Injetar código no MT5 / OCR da tela | ⚠️ frágil e detectável por prop firm — descartado |
Por isso o robô reporta do que ELE consegue ler (as 4 testemunhas acima), e o Journal no disco fica
só como reserva durável (lenta).
Em uma frase: o robô vigia os 2 postos ao mesmo tempo (F9 no terminal, OnTradeTransaction no
servidor), confirma o "abriu" pelo PositionsTotal, e tem o Journal no disco + o stop-loss como redes —
então nenhuma perna fica nua sem o robô saber.
Status: ACTIVE | Guia leigo dos valores do preço sintético do simulador.
SSoT técnico:specs/sim-vea-preco-temporal-fiel.md+server/sim/runner.py(make_seeded_price_book).
No simulador, o "robô-fake" (VEA) precisa reportar um preço pro ouro (XAUUSD). Esse preço
não é dado real de mercado — é uma fórmula determinística que anda no tempo (mesma
semente + mesmo instante = mesmo preço, replay perfeito). Ele existe pra exercitar a engine
do site (anti-detecção R-SAFE, abrir/fechar par, lifecycle), NÃO pra prever o mercado real.
| Valor | Atual | O que controla |
|---|---|---|
| Base | 4000 | nível do preço (centro da onda) — bate com a cotação real do ouro hoje |
| Banda | ±3% | o quanto o preço sobe/desce em torno da base (≈ 3880–4120) |
| Período | 40 min | quanto tempo leva pra completar 1 ciclo (sobe e desce) |
| Formato | triângulo | inclinação constante (sobe reto, desce reto), reverte à média |
| Ruído | < 0,30% | um tiquinho aleatório pra não ficar liso demais (abaixo do gate da R-SAFE2) |
Com isso o preço anda ~$0,20/s → ~1,5% em 5 min (= 5× o gate de 0,30% da R-SAFE2). A %
não muda com a base — só o nível: por isso a R-SAFE é intocada (o gate é 0,30% do preço).
| Aspecto | Veredicto |
|---|---|
| Faixa de 1 trade (SL/TP ~$17-34 em torno da base) | ✅ realista |
| Movimento ao longo de 1 trade | ✅ plausível (sessão um pouco agitada) |
| Tendência de dias/semanas | ❌ não existe (a onda reverte à média a cada 40 min) |
| Previsão de lucro real da estratégia | ❌ o ROI da sim é mecânico, não palpite de mercado |
Em resumo: os valores fazem sentido pro que a métrica mede — saúde da engine
(R-SAFE atua? pares abrem? lifecycle flui? ROI mecânico fecha?). Não são previsão de mercado.
A base já está em $4000 (cotação real do ouro hoje) — feito nesta sessão; foi neutro pra
R-SAFE (o gate é 0,30% do preço, escala junto) e os testes passaram a ler a base do código em
vez de fixar 2000. Pendência ainda em aberto (opcional): pra uma sessão "mais padrão" (menos
agitada), a banda poderia cair de ±3% pra ~1,5-2% — ao custo de mais reagendamentos da
R-SAFE2 (a única trava que custa tempo). Como esse custo é ~0,5s/rodada, é trade-off de
realismo, não de velocidade.
O relógio virtual do sim avança +1 hora por rodada (_sim_tick(delta_sec=3600)). O avanço
extra da R-SAFE2 (quando reagenda) tem teto de 30 min/rodada — sempre menor que a 1 hora,
pra nunca cruzar a virada de juros / fim do dia. Por isso toda rodada cabe folgada no dia e
nenhuma é abortada por tempo.
Importante: as métricas são contadas por RODADA, não por dia de calendário (no cérebro
das métricas, 1 rodada = 1 dia lógico — round_seconds = SECONDS_PER_DAY). Então o fato de o
relógio andar só 1h/rodada não distorce nenhuma métrica — elas se moldam pela rodada.
A ideia mais importante de teste que existe: um teste que só sabe passar não
vale nada. Ele tem que PROVAR que sabe reprovar quando deveria.
A pergunta errada — a que quase todo mundo faz — é: "meu teste passou?"
A pergunta certa é: "meu teste CONSEGUE reprovar quando o código está quebrado?"
Um teste que fica verde é reconfortante. Mas um teste que fica verde mesmo quando
o código está errado é pior que teste nenhum — porque dá uma falsa sensação de
segurança. Ele vira um vigia dormindo no posto: parece que tem alguém cuidando, mas
não tem.
Por isso, nos testes do EA Tester (a bancada que decide se um robô pode subir pra
rodar com dinheiro real), a gente coloca de propósito valores e situações que
têm que dar errado — e exige que o teste os reprove. Se ele não reprovar, o
teste é que está com defeito.
Não é um truque só — são três, cada um num nível diferente.
| Mecanismo | O que faz | Analogia | Se ele falhar em falhar... |
|---|---|---|---|
| Controle negativo | Alimenta a própria checagem com um dado sabidamente-ruim e EXIGE que ela reprove | Teste de COVID numa amostra que você SABE que é negativa — se der positivo, o kit está quebrado | A checagem diz "ok" pro lixo → ela é mentirosa (não consegue ficar vermelha) |
| Teste de mutação | A gente quebra o código de verdade (temporariamente) e confirma que o teste fica vermelho; depois restaura | Pra testar o alarme de incêndio, você acende um fósforo embaixo — se não apitar, o alarme é decorativo | Quebrei o código e o teste continuou verde → o teste é tautológico (não vigia nada) |
| Casos tristes | Cada checagem tem ramos explícitos "esse input DEVE dar reprovado" (não só o caminho feliz) | Testar a fechadura tentando abrir com a chave errada, não só com a certa | Só testei o caminho feliz → um bug no caminho triste passa batido |
A diferença entre eles: o controle negativo testa se a régua sabe medir
(alimenta ela com lixo). O teste de mutação testa se o teste está grudado no
código certo (quebra o código de verdade). Os casos tristes garantem que os
caminhos de erro também são exercitados, não só o sucesso.
Vale a honestidade: nenhuma dessas ideias é nova nem exclusiva daqui. São práticas
estabelecidas, algumas com décadas.
| Ideia | Origem | Idade |
|---|---|---|
| Controle negativo | Ciência experimental de laboratório | Décadas |
| Teste de mutação | Academia de ciência da computação | Desde ~1971 |
| Negative testing (casos tristes) | QA clássico | "Teste 101" |
O que é incomum não é conhecer os nomes — é exigir os três como porteiro. A
maioria dos times escreve só o caminho feliz, vê verde, e chama de pronto. Fazer do
controle negativo + teste de mutação um requisito obrigatório pra uma verificação
"contar" é disciplina acima da média — mas é disciplina madura, não moda passageira.
Separando duas coisas:
Resumindo: não é tecnologia moderna, é higiene de teste que muitos conhecem mas
poucos aplicam com rigor.
O custo de todo esse cuidado é proporcional ao risco. Essas verificações não são
enfeite: elas decidem se um robô sobe pra operar numa conta de prop firm (mesa que
banca o trader). Um teste falso-verde — que aprova sem realmente checar —
deixaria passar um robô quebrado direto pro dinheiro real.
Quanto maior a aposta, mais você paga pra garantir que o guarda não está dormindo.
Num script de brincadeira, teste falso-verde é chato. Aqui, seria caro.
Verificação fraca (só caminho feliz): manda uma ordem, o robô responde "abri",
e a gente acredita na palavra dele.
mandei OPEN -> robô respondeu "FILLED" -> teste VERDE
O furo: e se o robô responder "FILLED" mas não tiver posição nenhuma atrás
(um fantasma)? O teste fraco não percebe — ele confia no recibo do próprio robô.
Verificação forte (Contrato v2, a que usamos hoje): o veredito lê uma fonte
independente — a lista real de posições do broker — e ainda tem controle negativo
+ teste de mutação por trás.
mandei OPEN -> robô respondeu "FILLED" com ticket X
-> confiro na lista REAL do broker: o ticket X existe como posição viva?
- existe -> VERDE (efeito confirmado por quem não é o próprio robô)
- fantasma -> VERMELHO (o robô mentiu)
+ controle negativo: alimento a checagem com "ticket=0" e exijo que ela reprove
+ teste de mutação: quebro a checagem e confirmo que o teste fica vermelho
A diferença não é o robô — é quem dá a palavra final. No fraco, é o próprio robô.
No forte, é uma testemunha independente + a garantia de que a régua sabe medir.
A sacada, no fundo, é uma inversão mental: parar de perguntar "passou?" e passar
a perguntar "consegue reprovar quando deveria?". Um teste que nunca viu o vermelho
é um alarme que nunca foi testado com fumaça.