| Guide
← Dashboard
Nenhum resultado

Mapa do Sistema

Status: ACTIVE | Ultima revisao: S152 (2026-03-30)

Arquitetura

DASHBOARD (browser) → HTTP/WS → SERVIDOR (VPS FastAPI) → HTTP polling 1-3s → EAs MT5 (MQL5)

Fluxo de Uma Implementacao

 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
      │ ←── [100%] protect-specs.sh BLOQUEIA whitepapers sem aprovacao
      │ ←── [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)

Hierarquia de Garantias

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.

Hooks por Categoria

Bloqueantes (BLOCK — impedem a acao)

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

Alertas (WARN — avisam mas permitem)

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

Silenciosos (monitoram sem interferir)

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

Background (24/7 na VPS)

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

Ferramentas de Browser

Ferramenta Melhor para Tokens
Playwright MCP Unico browser MCP (snapshot, screenshot, automacao) ~114k

Skills (invocadas manualmente ou por keyword)

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

Auto-Melhoria (ciclo Patterns)

Bug encontrado → /learn captura pattern → Guardian injeta na proxima sessao
     │                                              │
     │                                              ▼
     │                                    Claude evita repetir
     │                                              │
     └──── 3x repetido? → Promover pro CLAUDE.md ──┘

Sistema de Qualidade

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

O que eh

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

Onde fica cada arquivo

scripts/quality/
  quality_monitor.py   <- Roda na VPS a cada 15min (cron)
  watchdog.py          <- Vigia se o monitor parou (cron cada 30min)
  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

Camada 1: Quality Monitor (VPS, automatico)

O que faz

A cada 15 minutos, roda 9 queries SQL no banco verificando regras que nunca devem ser quebradas. Se alguma quebrar, manda Telegram.

Cron

*/15 * * * *  quality_monitor.py  (9 checks)
*/30 * * * *  watchdog.py         (vigia o monitor)

Os 9 checks

# 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 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)

Alertas Telegram

Watchdog

O watchdog verifica se o quality_monitor esta rodando. Se o heartbeat file tiver mais de 30 minutos, avisa no Telegram: "Quality Monitor parou!"

Como adicionar um novo check

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>"
}

Camada 2: Property Tests (local, antes do deploy)

O que faz

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.

Como rodar

pytest tests/property/ -v       # Rodar todos (52 testes, ~30s)
pytest tests/property/ -q       # Resumido

Os testes

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.py refatorado — importa funcoes reais de app.af.engine (SSoT) em vez de manter copias locais (FakeProp/FakePA removidos, -154 linhas).

Hook pre-deploy (automatico)

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


Camada 3: Fault Injection (manual, sob demanda)

O que faz

Manda requests "errados" de proposito pro servidor e verifica que ele nao crasha (retorna 500).

Como rodar

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

Os 8 cenarios

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

Quando rodar


Bugs encontrados ate agora

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

Metricas de eficacia

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

Copy Trade

Status: ACTIVE | Ultima revisao: S315.1 (2026-05-02) — Onda 5 cleanup: Etapa 4 aponta canonical (create_signal_canonical + dispatch_pending_to_eas). Detalhes em specs/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


Visao Geral

┌─────────────────────────────────────────────────────────────────────────────┐
│                          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                         │
└─────────────────────────────────────────────────────────────────────────────┘

Etapa 1 — Deteccao de Trade (EA Master)

O EA tem 2 mecanismos paralelos que detectam trades. Ambos alimentam a mesma fila.

Mecanismo A: OnTradeTransaction (OTT) — instantaneo

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

Mecanismo B: SnapshotEngine (SE) — polling via timer

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"

Regra R1: Primeira execucao = baseline

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.


Etapa 2 — Fila de Sinais (SE Queue)

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)

Regra R2: CLOSE tem prioridade maxima

CLOSE vai para o INICIO da fila (linha 364-377). OPEN e MODIFY vao para o final.

Regra R3: Deduplicacao

MODIFY ja existente na fila para o mesmo ticket: atualiza sl, tp, volume no lugar (nao duplica).

Regra R4: Fila cheia = descarte

Se fila tem 200 itens, novos sinais sao descartados com log [ALERTA] Fila CHEIA.


Etapa 3 — Envio do Sinal (EA → Servidor)

Arquivo: SnapshotEngine.mqh, funcao SnapshotEngine_FlushQueueWeb() (linha 424)

Cooldowns por tipo (independentes entre si)

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

Retry por item

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"

Canal primario: WebSocket

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.

Canal fallback: HTTP POST

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

Etapa 4 — Processamento no Servidor

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() em server/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 EA POST /api/signals (batch insert ~linha 762) ainda usa sa_insert(Signal).values(signal_rows) batch insert manual (NAO o helper create_signal_canonical()) — sera migrado em Onda 4 lifecycle. Atualizacao S361 (doc-fresh): origin e signal_source JA 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 via dispatch_pending_to_eas(db, trade_group_id) 100% canonical em todos os paths. Detalhes completos + escopo Onda 4: specs/signal-dispatch-canonical.md.

Fluxo geral

  1. Valida API key (X-API-Keyverify_api_key)
  2. Identifica conta master pelo api_key
  3. Processa conforme action

OPEN

  1. Gera trade_group_id (UUID)
  2. Calcula sl_distance = abs(price - sl), tp_distance = abs(tp - price)
  3. Master: 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") atomico
  4. Busca peers do mesmo group_id (exceto master)
  5. Para cada peer:
  6. Se peer.invert != account.invert → caller aplica _invert_direction/_invert_sl_tp ANTES (R8: caller invert, canonical e agnostica)
  7. create_signal_canonical(create_pending_signal=True, expires_in_seconds=PENDING_SIGNAL_TTL_SECONDS) — cria Signal + PendingSignal (TTL 60s)
  8. Pos-commit: await dispatch_pending_to_eas(db, trade_group_id) faz WS push uniforme via _build_ws_payload (R11)

Regra R5: Circuit Breaker

Se conta slave tem >= CIRCUIT_BREAKER_MAX_POSITIONS (10) posicoes abertas, o OPEN e bloqueado para aquela conta.

MODIFY

  1. Busca TradeLink ativo pelo ticket do master
  2. Busca peers via trade_group_id
  3. Verifica suppress_marker — se existe para trade_group_id + MODIFY, ignora (anti-echo)
  4. Para cada peer: create_signal_canonical(action="MODIFY", ticket=local_ticket_do_peer, ...) — cria Signal + PendingSignal
  5. Caller aplica inversao SL/TP ANTES (R8) se peer.invert != account.invert
  6. Pos-commit: await dispatch_pending_to_eas(db, trade_group_id)

