| 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, Mahoraga 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
/quality-gates Pre-deploy, "validar" ~70%
/validate-loop "testar", "validar" ~70%
/verification "deploy feito", "testes passaram" ~70%
/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 Mahoraga)

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

Signal Lifecycle — Whitepaper Definitivo

Status: ACTIVE | Ultima revisao: S221 (2026-04-13) — adicionado EC-Test: variante de injecao de sinais de teste (Phase C, signal_source=test, is_test isolation)

Versao: 2.1
Data: 2026-03-31
Status: v2.1: Adicionado 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

Fluxo geral

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

OPEN (linhas 221-280)

  1. Gera trade_group_id (UUID)
  2. Calcula sl_distance = abs(price - sl), tp_distance = abs(tp - price)
  3. Cria Signal com origin="ea", expires_at = now + 1min
  4. Cria SignalAck com status="ORIGIN" para conta master
  5. Cria TradeLink com is_origin=True para conta master
  6. Busca peers do mesmo group_id (exceto master)
  7. Para cada peer:
  8. Se peer.invert != account.invert → aplica inversao (ver specs/invert-rules.md)
  9. Cria Signal + PendingSignal (TTL 60s)
  10. Push WS: ea_ws_manager.notify_account(peer.id, "new_signal")

Regra R5: Circuit Breaker

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

MODIFY (linhas 370-400)

  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: cria Signal + PendingSignal com ticket = local_ticket do peer
  5. Aplica inversao se necessario

CLOSE (linhas 497-520)

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

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.


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)

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)



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


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

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)
WebBridge_PostReverse() POST /api/reverse Emergency reverse

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.


AF Hedge

AF v2 — Regras Definitivas

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 NUNCA colocar 2 contas da MESMA prop uma contra a outra. Se nao tem par de prop 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. 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

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. 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: max_risk >= $2.500 AND max_dd >= 10%

# 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
- FTP Classic: max_dd 8% < 10%

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: Mais fraco manda (P57) desired_loss/desired_gain limitados a min(cushion, risk_usd) + margin. Margin ($2-10) preservada pra garantir cruzamento do floor/target (sem margem, conta pode empatar exatamente no limite). NUNCA usar cushion individual se trade foi dimensionado pelo risco do PAR.
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 (XAUUSD: $10, BTCUSD: $50). 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). >= $10 USD (XAUUSD), $50 (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?
     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): XAUUSD: $10 | BTCUSD: $50 | Default: $10
Configuravel por pool: rsafe2_price_gate no config JSONB (override via API ou dashboard)

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 / sl_max $25 / $50 $400 / $800
dz_min / dz_max $500 / $1000 $2000 / $4000
rsafe2_price_gate $10 $200
push_buffer_usd $100 $100
spread_pct 2% 2%

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)

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

EA

Status: ACTIVE | Ultima revisao: S194 (2026-04-06)

SSoT para: Modulos EA, compilacao, versionamento, erros MT5, armadilhas
Versao: 2.2 (EA v3.69.0, atualizado S194 — 2026-04-06). Desde 2.1: WebBridge usa Digits() no DoubleToString, MODIFY-VERIFY queue, heartbeat async retry, mensagens legiveis.
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)
GUIExecution.mqh 1156 g_gui_ GUI automation Win32. OPEN=F9, MODIFY=ListView+DBLCLK, CLOSE=ListView+Fechar. Lock file anti-conflito
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 399 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)
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

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

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: Copia .mq5 Dropbox→Terminal, compila via MetaEditor CLI, copia .ex5 de volta pro Dropbox.

Terminal MT5: C:\Users\mrodr\AppData\Roaming\MetaQuotes\Terminal\53785E099C927DB68A545C249CDBCE06\MQL5
Junction: MQL5\Include\CopyTrade\ -> Dropbox (editar no Dropbox, MetaEditor le direto)

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.25.0+):
- Backoff: imediato -> 5s -> 15s -> 30s (max)
- Health check 60s: se connected=true mas sem msgs -> reset
- Fallback: 3x WS fail -> HTTP polling (2-5s adaptive). WS retenta cada 120s

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)

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]

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
ExecStart=uvicorn app.main:app --host 127.0.0.1 --port 8000
Restart=always | RestartSec=5
WorkingDirectory=/opt/copytrade-server

1 worker (single-process). Restart limit: 5 em 10s, depois "failed".

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

Cron