CLOSE

  1. Busca TradeLink ativo pelo ticket
  2. Late DEAL_PROFIT: Se TradeLink ja foi fechado pelo heartbeat (orphan closer), atualiza profit + close_price do link existente e retorna "profit_updated" (sem re-fechar)
  3. Fecha TradeLink da conta master (is_closed=True, closed_at=now)
  4. P&L Priority Chain:
  5. deal_price do EA (preco exato de fill do deal) — prioridade maxima
  6. deal_profit do EA (DEAL_PROFIT + COMMISSION + SWAP consolidado)
  7. Fallback: OpenPosition cache (ultimo bid do heartbeat)
  8. Fallback: position cache (posicao pre-close)
  9. Fallback: zero
  10. Busca peers via trade_group_id
  11. Verifica suppress_marker — se existe para trade_group_id + CLOSE, ignora
  12. Para cada peer aberto: create_signal_canonical(action="CLOSE", ticket=local_ticket_do_peer, ...) — cria Signal + PendingSignal
  13. Cria SuppressMarker com TTL 10s para o trade_group_id (anti-echo servidor)
  14. Trade Event Log: log_trade_event(event_type="CLOSE", close_reason, profit, close_price)
  15. Pos-commit: await dispatch_pending_to_eas(db, trade_group_id)

Etapa 5 — PendingSignal (fila de distribuicao)

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

Regra R6: TTL de 5 minutos

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=True pelo cleanup periodico (loop a cada 15min em main.py).

Regra R7: Cleanup manual disponivel

Endpoint POST /api/admin/cleanup/pending-signals para forcar limpeza.


Etapa 6 — Distribuicao (Servidor → EA Slave)

Via WebSocket push (instantaneo)

O servidor chama ea_ws_manager.notify_account(account_id, "new_signal") que envia o sinal completo via WS.

Via HTTP polling (safety net)

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)

Regra R8: Dupla garantia WS + HTTP

O EA SEMPRE faz poll HTTP, mesmo com WS ativo. O WS acelera a entrega, o HTTP garante que nada se perde.

Retomada apos reconexao WS (v3.77+, S253 Onda 2)

Resume semantics — EA informa "ate onde ja processou":

Por 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.

Paridade HTTP/WS em detect_missing_positions (v3.77+, S253 Onda 2)

_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.


Etapa 7 — Execucao do Sinal (EA Slave)

Arquivo: Experts/LinniuC.mq5 + Include/CopyTrade/GUIExecution.mqh

Pre-execucao

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

OPEN (GUIExecution_Open, GUIExecution.mqh:765)

  1. GUI_AcquireGUILock() — lock exclusivo em arquivo (TTL 30s para stale locks)
  2. GUI_EnsureVisible() — restaura MT5 se minimizado
  3. GUI_CloseAllMT5Dialogs() — fecha dialogos residuais
  4. GUI_OpenF9()WM_COMMAND 32848 abre dialogo "Nova Ordem"
  5. GUI_WaitForDialog() — timeout InpTimeoutMs (5000ms)
  6. GUI_SelectSymbol() — seleciona simbolo no combo (ID 10331)
  7. GUI_WaitForSubDialog() — aguarda controles carregarem
  8. Preenche volume (10333), SL (10334), TP (10336)
  9. Clica BUY (10408) ou SELL (10409)
  10. Loop de confirmacao: aguarda dialogo fechar OU detecta popup do broker
  11. GUI_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)

MODIFY (GUIExecution_Modify, GUIExecution.mqh:844)

  1. GUI_AcquireGUILock()
  2. GUI_OpenPositionDialogSafe(ticket) — localiza na ListView (10328), duplo-clique
  3. GUI_VerifyDialogTicket() — verifica titulo contem ticket esperado
  4. Seleciona "Modificar" no ComboBox de tipo (10338)
  5. Aguarda botao Modificar (10351) aparecer
  6. Preenche SL e TP, clica Modificar
  7. SE_SuppressTicket(localTicket, 10) — suprime por 10 segundos

CLOSE (GUIExecution_Close, GUIExecution.mqh:948)

  1. GUI_AcquireGUILock()
  2. GUI_EnsureVisible()
  3. Ate 3 tentativas com GUI_ForceCloseResidualDialogs() entre elas
  4. GUI_OpenPositionDialogSafe(ticket) — ListView → duplo-clique
  5. Clica botao Close (10410)
  6. Loop aguarda fechamento
  7. SE_SuppressTicket(localTicket, 10) — suprime por 10 segundos

Etapa 8 — ACK (EA Slave → Servidor)

Arquivo: WebBridge.mqh:808
Endpoint: POST /api/signals/{signalId}/ack

Payload

{
  "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 possiveis

Status Quando
FILLED Execucao GUI bem-sucedida
FAILED GUI falhou ou ticket nao encontrado
SKIPPED Sinal muito antigo (> InpSignalMaxAge)

Retry do ACK

Fase Tentativas Backoff
Imediata 3x 1s, 2s (exponencial)
Fila persistente Ilimitado (max 20 na fila) 5s → 10s → 20s → 30s (cap)

O que o servidor faz com o ACK

  1. Cria/atualiza SignalAck com todos os campos
  2. Se FILLED: cria TradeLink com is_origin=False, local_ticket do slave
  3. Marca PendingSignal.resolved=True
  4. Push WS para dashboard
  5. MODIFY ACK (v2.0): Captura applied_sl vs target_sl, applied_tp vs target_tp — mostra divergencia entre o que foi pedido e o que o broker aplicou
  6. Trade Event Log: log_trade_event(event_type="ACK", status, action="OPEN"|"MODIFY", context)

Delay Audit Fields (v2.0)

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)

Exec Slippage Fields (v2.4 — S336+)

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

Regra R9: trade_group_id e o elo

Todo MODIFY e CLOSE usa o trade_group_id para encontrar quais slaves precisam ser notificados. Sem TradeLink = sinal nao propaga.


Caminhos Alternativos e Edge Cases

EC1: Falha de WebSocket

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

EC2: Orphan Checker (posicoes orfas) — 2 mecanismos

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).

EC3: Anti-Echo (2 niveis)

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_RETRIES define 5 em SnapshotEngine.mqh:43, mas FlushQueueWeb usa 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).

EC4: Partial Close

EC5: Posicao ja fechada no CLOSE

EC6: Signal do Dashboard (broadcast)


Estado Afogado + Visibilidade de Saturacao (S386, v2.4)

NAO altera as regras R1-R10 acima. Adiciona 3o estado de conta entre alive e offline + 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).

Estados de conta

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.

Regra R11: Beacon 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).

Regra R12: Dedup in-flight (mata classe do #330 na origem)

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.

Regra R13: Back-pressure por SKIP (hedge gating)

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%).

Visibilidade

Arquivos-chave

Specs relacionados


Sinais AF (AutoFund Hedge) — Canal Isolado (v2.0)

O sistema AF gera sinais que usam a mesma infraestrutura (Signal, PendingSignal, TradeLink) mas com isolacao total do copy-trade normal.

Signal Channel

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

AF Signal Types

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.

Isolacao (anti-broadcast)

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.

AF signals bypass


Reconciliation Post-Offline (S300)

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).

Caminho retroativo via HistorySelect (3 trigger points no EA)

# 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.

Idempotency Contract (R6)

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.

Override broker-canonical com tolerancia $0.01

_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).

Sanity gate (R28 — defesa adversarial)

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).

Watchdog 5min + market hours (R8 + R9)

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.

Limitacao conhecida

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).

Spec autocontida (referencia)

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).


SL/TP Reconciliation via Heartbeat (v2.0)

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"

Trade Event Log — Diario de Bordo (v2.0)

Todo evento significativo eh registrado na tabela TradeEvent para auditoria completa.

Funcao: 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.)

Pontos de captura

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

Trace ID — Rastreabilidade Ponta-a-Ponta (S224+)

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.

Geracao

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

Propagacao por Etapa

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

Event Types Rastreados (19+)

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).

Backward Compatibility

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

Consultas no Dashboard

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 Relacionados


Arquivos-Chave

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

Pipeline de Logs Remotos do EA (S1015)

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)

Eventos Logados pelo EA (28 tipos)

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

gui_trace Formato

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.

Queries Uteis

-- 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;

Mapa de Funcoes EA - Servidor (S1015)

EA: Experts/LinniuC.mq5

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

EA: Include/CopyTrade/WebBridge.mqh

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)

EA: Include/CopyTrade/GUIExecution.mqh

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)

Servidor: Endpoints Principais

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

ACK Validation V1-V4 (S1015)

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).

Reconciliation Pos-Trade

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


EC-Test: Variante de Injecao de Sinais de Teste (Phase C — S219)

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:

Diferencas vs fluxo normal

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)

Regra R10 — Isolamento de teste

R10: Sinal signal_source='test' NUNCA distribui para conta is_test=false.
- O handler valida que account.is_test=True antes de criar qualquer PendingSignal.
- Nao ha broadcast de group_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 = 0 SEMPRE.
- Conta 52 (Teste Sandbox) esta em blocked_account_ids do pairing AF — nunca entra em round real.

Etapas equivalentes pos-Etapa 4

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)

Ativacao

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.


Carteiros

Status: ACTIVE | Ultima revisao: S342 (2026-05-12) — trio carteiro fechado

3 carteiros canonicos cobrem o ciclo de vida completo de cada sinal de trade.
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.

Visao geral — quem faz o que

# Carteiro Quando dispara Helper Spec
1 Carteiro Canonical (IDA) — entrega 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

Fluxo end-to-end

[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?)  +-----+

Carteiro Canonical (IDA) — create_signal_canonical

Arquivo: 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.

O que faz

  1. Cria 1 row em signals (a "encomenda")
  2. Cria 1 row em pending_signals (a "lista de espera ate alguem entregar")
  3. Cria 1 row em trade_links se origem eh origin
  4. Opcionalmente cria signal_ack com status=SUCCESS (server-initiated routes)
  5. Logga evento em trade_events para auditoria
  6. Push WS imediato pro EA via dispatch_pending_to_eas

Tabelas tocadas

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

Regras criticas


Carteiro Conferente (VOLTA) — apply_ack_canonical

Arquivo: 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.

O que faz

  1. UPSERT atomico em signal_acks (race-safe via IntegrityError retry com _apply_fields interno)
  2. compute_exec_slippage ANTES do commit (1 commit so — D1 canonical)
  3. Detecta FALSE_ACK via validate_ack_data (V1-V4) e marca status=FAILED se aplicavel
  4. Cria/atualiza trade_links quando status=FILLED (preenche local_ticket)
  5. Marca pending_signals.resolved=True pra acabar o ciclo
  6. Logga trade_events (ACK + MODIFY-BUFFER mismatch + CLOSE P&L cascade)
  7. Dispara AF MODIFY post-fill (reconcile_af_pair + check_and_schedule_modify) quando 2 ACKs FILLED chegam no mesmo trade_group_id AF
  8. Telegram alerts: MODIFY-BUFFER MISMATCH, AF MODIFY BLOCKED, CLOSE RETRY EXHAUSTED
  9. CLOSE FAILED auto-retry (max 3 tentativas) — SE-2 cure do refactor
  10. Broadcast commit_then_publish("ack", ...) no final pro dashboard

Tabelas tocadas

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

Drift HTTP vs WS (catalog 12 divergencias)

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)

Regras criticas


Carteiro Plantonista (PLANTAO) — apply_heartbeat_canonical

Arquivo: 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.

O que faz (18 sub-tarefas por HB)

  1. _sanitize_nan recursive (NaN/Inf em qualquer profundidade)
  2. Backfill broker se conta foi auto-registrada por WS auth (SE-2)
  3. Zombie check 1x no inicio (timer_tick congelado por 2 HBs)
  4. Update broker_name, mt5_server (com guard 2+ chars apos strip)
  5. terminal_build change alert via Telegram (SE-3 cure)
  6. Update account.last_heartbeat_at SEMPRE (mesmo no throttle WS — S314.3 Bug 3 fix)
  7. Insert Heartbeat row (force_save=True; throttle 30s no WS)
  8. Equity snapshot (cooldown 5min)
  9. Diagnostics cache in-memory
  10. Chart price cache (BID/ASK fresh do MT5)
  11. SymbolInfo cache + DB upsert (tick_value, tick_size, contract_size, etc.)
  12. UPSERT open_positions (delete gone + capture P&L pra TradeLink antes do delete — CR-03)
  13. Settings sync verify (SE-4 cure: ambos canais via commit_then_publish)
  14. EA back-online alert (se estava em _offline_accounts)
  15. Desync check (group_peers mostra outras contas do grupo)
  16. SL/TP reconcile (_reconcile_sl_tp — MODIFY pra peer divergente)
  17. Detect missing positions (_detect_missing_positions com advisory lock)
  18. Broadcast HB unificado (SE-5/SE-6: super-set inclui ea_version + sl/tp/open_price)

Tabelas tocadas

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

Drift HTTP vs WS (catalog 18 divergencias)

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

Regras criticas

Frequencia em prod


Fluxo de dados na rede (payloads IN/OUT)

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.

Carteiro Canonical (IDA) — payloads

Entrada: chamada Python interna (nao tem JSON IN — eh callee de 20 callsites).

Saida 1 — push WS pro EA (WS_PAYLOAD_FIELDS, 16 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

Saida 2 — broadcast "new_signal" pro dashboard (dashboard ouve via WS, atualiza aba Sinais).


Carteiro Conferente (VOLTA) — payloads

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)

Carteiro Plantonista (PLANTAO) — payloads

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}.


Observabilidade

Prometheus REMOVIDO em S367. O endpoint /metrics e 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.


Como validar visualmente

  1. Aba Sinais — cada linha mostra OPEN/MODIFY/CLOSE (IDA cria, VOLTA confirma)
  2. Aba Contasequity, last_heartbeat (PLANTAO atualiza)
  3. Posicoes abertasopen_positions (PLANTAO mantem snapshot)
  4. Notificacao Telegram — alerts disparados pelos 3 carteiros (mt5_build_change, modify_buffer_mismatch, af_modify_blocked, close_retry_exhausted)
  5. /api/debug/health-full (auth JWT) — health consolidado de cada conta com ack_stats_1h, recent_logs, diagnostics

Historico


Slippage Telemetria

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_acks significa.

A ideia em 1 paragrafo

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:

  1. Vitrine (o preco que aparecia na hora do clique — bid_at_request / ask_at_request)
  2. Recibo (o preco que o broker realmente entregou — deal_price)
  3. Ordem escrita (o preco que o servidor anotou na ordem original do master — 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).

Fechamento automatico por stop/alvo (SL/TP) — a regua do NIVEL (S388)

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:

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 "—".

Tabela — o que e preenchido em cada tipo de operacao

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?".

Janela de validade da foto (R5)

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".

Tag 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:

Distingue de Layer 2 servidor (close_reason="ROLLOVER_FALLBACK" via close_pair_positions — fallback se EA falhar). Spec completa: rollover-dupla-camada.md.

Invariantes (provadas via property-based test T4, 1000 runs)

Exemplo real — primeira deteccao live (signal 13123, sandbox)

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.

Onde ver no dashboard

No painel /af (modulo af_hedge.js), cada card de signal completo mostra:

Se ambos slips forem NULL, as linhas sao omitidas (nao aparecem como "—" pra evitar poluicao).

Referencias tecnicas


AF Hedge

Status: 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_pair agora usa _invert_for_peer() (helper centralizado) que alem de aplicar invert grava audit row em signal_inversions via 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) + scripts/hedge/rules.py + scripts/hedge/test_rules.py


1. Regras de Pool (como organizamos as contas)

# 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). O group_id e' so RoTULO de isolamento da dupla; a copia legada que rotearia por nome fica SUPRIMIDA pra conta em pool AF. invert segue 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.

2. Regras de Risco (quanto apostar por partida)

# 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

3. Regras de Look-ahead (detalhe)

# 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

4. Regras de Vida/Morte (lifecycle)

# 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

5. Regras de Prop (variam por prop firm)

# 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

6. CLEAN_PROPS (pool AF v2)

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 do max_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 FK prop_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_eligible virou TRAVA DURA de entrada (antes era so
filtro de catalogo, com fallback que deixava mesa nao-elegivel entrar). Agora mesa com
is_af_eligible=false NAO entra em pool nenhuma — bloqueada em
_create_chairs_from_accounts (pula com motivo) e assign_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. Ver specs/prop-firm-dd-form-redesign-S382.md. max_risk migrado de USD
($2.500) pra max_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)

6b. Invert Dinamico por Rodada (S99)

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.

6c. Push Zone (precisao no fechamento)

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).

6d. MODIFY Post-Fill (ajuste de precisao)

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

Framework de seguranca F1-F6

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

7. Regras de Execucao (EA/servidor, nao simulador)

# 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

E5: Plano Diario — Fluxo Completo

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.

E5: Formato Telegram

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]

E6: Pre-check de Vida

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

E7: Retry com Price Gate (falha de execucao)

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.

8. Regras de Simulacao

# 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.

10. Regras Anti-Deteccao Same-Prop (R-SAFE)

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

R-SAFE5: Nivel Absoluto (nao distancia)

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.

R-SAFE6: Horario Variavel (Plano Diario)

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

R-SAFE2: Polling de Preco

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.

Ordem de execucao aleatoria

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.

Fluxo Range Valido (servidor)

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)

Close divergence (garantido por R-SAFE5)

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.

Escalabilidade (N pessoas por prop)

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).

Direcao dos siblings

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.

Futuro (~3 meses): Anti-bot individual

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.

Impacto na simulacao

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

10b. Validacao Pos-Rodada (L3-L5)

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

Dead Letter Queue (DLQ)

Sinais 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 — Formula de Calculo

volume = risk_usd / ((sl_distance + sl_buffer) / tick_size * tick_value)

Symbol Presets (configuracao por ativo)

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_max permanecem USD (equity-facing). Ver usd-to-pct-migration.md.

Arquitetura de Modulos (servidor)

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

9. Ordem do Dia (processamento na simulacao)

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

11. Operacoes Live (consolidado de af-live-operations.md)

Lifecycle (tudo MANUAL pelo usuario)

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.

Quando os checks rodam (S163)

Momento Funcao O que checa Arquivo
Inicio da rodada process_round() sync_real_balancescheck_passescheck_deathspair_accounts lifecycle.py:200-211
Apos CADA trade process_trade_result() Atualiza P&L → check_deathscheck_passes lifecycle.py:607-608
Regenerar regenerate_round() sync_real_balancescheck_passescheck_deaths → re-pair lifecycle.py:407-408
Round completo process_trade_result() Se todos pares completaram → round_completedrun_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.

Anti-One-Side-Betting (anti-OSB)

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 (multi-pessoa)

Telegram — Eventos AF

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)

Market Hours Guard (S288 — spec: market-hours-guard.md)

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.

Demo vs Real

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).


Resumo Visual

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

Historico de Regras

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

Campos (Regras)

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).


A confusão mais comum: são 3 limites DIFERENTES

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.


Regras da PROP FIRM (fixas por programa — tabela 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)

Settings da POOL (você ajusta — 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 por equity_floor_pct
(%) no S362. O servidor ainda lê o USD como fallback pra pools não-migrados.


Como o % vira dólar

A tela de preview das regras (e o motor) pegam o % × tamanho da conta:

Por 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.


O que é futuro / inerte hoje


Lendo o Config Settings da pool (telas novas — S383)

O 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:

Buffer por par (margem de segurança)

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:

Mapa de spread médio

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.

Filtro demo / real

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.

"Empresa — Desafio"

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.

"Buffers confirmados pelo EA?" — a conferência

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
:


Donos Prop Firms

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).

Veredicto atual

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.

Quem é dono de quem

Marca Grupo / dono real País (sede) Independente?
FTMO FTMO s.r.o. (holding OMHC) Chéquia Chéquia Sim
The 5%ers Five Percent Online Ltd Reino Unido Reino Unido Sim
FundedNext NEXT Ventures Emirados Emirados Sim
BrightFunded BrightFunded B.V. / Bright Global FZCO Holanda Holanda (opera de Dubai) Sim
City Traders Imperium CTI FZCO Emirados Emirados Sim
Funding Pips ANKH PROP FZCO Emirados Emirados Sim
Alpha Capital (Alpha Pro 10%) Alpha Capital Group Ltd (Kohler / AMGP) Reino Unido Reino Unido Sim
Funded Trading Plus Acello Ltd / Instant Funding Reino Unido Reino Unido Trocou de dono 26/05
For Traders FT Trading Ltd + BLN Tech Club DMCC Emirados Emirados (registro em St. Lucia) Sim
Maven MAVEN LLC Saint Lucia Saint Lucia (operação em Dubai) Sim
Fintokei Purple Group / Purple Trading Chéquia Chéquia Sim

Marcas-irmãs (não adicionar à pool sem checar)

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

Mapa de localização — onde ficam nossas 11

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 Emirados FundedNext · City Traders Imperium · Funding Pips · For Traders 4
Reino Unido Reino Unido The 5%ers · Alpha Capital · Funded Trading Plus 3
Chéquia Chéquia FTMO · Fintokei 2
Holanda Holanda BrightFunded (opera de Dubai) 1
Saint Lucia 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.

Fontes (prova) — auditoria 27/05/2026


EA Architecture

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_ms no ACK payload. Buffer circular g_signalReceivedAtMs[64] em WebBridge.mqh + helper RecordSignalReceived chamado dentro de LNC_BeginSignalObs (cobre WS via ExecuteSingleSignal:728 + HTTP via PollAndExecuteSignals:1221). Helper _S326_NowEpochMs() usa TimeGMT()*1000 com fallback TimeCurrent() se TimeGMT()=0. Sandbox account_id=3 validado 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)

LinniuC.mq5 (Orquestrador, ~1245 linhas)

Inputs