0 3 * * *    backup_daily.sh        # Backup diario 3h UTC
*/2 * * * *  healthcheck_v2.sh      # Watchdog: auto-restart se cair
*/5 * * * *  monitor.sh             # Monitor proativo + Telegram alert

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]

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)
tournament.js Tab Torneio (pairings, hedge)
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 | Prop Firms | Simulador
  13. Roadmap: Kanban drag-and-drop (4 colunas), CRUD cards com tags
  14. Prop Firms: Tabela comparativa
  15. Simulador: Monte Carlo AF v2

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')  // Torneio
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)

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 Endpoints lentos: usar fetch() direto com AbortController
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

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

Invert Rules — Referencia Rapida

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.
G5 reverse_signal e legado Endpoint antigo que cria CLOSE simples sem inversao nem trade_links. Nao usar no fluxo principal.

Auto-Update

Auto-Update EA — Referencia Rapida

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, heartbeat response automaticamente promove para a versao stable mais recente. Sem acao do operador — "pegar o trem" automaticamente.

Implementado em heartbeat.py:740-763. Util quando contas novas sao criadas apos um release.


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

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 | varchar | '' | Nome da prop firm (FTMO, etc) |
| prop_size | varchar | '' | 10k/25k/50k/100k/200k |
| prop_steps | varchar | '' | 1-step/2-step/3-step |
| 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) |

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(10) | Canal de envio (ws/poll) |
| detection_method | varchar(10) | Metodo de deteccao |
| 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 | char(32) | S224: W3C-inspired trace identifier (nullable pre-migration) |

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 | char(32) | S224: carrega mesmo trace_id do signal pai (nullable pre-migration) |

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

pending_signals

Fila de sinais aguardando execucao. TTL 5 minutos (300s).
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | Conta destino |
| symbol | varchar | |
| direction | varchar | BUY/SELL |
| volume | float | |
| signal_id | int FK | |
| trade_group_id | varchar | |
| expires_at | timestamp | TTL 5 min (300s) |
| resolved | boolean | false |
| created_at | timestamp | |

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 (FTMO Swing, FundedNext, etc) |
| default_steps | varchar(20) | '2-step' | 1-step/2-step/3-step |
| 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() | |
| program | varchar(100) | '' | Nome do programa (ex: High Stakes Challenge) |
| 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)
pairings — Pareamentos de torneio (pool, round_num, account_a/b, balance_a/b, diff_pct, status, bracket_id, match_label, trade_group_id, winner_account_id, profit_a, profit_b, started_at, match_duration_s)
tournament_brackets — Brackets de torneio (name, pool, status, config JSONB, account_ids INT[], bracket_structure JSONB, current_round, matches_today, last_match_date, champion_account_id, created_at, completed_at)
ip_whitelist — IPs permitidos (ip_address, description, is_active)

match_metrics (S100)

Metricas detalhadas de cada batalha de torneio — spread, swap, slippage, drawdown.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| pairing_id | int FK pairings | Pareamento relacionado |
| bracket_id | int | Bracket do torneio |
| match_label | varchar(50) | Label do match |
| symbol | varchar(20) | Simbolo (XAUUSD etc) |
| direction | varchar(10) | BUY/SELL |
| broker_a/b | varchar(100) | Corretoras |
| volume | float | Volume em lotes |
| open_price_a/b | float | Preco abertura conta A/B |
| close_price_a/b | float | Preco fechamento conta A/B |
| intended_sl/tp | float | SL/TP planejados |
| actual_sl_a/tp_a | float | SL/TP reais aplicados |
| profit_a/b | float | P&L por conta |
| swap_a/b | float | Swap por conta |
| net_pnl | float | P&L liquido combinado |
| spread_at_open/close | float | Spread no momento de abertura/fechamento |
| slippage_open_a/b | float | Slippage na abertura |
| slippage_close_a/b | float | Slippage no fechamento |
| started_at/ended_at | timestamp | Inicio/fim do match |
| duration_s | int | Duracao em segundos |
| close_trigger | varchar(20) | O que causou o close (sl/tp/timeout) |
| max_dd_a/b | float | Drawdown maximo |
| max_floating_loss_a/b | float | Perda flutuante maxima |
| balance_a/b_before/after | float | Balance antes/depois |
| risk_pct | float | Risco % |
| risk_money | float | Risco $ |
| latency_open/close_a/b | float | Latencia sinal→execucao (s) |
| commission_a/b | float | Comissao por conta |
| spread_broker_a/b | float | Spread do broker |
| winner_account_id | int | Conta vencedora |
| winner_reason | varchar(50) | Motivo da vitoria |
| created_at | timestamp | |

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