OnInit (13 passos)

  1. Verificar DLLs | 2. FindMT5Window | 3. OpenProcess ListView
  2. Init lock file gui_lock_<HWND>.txt | 5. SettingsManager_Init
  3. WebBridge_Init | 7. SymbolResolver_Init | 8. SnapshotEngine_Init
  4. Init local signal files (offline) | 10. Panel_Init
  5. EventSetMillisecondTimer(200) | 12. Print banner | 13. g_initOK=true

OnTimer — 2 ciclos

PollAndExecuteSignals

Poll -> parse JSON -> para cada signal: resolve symbol -> apply buffers -> GUI execute -> detect local ticket -> ACK (FILLED/FAILED)

Ticket Detection (v3.29.3+)

Modulos (.mqh)

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, Rollover camada 1 pausa. 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
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.

SnapshotEngine — Comportamentos Criticos

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

OTT Fast Close (v3.39.0+)

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

Heartbeat Server-Side Tracking (S314.3, Bug 3 fix Opcao C)

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.

Pulso leve vs Censo pesado (S386 Fase 2)

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.

Beacon 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".

MODIFY Readback Validation (v3.48.0+)

Apos GUIExecution_Modify retornar sucesso, o EA le de volta os valores do broker e so envia FILLED se realmente aplicou:

GUI Input Strategy (v3.49.0+)

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.

Volume/SL/TP Safety Guards (v3.32.0+)

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

Remote Logging — LNC_LogRemote (v3.34.0+)

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.

GUI-TRACE Steps (v3.34.0+)

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.

Buffer SL/TP (EA-side)

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.

Equity Floor Circuit Breaker (v3.65.0)

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)

Rollover Dual-Layer — Camada 1 EA (v3.72.0)

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.

Compilacao

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.

Dois deploys, dois universos

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).

Workflow Worktree (S301+)

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

Versionamento

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.

Auto-Update v3 Safety

CRITICO: Auto-update NUNCA deve rodar durante trade ativo — EA verifica g_se_hasOpenPositions.

WS 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

Erros MT5 Comuns

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.

Regras MQL5 (OBRIGATORIAS)

PROIBIDO: OrderSend / CTrade (REGRA ABSOLUTA)

Prop firms detectam ordens de EA (magic number, filling flags, deal comment). GUI automation cria ordens que parecem MANUAIS (F9 dialog).

Descoberta (S73): EA usava OrderSend desde v3.12.0 disfarçado como "fill mode fix" sem usuario saber.

ACK Format Completo

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

OrderCalcProfit Fallback

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.

API Key via arquivo (v3.25.1+)

EA le key de MQL5/Files/copytrade_key.txt no OnInit. Prioridade: FILE (se len >= 10) > INPUT. Whitespace trimmed.

Build Version History

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

Armadilhas


Servidor

Status: 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/af-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 = 13 jobs (ver seção Cron). [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

Stack

Internet -> Cloudflare (CDN/WAF) -> nginx (SSL linniuc.com) -> uvicorn 127.0.0.1:8000 (1 worker) -> FastAPI
                                                                                            |
                                                                                       PostgreSQL 16

Acesso

Systemd Service

[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), nao uvicorn direto como dizia antes. 1 worker, bind so em localhost. Verificado via systemctl 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

Nginx

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

Background Tasks (main.py lifespan)

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.

Cron

0 3 * * *    backup_daily.sh           # Backup diario 3h UTC (VPS-only)
*/2 * * * *  healthcheck_v2.sh         # Watchdog: auto-restart se cair (VPS-only)
*/5 * * * *  monitor.sh                # Monitor proativo + Telegram (VPS-only)
0 * * * *    build-context.sh          # Contexto pre-compilado do bot
*/30 * * * * error-monitor.sh          # Erros journalctl -> Telegram (sem LLM)
*/15 * * * * quality_monitor.py        # Quality monitor unificado
*/30 * * * * watchdog.py               # Watchdog quality
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)
0 12 * * *   vps/drift-detect.sh           # Detecta edicao direta VPS
0 10 * * 0   spec-freshness-alert.sh       # Freshness specs (VPS-only)

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 7 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) (filtra
stale=True; available=false[], degradação graciosa). Sobrevive a restart
(cooldown in-memory). 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).

Deploy Checklist (OBRIGATORIO)

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.

Rollback

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?

PostgreSQL

Pool: SQLAlchemy 5 connections, max overflow 10, recycle 30min.
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.

Telegram

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 🔍.

Middlewares

MCP Server (Local)

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.

Scripts Operacionais

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

Account Onboarding

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.

Referencia Rapida

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'));

Armadilhas


Dashboard

Status: 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.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_dd da 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 puro splitLastRoundByType em af_hedge_logic.js (testado em vitest). Cabecalho de grupo so quando ha 2+ classes. Ver specs/af-last-round-group-by-type.md. [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

Stack

Arquivos

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 Tools > Prop Firms (tabela comparativa) (~270 linhas)
risk_calc_v2.js Risk Calculator V2 (3 modos: %Bal, %Eq, USD) (~980 linhas)
simulator.js Monte Carlo AF v2 (IIFE) (~880 linhas). Backend: VPS routes/simulator.pyscripts/hedge/dashboard.pysimulator.pyrules.py. Deploy necessario pra atualizar regras.
styles.css Estilos globais + componentes extraidos (~3100 linhas)
themes.css Overrides de temas (OBRIGATORIO)

Arquitetura JS (S127)

Namespace 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.

Event Emitter wsEvents

WebSocket 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).

CSS

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.

Tabs (5 grupos, consolidado S107)

  1. Visao Geral (Alt+1) — Summary cards por grupo, equity chart overlay, desync alert sonoro, contas. Contas: classificacao obrigatoria (Mesa/Fase/Tamanho/Steps/Dono), botao X excluir permanente (hard delete com confirmacao dupla), auto-nome sem contar deletadas/indefinidas
  2. AF Hedge (Alt+2) — Pools, battle cards, force execute, pair history, debug mode, swap close countdown (global + per-card via WS), compact battle timeline. Ferramentas: Validar (in-memory, S170), Reset Historico, Deletar Pool (confirmacao dupla: modal + digitar nome). Simular Round e Stress Test removidos (deprecated 410, S170). Arquivo: af_hedge.js
  3. Battle cards (S205): Badges amigaveis (Risco Ajustado, Risco Otimizado, Ultima Chance) com tooltips. Mini-panel centralizado (Entry+SL+TP+Exit) com fundo sutil e borda arredondada. SL mostra badge "+" quando buffer aplicado (sl_buffer_offset via risk_detail, fallback $1 XAUUSD). Footer: mini-tabela de metricas com 4 linhas: Entry Gap (pts + $), Exit Gap (pts + $), Hedge Cost ($ + %), Exit Slip (pts + $) — cada uma com dot colorido (verde/amarelo/vermelho por threshold). Labels: "Risco desta batalha" (header), "Risco Max" (fighter). Nomes usam getAccountDisplay() — busca nome completo + cor do dono de CT.accounts (alinhado com Visao Geral)
  4. Trading (Alt+3) — Sub-pills: Sinais | Ordens | Posicoes
  5. Sinais: Historico filtrado, delay analysis (cards + tabela expansivel)
  6. Ordens: Broadcast, risk calculator V2, price ruler, preview por conta
  7. Posicoes: Posicoes abertas, close/modify, risco %, auto-refresh 30s
  8. Sistema (Alt+4) — Sub-pills: Saude | Journal | Configuracoes
  9. Saude: Health cards, monitor, diagnostics grid, trade trace timeline
  10. Journal: Diario de trades, CSV export UTF-8, filtros
  11. Configuracoes: Sessions, IP whitelist, users, audit, EA versions, master key
  12. Tools (Alt+5) — Sub-pills: Roadmap | Simulador | Simulacao E2E | Saude | EA Tester
  13. Roadmap: Kanban drag-and-drop (4 colunas), CRUD cards com tags
  14. Simulador: Monte Carlo AF v2
  15. Simulacao E2E / Saude / EA Tester: spec-driven sub-pills
  16. Cadastros (Alt+6, S359) — Sub-pills: Donos | Prop Firms (movida de Tools)
  17. Donos (S359, spec aba-cadastros-donos-conta.md): CRUD AccountOwner (Linniu/Lucas/etc).
    Risco em % por fase per dono (risk_mode='inherit_pool' | 'custom', max_risk_f{1,2}_pct).
    Mesa cap absoluto (D7 S358). Delete bloqueado se TEM contas FK vivas OU mortas
    (R8: usuario so renomeia, propaga via FK automatic). Preview tabela por conta
    mostra MIN(mesa, requested). Arquivo: cadastros.js. Hash legacy /tools/propfirms
    redireciona pra /cadastros/propfirms (window 30d).
  18. Prop Firms: Tabela comparativa (movida de Tools)

Atalhos: Alt+1..5 (tabs), R (refresh), Esc (fechar modal), ? (ajuda)
Backwards compat: switchTab('signals') redireciona automaticamente para Trading > Sinais

Browser Automation (validacao visual)

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
playwright-cli fill e8 "test_runner"                # usuario (ref do snapshot)
playwright-cli fill e11 "PW_test_runner_2026"       # 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

Padroes de Codigo

// 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);

Risk Calculator (Tab Ordens)

3 modos: % Balance, % Equity, USD fixo. Formula: riskMoney / (slDist / tickSize * tickValue)

WebSocket Events

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)

Classificacao de Contas Prop (PASSOU / BREACH / em progresso)

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.

Theme System

XSS (OBRIGATORIO)

Regra: 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)

Deploy Frontend

# 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

Debugging Visual

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

Armadilhas

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

Mudancas recentes (S230-S233)

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

Seletores Importantes

// 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

Debugging

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

Principio

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".

Fluxo Universal

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

Checklist de Contexto (OBRIGATORIO — ANTES de diagnosticar)

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.

Regra de Investigacao Completa

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

4 Fases (NAO PULAR)

Fase 1: Root Cause Investigation

CopyTrade-specific:
- [ ] Broker market hours? Terminal MT5 conectado? VPS SSH reachavel? DB connection?

Fase 2: Pattern Analysis

Fase 3: Hypothesis Testing

Fase 4: Implementation

Red Flags (PARAR)

CopyTrade: Fluxo de Copy (EA-to-EA)

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.

Anti-Loop (4 camadas)

  1. trade_links: Ticket ja em trade_links -> descarta
  2. pending_signals: PendingSignal ativo -> suprime sinais da conta
  3. suppress_markers: MODIFY/CLOSE TTL 10s -> descarta echo
  4. Processed Signal Tracker (EA): g_processedSignalIds[] 100 IDs circular

Quality Monitor (9 checks automaticos, S149)

Roda 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.

Race Conditions Conhecidas

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.

Ferramentas MCP

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).

Checklists por Camada

EA offline

  1. Rodando? tasklist | grep terminal64
  2. Heartbeat? SELECT * FROM heartbeats WHERE account_id=X ORDER BY created_at DESC LIMIT 3
  3. Log EA: powershell Get-Content ...Logs/YYYYMMDD.log -Encoding Unicode -Tail 50

Signal nao executou

  1. Signal existe? 2. Pending criado? 3. EA recebeu? 4. ACK enviado? 5. Trade link? 6. Se FAILED: motivo?

GUI execution falhou

  1. Qual operacao? 2. Log "[GUI]" 3. Posicao existe? 4. Lock file? 5. MT5 minimizado? 6. Popup broker?

Orfaos: WHERE is_closed=false AND created_at < NOW() - INTERVAL '1h'
Duplicados: GROUP BY trade_group_id HAVING COUNT(*) > 4

Dashboard nao atualiza

  1. Servidor rodando? 2. Health OK? 3. WS conectado? (F12 Network) 4. Erro JS? 5. Cache? (Ctrl+Shift+R)

Colunas Corretas (NAO chutar)

Data Sources (Dashboard vs API vs DB)

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

Blast Radius

Arquivos P0 (afeta TUDO): signals.py, ea_ws.py, heartbeat.py, settings.py, main.py.
Mudanca -> backup + teste local + deploy horario seguro + monitorar 5min.

Ferramentas Rapidas

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

Invert Rules

Status: ACTIVE | Ultima revisao: S230 (2026-04-14) — revisado, mudancas em signals.py e GUIExecution.mqh desde 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. Ver specs/ea-observability.md.

Versao: 1.2
Data: 2026-04-14
SSoT para: Logica exata de inversao de sinais no CopyTrade


Conceito

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.


Regra 1: Quem inverte

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.


Regra 2: Condicao de inversao

EA-to-EA (sinal vindo de outro EA)

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=true no mesmo grupo copiam entre si SEM inverter. So inverte quando os flags sao DIFERENTES.

Broadcast (sinal vindo do dashboard)

INVERTE se: account.invert == true

No broadcast nao ha conta de origem, entao a condicao e simplesmente if acct.invert.


Regra 3: O que e invertido (formula)

Pseudo-codigo (identico no servidor e no EA)

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

Exemplo concreto

MASTER abre:  BUY XAUUSD,  SL = 2000,  TP = 2100
SLAVE recebe: SELL XAUUSD, SL = 2100,  TP = 2000
                           (era TP)     (era SL)

Codigo real — Servidor (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

Codigo real — EA (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;
}

Regra 4: Inversao por tipo de sinal

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_ticket do TradeLink para saber qual posicao fechar. Direction/SL/TP sao zerados no CLOSE.


Regra 5: Inversao local (EA offline)

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

Regra 6: Protecao contra mudanca

# 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 invert de uma conta que tem posicoes abertas. Fechar todas antes.


Regra 7: Onde o flag vive

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.


Gotchas (armadilhas conhecidas)

# 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.

Auto-Update EA

Status: ACTIVE | Ultima revisao: S230 (2026-04-14) — revisado, mudancas em ea_ws.py desde S173 sao de observabilidade (Telegram alerts ack_failed, Fase E S228) e NAO afetam o fluxo de auto-update. Logica inalterada. Ver specs/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


Conceito

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.


Fluxo Ponta a Ponta

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)

Maquina de Estados (EA)

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

Como o EA Detecta Update

Fonte 1: Heartbeat HTTP (principal)

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
}

Fonte 2: WebSocket push (secundario)

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.

Fonte 3: Auto-Catchup (automatico)

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.


Comparacao de Versao

// 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).


Download e Verificacao

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

Posicoes Abertas

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.


Lock File (anti-race entre EAs)

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

Script PS1 (7 passos)

O EA gera dinamicamente Common/Files/linniuc_updater.ps1:

  1. Kill MT5Stop-Process -Id $mt5pid -Force (aguarda ate 20s)
  2. Backup — copia LinniuC.ex5LinniuC.ex5.bak
  3. Renomeia .mq5 — move para .mq5.disabled (evita recompilacao)
  4. Copia .ex5 — de linniuc_update.datMQL5/Experts/LinniuC.ex5
  5. Chart injection (v2):
  6. Detecta profile ativo via common.ini (ProfileLast=)
  7. Busca .chr apenas no profile ativo
  8. Se >1 charts com EA: remove extras
  9. Se 0 charts com EA: injeta no primeiro .chr
  10. Corrige InpWebAPIKey no .chr
  11. Restart MT5Start-Process terminal64.exe
  12. Cleanup — remove linniuc_update.dat + linniuc_update.lock

Se erro: Bloco catch restaura .ex5.bak, remove lock, reinicia MT5.


Staged Rollout

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

Monitor de rollout (background, 30min)


Servidor — Deteccao de Loop

# 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

Constantes

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

Endpoints

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)

Tabela DB: ea_versions

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)


Armadilhas Conhecidas

# 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

Mudancas Pendentes (decisao do usuario, S73)

# 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

Banco de Dados

Ultima revisao: S372 (2026-05-23) — DROP COLUMN accounts.prop_firm (era copia stale; Account.prop_firm agora @hybrid_property que deriva prop_firm_id->prop_firms.name). Anterior S294 (2026-04-25) — drift bookkeeping apos S288-S293 em server/models.py: schema base inalterado, snapshot prod em memory/prod-schema.json continua 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]

Conexao

PGPASSWORD=(ver .secrets.local) psql -U copytrade_user -h localhost -d copytrade

Tabelas (24)

accounts

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 |

signals

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) |

signal_acks

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 |

signal_events (S224 — ea-observability spec)

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)

pending_signals

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)

suppress_markers

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 | |

open_positions

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

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 |

ea_remote_logs

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() |

ea_versions

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 |

equity_snapshots

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() |

symbol_info

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() |

todos

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 (v2.71, expandida S161)

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) |
| updated_at | timestamp | null | Ultima atualizacao |

account_resets (v2.71)

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 |

Tabelas auxiliares

users — Usuarios do dashboard (username, password_hash, display_name, is_active)
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)

Tabelas AF v2 (S78)

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).

trade_events (S128)

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}

event_stream (S243 — Telemetria Sweetspot v8)

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

event_outbox (S243 — Telemetria Sweetspot v8)

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

Role 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.


Validacao Sim E2E

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_target da engine.py. Tambem antes de bater o branch
em prod.

Tempo: ~20s pra rodar os 2 quadrantes (death + target).


O que esse cenario testa (em linguagem leve)

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).


Como rodar

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


Checklist objetivo de validacao

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 ✅

Se falhar

pass: false com error: "MODIFY nao disparou em 60s"

Significa que o gate near_death/near_target nao ativou. Causas comuns:

  1. HB injection nao gravou no DB — server descarta HB via WS path se
    throttle WS_HB_DB_INTERVAL=30s esta ativo. Solucao: usar force_http=True
    no _send_hb_now (replica caminho que EA real usa em WS-down).
  2. _real_balance ficou stale — virtual_time avancou muito no SimClock
    e HB virou EXPIRED. Solucao: NAO chamar _sim_tick apos HB inject no
    scenario.
  3. Prop firm com max_dd diferente de 10%cushion = balance - dd_floor
    sai negativo, gate nao ativa. Verificar prop_config da pool SIM.

HTTP 504 do nginx

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 criado

Engine recusou o par. Olhar logs do _af_scheduler — provavel margin
validation, swap hour ou trading window (todos devem estar bypassados pela
config SIM).


Padroes aprendidos (S340 — pattern P224)

  1. VEA deve replicar EA real: comparar payload MQL5 + thresholds
    server-side ANTES de aprovar review E2E. Senao bugs como throttle WS
    ou heartbeat_interval divergente passam despercebidos.
  2. emit() kwargs colidem com **result: se o dict de result ja tem
    chave X, NAO passar X=... explicito alem do **result. Exception
    no caminho sobrescreve o diagnostico real.
  3. HB injection precisa de path HTTP (force_http=True): WS path tem
    throttle 30s server-side, HTTP nao. Replica caminho que EA real usa
    em WS-down.
  4. Resposta sincrona OK ate 60s do nginx: scenario roda em ~9s
    pos-fix. Se passar disso, voltar a investigar.

Spec 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]


Real Path do Simulador

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.


A ideia em 1 parágrafo

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.


Onde isso roda

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.


O fluxo de uma RUN, estação por estação

  [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]  ── passes, mortes, substitutos, invariantes por rodada

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_passescreate_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

Os DOIS gatilhos de morte por drawdown (importante)

Em produção a conta morre por DD de duas formas — e desde S378 #8 o sim cobre as duas:

  1. Pela via do SALDO (check_deaths): no fim da rodada, se o saldo fechado cruzou o piso
    da mesa (piso = tamanho × (1 − max_dd/100)), a conta é marcada morta e o saldo é congelado.
    O VEA cobre isso (manda saldo no heartbeat → servidor lê → check_deaths). É o que o
    cenário dd_recognition valida.
  2. Pela via da EQUITY, dentro do trade (_handle_equity_breach, routes/ea_ws.py): o EA
    de VERDADE vê a equity tocar o piso AO VIVO (antes do trade fechar), fecha tudo na hora e
    reporta equity_floor_breach. É o gatilho REAL e mais rápido em produção (foi a origem do
    bug $NaN do S374). Desde S378 #8 o VEA cobre essa via também: novo método
    report_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 marca
    dead + 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".


As estações do cockpit (cenários prontos)

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 (E2E real com VEA + agendador)
POST /api/sim/scenarios/lifecycle_full_recycle Ciclo de vida completo: pool→rodadas→F2→mortes→recompras (heatmap/timeline/invariantes)

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).


Como rodar uma run de verdade

# 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.


Isolamento e limpeza


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.


Camadas EA/Site

Status: ACTIVE | Ultima revisao: S348+1 (2026-05-15) | Documento vivo — atualizar conforme novas features entram.

Visao geral

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.


Mapa das 25 estacoes + 10 sub-asserts da bateria "Validar completo"

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

Sub-asserts (embutidos em estacao existente, sem custo separado)

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

Decisoes pendentes (recalibradas pos-discussao S348+1)


Como adicionar nova feature: arvore de decisao

Nova 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:

  1. Perda real garantida (rollover, equity floor, cap de lote, posicao orfa em janela critica)
  2. Vazamento sandbox → prod (signal test, isolamento de grupo, isolamento de conta)
  3. Estado corrompido silente (race conditions com efeito permanente)
  4. Hedge/invert errado (BUY virou SELL erradamente, SL/TP trocados)

Como atualizar este documento

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:

  1. Editar specs/camadas-ea-vs-site.md (este arquivo)
  2. Rodar bash scripts/guide-update-and-verify.sh camadas-ea-vs-site — script automatiza:
  3. Validar markdown sintaxe
  4. Commit + push pra VPS (post-receive sync)
  5. Aguardar 5s + curl /guide pra confirmar render OK
  6. Imprime URL final + sugere abrir browser
  7. Confirmar visualmente em https://linniuc.com/guide#camadas-ea-vs-site

Relacionado


Status Planos

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)

brainstorm-phase-auditor — 0/6 Provado

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

ea-tester-bateria — 0/11 Provado

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

spec-status-tracker — 8/10 Provado

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

Camadas Pool/Sim

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.

Visao geral (analogia do restaurante)

Rodar a pool tem 4 camadas. Pensa num restaurante:

  1. Gerente (agendador) — decide QUANDO abrir uma batalha nova.
  2. Chef (engine/cerebro) — dado um grupo de contas, decide a "receita".
  3. Cozinheiro (execucao) — faz a ordem acontecer no broker.
  4. Conta do dia (reset) — zera a perda acumulada do dia no horario certo.

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).


As 4 camadas — mapa rapido

# 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

O que o simulador FINGE (entradas do mundo — saudavel)

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 do dia (camada 4) — regra unica IMPLEMENTADA (S363 #E6)

Piso de risco minimo diario (camada 2, chef) — S364 #297


Resumo de 1 linha

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.


Como atualizar este documento

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:

  1. Editar specs/camadas-pool-vs-simulador.md (este arquivo)
  2. Rodar bash scripts/guide-update-and-verify.sh camadas-pool-vs-simulador (commit + push + verify render)
  3. Confirmar em https://linniuc.com/guide#camadas-pool-vs-simulador

Relacionado


Piso Risco/Dia

Status: ATIVO (dormente até o "Daily DD" ser ligado na pool) | Criado: S364 (2026-05-23) | #297

A ideia em 1 minuto

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.

Quando o piso age (e quando NÃO age)

Como configurar

No modal Config da pool (aba AF Hedge), ao lado do toggle "Daily DD", tem
o campo "Piso Risco/Dia (%)":

Em uma frase

"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."


GUI Ordens

Status: ACTIVE | Criado S371 (2026-05-23) | Fonte: GUIExecution.mqh + telemetria signal_events

Visão geral (a analogia)

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.

Os 3 modos (chaves liga/desliga do EA)

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).

O popup do broker (ler + decidir)

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.

Fluxo de cliques por ação (+ tempos reais medidos na sandbox)

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.

Travas de segurança (o que aborta ou marca FALHA)

Trava Quando dispara Resultado
Erro do broker Popup com texto de erro (rejeitado/insuficiente/off quotes) Aborta + ACK FALHA (não finge sucesso)
Volume não entra Campo de volume diverge do alvo após retries ABORTA com ZERO ordens (nunca abre com tamanho errado)
Delta de posição Após abrir, se não apareceu EXATAMENTE +1 posição (veio 0 ou 2) ACK FALHA (pega anomalia / posição manual concorrente)
Fechar ticket certo Sempre (S371 fix) Casa o botão Fechar pelo número do ticket no texto, nunca o 1º botão genérico → não fecha a oposta por engano
GUI lock Sempre Uma ação GUI por vez; auto-libera se travar >5min (sentinela)

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.

Quão completo está (status S371)

Aspecto Status Nota
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 S371 (sandbox id=3, b3.91.0): fechar 1 de 2 opostas deixou a
outra intacta (antes caíam as 2); fechar sem oposta limpa normal. Fix sandbox-only
até aqui — rollout fleet (deploy_ea.sh, 12 contas) sob aprovação.


Rechamar Contas

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.

O Raio-X da rodada

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":

Quando o botão "Rechamar" aparece

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.

O que o clique faz (e as regras que respeita)

Ao clicar, o sistema:

  1. Confere quem está online de verdade naquele instante (quem não responder
    fica de fora de novo — sem risco de abrir sozinho).
  2. Monta duplas novas só com quem voltou + quem estava online mas sem par,
    sempre cruzando empresas diferentes (a regra de nunca parear a mesma
    empresa continua valendo).
  3. Não toca nas duplas que já estão em batalha — só acrescenta as novas.
  4. Respeita o espaçamento de horário (anti-detecção): a dupla nova não abre
    perto demais de outra dupla da mesma empresa mesmo que essa já tenha
    fechado
    — porque a prop firm viu aquele trade. Também segue a checagem de
    preço e o piso de risco do dia, igual a qualquer dupla normal.
  5. Abre os dois lados juntos ou nenhum (hedge nunca fica com uma perna solta).

O selo "RECHAMADO"

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.

Em resumo


Cotação Recusada

Status: ATIVO (no ar desde EA 3.101.0) | Criado: S392 (2026-05-30) | #352

A ideia em 1 minuto

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.

O que o selo mostra

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.

O que o sistema confere no preço

  1. De quem veio: só aceita preço dos robôs do próprio grupo do par. Resposta de uma conta de
    fora é ignorada.
  2. Se está fresco: o robô manda junto o horário do último tick (a última vez que o preço
    mexeu). Tick velho (mais de 30s) = recusa "cotação velha".
  3. Se bate com o mercado: cruza com a referência do TradingView. Se o preço fugiu mais de 3%
    dela, recusa.

Se passar em tudo: abre normal, idêntico ao de sempre (zero diferença no dia bom). Só barra
quando o preço é ruim.

Por que isso te protege

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.

Em uma frase

"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."


Virada de Juros

Status: ATIVO (no ar desde EA 3.103.0) | Criado: S392 (2026-05-30)

A ideia em 1 minuto

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.

Como funciona (dupla camada)

O que mudou no build 3.103

  1. Robô calcula o horário sozinho: padrão de Nova York com horário de verão (21h UTC no
    verão, 22h UTC no inverno). Não depende mais do site mandar o horário — se o site cair, ainda
    fecha certo.
  2. Nunca fecha posição recém-aberta: o robô só fecha pela virada de juros uma posição que
    já viveu tempo suficiente pra estar de fato na janela. Antes, num caso raro, ele podia
    matar um par recém-aberto em segundos (foi exatamente o que derrubou o par 8875 em ~5s em
    29/05). Corrigido.
  3. Avisa se o horário do site divergir: se o horário que o site manda for diferente do que o
    robô calculou (ex: config da pool com horário de verão desatualizado), o robô avisa — e
    usa o do site quando a config está fresca, ou o calculado quando o site está fora.

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).

Em uma frase

"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."


Baixar Log

Status: ATIVO (no ar desde EA 3.99.0 / servidor S388) | Criado: S392 (2026-05-30) | Admin

A ideia em 1 minuto

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.

Como funciona

  1. Você (admin) pede: "conta X, sobe teu log do dia tal".
  2. O robô daquela conta lê o arquivo de log inteiro do dia no MT5 e sobe em pedacinhos pro
    servidor (sem travar — ele continua operando normalmente enquanto manda).
  3. O servidor remonta o arquivo e deixa baixável pra você ler.

Funciona pela mesma conexão robô↔servidor que já existe, então alcança as contas dos outros PCs
também.

Detalhes que importam

Quando usar

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.

Em uma frase

"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."