Status: ACTIVE | Ultima revisao: S152 (2026-03-30)
DASHBOARD (browser) → HTTP/WS → SERVIDOR (VPS FastAPI) → HTTP polling 1-3s → EAs MT5 (MQL5)
IDEIA / BUG REPORT
│
▼
CLAUDE INVESTIGA
│ ←── [100%] Guardian injeta patterns relevantes (59 ativos)
│ ←── [100%] Spec-loader carrega docs por topico
│ ←── [100%] Agent-router sugere subagent (VPS/EA/Dashboard)
│
▼
CLAUDE EDITA CODIGO
│ ←── [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)
| Nivel | Mecanismo | Garantia | Quando usar |
|---|---|---|---|
| 1 | Hook (31 ativos) | 100% — deterministico | Regras criticas, protecoes |
| 2 | CLAUDE.md | ~90% — pode esquecer | Guidelines gerais |
| 3 | Spec .md | ~70% — so carrega por keyword | Referencia detalhada |
| 4 | Memory .md | ~50% — le se consultado | Dados operacionais |
Regra de ouro: Se algo precisa ser 100%, precisa de HOOK. Regra so no CLAUDE.md nao basta.
| Hook | Quando | O que bloqueia |
|---|---|---|
| validate-commit.sh | git commit | Secrets, drift, specs, build |
| protect-specs.sh (Tier 1) | Edit/Write | Whitepapers sem aprovacao |
| pre-deploy-quality.sh | scp .py → VPS | Property tests falhando |
| protect-ea-deploy.sh | cp/mv .ex5 | Deploy manual (forcar deploy_ea.sh) |
| frontend-lint.py | Edit JS | Globals soltos, CSS injection |
| auto-capture.py | Fim sessao | Licoes nao capturadas |
| Hook | Quando | O que avisa |
|---|---|---|
| protect-specs.sh (Tier 2) | Edit/Write | Hooks, config, Patterns data |
| af-stress-protocol.sh | Edit AF engine | Lembrete de stress test |
| post-commit-learn.sh | Commit fix: | Sugere /learn |
| Hook | Quando | O que faz |
|---|---|---|
| Guardian (59 patterns) | Cada mensagem | Injeta warnings por keyword |
| Spec-loader | Cada mensagem | Carrega docs relevantes |
| Agent-router | Cada mensagem | Sugere subagent |
| session-diagnostics.sh | Inicio sessao | VPS health, git, EA build |
| framework-health.py | Inicio sessao | 9 pilares OK? |
| spec-freshness.py | Inicio sessao | Specs desatualizados? |
| sync-docs-vps.sh | Post-commit | Sincroniza specs → VPS /docs/ |
| track-skill-usage.py | Qualquer skill | Registra uso |
| post-deploy-verify.sh | Post-deploy | Health + invariantes SQL |
| Processo | Frequencia | O que faz |
|---|---|---|
| healthcheck_v2.sh | 2min | Auto-restart se cair |
| monitor.sh | 5min | Telegram se problema |
| backup_daily.sh | 3h UTC | DB + configs |
| _auto_close_expired_pendings | 15s | Fecha sinais expirados |
| _periodic_cleanup | 15min | Limpeza DB |
| _run_drawdown_checker | 5min | Alerta drawdown Telegram |
| Ferramenta | Melhor para | Tokens |
|---|---|---|
| Playwright MCP | Unico browser MCP (snapshot, screenshot, automacao) | ~114k |
| Skill | Trigger | Garantia |
|---|---|---|
| /validate | "validar", "varrer tudo", "pre-deploy", "deploy feito", "simular fluxo", "E2E" | ~70% (mesclada S271 de /prove + /validate-loop + /quality-gates) |
| /brainstorming | "design", "trade-off" | ~70% |
| /debug-sistematico | "debug", "mesmo erro" | ~70% |
| /learn | Licao descoberta | ~90% (CLAUDE.md) |
| /compound | Fim de sessao | ~90% (CLAUDE.md) |
| /commit | Commitar | Manual |
| /deploy | Deploy VPS | Manual |
| /status | Diagnostico | Manual |
Bug encontrado → /learn captura pattern → Guardian injeta na proxima sessao
│ │
│ ▼
│ Claude evita repetir
│ │
└──── 3x repetido? → Promover pro CLAUDE.md ──┘
Status: ACTIVE | Ultima revisao: S194 (2026-04-06) — property tests atualizados (test_risk_engine.py), skills /rapid + /triage adicionadas (S192)
SSoT para: Scripts de qualidade, testes automaticos, monitoramento em producao
Relacionados:specs/debugging.md,specs/sweetspot-pilares.md
Sistema de 3 camadas que previne e detecta problemas automaticamente:
1. Monitoramento — roda 24/7 na VPS, avisa no Telegram
2. Testes preventivos — roda antes de deployar, encontra bugs antes de irem pra producao
3. Testes de resiliencia — simula falhas pra ver se o servidor aguenta
scripts/quality/
quality_monitor.py <- Roda na VPS a cada 15min (cron)
watchdog.py <- 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
A cada 15 minutos, roda 9 queries SQL no banco verificando regras que nunca devem ser quebradas. Se alguma quebrar, manda Telegram.
*/15 * * * * quality_monitor.py (9 checks)
*/30 * * * * watchdog.py (vigia o monitor)
| # | 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)
O watchdog verifica se o quality_monitor esta rodando. Se o heartbeat file tiver mais de 30 minutos, avisa no Telegram: "Quality Monitor parou!"
Editar quality_monitor.py, adicionar entrada no dict CHECKS:
"nome_do_check": {
"description": "O que verifica em linguagem simples",
"severity": "CRITICAL|HIGH|MEDIUM",
"market_only": True|False,
"action": "O que fazer quando violacao encontrada",
"query": "SELECT ... FROM ... WHERE <condicao_de_violacao>"
}
Extrai funcoes do servidor (sem precisar de banco de dados) e joga milhares de inputs aleatorios tentando quebrar. Se quebrar, mostra exatamente qual input causou o problema.
pytest tests/property/ -v # Rodar todos (52 testes, ~30s)
pytest tests/property/ -q # Resumido
| Arquivo | Grupo | Testes | O que verifica |
|---|---|---|---|
| test_pure_functions.py | calculate_volume | 5 | Lot size nunca negativo, nunca NaN, sempre 2 decimais |
| test_pure_functions.py | validate_pair_pnl | 6 | Validacao hedge: mesmo sinal rejeitado, oposto aceito |
| test_pure_functions.py | clean_nan_json | 8 | NaN/Infinity limpos do JSON do EA |
| test_pure_functions.py | market_hours | 8 | Sabado fechado, domingo abre 18h, etc |
| test_pure_functions.py | financial_invariants | 3 | Propriedades matematicas do hedge |
| test_risk_engine.py | individual_risk | 5 | Risk nunca negativo pra conta viva, nunca > hard cap $3k, nunca > daily DD |
| test_risk_engine.py | calc_risk | 3 | Risk do par sempre >= 0 |
| test_risk_engine.py | daily_dd | 5 | DD remaining nunca negativo, nunca excede limite operacional |
| test_risk_engine.py | osb_weight | 5 | Anti-OSB detection correto |
| test_risk_engine.py | dist_cush | 4 | Distancia/cushion nunca negativo, dead account risk <= cushion |
Nota S194:
test_risk_engine.pyrefatorado — importa funcoes reais deapp.af.engine(SSoT) em vez de manter copias locais (FakeProp/FakePA removidos, -154 linhas).
Quando eu faco SCP de arquivo .py pro VPS, o hook pre-deploy-quality.sh roda automaticamente:
- Se testes passam → deploy prossegue
- Se testes falham → deploy BLOQUEADO com mensagem de erro
Manda requests "errados" de proposito pro servidor e verifica que ele nao crasha (retorna 500).
python scripts/quality/fault_injection.py --all # Todos os 8 cenarios
python scripts/quality/fault_injection.py --scenario X # Um cenario especifico
python scripts/quality/fault_injection.py --dry-run # Mostra o que faria
| Cenario | O que simula | Esperado |
|---|---|---|
| duplicate_signal | Signal repetido (mesmo ticket) | Anti-loop ignora o 2o |
| ack_without_signal | ACK pra signal que nao existe | 404 (nao 500) |
| malformed_heartbeat | Dados com NaN/Infinity | Trata sem crashar |
| rapid_fire_signals | 10 signals em 2 segundos | Nao crasha |
| stale_ack | ACK pra signal antigo | Trata com graca |
| wrong_direction | direction="SIDEWAYS" | Nao crasha |
| zero_volume | volume=0 | Nao crasha |
| huge_values | Numeros gigantes | Nao crasha |
| Data | Bug | Como encontrou | Impacto |
|---|---|---|---|
| 2026-03-29 | calculate_volume crash NaN | Property test (tick_value=inf) | Poderia travar calculo de volume |
| 2026-03-29 | clean_nan_json arrays | Property test ([NaN,...]) | NaN passava pro banco |
| 2026-03-29 | same_sign_pnl par 195524 | Quality monitor (1a rodada) | Edge case mercado, nao bug |
| Metrica | Como medir | Meta |
|---|---|---|
| Violations no monitor | Log + Telegram | 0 CRITICAL, < 3 HIGH/dia |
| Property tests passando | pytest tests/property/ |
52/52 (100%) |
| Fault injection passando | fault_injection.py --all |
8/8 (100%) |
| Bugs encontrados por property test | Contagem no changelog | Crescente |
| Tempo sem bug silencioso | Dias desde ultimo bug encontrado pelo monitor | Crescente |
Status: ACTIVE | Ultima revisao: S315.1 (2026-05-02) — Onda 5 cleanup: Etapa 4 aponta canonical (
create_signal_canonical+dispatch_pending_to_eas). Detalhes emspecs/signal-dispatch-canonical.md.
Versao: 2.3
Data: 2026-05-02
Status: v2.3 (S315.1 Onda 5): Etapa 4 atualizada — Signal+PendingSignal+TradeLink+SignalAck criados via create_signal_canonical() (SSoT em server/signal_dispatch.py). WS push pos-commit via dispatch_pending_to_eas() (substitui _push_signals_to_eas legacy, removido). Origin agora canonico via enum OriginType (lista exaustiva — ver specs/signal-dispatch-canonical.md). Fallback signal.origin or "ea" removido em ACK logging (signals legacy ficam origin=NULL, dashboard renderiza "(legacy)"). v2.2: Adicionado secao Trace ID. v2.1: EA remote logs pipeline, mapa de funcoes EA/servidor, ACK validation V1-V4, reconciliation pos-trade, grupo-only routing (S1015).
SSoT para: Ciclo completo de vida de um sinal — da deteccao ate o trade_link
┌─────────────────────────────────────────────────────────────────────────────┐
│ CICLO DE VIDA DE UM SINAL │
│ │
│ EA Master Servidor EA Slave │
│ ───────── ───────── ───────── │
│ 1. Detecta trade ──WS/HTTP──> 2. Valida + armazena │
│ (OTT ou SE) Signal + PendingSignal │
│ + TradeLink (origin) │
│ 3. Distribui ──WS push──> 4. Recebe sinal │
│ (aplica invert) HTTP poll (safety net) │
│ 5. Executa via GUI │
│ 6. Recebe ACK <──HTTP POST── 6. Envia ACK │
│ SignalAck + TradeLink │
│ PendingSignal.resolved │
└─────────────────────────────────────────────────────────────────────────────┘
O EA tem 2 mecanismos paralelos que detectam trades. Ambos alimentam a mesma fila.
| Aspecto | Detalhe |
|---|---|
| Arquivo | Experts/LinniuC.mq5:1342 |
| Trigger | TRADE_TRANSACTION_DEAL_ADD (qualquer deal executado) |
| Latencia | ~0ms (evento nativo MT5) |
| Campo | detection_method = "OTT" |
Fase 1 (sempre executa, mesmo com g_busy=true):
- Armazena SE_DealHint via SE_StoreDealHint() com: positionId, symbol, entry (IN/OUT), closeReason (SL/TP/STOP_OUT/MANUAL), dealVolume, dealPrice
- TTL do hint: 10 segundos | Buffer: 10 hints max (SE_MAX_DEAL_HINTS)
Fase 2 (so se g_busy=false):
- Chama SnapshotEngine_ProcessCycle() + SnapshotEngine_FlushQueueWeb() imediatamente
| Aspecto | Detalhe |
|---|---|
| Arquivo | Include/CopyTrade/SnapshotEngine.mqh:628 |
| Trigger | Timer a cada InpPollingMs (200ms) + OnTick() |
| Latencia | 0-200ms (pior caso = intervalo do timer) |
| Campo | detection_method = "SE" |
Logica de comparacao (SE_CompareSnapshots, linha 252):
| Situacao | Acao |
|---|---|
Ticket em curr mas nao em prev |
SE_EnqueueOpen() |
| Ticket em ambos, SL/TP/volume mudou | SE_EnqueueModify() (com anti-echo) |
Ticket em prev mas nao em curr |
SE_EnqueueClose() (com anti-echo) |
Como decide OTT vs SE: No momento do enqueue, verifica SE_HasDealHint(ticket):
- Hint existe → detection_method = "OTT"
- Hint nao existe → detection_method = "SE"
Na primeira chamada, o SE apenas armazena o snapshot atual como
g_se_prevSnap. Nenhum sinal e gerado. Isso evita que todas as posicoes existentes sejam reportadas como "novas" ao iniciar o EA.
Arquivo: SnapshotEngine.mqh
| Aspecto | Valor |
|---|---|
| Capacidade | SE_MAX_QUEUE = 200 sinais |
| TTL por item | 300 segundos (5 min) — expirado = descartado |
| Estrutura | SE_QueueItem: action, ticket, symbol, direction, volume, sl, tp, retryCount, enqueuedTick, close_reason, detection_method, deal_price (preco de fill exato do OTT), profit (DEAL_PROFIT + COMMISSION + SWAP) |
| Max retries | SE_MAX_RETRIES = 5 — sinal descartado apos 5 falhas |
| detection_method | OTT (OnTradeTransaction), SE (SnapshotEngine), af_engine (AF hedge), af_push_modify (AF MODIFY push) |
CLOSE vai para o INICIO da fila (linha 364-377). OPEN e MODIFY vao para o final.
MODIFY ja existente na fila para o mesmo ticket: atualiza sl, tp, volume no lugar (nao duplica).
Se fila tem 200 itens, novos sinais sao descartados com log
[ALERTA] Fila CHEIA.
Arquivo: SnapshotEngine.mqh, funcao SnapshotEngine_FlushQueueWeb() (linha 424)
| Tipo | Cooldown | Efeito |
|---|---|---|
| CLOSE | 500ms | CLOSE nao eh bloqueado por OPEN recente |
| MODIFY | 1000ms | Evita flood de MODIFY rapidos |
| OPEN | 500ms | Evita flood de OPEN rapidos |
| Tentativa | Delay |
|---|---|
| 1a | 1s |
| 2a | 2s |
| 3a | 4s |
| 4a | 8s |
| 5a | 16s (cap) |
| Max retries | 10 — depois descarta com log "orphan checker no servidor ira compensar" |
Funcao: SE_SendSignalViaWS() (linha 389)
{
"type": "signal",
"action": "OPEN",
"ticket": 123456,
"symbol": "XAUUSD",
"direction": "BUY",
"volume": 0.10,
"price": 2650.00,
"sl": 2640.00,
"tp": 2660.00,
"close_reason": "",
"signal_channel": "ws",
"detection_method": "OTT",
"queued_duration_ms": 15
}
Se WS_IsConnected() retorna false OU SE_SendSignalViaWS() falha → fallback imediato para HTTP.
Funcao: WebBridge_PostSignal() (WebBridge.mqh:686)
Endpoint: POST /api/signals
Headers: X-API-Key, X-Account-Num, Content-Type: application/json
| Tipo | Tentativas HTTP |
|---|---|
| CLOSE | 3 tentativas com Sleep(500*attempt) entre elas |
| OPEN/MODIFY | 1 tentativa |
Arquivo: /opt/copytrade-server/app/routes/signals.py
Endpoint: POST /api/signals
SSoT canonica (S314.6+, parcial — Onda 4 lifecycle pendente): Signal+PendingSignal+TradeLink+SignalAck sao criados via
create_signal_canonical()emserver/signal_dispatch.py(B7+B11 fix — single source) em rotas dashboard (broadcast_*) + AF engine 100% (af/signals.py + routes/af.py — S315.0). Master OPEN do endpoint EAPOST /api/signals(batch insert ~linha 762) ainda usasa_insert(Signal).values(signal_rows)batch insert manual (NAO o helpercreate_signal_canonical()) — sera migrado em Onda 4 lifecycle. Atualizacao S361 (doc-fresh):originesignal_sourceJA usam enums canonicos (OriginType.EA.value/SignalSource.EA.value, linhas ~747/755), NAO mais strings cruas"ea"/"manual"— o drift R9 de string hardcoded foi resolvido; resta apenas o batch insert nao passar pelo helper unico. WS push pos-commit viadispatch_pending_to_eas(db, trade_group_id)100% canonical em todos os paths. Detalhes completos + escopo Onda 4:specs/signal-dispatch-canonical.md.
X-API-Key → verify_api_key)actiontrade_group_id (UUID)sl_distance = abs(price - sl), tp_distance = abs(tp - price)create_signal_canonical(origin=OriginType.EA, signal_source=SignalSource.EA, create_pending_signal=False, create_trade_link=True, is_origin_link=True, create_origin_ack=True) — cria Signal + TradeLink(is_origin=True) + SignalAck("ORIGIN") atomicogroup_id (exceto master)peer.invert != account.invert → caller aplica _invert_direction/_invert_sl_tp ANTES (R8: caller invert, canonical e agnostica)create_signal_canonical(create_pending_signal=True, expires_in_seconds=PENDING_SIGNAL_TTL_SECONDS) — cria Signal + PendingSignal (TTL 60s)await dispatch_pending_to_eas(db, trade_group_id) faz WS push uniforme via _build_ws_payload (R11)Se conta slave tem >=
CIRCUIT_BREAKER_MAX_POSITIONS(10) posicoes abertas, o OPEN e bloqueado para aquela conta.
TradeLink ativo pelo ticket do mastertrade_group_idtrade_group_id + MODIFY, ignora (anti-echo)create_signal_canonical(action="MODIFY", ticket=local_ticket_do_peer, ...) — cria Signal + PendingSignalpeer.invert != account.invertawait dispatch_pending_to_eas(db, trade_group_id)TradeLink ativo pelo ticketprofit + close_price do link existente e retorna "profit_updated" (sem re-fechar)TradeLink da conta master (is_closed=True, closed_at=now)deal_price do EA (preco exato de fill do deal) — prioridade maximadeal_profit do EA (DEAL_PROFIT + COMMISSION + SWAP consolidado)OpenPosition cache (ultimo bid do heartbeat)trade_group_idtrade_group_id + CLOSE, ignoracreate_signal_canonical(action="CLOSE", ticket=local_ticket_do_peer, ...) — cria Signal + PendingSignalSuppressMarker com TTL 10s para o trade_group_id (anti-echo servidor)log_trade_event(event_type="CLOSE", close_reason, profit, close_price)await dispatch_pending_to_eas(db, trade_group_id)Tabela: pending_signals
| Campo | Descricao |
|---|---|
account_id |
Conta slave destino |
signal_id |
FK para signals |
trade_group_id |
UUID do grupo |
expires_at |
now + 60s (1 minuto, centralizado em constants.py) |
resolved |
false ate execucao ou expiracao |
PendingSignal expira em 60s (1 min). Sinais OPEN nao executados sao auto-fechados pela background task (a cada 15s). Sinais MODIFY/CLOSE expirados sao marcados
resolved=Truepelo cleanup periodico (loop a cada 15min emmain.py).
Endpoint
POST /api/admin/cleanup/pending-signalspara forcar limpeza.
O servidor chama ea_ws_manager.notify_account(account_id, "new_signal") que envia o sinal completo via WS.
Endpoint: GET /api/signals/pending
Funcao EA: WebBridge_PollSignals() (WebBridge.mqh:760)
| Situacao | Intervalo de poll |
|---|---|
| WS conectado e ativo | 10 segundos (safety net) |
| WS desconectado | 1-3 segundos (configuravel InpPollInterval) |
O EA SEMPRE faz poll HTTP, mesmo com WS ativo. O WS acelera a entrega, o HTTP garante que nada se perde.
Resume semantics — EA informa "ate onde ja processou":
g_ws_lastSignalId (long) persistido em MQL5/Files/ws_last_signal_id.txtLNC_MarkSignalProcessed (monotonic: so cresce){"type":"auth", "api_key":"...", "account_num":"...", "last_signal_id":N}_flush_pending_signals em ea_ws.py) filtra WHERE signal_id > last_signal_idPor que: antes, ao reconectar, servidor re-enviava TODOS os pendings unresolved. Se EA ja processou via HTTP poll no intervalo (comum — HTTP 1-3s vencia WS reconnect 22s backoff na S252), o buffer circular g_processedSignalIds[100] protegia mas gerava log noise + trafego redundante.
Fail-open (backward compat):
- EA velho sem campo: last_signal_id=0 -> envia tudo (comportamento pre-S253 preservado)
- Arquivo corrompido: StringToInteger retorna 0 -> mesmo fallback
- Escrita em disco falha: global atualizado mas nao persistido -> proximo restart re-flusha (buffer 100 IDs protege)
Monotonic: WS_SetLastSignalId so aceita sigId > g_ws_lastSignalId. Evita regressao se LNC_MarkSignalProcessed receber IDs fora de ordem.
_detect_missing_positions (detecta posicoes orfas comparando HB vs TradeLink) agora roda em AMBOS os paths:
- WS heartbeat (_handle_ws_heartbeat): desde S224+
- HTTP heartbeat (POST /api/heartbeat): desde S253 Onda 2 (antes: delay de 15min via orphan task)
Anti-race: Advisory lock Postgres (pg_try_advisory_lock(account_id)) impede concorrencia. Se WS path ja esta rodando, HTTP path skipa (nao-blocking). Evita double-close de posicao.
Arquivo: Experts/LinniuC.mq5 + Include/CopyTrade/GUIExecution.mqh
| Check | Detalhe |
|---|---|
| Anti-retry | g_processedSignalIds[] (buffer circular de 100 IDs). Sinal ja processado → ignora, mesmo que execucao tenha falhado |
| Idade maxima | (TimeGMT() - signalTime) > InpSignalMaxAge (60s) → descarta com ACK SKIPPED |
GUIExecution_Open, GUIExecution.mqh:765)GUI_AcquireGUILock() — lock exclusivo em arquivo (TTL 30s para stale locks)GUI_EnsureVisible() — restaura MT5 se minimizadoGUI_CloseAllMT5Dialogs() — fecha dialogos residuaisGUI_OpenF9() — WM_COMMAND 32848 abre dialogo "Nova Ordem"GUI_WaitForDialog() — timeout InpTimeoutMs (5000ms)GUI_SelectSymbol() — seleciona simbolo no combo (ID 10331)GUI_WaitForSubDialog() — aguarda controles carregaremGUI_TryConfirmBrokerPopup() — confirma popups (leverage, risk) ou detecta erros (failed, insufficient)Pos-execucao:
- GUIExecution_DetectNewTicket() — compara lista de posicoes antes/depois
- Fallback: SE_FindRecentOpenDeal() usa DealHint do OTT
- SE_SuppressTicket(newTicket, 5) — suprime re-deteccao por 5s (Pattern #34, S84)
GUIExecution_Modify, GUIExecution.mqh:844)GUI_AcquireGUILock()GUI_OpenPositionDialogSafe(ticket) — localiza na ListView (10328), duplo-cliqueGUI_VerifyDialogTicket() — verifica titulo contem ticket esperadoSE_SuppressTicket(localTicket, 10) — suprime por 10 segundosGUIExecution_Close, GUIExecution.mqh:948)GUI_AcquireGUILock()GUI_EnsureVisible()GUI_ForceCloseResidualDialogs() entre elasGUI_OpenPositionDialogSafe(ticket) — ListView → duplo-cliqueSE_SuppressTicket(localTicket, 10) — suprime por 10 segundosArquivo: WebBridge.mqh:808
Endpoint: POST /api/signals/{signalId}/ack
{
"status": "FILLED|FAILED|SKIPPED",
"local_ticket": 789012,
"error_msg": "",
"open_price": 2650.12,
"applied_sl": 2640.00,
"applied_tp": 2660.00,
"actual_volume": 0.10,
"receive_channel": "ws_push|http_poll",
"gui_duration_ms": 1450
}
| Status | Quando |
|---|---|
FILLED |
Execucao GUI bem-sucedida |
FAILED |
GUI falhou ou ticket nao encontrado |
SKIPPED |
Sinal muito antigo (> InpSignalMaxAge) |
| Fase | Tentativas | Backoff |
|---|---|---|
| Imediata | 3x | 1s, 2s (exponencial) |
| Fila persistente | Ilimitado (max 20 na fila) | 5s → 10s → 20s → 30s (cap) |
SignalAck com todos os camposFILLED: cria TradeLink com is_origin=False, local_ticket do slavePendingSignal.resolved=Trueapplied_sl vs target_sl, applied_tp vs target_tp — mostra divergencia entre o que foi pedido e o que o broker aplicoulog_trade_event(event_type="ACK", status, action="OPEN"|"MODIFY", context)| Campo | Descricao |
|---|---|
server_received_at |
Timestamp de chegada no servidor |
server_processing_ms |
Tempo do recebimento ate commit no DB |
receive_channel |
ws_push ou http_poll (como o slave recebeu) |
gui_duration_ms |
Tempo de execucao GUI no slave |
total_delay_ms |
De signal.created_at ate ack.executed_at (latencia total) |
Capturados pelo EA via SymbolInfoTick() 1 linha antes de GUIExecution_Open/Close/Modify. Servidor calcula slippages no handler ACK. Backwards-compat: ACKs pre-S336 (EA build < 3.85.3) tem campos NULL.
| Campo | Tipo | Descricao |
|---|---|---|
bid_at_request |
Float | BID via SymbolInfoTick no momento do click GUI (R1) |
ask_at_request |
Float | ASK via SymbolInfoTick no mesmo instante (par sempre junto — INV4) |
exec_slip_pts |
Float | Slippage GUI+broker: \|deal_price - quote_at_request_lado_correto\|. NULL pra MODIFY (sem deal_price) ou stale (>60s). |
pipeline_slip_pts |
Float | Slippage rota inteira: \|deal_price - signal.price\|. Independe de bid/ask. |
Lados da cotacao (R2):
- OPEN BUY -> ASK (compra ao ASK)
- OPEN SELL -> BID (vende ao BID)
- CLOSE BUY -> BID (vende pra fechar)
- CLOSE SELL-> ASK (compra pra fechar)
- MODIFY -> guarda bid+ask pra audit (saber se preco passou pelo SL/TP em transito), exec_slip = NULL
Tag close_reason="ROLLOVER" (Layer 1 EA anti-swap): Rollover_TryClose chama Rollover_TagTicket antes do GUI close; SnapshotEngine override close_reason -> "ROLLOVER" em vez de "MANUAL" (default DEAL_REASON_CLIENT). Distingue de Layer 2 servidor (close_reason="ROLLOVER_FALLBACK" via close_pair_positions).
Spec completa: specs/exec-slippage-telemetria.md.
Tabela: trade_links
| Campo | Master | Slave |
|---|---|---|
trade_group_id |
UUID (gerado no OPEN) | mesmo UUID |
is_origin |
True |
False |
local_ticket |
ticket MT5 master | ticket MT5 slave |
account_id |
ID conta master | ID conta slave |
is_closed |
false → true | false → true |
Todo MODIFY e CLOSE usa o
trade_group_idpara encontrar quais slaves precisam ser notificados. Sem TradeLink = sinal nao propaga.
| Situacao | Comportamento |
|---|---|
| WS desconecta | Poll HTTP assume (1-3s). WS tenta reconectar com backoff 10s → 300s |
| WS morto >30s sem atividade | g_ws_connected resetado, volta ao HTTP |
| WS reconecta | Servidor envia PendingSignals pendentes |
Mecanismo A — Background task (ativo):
| Aspecto | Detalhe |
|---|---|
| Intervalo | 15 minutos (main.py:24) |
| Logica | Busca TradeLink WHERE is_closed=False cujo local_ticket nao aparece em open_positions |
| Acao | Marca TradeLink como fechado. Propaga CLOSE para peers abertos (Signal com origin="server", TTL 1min) |
Mecanismo B — Heartbeat passivo (v2.0):
| Aspecto | Detalhe |
|---|---|
| Trigger | Cada heartbeat POST do EA |
| Logica | _detect_missing_positions(): compara posicoes reportadas pelo EA vs TradeLinks abertos |
| Threshold | Posicao ausente por >30s |
| Acao | Cria CLOSE signal com origin="server", propaga para peers |
Mecanismo B detecta closes mais rapido que A (dentro de 1 heartbeat vs 15min).
Problema: EA slave executa um trade → SnapshotEngine detecta como "novo" → reenvia ao servidor → loop infinito.
Nivel 1 — EA (SE_SuppressTicket):
| Acao executada | TTL de supressao |
|---|---|
| OPEN | 5 segundos |
| MODIFY | 10 segundos |
| CLOSE | 10 segundos |
Buffer: g_se_suppressTickets[] (max 20). Antes de enfileirar MODIFY ou CLOSE, verifica SE_IsTicketSuppressed().
Nota:
SE_MAX_RETRIESdefine 5 em SnapshotEngine.mqh:43, masFlushQueueWebusa hardcoded>= 10. O valor efetivo e 10 retries.
Nivel 2 — Servidor (suppress_markers):
| Origem do sinal | TTL do marker |
|---|---|
| MODIFY/CLOSE (qualquer origem) | 10 segundos |
Nota (v2.0): Implementacao atual usa 10s para TODAS as origens (EA e dashboard). A diferenciacao 10s/15s documentada na v1.0 nao foi implementada.
Quando master envia MODIFY/CLOSE, servidor verifica se existe marker ativo → ignora (evita loop).
abs(curr.volume - prev.volume) > 0.0001close_reason = "PARTIAL_CLOSE"PositionSelectByTicket() retorna falseFILLED direto (posicao ja nao existe — nada a fazer)if acct.invert (sem comparar com outra conta, diferente do EA-to-EA)NAO altera as regras R1-R10 acima. Adiciona 3o estado de conta entre
aliveeoffline+ 2 guardas no dispatch (dedup #330 + back-pressure SKIP). Invariante S2-zero-sum de hedge preservado por construcao.Nota: as referencias a "invariante S2-zero-sum" abaixo apontam pra regra interna do modulo
saturation.py— NAO confundir com a R2 deste documento ("CLOSE tem prioridade maxima").
Problema: o EA executa ordens via cliques de GUI Win32 (GUIExecution.mqh), seriais, no MESMO OnTimer que envia batimento. Quando a tela ocupa o laco, o batimento estica e o servidor fica cego — sem saber distinguir "EA morreu" de "EA esta ocupado executando". Pior: o servidor reemitia CLOSEs do mesmo ticket em loop (classe do #330) achando que o EA nao recebeu.
Solucao: 3o estado saturated + dedup in-flight + back-pressure SKIP. Modulo central: server/saturation.py (3 funcoes puras + store in-memory).
| Estado | Quando | Efeito no dispatch |
|---|---|---|
alive |
gap < SATURATED_HB_GAP_SEC (15s) OU gap < 60s sem trabalho pendente | Round novo entra, dispatch normal |
saturated |
beacon ea_busy ativo dentro de BEACON_HARD_CAP_SEC (90s) OU gap > 15s com trabalho pendente/sem-ACK |
Conta NAO entra em round novo; OPEN novo independente eh SKIPADO (S2-zero-sum preservado — perna completante + CLOSE/MODIFY passam) |
offline |
gap > 60s E sem beacon ativo | Round novo nao entra; pares ja abertos seguem para fechamento normal |
Backstop: mesmo sem beacon (EA velho/travou), inferencia por ritmo marca saturated se gap > 15s com pendente.
ea_busy (EA → Servidor, dispara-e-esquece)EA dispara frame WS {"type":"ea_busy","op":<OPEN|MODIFY|CLOSE>,"ticket":<n>,"expected_ms":<n>} IMEDIATAMENTE APOS pegar o lock de GUI (GUI_AcquireGUILock em GUIExecution.mqh), antes do trabalho lento de tela (GUI_EnsureVisible, GUI_ExecuteOpenInternal). Se o lock falhar, o beacon NAO eh enviado. Dispara-e-esquece — sem retry, sem ACK queue, timeout <=200ms. Handler servidor: routes/ea_ws.py::_handle_ws_ea_busy -> saturation.set_busy(account_id, op, ticket). Beacon explica silencio longo (gap > 60s mas conta esta saturated, nao offline).
Antes de criar PendingSignal pra (conta, ticket, action=CLOSE), signal_dispatch.create_signal_canonical checa se ja ha pendente NAO-resolvido (sem ACK, dentro do TTL) pro mesmo (conta, ticket) -> pula + emite evento DISPATCH_DEDUP + retorna skipped_duplicate=True, skip_reason="dedup_inflight_close". Excecao: ACK FAILED do EA resolve o pendente legado MAS cria retry-PS novo (+70s) — dedup AINDA suprime re-CLOSE server-originated enquanto retry-PS estiver em-voo (correto, anti-#330: nao empilhar enquanto o retry do EA esta pendente). Liberacao real: FILLED, 3 retries exauridos, OU TTL expirou. Escopo: SO CLOSE (idempotente). MODIFY fica com a supressao anti-echo do SuppressMarker (descrita acima nas regras de processamento server-side) + dedup da fila do EA.
Gating primario em af/signals.py::check_all_online_in_pool (sync+async): conta saturated NAO entra em round novo, mesmo com HB fresco. Reason: "saturated (afogada — GUI gargalo, fora de round novo)". Filtra antes do generate_signals_for_pair/scheduler.
Defesa em profundidade em signal_dispatch.create_signal_canonical: antes do PendingSignal create, consulta should_hold_dispatch(state, action, is_completing_hedge_leg, is_existing_position_op). Se True -> skip + evento DISPATCH_HELD + retorna skipped_duplicate=True, skip_reason="held_saturated".
Invariante S2-zero-sum (CRITICO, regra do modulo saturation.py): should_hold_dispatch retorna False (passa direto) se:
- is_completing_hedge_leg=True (perna que completa par ja aberto — naked leg = quebra zero-sum)
- action in (CLOSE, MODIFY) ou is_existing_position_op=True (mexe posicao broker existente)
Sobra apenas OPEN novo independente em conta saturated como alvo do skip.
Abort do par (HR-iter2-01, race protection): em generate_signals_for_pair (af/signals.py), apos master canonical: se master_result.skip_reason == "held_saturated" (race entre check_all_online e dispatch) -> db.rollback() + raise ValueError. Caller main.py af_scheduler tem except ValueError que loga warning + nao marca pair.status='failed' (proxima rodada reavalia). Slave nunca abre sozinho.
Trade-off honesto: Hold = SKIP, nao ADIA. Signal record fica pra audit, mas sem PendingSignal e sem retry server-side. Quando conta sai de saturated, proximo signal natural (proxima rodada AF / novo broadcast) reentra. Retry temporizado dedicado fica como TODO S386-FU2 (sweetspot inicial confia no fluxo upstream — gating primario cobre 99%).
GET /api/accounts retorna account_state na carga inicial (evita janela ~10s F5 sem badge).alert_ea_saturated edge-trigger + market-hours + dedup (50s) descreve as 4 salvaguardas (distingue, nao reemite CLOSE, fora de round novo, PULA OPEN novo).DISPATCH_DEDUP, DISPATCH_HELD em trade_events/signal_events. ea_saturated no canal de alertas.server/saturation.py — 3 funcoes puras (classify_state, should_dedup_dispatch, should_hold_dispatch) + store in-memory + helpers ingest_pulse/set_busy/clear_busy/get_state/reset_state.server/heartbeat_helpers.py — ingestao do pulso enriquecido + broadcast account_state no WS.server/signal_dispatch.py — wire dedup + hold + skip_reason no canonical.server/af/signals.py — check_all_online_in_pool (sync+async) filtro saturated + 8 callsites com flags is_completing_hedge_leg / is_existing_position_op + abort do generate_signals_for_pair se master held.server/routes/ea_ws.py — handler frame ea_busy.server/routes/accounts.py — REST initial state + reset_state em hard delete.Include/CopyTrade/WinHttpWS.mqh — WS_SendHeartbeat enriquecido (queue_depth, busy_*, oldest_age_ms) + WS_SendBusyBeacon dispara-e-esquece.static/js/{overview,positions,websocket}.js — selo AFOGADO + sync WS.specs/saturacao-visibilidade-gui-S386.md — spec autocontida (Tier L, sweetspot 3 pilares + Fase 4 hedge gating)..planning/PLAN-S386-saturacao-visibilidade-gui.md — plano atomico (5 fases).O sistema AF gera sinais que usam a mesma infraestrutura (Signal, PendingSignal, TradeLink) mas com isolacao total do copy-trade normal.
| Canal | Origem | Distribuicao |
|---|---|---|
ws |
EA (copy-trade) | Broadcast para peers do grupo |
af |
AF engine (hedge) | SEM broadcast — direto para contas do par AF |
http |
EA fallback | Broadcast normal |
| Tipo | Origin | detection_method | Detalhe |
|---|---|---|---|
| AF OPEN | af |
af_engine |
sl_distance=0, tp_distance=0 (SL/TP absolutos, EA aplica buffer) |
| AF CLOSE | af_close |
af_engine |
trade_group_id = "AF_CLOSE_{pair_id}" |
| AF MODIFY | af_modify |
af_push_modify |
Push zone precision (ver §6d AF whitepaper). F2/F3 cap preserva margem ($2-10), F7 desconta sl_buffer do SL. |
Mecanismo A — AF Pending Linkage: Quando conta re-detecta sua propria execucao AF (via SE/OTT), servidor reconhece signal_channel='af' no PendingSignal, vincula TradeLink sem criar broadcast novo, auto-ACK como FILLED.
Mecanismo B — Pool Active Suppression: Se conta esta em pool AF ativo/pausado, OPEN cria signal + auto-ACK com status='ORIGIN', define signal_channel='af', retorna 'af_pool_suppressed'. Peers NAO recebem.
P19 (CRITICO): Sem esta isolacao, sinais AF propagariam para TODAS as contas do grupo. Bug causou 63 posicoes e -$46k.
Mecanismo retroativo que recupera profit/close_price quando o EA volta de
janela offline durante a qual o broker fechou posicoes (SL hit, manual close,
margin call). Causa raiz coberta: profit_source=none em _detect_missing_positions
ao detectar ticket sumido (3 fallbacks retornavam None → profit gravado=0 →
AfPair classificada erroneamente como pnl_suspect).
| # | Trigger | Quando dispara | Cobre |
|---|---|---|---|
| 1 | OnInit |
Apos boot do EA (recompile, restart MT5) | Janela offline + crash recovery |
| 2 | WS reconnect callback | Apos WS_Reconnect() retornar true |
Disconnect transiente (proxy, rede) |
| 3 | OnTimer |
A cada 60s (debounce 30s interno) | Cinturao+suspensorio: OTT fila 1024, erros transientes |
Include/CopyTrade/ReconcileEngine.mqh — modulo:
- RE_FetchOpenTradeLinks: GET /api/ea/open-tradelinks retorna lista de tickets que o servidor ainda considera abertos.
- RE_ScanHistory: HistorySelect(from_time, now) + filtra DEAL_ENTRY_OUT cujo DEAL_POSITION_ID ∈ open_tickets.
- RE_EnqueueRetroactive: POST /api/signals com is_retroactive=true + profit consolidado (R4: DEAL_PROFIT + DEAL_SWAP + DEAL_COMMISSION) + deal_price.
- Mutex g_reconcile_running (R10) impede multi-trigger concorrente.
- Checkpoint local MQL5/Files/reconcile_checkpoint_<account>.txt (R11) reduz custo de scan na N-esima execucao. Primeiro run varre ate 7d atras.
signals.is_retroactive BOOLEAN NOT NULL DEFAULT FALSE + index parcial:
CREATE UNIQUE INDEX ix_signals_retroactive_unique
ON signals (source_account, ticket, action)
WHERE is_retroactive = TRUE;
Garante: 1 retroactive por (conta, ticket, action). Multi-trigger seguro — segunda chamada bate no IntegrityError ou no SELECT prefix-check, retorna skipped_idempotent. Signals normais (is_retroactive=false) NAO sao afetados pelo index.
_process_retroactive_close em routes/signals.py:
1. Idempotency check (R6): SELECT existing → skip silent.
2. Sanity check (R28): validate_retroactive_profit(profit, balance) → |profit| > 2*balance rejeita 422 + Telegram alert.
3. Tolerance check (R5): |old_profit - new_profit| < $0.01 → skip silent (sem audit churn).
4. Override absoluto: link.profit = data.profit; link.close_price = data.deal_price. Broker eh fonte canonica.
5. Audit: log_trade_event(origin='reconcile_retroactive') com before/after profit + delta.
6. Re-validate: atualiza AfTrade.profit do lado; se ambos lados tem profit, chama process_trade_result(pair_id, profit_a, profit_b).
validate_retroactive_profit(profit, balance) em af/engine.py:
- balance <= 0 → reject balance_invalid
- |profit| > 2 * balance → reject absurd_value
- caso contrario → ok
Tests cobrem 11 cenarios (5 ex + 3 inv + 3 property-based via Hypothesis em test_reconcile_retroactive.py).
Coroutine async em main.py:_watchdog_offline_with_trade():
- Tick 60s. Lista accounts com TradeLink open.
- Compara max(Heartbeat.created_at) com NOW(). age > 300s E acc ∈ TradeLink open → alert candidato.
- Edge-trigger via dict _watchdog_alerted — alerta dispara 1x na transicao online→offline. Reset com heartbeat fresco.
- R9 market hours: is_market_open(symbol) cruzado antes de alertar (suprime weekend/rollover).
- Telegram via send_watchdog_offline_with_trade com debounce 60s.
Reconcile cobre apenas CLOSE retroativo. Se EA estava offline durante OPEN, pair vira failed/timeout e nao eh recuperavel via reconcile. Exemplo: Pair 81 (Round 21) — af_trades.status='timeout' sem ACK de OPEN, fix via SQL manual em S299 (CA-8).
specs/reconciliacao-pos-offline.md — 14 casos de borda, 11 regras, 27 riscos, prior art externo (MQL5 Article #11248, HistorySelect docs). Status: APROVADO (S299) → IMPLEMENTADO (S300).
Mecanismo server-side que detecta e corrige divergencias de SL/TP entre peers.
| Aspecto | Detalhe |
|---|---|
| Trigger | Cada heartbeat POST do EA (routes/heartbeat.py) |
| Logica | _reconcile_sl_tp(): compara SL/TP das posicoes do EA vs valores esperados (TradeLink) |
| Threshold | Mismatch detectado |
| Acao | Cria MODIFY signal com origin="server_reconcile" — direto pro peer divergido (sem broadcast) |
| Cooldown | Skip se MODIFY recente (<300s) para o mesmo ticket |
| Log | "[SL/TP-RECONCILE] Mismatch conta X ticket Y: SL=A->B, TP=C->D" |
Todo evento significativo eh registrado na tabela TradeEvent para auditoria completa.
log_trade_event(db, event_type, ...)| Campo | Tipo | Descricao |
|---|---|---|
event_type |
str | OPEN, MODIFY, CLOSE, PROPAGATE, ACK |
account_id |
int | Conta envolvida |
ticket |
int | Ticket do broker |
trade_group_id |
str | UUID do grupo |
signal_id |
int | FK para signals |
origin |
str | ea, server_reconcile, af, af_close, af_modify |
close_reason |
str | SL, TP, STOP_OUT, MANUAL (so CLOSE) |
price |
float | Preco do evento |
profit |
float | P&L consolidado (so CLOSE) |
volume |
float | Volume |
context |
JSON | Metadados extras (distributed_to, applied_sl/tp, etc.) |
| Momento | event_type | Contexto extra |
|---|---|---|
| Signal OPEN criado | OPEN |
distributed_to (numero de peers) |
| Signal MODIFY criado | MODIFY |
SL/TP alvos |
| Signal CLOSE criado | CLOSE |
close_reason, profit, close_price |
| CLOSE propagado pra peers | PROPAGATE |
Peers notificados |
| ACK recebido (OPEN) | ACK |
status, channel, latency_ms |
| ACK recebido (MODIFY) | ACK |
applied_sl vs target_sl, applied_tp vs target_tp |
Analogia: O trace_id e como o numero de rastreio de uma encomenda. Cada sinal recebe um codigo unico no momento em que nasce, e esse codigo acompanha TODOS os passos — da deteccao no EA master ate o ACK do EA slave e a timeline no dashboard. Se algo deu errado, basta buscar o trace_id pra ver exatamente onde parou.
| Aspecto | Detalhe |
|---|---|
| Formato | 32 caracteres hexadecimais (padrao W3C Trace Context) |
| Onde nasce | Servidor, no momento do INSERT INTO signals (routes/signals.py) |
| Header HTTP | traceparent (preparado pra integracao futura com OTel/Grafana) |
| EA fallback | Telemetry_GenTraceId() (Telemetry.mqh) gera trace_id local se servidor nao fornecer — composto de account_id + timestamp + tick_counter |
| Enriquecimento | Middleware _ensure_trace_id() (telemetry.py) gera automaticamente se ausente no request |
| Etapa | Componente | Como trace_id chega |
|---|---|---|
| 1. Deteccao (EA master) | SnapshotEngine.mqh |
Ainda sem trace_id — sinal detectado localmente |
| 3. Envio (EA -> servidor) | WebBridge_PostSignal() |
Sem trace_id no payload (servidor gera) |
| 4. Processamento (servidor) | signals.py INSERT |
trace_id gerado aqui — salvo em signals.trace_id |
| 5. PendingSignal | pending_signals |
Herdado via signal_id FK (JOIN com signals) |
| 6. Distribuicao (WS push) | ea_ws_manager.push_signal() |
trace_id incluido no payload WS |
| 7. Execucao (EA slave) | GUIExecution.mqh hooks |
EL_SetCurrentContext(signalId, traceId) — todos os EL_Emit() herdam |
| 8. ACK | signal_acks.trace_id |
Salvo na tabela signal_acks via handler HTTP/WS |
| 9. TradeLink | trade_links |
Correlacionado via trade_group_id (mesmo grupo) |
| 10. EventLog | signal_events |
Cada evento carrega trace_id — UNIQUE(signal_id, event_type, seq_num) |
| 11. Telemetria | event_stream |
Eventos telemetricos vinculados pelo mesmo trace_id |
O EventLog (EventLog.mqh) emite eventos em cada passo da execucao GUI:
| Grupo | Events | Descricao |
|---|---|---|
| OPEN | gui_s1..gui_s11 |
11 passos: F9 dialog -> symbol -> volume -> SL/TP -> click -> confirmacao |
| MODIFY | gui_m1..gui_m5 |
5 passos: dialog -> combo -> painel -> click -> resultado |
| CLOSE | gui_c1..gui_c3 |
3 passos: dialog -> botao -> resultado |
| ACK | ack_sent, ack_failed |
Confirmacao de execucao enviada/falhada |
| Recepcao | ea_received |
EA slave recebeu o sinal |
Cada event type tem variante _ok e _fail (ex: gui_s5_ok, gui_s5_fail).
| Cenario | Comportamento |
|---|---|
| Signal antigo (pre-S224, sem trace_id) | trace_id = NULL — queries usam LEFT JOIN, timeline funciona sem ele |
| EA versao antiga sem Telemetry.mqh | Servidor gera trace_id normalmente — EA so nao emite eventos de telemetria |
| Mix de EA builds no deploy gradual | Campos novos sao opcionais com default no Pydantic schema |
| Acao | Endpoint | O que mostra |
|---|---|---|
| Timeline por signal | GET /api/signals/{id}/timeline |
Todos eventos ordenados por ts_ea ASC com payload expandivel |
| Timeline por trace | GET /api/telemetry/trace/{trace_id} |
Eventos de telemetria vinculados ao trace |
| Anomalias recentes | GET /api/telemetry/anomalies |
Eventos com late=true (gap > 10s entre broker e servidor) |
specs/ea-observability.md — Event sourcing hibrido, buffer persistente, idempotencia (12/12 CAs)specs/telemetria-sweetspot.md — Gap detection, anomaly detector, Telegram alerts (9/11 CAs)| Componente | Arquivo | Responsabilidade |
|---|---|---|
| EA principal | Experts/LinniuC.mq5 |
Orquestrador, OnTimer, OnTradeTransaction |
| Deteccao | Include/CopyTrade/SnapshotEngine.mqh |
Snapshot, fila, anti-echo EA |
| HTTP | Include/CopyTrade/WebBridge.mqh |
POST signals, GET pending, ACK, heartbeat |
| WebSocket | Include/CopyTrade/WinHttpWS.mqh |
WS connect, read, send |
| GUI | Include/CopyTrade/GUIExecution.mqh |
Win32 automation: OPEN/MODIFY/CLOSE |
| Settings | Include/CopyTrade/SettingsManager.mqh |
Sync servidor, invert |
| Servidor sinais | routes/signals.py |
POST signals, GET pending, ACK |
| Servidor WS | routes/ea_ws.py |
WS /api/ws/ea, push sinais |
| Servidor cleanup | main.py |
Orphan checker, TTL cleanup |
| Servidor heartbeat | routes/heartbeat.py |
POST heartbeat, posicoes, sync |
O EA envia logs pro servidor via heartbeat. Pipeline completo:
EA (MQL5) Servidor (Python)
───────── ─────────────────
LNC_LogRemote(msg)
→ g_logBuffer[100] (circular)
→ LNC_BuildLogJSON()
→ heartbeat body: "last_logs": [...]
POST /api/heartbeat
→ heartbeat.py:385-418
→ dedup (5min window por account)
→ INSERT ea_remote_logs
→ level auto-detect:
"ERROR"/"FAIL"/"FALHOU" → ERROR
"WARN"/"SKIP" → WARN
else → INFO
GET /api/heartbeat/diagnostics
→ recent_logs (20 ultimas)
→ recent_errors (10 ultimas, level=ERROR)
Cleanup: TTL 7 dias (main.py startup)
| Evento | Formato | Level |
|---|---|---|
| EA iniciado | EA iniciado BUILD X.Y.Z |
INFO |
| OPEN OK | OPEN OK #ticket symbol dir vol=X Xms [gui_trace] |
INFO |
| OPEN FAIL (no ticket) | OPEN FAIL no-ticket symbol [gui_trace] |
ERROR |
| OPEN FAIL (GUI) | OPEN FAIL gui symbol [gui_trace] |
ERROR |
| MODIFY OK | MODIFY OK #ticket Xms [gui_trace] DIAG{...} |
INFO |
| MODIFY FAIL | MODIFY FAIL #ticket [gui_trace] |
ERROR |
| MODIFY SKIP | MODIFY SKIP #ticket (already correct) |
WARN |
| MODIFY READBACK FAIL | MODIFY READBACK FAIL #ticket msg DIAG{...} |
ERROR |
| CLOSE OK | CLOSE OK #ticket Xms [gui_trace] |
INFO |
| CLOSE FAIL | CLOSE FAIL #ticket [gui_trace] |
ERROR |
| CLOSE already closed | CLOSE already closed #ticket |
INFO |
| CLOSE race-closed | CLOSE race-closed #ticket [gui_trace] |
INFO |
| SKIP (signal old) | SKIP #signalId old Xs |
WARN |
| WS debug | [WS-DBG] type=X len=Y |
INFO |
| WS price request | [WS] Price request recebido: symbol id=X |
INFO |
| OTT event | [OTT] Deal #X ENTRY_IN/OUT REASON |
INFO |
| AutoUpdate diag | [AU-DIAG] ... |
INFO |
TAB:0|C1d:rows=1|SEL:0|F9:ok|DLG:found|SYM:set|DIR:set|VOL:set|SLTP:set|PLACE:ok
Cada passo da execucao GUI separado por |. O ultimo passo indica onde parou se falhou.
-- Ultimos 30 logs de uma conta
SELECT message, level, created_at FROM ea_remote_logs
WHERE account_id = X ORDER BY created_at DESC LIMIT 30;
-- Erros das ultimas 24h
SELECT account_id, message, created_at FROM ea_remote_logs
WHERE level = 'ERROR' AND created_at > NOW() - INTERVAL '24 hours'
ORDER BY created_at DESC;
-- GUI traces (buscar passos que falharam)
SELECT message FROM ea_remote_logs
WHERE account_id = X AND message LIKE '%gui_trace%'
ORDER BY created_at DESC LIMIT 10;
| Funcao | Linha | O que faz |
|---|---|---|
LNC_LogRemote(msg) |
134 | Buffer circular de logs (50 msgs) |
LNC_BuildLogJSON() |
141 | JSON array dos logs pra heartbeat |
LNC_BuildDiagnosticsJSON() |
162 | JSON completo de diagnosticos |
LNC_IsSignalProcessed(id) |
222 | Anti-retry: checa se sinal ja foi processado |
LNC_MarkSignalProcessed(id) |
229 | Marca sinal como processado |
ExecuteSingleSignal(json) |
523 | Executa 1 sinal (OPEN/MODIFY/CLOSE via GUI) |
PollAndExecuteSignals() |
968 | Poll /api/signals/pending + executa |
EquityFloor_Check() |
1560 | Circuit breaker: fecha tudo se equity < floor |
OnTimer() |
1597 | Ciclo 200ms: SE + poll + WS + ACK queue |
OnTradeTransaction() |
1739 | Deteccao instantanea (OTT) de deals |
| Funcao | Endpoint | Descricao |
|---|---|---|
WebBridge_Heartbeat() |
POST /api/heartbeat |
Status: balance, equity, positions, logs |
WebBridge_PostSignal() |
POST /api/signals |
Master reporta trade detectado |
WebBridge_PollSignals() |
GET /api/signals/pending |
Slave busca sinais pendentes |
WebBridge_SendAck() |
POST /api/signals/{id}/ack |
Reporta resultado (FILLED/FAILED) |
| Funcao | Linha | Descricao |
|---|---|---|
GUIExecution_Open() |
1183 | Abre posicao via F9 dialog |
GUIExecution_Modify() |
1401 | Modifica SL/TP via Position dialog |
GUIExecution_Close() |
1488 | Fecha posicao via Position dialog |
GUI_Trace(step) |
165 | Acumula passo no trace buffer |
GUI_EnsureTradeTabActive() |
472 | Garante Trade tab ativa |
GUI_AcquireGUILock() |
393 | Lock exclusivo (1 EA por vez) |
| Metodo | Endpoint | Arquivo:Linha | Descricao |
|---|---|---|---|
| POST | /api/signals |
signals.py:96 | Master reporta trade (anti-loop + broadcast) |
| GET | /api/signals/pending |
signals.py:722 | Slave busca sinais pendentes |
| POST | /api/signals/{id}/ack |
signals.py:811 | ACK com validacao V1-V4 |
| POST | /api/heartbeat |
heartbeat.py:201 | Status + logs + positions |
| POST | /api/signals/broadcast |
signals.py:1094 | Dashboard broadcast manual |
| POST | /api/signals/broadcast-close |
signals.py:1532 | Fechar todas posicoes de grupo |
| GET | /api/diagnostics |
heartbeat.py:785 | Diagnosticos de todos EAs |
Checks automaticos no handler de ACK (HTTP + WS):
| Check | Condicao | Acao |
|---|---|---|
| V1: Volume zero | actual_volume < 0.01 em OPEN |
Rejeitar como FAILED + Telegram |
| V2: Open price zero | open_price <= 0 em OPEN |
Rejeitar como FAILED + Telegram |
| V3: Ticket reutilizado | ticket ja existe em trade_links (outro trade_group) |
Rejeitar como FAILED + Telegram |
Implementado em validate_ack_data() (signals.py), chamado por ambos handlers (HTTP e WS).
Apos ambos ACKs de um par AF (_af_ack_count >= 2):
- reconcile_af_pair() verifica: 2 trades, tickets diferentes, direcoes opostas, volumes iguais, open_price > 0
- Se falha: alerta Telegram + bloqueia MODIFY scheduling
Endpoint: POST /api/test/inject (so disponivel quando TEST_INJECTION_ENABLED=true)
signal_source: "test" (vs "ea" no fluxo normal)
O lifecycle normal comeca na Etapa 1 (deteccao de trade no EA master). A injecao de teste entra direto na Etapa 4 (processamento no servidor), pulando as etapas 1-3:
| Aspecto | Fluxo normal | Injecao de teste |
|---|---|---|
| Origem | EA master (OTT ou SnapshotEngine) | POST /api/test/inject (servidor) |
| Auth | X-API-Key (ea_api_key) | Bearer JWT (dash auth) |
signal_source |
"ea" |
"test" |
| Conta target | peers do mesmo group_id (broadcast) |
account_id especifico (obrigatorio is_test=true) |
| Filtro dashboard | Incluido por default | Excluido por default (include_test=false) |
| ACK path | EA Slave → /api/ack |
EA Slave (conta 52) → /api/ack (mesmo handler) |
R10: Sinal
signal_source='test'NUNCA distribui para contais_test=false.
- O handler valida queaccount.is_test=Trueantes de criar qualquerPendingSignal.
- Nao ha broadcast degroup_id— so a conta alvo recebe.
- Invariant CA-7/R1:COUNT(*) FROM pending_signals JOIN signals JOIN accounts WHERE signal_source='test' AND is_test=false = 0SEMPRE.
- Conta 52 (Teste Sandbox) esta emblocked_account_idsdo pairing AF — nunca entra em round real.
A partir da distribuicao (Etapa 5), o fluxo e identico ao normal:
- PendingSignal criado → EA da conta 52 polls/recebe WS → executa via GUI automation → ACK volta com ticket real
- signal_source='test' preservado em todos os registros (Signal, SignalAck, TradeLink)
- Dashboard filtra sinais test por default (include_test=false em /api/signals/history e /api/accounts)
O endpoint so existe quando TEST_INJECTION_ENABLED=true (env). Em producao fica false — qualquer chamada retorna 404.
Isolamento via porta: o runner de testes sobe uvicorn :8001 --lifespan off separado do prod :8000.
Status: ACTIVE | Ultima revisao: S342 (2026-05-12) — trio carteiro fechado
3 carteiros canonicos cobrem o ciclo de vida completo de cada sinal de trade.
Cada um eh um helper unico ("1 cabeca, varias bocas") — wrappers HTTP e WS
finos delegam toda a logica pro mesmo helper, eliminando drift por construcao.
| # | Carteiro | Quando dispara | Helper | Spec |
|---|---|---|---|---|
| 1 | Carteiro Canonical (IDA) — entrega o sinal | Dashboard ou EA cria sinal de trade | create_signal_canonical em server/signal_dispatch.py |
specs/signal-dispatch-canonical.md |
| 2 | Carteiro Conferente (VOLTA) — confere recibo | EA responde se executou (FILLED/FAILED/REJECTED) | apply_ack_canonical em server/ack_helpers.py |
specs/carteiro-conferente-ack.md |
| 3 | Carteiro Plantonista (PLANTAO) — ronda telemetria | EA bate ponto a cada 10s (WS) ou 30s (HTTP) | apply_heartbeat_canonical em server/heartbeat_helpers.py |
specs/carteiro-plantonista-hb.md |
[Dashboard/Master EA]
|
| (1) cria sinal
v
+---------+ sinal +-----+
| IDA | ---- via WS ---> | EA |
+---------+ ou HTTP +-----+
^ |
| | (2) executou (ou falhou)
| broadcast WS v
| +---------+
+---------------------- | VOLTA |
+---------+
[PLANTAO roda em paralelo, sempre — 10s ou 30s]
+-----------+ HB telemetria +-----+
| PLANTAO | <---------------- | EA |
+-----------+ (vivo? saudo?) +-----+
create_signal_canonicalArquivo: server/signal_dispatch.py:create_signal_canonical
Quem chama: 20 callsites em routes/signals.py, af/signals.py, routes/heartbeat.py:_reconcile_sl_tp, etc. (python scripts/show_canonical_api.py --callsites)
Origem: S317 Onda 4a (2026-05-02) — substituiu Signal() raw nas rotas e AF engine.
signals (a "encomenda")pending_signals (a "lista de espera ate alguem entregar")trade_links se origem eh originsignal_ack com status=SUCCESS (server-initiated routes)trade_events para auditoriadispatch_pending_to_eas| Tabela | Colunas-chave | Observacao |
|---|---|---|
signals |
id, action, symbol, direction, volume, sl, tp, trade_group_id, origin, signal_source, trace_id |
origin eh OriginType enum (20 valores); signal_source eh SignalSource enum (4 valores) |
pending_signals |
signal_id, account_id, trade_group_id, expires_at, resolved |
TTL via expires_at (PENDING_SIGNAL_TTL_SECONDS) |
trade_links |
signal_id, account_id, local_ticket, is_origin, trade_group_id |
local_ticket so chega no ACK FILLED — IDA cria com NULL e VOLTA preenche |
signal_acks |
(opcional) status=SUCCESS pra server-initiated |
Bypass de aguardo do EA |
trade_events |
event_type=CREATE, origin, context |
Audit trail JSONB |
(account_id, trade_group_id, action) — race UNIQUE constraint detecta duplicata e retorna skipped_duplicate=Truedb.commit() (commit_then_publish pattern, evita idle in transaction)flag_modified() em qualquer mutacao JSONB (pattern P12)OriginType.SERVER_RECONCILE, MASTER_ENTRY, AF_PAIR_ENTRY respeitam invert da conta destinoapply_ack_canonicalArquivo: server/ack_helpers.py:apply_ack_canonical
Quem chama:
- server/routes/signals.py:ack_signal (HTTP POST /api/ack)
- server/routes/ea_ws.py:_handle_ws_ack (WS msg_type=ack)
Origem: S341 (2026-05-12) — refactor extraiu 600 linhas duplicadas entre HTTP e WS handlers.
signal_acks (race-safe via IntegrityError retry com _apply_fields interno)compute_exec_slippage ANTES do commit (1 commit so — D1 canonical)validate_ack_data (V1-V4) e marca status=FAILED se aplicaveltrade_links quando status=FILLED (preenche local_ticket)pending_signals.resolved=True pra acabar o ciclotrade_events (ACK + MODIFY-BUFFER mismatch + CLOSE P&L cascade)reconcile_af_pair + check_and_schedule_modify) quando 2 ACKs FILLED chegam no mesmo trade_group_id AFcommit_then_publish("ack", ...) no final pro dashboard| Tabela | Colunas-chave | Observacao |
|---|---|---|
signal_acks |
signal_id, account_id, status, local_ticket, executed_at, exec_slip_pts, pipeline_slip_pts, bid_at_request, ask_at_request, ea_received_at_ms, trace_id |
Status: FILLED, SUCCESS, FAILED, REJECTED, PARTIAL |
pending_signals |
resolved=True apos terminal status |
Fecha o ciclo da IDA |
trade_links |
local_ticket, close_price, profit, is_closed |
Preenchido aqui (IDA criou com NULL) |
trade_events |
event_type=ACK, context.profit_source |
Cascade deal_ack > broker > cache > unknown |
signals |
so leitura (busca por signal_id) |
Nao escreve |
Auditoria completa em specs/scratch/ack-drift-S341.md. 2 bugs latentes corrigidos:
- SE-1: HTTP nao usava commit_then_publish (B5/P224 nao propagado)
- SE-2: WS nao tinha CLOSE FAILED auto-retry (90% dos ACKs vem por WS)
db aberto, NAO chama db.close() (lifecycle do FastAPI Depends)_apply_fields (race-safe upsert)flag_modified() em qualquer mutacao JSONB (pattern P12)SignalAck.trace_id em INSERT e UPDATE (race recovery cobre branch UPDATE manualmente)apply_heartbeat_canonicalArquivo: server/heartbeat_helpers.py:apply_heartbeat_canonical
Quem chama:
- server/routes/heartbeat.py:receive_heartbeat (HTTP POST /api/heartbeat)
- server/routes/ea_ws.py:_handle_ws_heartbeat (WS msg_type=heartbeat)
Origem: S342 (2026-05-12) — refactor extraiu 1130 linhas duplicadas entre HTTP e WS handlers.
_sanitize_nan recursive (NaN/Inf em qualquer profundidade)broker_name, mt5_server (com guard 2+ chars apos strip)terminal_build change alert via Telegram (SE-3 cure)account.last_heartbeat_at SEMPRE (mesmo no throttle WS — S314.3 Bug 3 fix)_offline_accounts)_reconcile_sl_tp — MODIFY pra peer divergente)_detect_missing_positions com advisory lock)ea_version + sl/tp/open_price)| Tabela | Colunas-chave | Observacao |
|---|---|---|
accounts |
last_heartbeat_at, broker, mt5_server, terminal_build, desired_version, update_attempts, settings_dirty |
UPDATE sempre, throttle so afeta heartbeats row |
heartbeats |
balance, equity, margin, free_margin, positions, ea_version, applied_settings, created_at |
INSERT throttled: WS=30s; HTTP=sempre (polling natural ja 30s) |
open_positions |
account_id, ticket, symbol, direction, volume, profit, sl, tp, open_price, current_price, pips, swap, magic, commission, spread, tick_value, tick_size, profit_at_sl, profit_at_tp |
Upsert por diff incoming vs existing |
equity_snapshots |
balance, equity, margin, positions |
Snapshot permanente cada 5min |
symbol_info |
account_id, symbol, tick_value, tick_size, etc. |
Bulk upsert ON CONFLICT |
trade_events |
event_type=CLOSE, origin=heartbeat_gone, context.profit_source |
Audit trail das posicoes que sumiram |
trade_links |
is_closed=True, closed_at, close_price, profit |
Quando ticket some entre HBs |
Auditoria completa em specs/scratch/hb-drift-S342.md. 6 bugs latentes corrigidos:
- SE-1: WS NaN/Inf so cobria 5+8 campos
- SE-2: WS auto-reg criava broker=""
- SE-3: WS nao detectava terminal_build change
- SE-4: HTTP settings_verify_fail nao usava commit_then_publish (B5/P224)
- SE-5: broadcast HTTP nao incluia ea_version
- SE-6: broadcast HTTP open_positions sem sl/tp/open_price
db aberto, NAO chama db.close() (lifecycle FastAPI/SessionLocal)force_save: bool — HTTP sempre True; WS = should_save_db decide_sanitize_nan recursive cobre dict/list/tuple/set/frozenset (SE-1 cure)commit_then_publish (SE-4 cure)heartbeats table pra 30s/conta — sem perder telemetria (broadcast + last_heartbeat_at continuam em cada HB)Cada carteiro recebe um payload JSON e responde/broadcasta outro. Aqui o que efetivamente trafega entre EA, servidor e dashboard — diferente das tabelas DB acima que mostram o que persiste.
Entrada: chamada Python interna (nao tem JSON IN — eh callee de 20 callsites).
Saida 1 — push WS pro EA (WS_PAYLOAD_FIELDS, 16 campos exatos — schema estrito, contrato com EA):
| Campo | Tipo | Origem | Notas |
|---|---|---|---|
id |
int | Signal.id |
Chave de correlacao com SignalAck |
action |
str | OPEN/MODIFY/CLOSE |
Discriminador do tipo |
ticket |
int | 0 em OPEN, MT5 ticket em MODIFY/CLOSE | |
symbol |
str | XAUUSD, BTCUSD, etc | |
direction |
str | BUY/SELL |
NUNCA invertido aqui (EA aplica invert) |
volume |
float | Lotes | |
price |
float | Preco entrada (0 em CLOSE) | |
sl |
float | Stop Loss (preco absoluto) | |
tp |
float | Take Profit (preco absoluto) | |
sl_distance |
float | Distancia em pontos (alternativa) | |
tp_distance |
float | Distancia em pontos (alternativa) | |
created_at |
str ISO8601+Z | Server time UTC | |
trade_group_id |
str | AF_P5, AF_R9_P3, etc | |
signal_channel |
str|null | Canal origem | Audit trail |
detection_method |
str|null | OTT, SE, manual, AF | Audit trail |
close_reason |
str|null | SL/TP/STOP_OUT/MANUAL | So em CLOSE |
Saida 2 — broadcast "new_signal" pro dashboard (dashboard ouve via WS, atualiza aba Sinais).
Entrada AckIn (EA -> servidor, schema Pydantic em server/schemas.py:120):
| Campo | Tipo | Obrigatorio | Notas |
|---|---|---|---|
status |
str | SIM | FILLED, SUCCESS, FAILED, REJECTED, PARTIAL |
local_ticket |
int | NAO (0 default) | Ticket MT5 que o EA gerou |
error_msg |
str | NAO ("" default) | Mensagem de erro se FAILED |
open_price |
float | NAO (0 default) | Preco que o EA conseguiu |
applied_sl |
float | NAO (0 default) | SL que o EA conseguiu setar |
applied_tp |
float | NAO (0 default) | TP idem |
actual_volume |
float | NAO (0 default) | Volume efetivo (partial fill) |
receive_channel |
str|null | NAO | http_poll, ws_push (telemetria latencia) |
gui_duration_ms |
int|null | NAO | Tempo dialog F9 |
ea_received_at_ms |
int|null | NAO (build 3.85+) | T4 epoch ms UTC pra calc latencia exata |
deal_price |
float|null | NAO (v3.63+) | Preco exato do MT5 deal (CLOSE enrichment) |
deal_profit |
float|null | NAO (v3.63+) | P&L do MT5 deal |
bid_at_request |
float|null | NAO (3.85+) | BID via SymbolInfoTick no click GUI (slippage) |
ask_at_request |
float|null | NAO (3.85+) | ASK idem |
Saida 1 — response HTTP (apenas via path HTTP, WS path retorna None): {ok: bool, ack_id: int|null, status: str, errors: list[str]}.
Saida 2 — broadcast "ack" pro dashboard (via commit_then_publish):
| Campo | Tipo | Notas |
|---|---|---|
signal_id |
int | Correlaciona com Signal IDA |
account_id |
int | |
status |
str | Espelha AckIn.status |
action |
str | Vindo do Signal: OPEN/MODIFY/CLOSE |
local_ticket |
int | |
account_name |
str | (so HTTP path enriquece) |
open_price |
float | (so HTTP path enriquece) |
applied_sl |
float | (so HTTP path enriquece) |
applied_tp |
float | (so HTTP path enriquece) |
actual_volume |
float | (so HTTP path enriquece) |
Entrada HeartbeatIn (EA -> servidor, schema Pydantic em server/schemas.py:46):
| Campo | Tipo | Obrigatorio | Notas |
|---|---|---|---|
account_num |
int | SIM | Numero MT5 (chave de auth) |
balance |
float | SIM | Saldo do broker |
equity |
float | SIM | Equity (saldo + P&L flutuante) |
margin |
float | NAO (0 default) | Margem usada |
free_margin |
float | NAO (0 default) | Margem livre |
positions |
int | NAO (0 default) | Quantas posicoes abertas |
server_time |
str (max 64) | NAO | Server time do MT5 |
ea_version |
str (max 32) | NAO | LinniuC_b3.85.5 (broker|build) |
open_positions |
list[PositionItem] | NAO | Array de posicoes abertas (snapshot) |
applied_settings |
dict|null | NAO | Snapshot das configs aplicadas pelo EA |
broker_name |
str | NAO | "Exness", "FTMO", etc |
mt5_server |
str | NAO | Servidor MT5 reportado |
diagnostics |
dict|null | NAO | timer_tick, last_logs, etc (zombie detection) |
symbol_info |
list|null | NAO | tick_value, tick_size, contract_size por simbolo |
chart_symbol |
str|null | NAO | Simbolo atual no chart EA |
chart_bid |
float|null | NAO | BID fresh do MT5 (Olheiro consome) |
chart_ask |
float|null | NAO | ASK fresh idem |
terminal_build |
int|null | NAO (v3.85+) | Build do MT5 — mudanca dispara alerta |
Cada item de open_positions: {ticket, symbol, direction, volume, profit, sl, tp, open_price, current_price, pips, swap, magic, open_time, spread, commission, tick_value, tick_size, profit_at_sl, profit_at_tp} (19 campos).
Saida 1 — response HTTP HeartbeatOut (HTTP-only, enriquecido com auto-update):
| Campo | Tipo | Notas |
|---|---|---|
status |
str | "ok" |
account_id |
int | |
next_heartbeat |
str | Slot de 30s alinhado ao minuto |
wait_seconds |
int | Quanto esperar ate proximo HB |
group_peers |
str | "1:ON:2pos:5s:Exness:12345,2:OFF:0pos:never:..." |
update_available |
bool? | (so se desired_version pendente) |
update_version, update_hash, update_size, update_url, update_force |
str/int/bool | Dados do EaVersion pra EA baixar |
Saida 2 — push WS update_push (WS-only, EA recebe via send_text):
{ "type": "update", "version": "3.85.6", "hash": "...", "size": 102400, "url": "/api/ea/download/3.85.6" }
Saida 3 — broadcast "heartbeat" pro dashboard (super-set unificado, SE-5/SE-6 cures):
| Campo | Tipo | Notas |
|---|---|---|
account_id, name |
int, str | |
balance, equity, positions |
float, float, int | |
ea_version |
str | (SE-5 cure: sempre incluido) |
open_positions |
array | Schema super-set: {ticket, symbol, direction, volume, profit, pips, sl, tp, open_price} (SE-6 cure) |
settings_dirty |
bool | UI mostra badge "sync pendente" |
applied_settings |
dict | (so se truthy) |
diagnostics |
dict | (so se truthy) |
Saida 4 — alerta drawdown (so se dd_pct > max_risk_pct * 2): broadcast {type: drawdown, account_id, account_name, account_num, drawdown_pct, threshold, balance, equity}.
Prometheus REMOVIDO em S367. O endpoint
/metricse os counters
copytrade_*existiam mas NADA os consumia (sem Prometheus/Grafana rodando na
VPS). A observabilidade dos carteiros hoje eh via aba de saude customizada
(/api/health,/api/debug/health-full), logs e alertas Telegram. Se um dia
precisar de tendencia historica/grafico, considerar Netdata (1 binario,
zero-config) em vez de reerguer Prometheus+Grafana.
OPEN/MODIFY/CLOSE (IDA cria, VOLTA confirma)equity, last_heartbeat (PLANTAO atualiza)open_positions (PLANTAO mantem snapshot)mt5_build_change, modify_buffer_mismatch, af_modify_blocked, close_retry_exhausted)/api/debug/health-full (auth JWT) — health consolidado de cada conta com ack_stats_1h, recent_logs, diagnosticscreate_signal_canonical criado, migrou 4 callsites primarioscanonical-carteiro + script show_canonical_api.py (cheatsheet auto-fresh)apply_ack_canonical extraido (5 iters review)apply_heartbeat_canonical extraido (10 iters validate+review — pattern P228 capturado: zero MED/LOW e bar absoluta, nao apenas zero CRITICAL/HIGH)Status: ACTIVE | Adicionado S336+ (2026-05-10, deploy 02:20 UTC) — explica em linguagem leiga + tabela tecnica o que cada um dos 4 campos novos em
signal_ackssignifica.
Imagina que voce pediu uma pizza por delivery por R$50, mas quando chegou cobraram R$52. A diferenca (R$2) é o slippage — o quanto voce foi "pisado" entre o pedido e o recibo. No copy trade, cada operacao real tem 3 momentos de preco:
bid_at_request / ask_at_request)deal_price)signal.price)A telemetria S336+ mede duas distancias:
Ambos sao modulo (sempre positivo) e em pontos (a unidade nativa do simbolo: 1 ponto BTCUSD = 0.01, 1 ponto XAUUSD = 0.01).
Quando a posicao fecha sozinha porque bateu o stop loss ou o take profit, nao houve clique — foi o broker que disparou. Logo nao existe "foto da vitrine" (bid/ask no clique) pra comparar. Antes, esse fechamento ficava sem medicao ("—" no painel) — e esse era o caminho MAIS comum (a maioria das batalhas morre batendo TP/SL).
A correcao S388: pra esse caso a regua certa nao e a cotacao, e o NIVEL pedido. O EA passou a guardar o nivel do stop/alvo que disparou (lendo do historico do broker) junto do preco real do fechamento, e o servidor calcula:
|preco real do fechamento − nivel do stop/alvo| = o quanto o broker honrou MAL o stop que voce pediu.Exemplo real (sandbox, fechamento por SL): voce pediu stop em 73640, o broker fechou exatamente em 73640 → Exec Slip = 0.0 (honrou perfeito). Se tivesse fechado em 73638, seria 0.02 pra baixo (broker escorregou 2 pontos contra voce).
Isso vale pros 3 caminhos de fechamento: ao vivo (deteccao instantanea), por varredura, e ate offline — se o EA estava fora do ar quando o stop bateu, ao voltar ele le do historico e atualiza a medicao. So fechamento MANUAL (sem stop disparado) fica "—" honesto: nao ha "nivel pedido" pra comparar.
Resumo das 3 reguas: abertura/fechamento-por-comando usam a cotacao (foto no clique); fechamento-por-stop usa o nivel; quem nao tem nem clique nem stop (manual) fica "—".
| Campo (signal_acks) | OPEN | MODIFY | CLOSE |
|---|---|---|---|
bid_at_request |
sim — foto do BID 1 tick antes do click | sim — foto antes de aplicar SL/TP novo | sim — foto antes do click |
ask_at_request |
sim — foto do ASK no mesmo instante | sim | sim |
exec_slip_pts |
sim — \|deal_price − ASK\| (BUY) ou \|deal_price − BID\| (SELL) |
NULL — MT5 nao devolve deal_price em MODIFY (sem fill novo) | sim — \|deal_price − BID\| (fechar BUY) ou \|deal_price − ASK\| (fechar SELL) |
pipeline_slip_pts |
sim — \|deal_price − signal.price\| (se signal.price > 0) |
NULL — idem acima | sim |
open_price (deal_price) |
sim — broker reporta | NULL | sim — broker reporta |
applied_sl / applied_tp |
sim | sim — valor novo aplicado | n/a |
actual_volume |
sim | NULL — MODIFY nao altera volume | sim |
gui_duration_ms |
sim | sim | sim |
Os NULLs em MODIFY nao sao bugs — sao consequencias fisicas: MT5 MODIFY so altera SL/TP sem criar deal novo, entao nao ha deal_price pra comparar. A foto bid/ask e capturada mesmo assim pra auditar "o preco passou pelo SL em transito?".
A foto bid/ask vale por 60 segundos (stale_threshold_ms = 60000). Se o tempo entre captura e fill no broker passar disso, o servidor descarta o calculo de exec_slip e seta NULL — mas mantem bid/ask pra audit trail. Assim sempre se sabe "qual era a vitrine quando clicaram".
close_reason="ROLLOVER" (Layer 1 anti-swap)Quando o EA fecha uma posicao perto da meia-noite pra fugir do juro overnight (swap), o Rollover_TryClose tagueia o ticket num buffer em memoria (g_rolloverTagged). O SnapshotEngine consulta esse buffer ao montar o CLOSE signal e:
close_reason → "ROLLOVER" em vez de "MANUAL" (default DEAL_REASON_CLIENT)bid_at_request/ask_at_request no payloadDistingue de Layer 2 servidor (close_reason="ROLLOVER_FALLBACK" via close_pair_positions — fallback se EA falhar). Spec completa: rollover-dupla-camada.md.
bid_at_request ≤ ask_at_request sempre que ambos NOT NULLexec_slip_pts ≥ 0 sempre (modulo)pipeline_slip_pts ≥ exec_slip_pts em 99% dos casos (transporte adiciona ruido, raro subtrair)bid_at_request IS NULL ⇔ ask_at_request IS NULL (par sempre junto)| Coluna | Valor |
|---|---|
| signal_id | 13123 |
| action | OPEN |
| symbol | BTCUSD |
| direction | BUY |
| bid_at_request | 81460.86 |
| ask_at_request | 81465.61 |
| deal_price (open_price) | 81465.82 |
| exec_slip_pts | 0.21 |
| pipeline_slip_pts | NULL (signal.price=0, signal manual) |
Calculo: BUY pega ASK como referencia. \|81465.82 − 81465.61\| = 0.21 pts. Significa que o GUI levou ~0.21 ponto a mais que a vitrine mostrava no momento do click.
No painel /af (modulo af_hedge.js), cada card de signal completo mostra:
close_reason="ROLLOVER" ou "ROLLOVER_FALLBACK"Se ambos slips forem NULL, as linhas sao omitidas (nao aparecem como "—" pra evitar poluicao).
specs/exec-slippage-telemetria.mdspecs/plans/exec-slippage-telemetria-PLAN.mdserver/signal_dispatch.py::compute_exec_slippageInclude/CopyTrade/WebBridge.mqh::SRolloverTagsignal-lifecycle.md linhas 401-413specs/reviews/S336-exec-slippage-REVIEW.mdStatus: ACTIVE | Ultima revisao: S233 (2026-04-14) — addendum S231 invert audit (ver
specs/invert-log.md)
Versao: 3.2
Data: 2026-03-29
Addendum S231 (invert audit): o step "Slave signal via invert pipeline" em
server/af/signals.py::generate_signals_for_pairagora usa_invert_for_peer()(helper centralizado) que alem de aplicar invert grava audit row emsignal_inversionsvia savepoint fire-and-forget. Zero mudanca de regra — so observabilidade. Detalhes:specs/invert-log.md. [drift-flush S233]
Status: CONFIRMADO — Regras core (S67), R-SAFE v1 (S95), Plano Diario + R-SAFE v2 (S96), Invert Dinamico + Pipeline (S99), Push Zone + MODIFY Post-Fill + Anti-Rollover + Post-Round Validation + Price Freshness + DLQ (S150).
Enforced em:server/af/(6 modulos) +scripts/hedge/rules.py+scripts/hedge/test_rules.py
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| R1 | Pool fixo = N cadeiras | Sempre N contas ativas. Se morre (F1/F2) ou chega funded: compra nova F1 no slot. Nunca mais, nunca menos. Hoje N=12. | Perde throughput (menos) ou gasta atoa (mais) |
| R7 | Nunca same-prop (= mesma Empresa, S362) | NUNCA colocar 2 contas da MESMA Empresa (prop_firms.company) uma contra a outra. S362: compara Empresa, nao o nome completo — 2 Programas diferentes da mesma Empresa (ex: "FTMO Swing" + "FTMO Aggressive", ambas company "FTMO") contam como same-prop e NAO pareiam. Se nao tem par de Empresa diferente, conta espera. SEM FALLBACK. |
Prop detecta hedge = ban |
| R8 | So mesma fase | F1 vs F1, F2 vs F2, Funded vs Funded. Nunca misturar fases. | Risco inconsistente entre fases |
| R12 | Max 6 contas/prop | No maximo 6 contas por pessoa na mesma prop firm. S362: conceitualmente conta por Empresa (a firma ve todos os Programas dela). Limite operacional (nao enforced no pareador). | Regra da prop |
| D6 | Parear por distancia | Ordenar contas por distancia ao target. Parear vizinhos (adjacentes). | Desperdicar trades pareando longe |
| D7 | Impar = 1 ociosa | Se N eh impar, 1 conta descansa no dia. Normal. | N/A |
Roteamento dentro da pool (S392 — nota): a ordem e' entregue pelo NUMERO da conta de cada lado do par (
AfPair.account_a_id/account_b_id->create_signal_canonical(target_account_id=...)), NAO pelo nome do grupo (group_id). Ogroup_ide' so RoTULO de isolamento da dupla; a copia legada que rotearia por nome fica SUPRIMIDA pra conta em pool AF.invertsegue sendo a oposicao do hedge (motor forca o invert do parceiro). Provado em producao (7 batalhas -> exatamente os 2 do par). Detalhe + codigo (arquivo:funcao):specs/af-roteamento-conta-vs-grupo.md.
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| R2 | F1 max = $2.500 | Na fase 1 (Challenge), risco maximo = $2.500 por trade (2.5% de $100k). | Ban pela prop |
| R3 | F2 max depende da prop | Props COM profit days (5ers, City): max $2.000 (2%). Props SEM profit days: max $2.500 (2.5%). | Ban ou trades insuficientes pra profit days |
| R4 | Smart risk | Nao apostar mais que precisa pra fechar. Se falta $800: aposta = ceil($800 / (1 - 0.02)) = $816. Minimo entre teto e distancia. | Overshoot = dinheiro jogado fora |
| R5 | Look-ahead (zona morta) | Antes de apostar, simula resultado. Se deixaria distancia OU colchao entre $0 e $500: reduz o risco. | Conta fica "quase la" com micro-trades inuteis |
| R5b | Spread compensation | No ULTIMO trade (distance <= max_risk + $150): permite risk = ceil(distance/(1-spread)), mesmo que ultrapasse max_risk interno. Safety cap = max_risk + $150: Funding Pips cap=$2,650 (2.65%, margem $350 pro hard breach 3%). Props com max_risk=$5k: cap=$5,150 (nunca atinge). Seguro com qualquer spread. Look-ahead do OPONENTE continua ativo normalmente. S362 #291: o cap deriva de max_risk_pct/100 * prop_size + $150 (_spread_comp_cap, escala por tamanho da conta; em 100k = identico). O cap e mascarado pelo hard-cap final (push_limit), entao a migracao de prop.max_risk USD pra max_risk_pct e zero mudanca de comportamento — so escala correto p/ contas != 100k. |
1 dia a mais por fase |
| R6 | Death trade | Colchao < max risk efetivo = modo death trade. Risk = colchao (aposta o que tem, SL/TP cabe na vida restante). Look-ahead nao protege. Ainda limitado pelo oponente (smart risk). Motivo: apostar mais que o colchao = SL ultrapassa piso DD = conta morre intraday mesmo "ganhando". | Sangrar devagar numa conta moribunda |
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| L1 | Zona morta = $0 a $500 inclusivo | Resultado entre $0 e $500 (inclusive) = zona morta. Precisa ser > $500 pra ser OK. Inclui rem <= 0 (morte por spread): contas com colchao em [max_risk, max_risk*(1+spread)) morreriam sem protecao. LA reduz risk pra manter rem > $500. | Micro-trades inuteis / morte evitavel |
| L2 | Threshold dinamico = max risk | Look-ahead so protege quando distancia OU colchao >= max risk efetivo da conta. Abaixo = 1 trade resolve, ignora look-ahead. F1: threshold $2.500. F2 profit-day (5ers/City): $2.000. F2 normal: $2.500. | Deadlock (S65) se threshold muito baixo |
| L2b | Push Zone = sem LA | Se distancia OU colchao estao na push zone (dentro de push_limit = max_risk + buffer), look-ahead NAO ativa. Conta vai passar ou morrer em 1 trade — LA seria contraproducente. Ver secao "Push Zone" abaixo. |
LA bloqueia fechamento natural |
| L3 | Convergencia 3x | Look-ahead roda ate 3 iteracoes (ajuste num check pode afetar outro). | Resultado sub-otimo |
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| D1 | Morte = breach real | Conta so morre quando balance cai ABAIXO do piso de DD ($90k com 10% DD). Nao tem "eutanasia antecipada". | Reciclar conta que ainda tem chance |
| D2 | Reciclar = nova F1 | Conta morta: comprar nova prop, voltar pra F1 ($100k). Slot preenchido. | Slot desperdicado |
| D3 | Funded = reciclar | Conta que passa F2: FUNDED! Comprar nova F1 pro slot. | Slot parado |
| D4 | F1 → F2 = CONTA NOVA | Passou target F1: status=passed. Prop firma DESATIVA conta F1 e emite NOVA conta com login/senha diferentes pra F2. Usuario cadastra nova conta manualmente no pool como F2. NUNCA promover mesma conta automaticamente — sao contas fisicamente diferentes no broker. |
Falso funded, equity errada |
| D4b | Cada fase = conta separada | Equity da conta F1 eh IRRELEVANTE pra F2 (conta F1 ja desativada). _has_passed() avalia o balance efetivo via _bal(pa): em modo live/demo usa _real_balance (sincronizado do heartbeat, transiente — nunca persistido), em simulacao usa virtual_balance. Virtual balance rastreia APENAS P&L do AF (recalc via recalc_virtual_balance()). P27: mundos separados — NUNCA misturar virtual com real. |
Avaliar com equity errada |
| D5 | Spread = 2% | XAUUSD: ganha = risk * (1 - 0.02), perde = risk * (1 + 0.02). Custo real por trade. | Simulacao nao reflete realidade |
| D6 | Transicao = dia seguinte | TODA transicao (passed, morte, funded) tem 1 dia de gap. Conta nova entra no pool no dia SEGUINTE. Slot fica vazio no dia da transicao. | Na realidade leva dias pra comprar prop |
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| P1 | Profit days | 5ers e City: 3 dias lucrativos (> $500 cada). Demais: sem exigencia. Se ja bateu target mas falta dias: atrasa 1 dia (micro-lote, sem hedge). Maioria bate naturalmente. | Rejeicao pela prop |
| P2 | Min dias trading | Varia por prop (3 a 5 dias F1/F2). Completar com 0.01 lote se atingiu target antes. Na simulacao: +1 dia, zero impacto no balance. | Rejeicao pela prop |
| P3 | Tipo DD diario | Equity (5ers, FTMO, Bright, FundingPips) = queda do pico do dia. Balance (City, FundedNext) = perda liquida. Com 1 trade/dia: nao faz diferenca. So importa com 2+ trades/dia. | Daily DD breach |
Filtro: steps == '2-step'
S389-cont5 (2026-05-30): removido o termo
max_risk_pct >= 2.5%do filtro de
elegibilidade — era a ULTIMA curadoria legada (mesma classe domax_dd >= 10
removido no S382: herdada da era do "10% fixo", NAO-validada). Risco minimo NAO
eh requisito de pool. Os requisitos REAIS sao: mesmo TAMANHO (prop_size, nivel da
conta), mesma FASE (R8, agrupada no pareamento) e mesmos STEPS (2-step, unico
requisito de prop firm — mantido). Mesa apertada (ex: Alpha 1.5%) opera SEGURA: o
motor cap no teto dela via a FKprop_firm_id -> prop_firms.max_risk_pct(S389
_ROUND_PROP_CACHE). ANTES do S389 ela furava (operava a 2.5% da pool), por isso a
curadoria fazia sentido defensivo; com o S389 virou restricao indevida. Decisao do
usuario.is_af_eligible = (steps == '2-step')— ver_compute_af_eligible.S382b (2026-05-27):
is_af_eligiblevirou TRAVA DURA de entrada (antes era so
filtro de catalogo, com fallback que deixava mesa nao-elegivel entrar). Agora mesa com
is_af_eligible=falseNAO entra em pool nenhuma — bloqueada em
_create_chairs_from_accounts(pula com motivo) eassign_account(400), via helper
_resolve_eligible_prop. Contas JA sentadas nao sao afetadas (trava so vale pra ENTRADA
de conta nova). Decisao do usuario S382.S382 (2026-05-27): o portao
max_dd >= 10%foi REMOVIDO do filtro de
elegibilidade. Era curadoria preventiva NAO-validada (herdada da era do "10% fixo"),
nao uma trava de seguranca real — cada mesa usa seu PROPRIO piso de morte
(_max_dd_val = prop_size*(1-max_dd/100), lido por-mesa). Mesa de 8% validada no
simulador E2E: morre no piso correto (92k em 100k) e o lado perdedor sobrevive ate o
par fechar. Verspecs/prop-firm-dd-form-redesign-S382.md.max_riskmigrado de USD
($2.500) pramax_risk_pct(2.5%) em S358/S362.
| # | Prop | Preco | F1% | F2% | Max Risk | Profit Days |
|---|---|---|---|---|---|---|
| 1 | The 5%ers | $545 | 8 | 5 | $5.000 | 3 lucrativos |
| 2 | FTMO Swing | $635 | 10 | 5 | $5.000 | nao |
| 3 | BrightFunded | $582 | 8 | 5 | $5.000 | nao |
| 4 | City Traders | $689 | 10 | 5 | $5.000 | 3 lucrativos |
| 5 | FundedNext | $549 | 8 | 5 | $5.000 | nao |
| 6 | Funding Pips | $529 | 8 | 5 | $2.500 | nao |
12 cadeiras = CLEAN_PROPS x 2 (2 contas por prop)
Excluidas:
- ~~Alpha Pro: max_risk $1.500 < $2.500 base~~ — S389-cont5: NAO mais excluida (portao max_risk>=2.5% removido; opera capada em 1.5% via S389 _ROUND_PROP_CACHE, validado no sim: par Alpha cap em $1.500 vs $2.500 das demais)
- ~~FTP Classic: max_dd 8% < 10%~~ — S382: NAO mais excluida (portao max_dd removido; 8% e' valido, validado no sim)
- (hoje so mesas NAO-2-step ficam de fora — unico requisito estrutural)
Pares podem ser same-person (5ers-Linniu vs FTMO-Linniu). Pipeline copy-trade usa invert pra hedge. Se ambos tem mesmo invert → mesma direcao → nao eh hedge.
Regra: AF engine garante master.invert != slave.invert antes de criar signal. Se iguais, flipa o slave.
| Passo | Acao |
|---|---|
| 1 | Sortear master/slave (aleatorio) |
| 2 | Group_id unico: AF_P{pool}_R{round}_P{pair} |
| 3 | Se master.invert == slave.invert → flip slave no DB |
| 4 | Sortear direcao (BUY/SELL) + anti-OSB check |
| 5 | Criar signal do master via pipeline copy-trade |
| 6 | Servidor aplica invert → slave recebe oposto |
Pre-condicao flip: 0 open positions na conta (Rule 6 invert-rules.md). Se tem posicao → skip par.
Pares same-person validos. Unica restricao: R7 (nunca same-prop).
Por que pipeline copy-trade: Trade_links automaticos, CLOSE propagacao, anti-echo, 70+ sessoes testado.
Quando uma conta esta muito perto de passar ou morrer, o risco normal + look-ahead podem ser contraproducentes (reduzir risco = mais trades = mais spread = mais chances de nao fechar). A push zone permite risco levemente acima do max_risk pra fechar em 1 trade.
| Aspecto | Detalhe |
|---|---|
| Constante | PUSH_BUFFER_PCT = 0.001 (0.1% de $100k = $100 para XAUUSD) |
| Limite | push_limit = max_risk + buffer ($2.600 para F1 XAUUSD) |
| Ativa quando | 0 < distance <= push_limit OU 0 < cushion <= push_limit |
| Efeito no risco | Permite risk > max_risk (ate push_limit) para fechar em 1 trade |
| Efeito no LA | Look-ahead NAO ativa na push zone (L2b) — conta resolve em 1 trade |
| Uso em MODIFY | Detecta near-pass/near-death para agendar MODIFY pos-fill |
| Config por simbolo | presets.py: BTCUSD usa buffer diferente de XAUUSD |
Exemplo: Conta F1 com distance=$2.550 (dentro de push_limit=$2.600). Risco normal seria limitado a $2.500 (max_risk). Na push zone: risk = ceil($2.550 / 0.98) = $2.602. Fecha em 1 trade ao inves de 2 (economia de 1 dia + 1 spread).
Apos AMBOS os lados de um par serem FILLED (executados), o servidor pode agendar um MODIFY (ajuste de SL/TP) para otimizar o fechamento — especialmente quando o par esta na push zone.
| Aspecto | Detalhe |
|---|---|
| Trigger | Ambos sides FILLED + par na push zone (near-pass ou near-death) |
| Delay | 30-120s apos fill (configuravel) — parece humano |
| Resultado | Novos sinais MODIFY com SL/TP ajustados |
| Log | Decisao salva em risk_detail do par |
| Regra | Protecao |
|---|---|
| F1: Anti-duplicata | Flag modify_scheduled impede MODIFY duplicado no mesmo par |
| F2/F3: Cap pelo PERDEDOR (S258) | desired_loss/desired_gain limitados a min(cushion_ou_dist, risk_max(perdedor) + push_buffer_usd) + margin. No hedge, ganho_vencedor == perda_perdedor: quem paga (perdedor) define o cap. Push buffer autoriza zona push near-target/near-death (+$100 default), coerente com get_push_limit usado pra ATIVAR. Regra antiga min(max_a, max_b) era aplicacao indevida da regra de abertura no pos-fill — punia injustamente vencedor fraco. Margin ($2-10) preservada. Em near_death o bug antigo era dormente (c pequeno raramente ativa cap); em near_target materializou no Pool 20 R3 BrightFunded ficando a $23 de passar F1. |
| F4: Sanidade | Shift maximo de SL/TP = 2x distancia original. Rejeita ajustes extremos |
| F5: Limite original | Risco pos-MODIFY nao pode exceder risco original do par |
| F6: Margem minima | Skip MODIFY se par ja esta dentro de margin_max do target |
| F7: Buffer SL | MODIFY SL desconta sl_buffer ($1.00 XAUUSD) da distancia — EA aplica buffer ao SL inclusive no MODIFY, entao sl_dist = desired/vol - buffer. Sem isso, perda real = desired + buffer_cost. So aplica pra SL (death), nao TP (target). |
Fluxo:
Ambos FILLED → Detecta push zone → Delay 30-120s → Calcula novo SL/TP
→ F1 (duplicata?) → F2/F3 (mais fraco) → F4 (sanidade) → F5 (limite)
→ F6 (ja proximo?) → Cria sinais MODIFY → EA aplica via GUI
| # | Regra | Detalhe |
|---|---|---|
| E1 | 1 trade/dia/conta | Maximo 1 operacao por conta por dia |
| E2 | SL = TP simetrico | Valor aleatorio $25-$50. Buffer $1 no SL |
| E3 | Anti-padrao | Variar SL/TP aleatoriamente a cada trade |
| E4 | Janela de abertura | 22:30 UTC (30min apos Sydney) ate 12:00 UTC (NY forex open). = 13.5h. Ordens abertas fecham por SL/TP a qualquer hora |
| E5 | Plano diario | Servidor gera plano completo no rollover (~22:00 UTC). Usuario aprova via Telegram antes de executar |
| E6 | Pre-check heartbeat | ANTES de enviar signal a um par, checar heartbeat de AMBAS as contas (< 90s). Se uma offline: notificar, esperar, skip se timeout |
| E7 | Retry com price gate | Se uma conta do par falha: retry via TV WS streaming. So abre se |
| E8 | EA = executor (excecao time-critical) | Regra geral: EA executa ordens via GUI e reporta status, servidor decide. Excecao: operacoes time-critical onde latencia de rede eh intoleravel (ex: E9 rollover close) — EA eh PRIMARY, servidor eh fallback+alerta |
| E9 | Rollover Dupla Camada (dual-layer swap close) | Camada 1 (EA primary, v3.72.0+): EA fecha posicoes dentro da janela [swap - min_before, swap - max_before] (default [21:00, 21:30] UTC). Horario sorteado POR POSICAO via DJB2 hash deterministico (account+ticket+UTC_date) % window_sec — reproducivel, sem persistencia, dispersa fechamentos entre posicoes pra evitar timing correlacionado. Camada 2 (servidor fallback): se qualquer pair AF ainda estiver executing em T-min_before (default T-30min), servidor forca close emergencial via check_rollover_fallback + Telegram CRITICAL rollover_fallback_fired + safecheck log. EA offline/drift/GUI fail: camada 1 emite WS rollover_close_failed ou rollover_drift e servidor cobre. SSoT detalhado: specs/rollover-dupla-camada.md. |
| E10 | Anti-self-hedge | Antes de gerar sinais, verifica se NENHUMA das contas do par tem posicao aberta. Se alguma tem: skip o par. Tambem filtrado no pareamento (contas com posicoes removidas do pool). Previne hedge contra si mesmo |
22:00 UTC (rollover):
1. Servidor coleta estado atualizado de todas as contas (balance, fase, distancia, colchao)
2. Gera plano:
a. Quais contas estao ativas (nao em transicao, nao mortas)
b. Pareamento (fase, distancia, never same-prop)
c. Risco por par (smart risk, LA, death trade, R-SAFE4)
d. Horarios de execucao (R-SAFE6: espacamento entre siblings)
e. Ordem aleatoria entre siblings da mesma prop
3. Envia resumo compacto via Telegram
4. Aguarda aprovacao do usuario
APROVACAO:
[Aprovar] -> execucao automatica nos horarios planejados
[Skip dia] -> nenhum trade executa
[Replanejar] -> recalcula plano com janela restante
(bloqueado se ha trades abertos da rodada)
Sem timeout — plano fica pendente ate resposta.
Se demorou: [Replanejar] gera novo plano a partir do horario atual.
PLANO 20/Mar — 6 pares, 12 contas
P1: 5ers-A vs FTMO-B | F1 | $2,500 | ~22:40
P2: City-C vs Bright-D | F2 | $1,920 | ~01:15
P3: 5ers-E vs FNext-F | F1 | $2,500 | ~05:45
P4: FTMO-G vs FPips-H | F1 | $2,500 | ~08:00
P5: 5ers-I vs City-J | F1 | $1,200 | ~10:30
P6: Bright-K vs FNext-L | F2 | $2,500 | ~11:45
Same-prop: 5ers(3): 7h+5h | FTMO(2): 9.3h
[Aprovar] [Skip dia] [Replanejar]
ANTES de enviar signal ao par:
Conta A: heartbeat < 90s? SIM
Conta B: heartbeat < 90s? SIM
-> Ambas online: prosseguir
Conta B: heartbeat > 90s? -> OFFLINE
-> Notificar Telegram: "Conta B offline, esperando..."
-> Esperar ate ficar online (sem timeout fixo, janela de abertura limita)
-> Se nao voltar antes do proximo par: skip este par
Signal enviado pra A e B (trade_group_id compartilhado)
A executa, ACK recebido (entry_price salvo)
B falha (GUI timeout, ACK fail)
Retry via TradingView WS (streaming, tempo real):
A cada tick: B online (HB < 90s) E |tv_price - entry_A| <= $2?
SIM -> re-envia signal pra B com MESMO trade_group_id
NAO -> continua esperando
Timer random(60, 90)s expira sem B abrir:
-> Fecha A via auto-close (origin="server_auto_close")
-> Notifica Telegram
Cenarios:
A ok B ok -> hedge OK
A ok B falha preco OK -> retry abre B, hedge OK
A ok B falha preco >$2 -> timeout, fecha A
A falha B falha -> nada abriu, notifica, skip
Price gate $2 (XAUUSD): Conservador. Cobre ~15-30s de movimento em NY. Garante hedge simetrico.
Futuro (BTCUSD, USDJPY): Threshold em % do preco em vez de USD fixo.
| # | Regra | Detalhe |
|---|---|---|
| S1 | 2 fases | Simulador DEVE modelar F1 E F2 separadamente |
| S2 | Same-phase pareamento | Respeitar R8 |
| S3 | Spread > 0 | Min 1%, padrao 2%. Spread 0% so pra sanity check |
| S4 | Min 500 seeds | Monte Carlo com >= 500 seeds |
| S5 | Float (decimal) | Balances e riscos em float. Sem arredondamento. |
| S6 | Custo reciclagem | Cada morte ou funded = custo da prop (preco normal, sem desconto). ROI desconta TODOS os custos. |
| S7 | Profit days tracking | Props com min_profit_days: contar dias lucrativos separadamente. Conta so passa quando balance >= target E profit_days >= exigencia. Na pratica, com risk $2k, matematicamente garantido bater 3 profit days antes do target. |
Principio: VARIACAO MAXIMA — Entre quaisquer contas same-prop (pessoas diferentes, pares diferentes), nenhuma dimensao observavel pode ter correlacao estatistica. O foco: nunca parecer copy trade.
Aplica-se quando: 2+ contas da mesma prop (pessoas diferentes) estao ativas no mesmo dia — INDEPENDENTE DA FASE. F1 vs F2 da mesma prop TAMBEM precisa divergir. A prop nao sabe o que eh "F1" ou "F2" — ela ve TODAS as contas.
Setup: PCs locais separados por pessoa (IP diferente, KYC diferente). Servidor coordena, EA so executa (E8).
Implementacao: Range Valido. Calcular o conjunto de valores validos ANTES de sortear — garantido encontrar se existir, zero tentativa-e-erro.
| # | Regra | Detalhe | Threshold | Se violar |
|---|---|---|---|---|
| R-SAFE1 | Espacamento minimo | Minimo random(1h, 2h) entre execucao de same-prop siblings. Planejado no rollover como parte do plano diario (E5). Sempre tenta agendar todos os siblings. | >= 1h (randomizado) | Prop ve abertura correlacionada |
| R-SAFE2 | Price gate | Preco atual deve diferir do preco de entrada do sibling em >= rsafe2_price_gate_pct * price / 100 (XAUUSD: 0.35%, BTCUSD: 0.08%, _default: 0.30%). Se nao moveu: retry a cada 5min, ate 60x (rsafe2_max_retries). Se fim da janela (12:00 no timezone do pool, via trading_tz_offset) OU max retries: skip (terminal). S255: threshold em % (era USD) — auto-escala cross-symbol. |
>= 0.35% (XAUUSD), 0.08% (BTCUSD) | Prop ve mesmo preco de entrada |
| R-SAFE3 | Lote != sibling | Lote resultante DEVE diferir do lote do sibling. Se igual: re-rolar SL/TP distance ate lote divergir. Hard check APOS todo calculo de risco. Lote = risk / (SL_distance * point_value) — SL/TP distance eh o motor principal de divergencia de lote. | >= 0.05 lots | Prop ve mesmo volume |
| R-SAFE4 | Risco variavel | DESATIVADO (S172). Reduzia risco em 20% quando siblings tinham risco < $100 diferenca. Removido: SL distance aleatorio ja cria variacao suficiente. | ~~< $100~~ | ~~P&L~~ |
| R-SAFE5 | SL/TP nivel absoluto != sibling | O NIVEL DE PRECO do SL/TP (nao a distancia) deve diferir >= $10 do sibling. Garante que closes acontecem em precos diferentes. Se nao cabe: skip a conta (sibling que ja executou fica OK). | >= $10 USD nivel absoluto | Prop ve close correlacionado |
| R-SAFE6 | Horario variavel (graph coloring) | Usa graph coloring pra atribuir slots a pares same-prop. rsafe_gap_min (1h), rsafe_gap_max (2h) — gap randomizado por par. Jitter 5-55s por par dentro do slot (anti-robotico). Ordem dos slots randomizada a cada rodada. Janela definida por trading_start/trading_end e trading_tz_offset do pool. |
Janela configuravel, min ~1-2h | Prop detecta padrao temporal |
Problema da versao anterior (S95): Checar distancia SL/TP (>= $3) nao garante closes em precos diferentes. Entry diferente + distancia diferente podem se cancelar:
Conta A: entry $3,050, SL dist $28 -> SL em $3,022
Conta B: entry $3,063, SL dist $41 -> SL em $3,022 <- MESMO nivel!
Distancia diferiu $13 (OK), mas nivel absoluto IDENTICO (PROBLEMA).
Solucao (S96): Checar o nivel absoluto do SL/TP (onde a ordem realmente fecha):
SL_level_A = entry_A - SL_dist_A (se BUY)
SL_level_B = entry_B - SL_dist_B (se BUY)
|SL_level_A - SL_level_B| >= $10 <- o que a prop realmente ve
TP_level_A = entry_A + TP_dist_A (se BUY)
TP_level_B = entry_B + TP_dist_B (se BUY)
|TP_level_A - TP_level_B| >= $10
Se nao cabe (range valido de SL/TP nao satisfaz >= $10 de nivel absoluto): skip a conta.
O sibling que ja executou continua normal — apenas o que nao conseguiu divergir fica ocioso.
Integrado com o plano diario (E5). Nao eh por sessao (robotico). Nao eh proporcional exato (robotico).
Regras:
- Janela: 22:30 UTC (30min apos Sydney open) ate 12:00 UTC (NY forex open)
- Espacamento entre siblings: minimo random(1h, 2h) por par de siblings, nao fixo
- Gaps irregulares: as vezes 1.5h, as vezes 8h. Sem padrao
- Ordem aleatoria cada dia (quem opera primeiro eh sorteado)
- Todos os siblings sao SEMPRE agendados. Skip so acontece se preco nao coopera no polling
Exemplo com 3 siblings (janela 13.5h):
Dia 1: 22:40, 01:15, 09:50 (gaps: 2.5h, 8.5h) ordem: C, A, B
Dia 2: 23:55, 06:20, 07:45 (gaps: 6.4h, 1.4h) ordem: B, C, A
Dia 3: 00:10, 04:30, 11:50 (gaps: 4.3h, 7.3h) ordem: A, B, C
Quando chega o horario de um sibling e o preco nao moveu >= gate do sibling anterior:
Timer encerra (ex: 05:45)
-> Polling a cada 5 min (rsafe2_retry_min, configurable)
-> A cada check: |preco_atual - entry_sibling| >= rsafe2_price_gate_pct * price / 100?
SIM -> executa (com jitter R-SAFE6)
NAO -> re-agenda +5min, incrementa retry counter
Fim da janela (12:00 no timezone do pool, via trading_tz_offset)
OU max retries (60, configurable):
-> Skip terminal (status = skipped_rsafe)
Gates por simbolo (presets.py, S255 em %): XAUUSD: 0.35% | BTCUSD: 0.08% | _default: 0.30%
Configuravel por pool: rsafe2_price_gate_pct no config JSONB (override via API ou dashboard). S255: migrado USD -> % pra auto-escalar cross-symbol.
A ordem entre same-prop siblings eh ALEATORIA. Nao eh sempre "Pessoa A primeiro, Pessoa B depois".
Cada dia, o servidor sorteia quem opera primeiro. Isso impede o padrao "Account X sempre precede Account Y".
Com 3 pessoas: ordem A-B-C, B-C-A, C-A-B, etc. Tudo aleatorio.
PARA CADA conta same-prop que vai operar apos siblings (ordem aleatoria):
1. Esperar horario planejado (R-SAFE6, definido no plano diario)
2. Checar preco vs TODOS os siblings ja executados via TradingView WS
(R-SAFE2: |preco_atual - preco_sibling_N| >= $10 pra cada N?)
- NAO: esperar ticks ate mover. Delay random(30s, 180s) apos gate.
- Fim da janela sem mover: skip
3. Calcular risco normalmente (smart risk, LA, death trade, etc.)
4. R-SAFE4: checar risco vs TODOS os siblings
- Se |risco - risco_sibling_N| < $100 pra algum N:
risco *= random(0.80, 0.90)
5. Calcular range valido de SL/TP:
- Range base: $25-50
- Pra cada sibling: calcular nivel absoluto e excluir SL/TP que ficaria
a menos de $10 do nivel absoluto de qualquer sibling
- Range valido = base - todas exclusoes
- Se range vazio: skip a conta
6. Sortear SL/TP do range valido
7. Calcular lote = risco / (SL/TP_distance * valor_ponto)
8. Checar lote vs TODOS os siblings:
- Se |lote - lote_sibling_N| < 0.05 pra algum N:
re-sortear SL/TP do range valido (novo valor muda lote)
Se persistir: aceitar (risco/preco ja divergiram)
9. Pre-check heartbeat (E6): ambas contas do par online?
10. Executar ordem. Se falha: retry com price gate $2 (E7)
R-SAFE2 (entry >= $10 diferente) + R-SAFE5 (SL/TP nivel absoluto >= $10 diferente) = closes em precos diferentes = closes em momentos diferentes. Garantido, nao por consequencia.
Funciona com N pessoas. Cada adicional = +1 slot de tempo, +1 check pairwise.
3 pessoas: todos checks sao pairwise (A vs B, A vs C, B vs C).
Coincidencia de direcao (ambos BUY no mesmo dia) NAO eh problema. Cada sibling esta num par diferente — direcao determinada pelo par (anti-OSB). Traders independentes podem comprar no mesmo dia naturalmente.
Padroes que nao sao de correlacao same-prop, mas de "parecer humano". Baixa prioridade — implementar quando R-SAFE1-6 estiver rodando em producao. TODOs #105-107.
No Monte Carlo (50/50, sem preco real), R-SAFE eh modelado como:
- Skip rate: ~10% dos trades second-pair (R-SAFE1+R-SAFE2 combinados)
- Risk reduction: Quando risco proximo do sibling (< $100), multiplicador 0.80-0.90
- Impacto medido (500 seeds): -8% funded, -6% ROI vs baseline. Aceito como custo de seguranca.
- Horario, SL/TP absoluto e lote sao detalhes de execucao sem impacto no 50/50
Apos cada rodada completar, o servidor roda 3 camadas de validacao automatica (run_post_round_validation()):
| Camada | Nome | Checks | Detalhe |
|---|---|---|---|
| L3 | Invariantes | I1-I11 | Contas, pares, sanidade: contas ativas no pool, pares validos, sem orfaos |
| L4 | Audit R-SAFE | I12-I17 | Compliance R-SAFE: espacamento (R-SAFE1), preco (R-SAFE2), volume (R-SAFE3), risco (R-SAFE4), niveis SL/TP (R-SAFE5) |
| L5 | Health | H1-H5 | Dead letters, timeouts, precos stale, falhas de margem, heartbeats ausentes |
post_round_validation no audit logSinais que falharam sao registrados na tabela AfDeadLetter para auditoria:
| Campo | Valores |
|---|---|
| failure_type | timeout, margin, rsafe, invert, gui_fail, e7_exhausted |
| Contexto | pair_id, account_id, prop_name, reason, attempts, context (JSON) |
volume = risk_usd / ((sl_distance + sl_buffer) / tick_size * tick_value)
sl_buffer = $1 — garante que TP trigger antes do SL no par (safety margin)Cada simbolo pode ter defaults diferentes via presets.py. Valores customizados por simbolo sao mergeados com config do banco: defaults ← preset ← database.
| Parametro | XAUUSD (default) | BTCUSD |
|---|---|---|
sl_min_pct / sl_max_pct (S255) |
0.85% / 1.70% | 0.65% / 1.60% |
dz_min / dz_max |
$500 / $1000 | $500 / $1000 |
rsafe2_price_gate_pct (S255) |
0.35% | 0.08% |
rsafe5_min_gap_pct (S255) |
0.35% | 0.30% |
push_buffer_usd |
$100 | $100 |
spread_pct |
2% | 3% |
S255 USD -> %: 4 thresholds price-facing migrados pra % do preco (auto-escala cross-symbol).
dz_min/dz_maxpermanecem USD (equity-facing). Verusd-to-pct-migration.md.
O codigo AF esta modularizado em 6 arquivos no diretorio server/af/:
| Camada | Modulo | Responsabilidade |
|---|---|---|
| Layer 0 | constants.py |
Constantes puras, sem imports internos |
| Layer 1 | risk.py |
Calculo de risco, R-SAFE |
| Layer 1 | pairing.py |
Pareamento de contas |
| Layer 1 | audit.py |
Invariantes, validacao pos-rodada |
| Layer 2 | lifecycle.py |
Orquestracao de rodadas, lifecycle |
| Layer 2 | signals.py |
Comunicacao com broker (OPEN/MODIFY/CLOSE) |
| Helper | presets.py |
Defaults por simbolo |
INICIO DO DIA:
1. Adicionar novas F1 (slots reciclados ontem)
2. Checar passes (balance >= target + profit days ok) → promover F1→F2 ou FUNDED
3. Checar mortes (balance < piso DD) → marcar pra reciclar amanha
4. Parear contas restantes (por fase, distancia, same-prop)
5. Calcular risco (smart risk + look-ahead)
6. Executar trades (50/50)
7. Atualizar balances (float)
FIM DO DIA
| Evento | Sistema faz | Usuario faz |
|---|---|---|
| Conta bate meta F1 | Sai do pool (stand-by), Telegram avisa | Esperar prop aprovar, cadastrar nova conta F2 |
| Conta bate meta F2 | Sai do pool (funded), Telegram avisa | Solicitar payout, comprar nova F1 pro slot |
| Conta morre | Sai do pool (dead), Telegram avisa | Comprar novo teste (~$500+) |
Transicoes NAO sao automaticas. Toda mudanca de conta/fase depende de acao humana.
| Momento | Funcao | O que checa | Arquivo |
|---|---|---|---|
| Inicio da rodada | process_round() |
sync_real_balances → check_passes → check_deaths → pair_accounts |
lifecycle.py:200-211 |
| Apos CADA trade | process_trade_result() |
Atualiza P&L → check_deaths → check_passes |
lifecycle.py:607-608 |
| Regenerar | regenerate_round() |
sync_real_balances → check_passes → check_deaths → re-pair |
lifecycle.py:407-408 |
| Round completo | process_trade_result() |
Se todos pares completaram → round_completed → run_post_round_validation → auto-gera proxima rodada |
lifecycle.py:617-650 |
Fluxo automatico: Rodada completa → nova rodada gera automaticamente → checks rodam no inicio → contas mortas/passadas sao excluidas antes de parear.
Gap: Se pool esta inativo (sem rodadas), mortes entre rodadas NAO sao detectadas ate proxima rodada.
Props flaggam contas que operam sempre na mesma direcao. Sistema sorteia direcao aleatoria com peso crescente:
- 1 seguido = ok | 2 seguidos = alerta | 3+ = critico (forca inversao)
- Se ambas contas do par tem peso alto: prioriza a mais critica
chair_owners no JSONB do pool (chave = # cadeira, valor = nome)owner_colors no JSONB do pool (chave = nome, valor = cor hex)| Evento | Conteudo |
|---|---|
| Par iniciado | Props, direcao, preco, volume, risco |
| Par concluido | Resultado por lado, custo spread |
| Rodada concluida | Resumo W/L, spread total |
| Transicao | Morte/Funded/Passed + detalhes |
| Plano diario | Resumo com pares + horarios (E5) |
Regra (I1): Sistema NUNCA envia OPEN signal pra symbol em mercado fechado.
4 camadas de defesa:
| Camada | Onde | O que faz |
|---|---|---|
| 1. Per-pair guard | main.py:1252 (scheduler) |
Checa is_market_open(pair.symbol) por par (NAO pool.symbol — bug S287) |
| 2. Dashboard badge | routes/af.py + static/js/af_hedge.js |
Endpoint expoe market_status no par; UI mostra badge amarelo "Mercado fechado" |
| 3. Cleanup periodico | _run_af_scheduler a cada 30s |
Pairs scheduled em symbol fechado >30min viram failed_market_closed (ou failed_swap_window em rollover) |
| 4. TV defensivo | is_market_open Camada 4 |
TV WS stale >5min + hardcoded=open = ambiguidade -> circuit breaker 3 strikes -> alerta HIGH |
Por que pair.symbol (nao pool.symbol): AfPair.symbol eh snapshot imutavel no momento de criar par. Pool symbol pode mudar (PUT /api/af/pool/X) mas pares ja criados com symbol antigo precisam ser checados pelo SEU symbol.
Estados terminais novos (TERMINAL_PAIR_STATES):
- failed_market_closed — pair scheduled em symbol fechado >cutoff
- failed_swap_window — pair scheduled em rollover ativo
Categoria separada no daily report (R7): failed_market_closed NAO conta como falha de execucao (sistema funcionou — mercado estava fechado). Renderiza segregado pra nao inflar taxa de erro.
Telegram dedup: f"market_closed:{pool_id}:{symbol}:{YYYYMMDD}" — 1 alerta/pool/symbol/dia.
Bug exposto (S287): Apos PUT /api/af/pool/20 trocando symbol XAU->BTC, par 81 (scheduled XAU) disparou OPEN em sexta 23:25 UTC (XAU fechado). Causa: guard checava pool.symbol (BTC, aberto). Fix: pair.symbol.
O sistema NAO diferencia — logica identica. Demo (Exness): morte = resetar gratis. Real (prop firms): morte = comprar novo teste.
Fluxo de reset (S207, teste only): reset-classification → acha cadeira passed/dead → restaura group_id → seta account_id=NULL → cadeira vira registro historico (prop_config tem snapshot: nome, dono, tipo). Conta fica livre pra re-classificar (#2 se mesmo combo prop/fase/dono). Re-adicionar na pool cria cadeira nova. Dashboard mostra cards historicos com saldo congelado (virtual_balance) e label "Saldo final (historico)". Timer da pool usa paused_at como referencia quando pausada (congela countdowns).
PAREAMENTO (por fase, separado):
F1 pool ──┬── F1 vs F1 (prop diferente, adjacente por distancia)
└── F1 ociosa (se sem par valido)
F2 pool ──┬── F2 vs F2 (prop diferente, adjacente por distancia)
└── F2 ociosa (se sem par valido)
RISCO (hierarquia, primeiro que limita ganha):
1. Teto da fase (F1: $2.500 | F2: $2.000 ou $2.500 conforme prop)
2. Smart risk: min(teto, ceil(dist / (1-spread)))
3. Look-ahead: evitar zona morta $0-$500 (so se >= max risk)
- Distancia < max_risk = reta final (ignora LA distancia)
- Colchao < max_risk = death trade (ignora LA colchao, risk = colchao)
4. Risco do PAR = min(risco_A, risco_B)
CICLO DE VIDA:
Comprar prop → F1 → [passa] → F2 → [passa] → FUNDED ($8k)
↓ ↓
[morre] [morre]
↓ ↓
Nova F1 Nova F1
| Data | Regra | Mudanca | Sessao |
|---|---|---|---|
| 2026-03-04 | R7 | Same-prop proibido, sem fallback | S64 |
| 2026-03-05 | L2 | Reta final fix (dist < 2*DZ skip check) | S65 |
| 2026-03-05 | R8 | Same-phase only (F1vsF1, F2vsF2) | S66 |
| 2026-03-05 | S1 | Simulador DEVE usar 2 fases | S66 |
| 2026-03-05 | R3 | F2 risk depende da prop (profit days: 2%, senao: 2.5%) | S67 |
| 2026-03-05 | R6 | Death trade: colchao < $2.500 = all-in, sem LA | S67 |
| 2026-03-05 | L2 | Threshold unificado $2.500 (distancia E colchao) | S67 |
| 2026-03-05 | D1 | Morte = breach real (balance < piso DD), nao threshold fixo | S67 |
| 2026-03-05 | R13 | REMOVIDA (piso $100 considerado inutil) | S67 |
| 2026-03-05 | R5 boost | REMOVIDA (boost reta final, coberta por L2) | S67 |
| 2026-03-05 | L2 | Threshold dinamico = max risk efetivo (nao fixo $2.500) | S67 |
| 2026-03-05 | R6 | Death trade threshold = max risk efetivo (consistente com L2) | S67 |
| 2026-03-05 | R2 | F1 max = $2.500 universal confirmado (todas props) | S67 |
| 2026-03-05 | — | Funded vs funded: futuro, nao simulado agora | S67 |
| 2026-03-05 | R5b | Spread compensation no fechamento (evita 3o trade em F2) | S67 |
| 2026-03-05 | L1 | Zona morta inclusiva: $0 a $500 (inclusive, precisa > $500) | S67 |
| 2026-03-05 | S5-S7 | Float, custo reciclagem, profit days tracking | S67 |
| 2026-03-05 | §9 | Ordem do dia definida (7 passos) | S67 |
| 2026-03-05 | R5b | Clarificado: seguro pra Funding Pips (hard breach 3%). LA do oponente ativo. | S68 |
| 2026-03-05 | D6 | Clarificado: F1→F2 TAMBEM tem 1 dia de gap (toda transicao) | S68 |
| 2026-03-05 | L1 | Expandido: inclui rem <= 0 (morte por spread). Contas com colchao em [max_risk, max_risk*(1+spread)) agora protegidas. +3.2 LA/seed. | S68 |
| 2026-03-05 | R5b | Safety cap: R5b limitado a max_risk + BUFFER ($150). FP: cap $2,650 (2.65%), margem $350 pro hard breach 3%. Seguro com qualquer spread. | S68 |
| 2026-03-07 | R6 | Death trade: risk = colchao (nao max_risk). Apostar mais que colchao = SL ultrapassa piso DD = morte intraday. Simulacao: 46.4 funded/yr (vs 48.8), 300% ROI (vs 324%), mas numeros realistas. | S69 |
| 2026-03-19 | R-SAFE1-6 | Regras anti-deteccao same-prop: timer minimo 1h, price gate $10-15, lote/risco/SL-TP divergentes, horario variavel Sydney-NY. Check & Re-roll. Principio: variacao maxima. | S95 |
| 2026-03-19 | — | advancing-front-v2.md arquivado. SSoT reduzido pra 2 arquivos: whitepaper + rules.py | S95 |
| 2026-03-19 | R-SAFE4 | Threshold $100 mantido. Sweep (0/50/100/200/500) mostrou custo ~8% funded constante. Seguranca > ROI. | S96 |
| 2026-03-19 | R-SAFE5 | Evoluido: distancia SL/TP (>=$3) -> nivel absoluto SL/TP (>=$10). Garante closes em precos diferentes. | S96 |
| 2026-03-19 | R-SAFE6 | Evoluido: horario generico -> plano diario. Janela 22:30-12:00 UTC (13.5h). Espacamento random min 1-2h. | S96 |
| 2026-03-19 | E4-E8 | Novas regras execucao: janela, plano diario, pre-check HB, retry price gate $2 via TV WS, EA executor. | S96 |
| 2026-03-19 | — | Versao whitepaper 2.0 -> 3.0 | S96 |
| 2026-03-19 | §6b | Invert dinamico por rodada: flip slave se master.invert == slave.invert. Pares same-person validos. Pipeline copy-trade pra signals. | S99 |
| 2026-03-19 | — | Versao whitepaper 3.0 -> 3.1 | S99 |
| 2026-03-29 | §6c | Push Zone: permite risk > max_risk na zona de fechamento (push_limit = max_risk + buffer) | S150 |
| 2026-03-29 | §6d | MODIFY Post-Fill: ajuste SL/TP pos-execucao com framework F1-F6 (mais fraco manda, P57) | S150 |
| 2026-03-29 | L2b | Push Zone Exemption: LA nao ativa se dist/cushion na push zone | S150 |
| 2026-03-29 | E7 | Price freshness (5s max age, 10 retries) + vol mismatch handling | S150 |
| 2026-03-29 | E9 | Anti-rollover: fecha pares antes do swap (janela configurable) | S150 |
| 2026-03-29 | E10 | Anti-self-hedge: skip par se conta tem posicao aberta | S150 |
| 2026-03-29 | D4b | P27: virtual vs real balance — _bal() usa real em live, virtual em sim | S150 |
| 2026-03-29 | R-SAFE2 | Retries (60x max) + price gate por simbolo (BTCUSD: $200) | S150 |
| 2026-03-29 | R-SAFE6 | Graph coloring + jitter 5-55s por par | S150 |
| 2026-03-29 | §10b | Validacao pos-rodada L3-L5 + DLQ + volume formula + presets + arquitetura | S150 |
| 2026-03-29 | — | Versao whitepaper 3.1 -> 3.2 | S150 |
| 2026-04-02 | §11 | Health endpoint: GET /pool/{id}/health — detecta missing snapshots, gap anomalies (>3x gap_max), stuck pairs (>2h executing), balance divergence (>15% virtual vs real). Severidade medium/high. | S173 |
| 2026-04-02 | — | Versao whitepaper 3.2 -> 3.3 | S174 |
| 2026-04-06 | §6d F2/F3 | F2/F3 cap preserva margem: desired = min(cushion, risk_usd) + margin — garante cruzamento floor/target |
S193 |
| 2026-04-06 | §6d F7 | Novo filtro F7: MODIFY SL desconta sl_buffer ($1.00 XAUUSD) — EA aplica buffer no MODIFY | S193 |
| 2026-04-06 | — | Versao whitepaper 3.3 -> 3.4 | S194 |
| 2026-04-19 | §6d F2/F3 | Cap usa risk_max(perdedor) + push_buffer, nao MIN das duas contas. Corrige case Pool 20 R3 BrightFunded F1 ficar a $23 de passar F1 (vencedor fraco com perdedor forte) | S258 |
| 2026-04-19 | — | Versao whitepaper 3.4 -> 3.5 | S258 |
| 2026-05-21 | R7 + R-SAFE | Empresa: R7 e sibling key R-SAFE comparam prop_firms.company (Empresa), nao o nome completo. 2 Programas da mesma Empresa (FTMO Swing + FTMO Aggressive) contam como same-prop. Coluna company obrigatoria. Preventivo (8 firmas distintas hoje = company=name, zero regressao). |
S362 |
| 2026-05-22 | R5b | Cap do R5b + fallback de get_max_risk migrados de prop.max_risk (USD fixo em 100k) pra max_risk_pct/100 * prop_size (_spread_comp_cap). Escala por tamanho da conta; em 100k identico. Cap mascarado pelo push_limit → zero mudanca de comportamento. Pre-requisito do drop da coluna max_risk (USD). |
S362 #291 |
Referência rápida pra consultar sempre. Explica cada valor que aparece na
tabela de regras das mesas (prop firms) e nos ajustes da pool — em linguagem
leiga. Última revisão: S362 (2026-05-21).
Muita gente mistura "risco" com "drawdown". São três coisas separadas:
| Limite | Pergunta que ele responde | Quem define | Exemplo em $100k |
|---|---|---|---|
| Risco por trade | Quanto arrisco em UMA operação? | A pool (max_risk_f1/f2_pct), com teto da mesa |
2,5% = $2.500/trade |
| Daily DD (drawdown diário) | Quanto posso perder SOMANDO o dia inteiro? | A prop firm (daily_dd_op_pct) |
4,5% = $4.500/dia |
| Max DD (drawdown total) | A partir de quanto a conta MORRE de vez? | A prop firm (max_dd) |
10% = morre em $90.000 |
Como eles conversam no motor: o risco por trade é o tamanho normal de cada
aposta. O Daily DD é um teto do dia — se você já perdeu muito hoje, ele
aperta o risco do próximo trade pra não estourar o dia (risco = min(risco_do_trade,
quanto_ainda_posso_perder_hoje)). O Max DD é o piso de morte: cruzou, a
conta acabou.
Hoje rodamos ~1 round/dia em demo, então o Daily DD quase nunca "morde" (1
trade de $2.500 < $4.500/dia). Quando operarmos vários trades/dia no real, ele
passa a limitar de verdade.
prop_firms)São as regras da mesa. Cada programa (ex: "FTMO Swing") tem as suas. Viram dólar
multiplicando o % pelo tamanho da conta.
| Campo | O que é (leigo) | Unidade | Usado hoje? |
|---|---|---|---|
name |
Nome do programa (ex "FTMO Swing") | texto | sim |
company |
Empresa dona do programa (ex "FTMO") — usada pra nunca parear 2 contas da mesma Empresa no hedge (R7) | texto | sim (S362) |
price |
Custo do desafio | USD | sim (relatório de custo) |
max_dd |
Piso de morte total: perda máxima desde o início. 10% = conta de $100k morre em $90k | % | SIM (motor) |
daily_dd |
Perda máxima do dia nominal (a regra oficial da mesa) | % | rótulo (futuro) |
daily_dd_op_pct |
Perda máxima do dia operacional (com folga: ~0,5% abaixo do nominal pra não chegar no limite). É o que o motor usa | % | SIM (motor, mas inerte com 1 round/dia) |
daily_dd_type |
Mede o DD diário por equity (valor vivo) ou balance (saldo fechado) |
enum | rótulo (futuro) |
target_f1 |
Alvo de lucro pra passar a Fase 1 | % | SIM (motor) |
target_f2 |
Alvo de lucro pra passar a Fase 2 | % | SIM (motor) |
min_days_f1 |
Dias mínimos de trade na Fase 1 | dias | rótulo (futuro) |
min_days_f2 |
Dias mínimos de trade na Fase 2 | dias | rótulo (futuro) |
min_profit_days |
Dias mínimos com lucro | dias | rótulo (futuro) |
max_risk_pct |
Teto da mesa: risco máximo por trade que a prop permite (o "mesa cap") | % | SIM (motor) |
limitation |
Observação textual (ex "SL obrigatório") | texto | rótulo |
steps |
Quantas fases o desafio tem (1 step / 2 steps) | número | sim |
max_risk |
LEGADO USD (risco/trade calibrado em $100k) — sendo aposentado | USD | em remoção |
daily_dd_op |
LEGADO USD (daily DD calibrado em $100k) — aposentado: motor não lê mais | USD | não (S362) |
af_pools.config)São os ajustes operacionais da pool, não da mesa.
| Campo | O que é (leigo) | Unidade |
|---|---|---|
max_risk_f1_pct |
Risco por trade na Fase 1 (o "risco por round") | % |
max_risk_f2_pct |
Risco por trade na Fase 2 | % |
dead_zone_min_pct / dead_zone_max_pct |
Zona morta (folga aleatória anti-detecção) | % |
equity_floor_enabled |
Liga/desliga o piso de segurança (airbag) | bool |
equity_floor_pct |
Piso de segurança em %: se o equity cair abaixo desse % do tamanho, EA fecha tudo. 8% = fecha em $92k (numa conta de $100k) | % |
spread_pct |
Spread assumido no cálculo do hedge | % |
sl_buffer_* / tp_buffer_* |
Folga de SL/TP por grupo de ativo (metais/forex/default) | pips ou preço |
buffer_mode |
Como o buffer é medido: PIPS ou PRICE |
enum |
swap_hour_utc / janela de trading |
Horários de rollover e janela operacional | hora UTC |
max_rounds_per_day |
Limite de rounds por dia (0 = ilimitado) | número |
pairing_strategy |
Algoritmo de pareamento (default greedy) |
texto |
Legado:
equity_floor_value(piso em USD) foi trocado porequity_floor_pct
(%) no S362. O servidor ainda lê o USD como fallback pra pools não-migrados.
A tela de preview das regras (e o motor) pegam o % × tamanho da conta:
target_f1 % × tamanho → 8% × $100k = $108.000(1 − max_dd%) × tamanho → (1 − 10%) × $100k = $90.000max_risk_f1_pct % × tamanho (limitado pelo max_risk_pct da mesa)(1 − equity_floor_pct%) × tamanho → (1 − 8%) × $100k = $92.000Por isso o S362 conectou o tamanho real da conta ao motor: antes ele assumia
$100k fixo (funcionava por coincidência, porque tudo é $100k). Agora escala
sozinho — uma conta de $50k calcula alvo $54k e piso $45k.
daily_dd, daily_dd_op_pct, daily_dd_type): só "morde"min_days_f1/f2, min_profit_days): só preenchem a tabela,max_risk, daily_dd_op): em remoção. daily_dd_op já estádaily_dd_op_pct). max_risk USD ainda tem 1 uso vivo naO botão Config Settings de cada pool abre um painel pra ajustar os buffers
(margens de segurança) e conferir como as contas estão. O que cada parte mostra:
O buffer é uma folga que você dá no preço de saída (stop/alvo) pra não ser
tirado por um detalhe do mercado. Ao digitar o buffer de cada tipo (Metais /
Forex / Cripto), o painel mostra, pra cada par real:
Tabela com o spread típico de cada par, juntado das contas. É uma média que
vai acumulando (o spread "normal", não o de um instante isolado). Dá pra ver
por par, por mesa (empresa) ou por conta.
Um interruptor "Só contas demo (simulando mesa)". Hoje todas as 12 contas
são demo (treino). Quando entrarem contas reais de prop firm, desmarque pra
ver só elas. Essa marca é definida num checkbox na hora de classificar a
conta — conta nova entra como real por padrão. No painel inicial, cada
conta demo ganha uma etiqueta azul "demo" no card, pra diferenciar de bate-
pronto.
Onde antes aparecia só o nome do desafio (ex: "Standard 2 Steps"), agora aparece
a empresa junto (ex: "FundedNext — Standard 2 Steps"), pra não confundir qual
mesa é qual.
O robô (EA) que roda em cada conta recebe os buffers e confirma de volta o
que aplicou. Esse painel compara o que você salvou com o que o robô
confirmou:
Por que isto existe: o hedge cruzado assume que cada prop firm é uma empresa independente — dono diferente, time de risco diferente, e elas não comparam contas entre si. Se duas "marcas diferentes" forem do mesmo grupo-mãe, o hedge entre elas vira uma independência ilusória (a empresa percebe que as duas contas são a mesma jogada e pode eliminar as duas). O setor consolida rápido — compras e fusões mês a mês — então esta lista envelhece e precisa de revisão periódica.
Última auditoria: 27 de Maio de 2026 · Próxima revisão recomendada: ~Agosto/2026 (ou assim que sair notícia de nova aquisição).
As 11 marcas cadastradas são, hoje, 11 grupos donos distintos — nenhum par da pool compartilha dono, então a premissa do hedge se sustenta. A única mudança recente: a Funded Trading Plus foi comprada pela Instant Funding (grupo Acello, Reino Unido) em 26/05/2026. Como a Instant Funding não está na nossa lista, não cria conflito interno hoje — mas a Funded Trading Plus deixou de ser independente.
| Marca | Grupo / dono real | País (sede) | Independente? |
|---|---|---|---|
| FTMO | FTMO s.r.o. (holding OMHC) | Sim | |
| The 5%ers | Five Percent Online Ltd | Sim | |
| FundedNext | NEXT Ventures | Sim | |
| BrightFunded | BrightFunded B.V. / Bright Global FZCO | Sim | |
| City Traders Imperium | CTI FZCO | Sim | |
| Funding Pips | ANKH PROP FZCO | Sim | |
| Alpha Capital (Alpha Pro 10%) | Alpha Capital Group Ltd (Kohler / AMGP) | Sim | |
| Funded Trading Plus | Acello Ltd / Instant Funding | Trocou de dono 26/05 | |
| For Traders | FT Trading Ltd + BLN Tech Club DMCC | Sim | |
| Maven | MAVEN LLC | Sim | |
| Fintokei | Purple Group / Purple Trading | Sim |
Estas marcas não estão na nossa lista hoje, mas pertencem ao MESMO grupo de uma marca que já está. Se qualquer uma entrar na pool, não pode parear (hedge) com a marca-mãe — seria a mesma empresa disfarçada. A regra R7 do sistema hoje compara o nome da marca, não o grupo-mãe, então este radar é manual até a regra olhar o grupo.
| Marca-irmã | Mesmo grupo de | Tipo |
|---|---|---|
| Instant Funding | Funded Trading Plus | prop firm (a compradora) |
| IF Crypto / IF Pro | Funded Trading Plus | sub-marca / broker do grupo Acello |
| Trade The Pool | The 5%ers | prop firm de ações |
| TSG ("Trade Set Go") | The 5%ers | broker dos fundadores |
| FundYourFX | The 5%ers | prop firm (co-fundador virou CEO) |
| FNmarkets | FundedNext | broker próprio do grupo |
| OANDA | FTMO | broker (comprado pela FTMO) |
| Quantlane | FTMO | tech (comprada pela FTMO) |
| Alpha Futures / Alpha Prime | Alpha Capital | sub-marcas internas |
| Purple Trading | Fintokei | broker que respalda a Fintokei |
Inspirado no mapa "Locations of Prop Firms" do PropFirmMatch. Repare na concentração: a maioria fica em Emirados e Reino Unido. Atenção importante — ficar no mesmo país NÃO significa mesmo dono; é só onde a empresa se registrou (muitas escolhem Emirados ou paraísos fiscais por imposto e regulação mais leve).
| País | Marcas | Quantas |
|---|---|---|
| FundedNext · City Traders Imperium · Funding Pips · For Traders | 4 | |
| The 5%ers · Alpha Capital · Funded Trading Plus | 3 | |
| FTMO · Fintokei | 2 | |
| BrightFunded (opera de Dubai) | 1 | |
| Maven (operação em Dubai) | 1 |
O que o "mapa" ensina pro hedge: várias firmas independentes dividem o mesmo endereço (Emirados, principalmente). Isso é normal e não compromete o hedge — o que importa é o dono, não o CEP. O risco de verdade aparece quando duas marcas têm o mesmo grupo-mãe (como Funded Trading Plus e Instant Funding agora) ou usam o mesmo provedor de liquidez nos bastidores — aí a execução pode ficar correlacionada mesmo com donos diferentes.
Status: ACTIVE | Ultima revisao: S326 (2026-05-04) — bump EA v3.82.0 -> v3.84.2 (atual). Drift coberto desde S294: S300 RECONCILE retroativo pos-offline (ReconcileEngine.mqh + 3 hooks LinniuC.mq5), S301 fix log spam ReconcileEngine (gate 60s OnTimer), S313 remove MODIFY precheck por tolerancia (formula 5point10^(digits-1) sempre dava 0.5 USD constante), S315.2 popup_text Unicode escape \uXXXX (preserva PT-BR no JSON), S319 cleanup WebBridge_PostReverse codigo morto. S326 Sessao A+B (NOVO): telemetria T4 EXATA via campo
ea_received_at_msno ACK payload. Buffer circularg_signalReceivedAtMs[64]em WebBridge.mqh + helperRecordSignalReceivedchamado dentro deLNC_BeginSignalObs(cobre WS via ExecuteSingleSignal:728 + HTTP via PollAndExecuteSignals:1221). Helper_S326_NowEpochMs()usa TimeGMT()*1000 com fallback TimeCurrent() se TimeGMT()=0. Sandboxaccount_id=3validado live em 4 signals (WS+HTTP+OPEN/MODIFY/CLOSE). Modulos e responsabilidades inalterados. Tabela WebBridge.mqh "v3.69.0" e nota historica preservada. [allow-spec]SSoT para: Modulos EA, compilacao, versionamento, erros MT5, armadilhas
Versao: 2.4 (EA v3.84.2, atualizado S326 — 2026-05-04). Desde 2.3: S300 ReconcileEngine retroativo, S301 fix log spam gate 60s, S313 remove MODIFY precheck constante, S315.2 popup_text Unicode escape, S319 cleanup PostReverse, S326 telemetria T4 ea_received_at_ms (Sessao A backend+UI + Sessao B EA exato).
Consolida:memory/ea-modules.md+.claude/knowledge/copytrade-ea.md
Relacionados:specs/invert-rules.md(inversao),specs/auto-update-flow.md(auto-update),specs/settings-sync-guardian.md(sync settings),memory/business-constants.md(constantes)
g_floorBreached=true → EA para TUDO (one-way, sem recovery)gui_lock_<HWND>.txt | 5. SettingsManager_InitPoll -> parse JSON -> para cada signal: resolve symbol -> apply buffers -> GUI execute -> detect local ticket -> ACK (FILLED/FAILED)
OnTradeTransaction — evento instantaneo do MT5. Chama SE_FindRecentOpenDeal que detecta o ticket no momento da transacaoDetectNewTicket — polling (antes era primario). Usado se OnTradeTransaction nao capturar| Modulo | Linhas | Prefixo | Responsabilidade |
|---|---|---|---|
| WebBridge.mqh | 809 | g_wb_ | HTTP via WinInet. Heartbeat, poll, ACK, signal post. Headers: X-API-Key + X-Account-Num. v3.69.0: ACK JSON precos usam Digits() do simbolo (antes: hardcoded 5). v3.84.0+ (S326 Sessao B): novo bloco no inicio do arquivo com struct SSignalRcvTs, buffer circular g_signalReceivedAtMs[64] (TTL implicito por overwrite, suficiente pra multi-ordem N=20 stress) + 3 helpers (_S326_NowEpochMs com fallback TimeCurrent quando TimeGMT()=0, RecordSignalReceived(signalId), GetSignalReceivedMs(signalId)). WebBridge_SendAck agora chama GetSignalReceivedMs(signalId) e adiciona campo "ea_received_at_ms":<long> no JSON do ACK quando valor > 0 (omitido quando 0 = entrada nao encontrada, preserva backward compat). Helpers movidos de LinniuC.mq5 em v3.84.1 (forward-dep cross-file MQL5 falhava silente). |
| GUIExecution.mqh | 1185 | g_gui_ | GUI automation Win32. OPEN=F9, MODIFY=ListView+DBLCLK, CLOSE=ListView+Fechar. Lock file anti-conflito. v3.83.8 (S315.2): GUI_TryConfirmBrokerPopup(hDlg, &popupText) retorna texto do popup via out param. GUI_JsonEscapeStr faz Unicode escape canonico \uXXXX (BMP) em chars >= 0x7F pra preservar acentos PT-BR no JSON HTTP body sem quebrar FastAPI parser. 3 emits gui_*_manual_close (OPEN s12 / MODIFY m6 / CLOSE) incluem field popup_text no payload signal_events. Spec: specs/decisions/0014-popup-text-mql5-unicode-escape.md |
| SnapshotEngine.mqh | 417 | g_se_ | Detecta OPEN/MODIFY/CLOSE. Compare snapshots -> enfileira. Cooldowns (CLOSE 0.5s, MODIFY 1s, OPEN 0.5s — OPEN guard via SE_SuppressTicket(newTicket, 5s)). SE_MAX_RETRIES=5: sinal descartado apos 5 falhas. SE_MAX_QUEUE=200 |
| SymbolResolver.mqh | 401 | g_sr_ | 3 camadas: Override -> Cache normalizado -> Alias (30 hardcoded). Buffers por classe |
| SettingsManager.mqh | 541 | g_sm_ | Cascata: SERVER -> LOCAL+JSON -> LOCAL -> OFFLINE. Persiste em Common/Files/. Init nunca falha. v3.66.0: Settings Sync Guardian — WS push notifica EA de mudancas, EA verifica e aplica (ver specs/settings-sync-guardian.md). v3.75.0: Config Freshness — AgeSec(), IsFresh(), IsStale(), CheckFreshness(). Config >5min sem sync = stale, Rollover camada 1 pausa. BuildAppliedJSON reporta config_age_sec + config_fresh |
| PanelDisplay.mqh | 440 | g_pnl_ | Painel Apple Finance Dark. Rounded corners, semi-transparent. Throttle 2s |
| AutoUpdate.mqh | 662 | g_au_ | States: IDLE->WAITING->DOWNLOADING->VERIFYING->LAUNCHING->DONE. Lock file anti-race. PS1 profile-aware |
| EventLog.mqh | 294 | g_el_ / EL_ | v3.73.2 (Fase C obs): Buffer per-signal de eventos de observabilidade (struct EL_Event, max 500 com FIFO overflow + WARN). EL_Emit() persiste cada evento em MQL5/Files/signal_events_pending.jsonl (append) — R3 zero-perda em crash. EL_LoadPersistent() em OnInit recupera buffer pos-crash. EL_FlushBulk() em OnTimer envia via POST /api/events (throttle 5s, batch 50, idempotente no servidor via UNIQUE). EL_RewritePersistFile + EL_BuildBulkJson auxiliares. Auto seq_num per-signal via scan linear. Sem call sites de emit ainda — integracao em WebBridge_SendAck/GUIExecution S1-S11/Poll fica em C10-C14. Spec: specs/ea-observability.md |
| WinInetHTTP.mqh | 325 | -- | Conexao persistente, request por chamada, leitura em chunks 8KB. Sem whitelist MT5 |
| WinHttpWS.mqh | 391 | -- | WebSocket async via DLL (MQL5 tem sockets TCP nativos mas sincrono/bloqueante; DLL da async + WS pronto + sem whitelist). Auth via 1a mensagem JSON. Fallback: HTTP polling sempre ativo |
| asyncwebsocket.mqh | 502 | -- | DLL wrapper para WS async |
| asyncwinhttp.mqh | 233 | -- | DLL wrapper para WinHTTP async |
| ReconcileEngine.mqh | ~210 | RE_ / g_reconcile_ | v3.83.3 (S301): Reconcile retroativo pos-offline. 3 trigger points: OnInit + OnTimer (gate 60s no LinniuC.mq5 + debounce interno 30s) + WS reconnect. RE_FetchOpenTradeLinks (GET /api/ea/open-tradelinks) + RE_ScanHistory (HistorySelect filtra DEAL_ENTRY_OUT por position_id) + RE_EnqueueRetroactive (POST /api/signals com is_retroactive=true). Profit canonico R4: DEAL_PROFIT + DEAL_SWAP + DEAL_COMMISSION. Mutex g_reconcile_running (R10) + checkpoint local (R11). Servidor garante idempotency (R6). NAO substitui _detect_missing_positions — eh fallback adicional. Decisao S302 (specs/decisions/0009-reconcile-architecture-simples-vs-esperto.md): manter gate 60s simples. Tentativa de smart short-circuit (v3.83.4) revertida — ganho irrelevante (~0.6s CPU/min total na pool) em troca de cobertura degradada nos cenarios 4 (OTT missado) e 5 (Snapshot bug). Specs: specs/reconciliacao-pos-offline.md + ADR 0009. |
Restart: Posicoes existentes viram snapshot inicial. NAO gera falsos OPEN ao reiniciar.
Anti-Echo (Pattern #34):
- SE_SuppressTicket(ticket, TTL): impede re-deteccao de ticket recem-operado
- OPEN: 5s TTL (chamado em LinniuC.mq5 apos GUI execute com sucesso)
- MODIFY/CLOSE: 10s TTL (chamado dentro do SnapshotEngine)
SettingsManager — Invert:
- ONLINE: Invert eh informacional. Servidor ja entrega sinais invertidos
- OFFLINE/LOCAL: EA faz inversao localmente via ApplyInversion
- SyncFromServer: So sobrescreve campo se servidor envia valor nao-nulo
Quando um deal fecha (SL/TP/STOP_OUT/MANUAL), o EA detecta no callback OnTradeTransaction e envia CLOSE direto via WebSocket — sem esperar o polling do SnapshotEngine (3s).
| Aspecto | Detalhe |
|---|---|
| Latencia | 10-50ms (vs 3s via SE polling) |
| Payload | deal_price (fill exato) + deal_profit (DEAL_PROFIT + COMMISSION + SWAP) |
| Fallback | Se WS fail → SnapshotEngine envia via HTTP normalmente |
| Guard | SE_MarkHintSent(posId) evita duplicata na fila SE |
| Close reasons | SL, TP, STOP_OUT, MANUAL |
EA manda HB cada 10s (WS) ou 30s (HTTP fallback). Servidor tem 2 lugares pra rastrear:
| Lugar | Campo | Throttle | Lag | Uso |
|---|---|---|---|---|
accounts.last_heartbeat_at (NOVO S314.3) |
UPDATED em CADA HB | NENHUM | <=10s | API canonica is_account_alive(), EA Tester badge |
heartbeats table (legacy) |
INSERT row + created_at |
WS_HB_DB_INTERVAL=30s (R2) |
ate 30s | Historico, balance/equity audit, callsites legacy (lazy migration) |
Helper canonico (server/account_filters.py:is_account_alive):
from app.account_filters import is_account_alive
if not is_account_alive(account, threshold_sec=30):
return jsonify({"error": "EA offline"}), 503
NULL-safe: retorna False se last_heartbeat_at IS NULL (conta nova/pre-migration).
Adopcao lazy — 6 callsites legacy (ea_update.py:529, monitoring.py:64, daily_report.py,
settings.py, heartbeat.py:1184, accounts.py:565) continuam usando
heartbeats.created_at. Cada PR que tocar arquivo migra pro helper. (S320: tournament.py removido.)
Spec completo: heartbeat-last-seen.md.
A partir de S386, o batimento (WS_SendHeartbeat em Include/CopyTrade/WinHttpWS.mqh) virou pulso leve — enriquecido com campos novos pra observabilidade da saturacao SEM tocar caminhos pesados:
| Campo | De onde vem | Custo |
|---|---|---|
balance, equity, positions_count |
cache do OnTradeTransaction (ja calculados) |
zero |
queue_depth |
SnapshotEngine_QueueCount() |
O(1) |
oldest_queued_age_ms |
SnapshotEngine_OldestQueuedAgeMs() — MAX de now - enqueuedTick per-item, GetTickCount monotonico |
O(N) na fila ate teto pequeno |
busy, busy_op, busy_ticket |
globais em GUIExecution.mqh (g_gui_busyOp, g_gui_busyTicket, g_gui_busySince), setados pelo beacon. NOTA: o pulso transmite apenas busy/busy_op/busy_ticket; o servidor calcula busy_since server-side via set_busy() no momento que recebe o beacon (nao depende do pulso transmitir). |
zero |
last_gui_ms |
GUIExecution.mqh::GUIExecution_LastGuiMs() (retorna g_gui_lastAnyMs — duracao da ULTIMA op de GUI, qualquer tipo) |
zero |
ea_version, timer_tick |
constantes/contador | zero |
Invariante R1 (S386): WS_SendHeartbeat NUNCA chama HistorySelect* nem GUI_*. Censo pesado (WB_BuildPositionsJSON) fica no ciclo separado de ~30s, inalterado.
ea_busy (S386 Fase 2)GUIExecution.mqh em OPEN/MODIFY/CLOSE dispara WS_SendBusyBeacon(op, ticket, expected_ms) IMEDIATAMENTE APOS pegar o lock de GUI (GUI_AcquireGUILock), antes do trabalho lento de tela. Se o lock falhar, o beacon NAO eh enviado. Frame WS {"type":"ea_busy",...}, dispara-e-esquece (timeout <=200ms, sem retry, sem ACK queue). Globais g_gui_busyOp + g_gui_busyTicket + g_gui_busySince setados sincronos pro pulso reportar status atual. Limpeza ocorre via GUI_ClearBusy() (em GUIExecution.mqh:204, chamado pelo GUI_ReleaseGUILock em :694) ao final de cada op. O servidor reflete o estado via pulso quando g_gui_busyOp="". Em paralelo, o ACK transporta gui_duration_ms (montado em Include/CopyTrade/WebBridge.mqh:1249) pra calcular latencia.
Spec completo: saturacao-visibilidade-gui-S386.md + signal-lifecycle.md secao "Estado Afogado".
Apos GUIExecution_Modify retornar sucesso, o EA le de volta os valores do broker e so envia FILLED se realmente aplicou:
max(5.0 * point * 10^(digits-1), 0.01) — acomoda precisao do simboloFAILED com diagnostico completo (g_gui_modifyDiag)O EA usa 3 estrategias em cascata pra preencher campos do dialog F9:
| Prioridade | Metodo | Como funciona | Quando falha |
|---|---|---|---|
| 1 (primario) | GUI_TypeText (WM_CHAR) |
Envia caractere por caractere — unico que atualiza o valor INTERNO do MT5 | Terminal extremamente lento |
| 2 (fallback) | SlowType |
Mesmo que #1 mas com 20ms delay por caractere | Raro |
| 3 (ultimo) | WM_SETTEXT |
Muda display mas NAO valor interno — broker pode rejeitar | Campo "validado" pelo MT5 |
Sync Barrier (WaitForEditValue):
1. SendMessageW(WM_NULL) — flush da fila de mensagens do OS (barreira de sincronizacao)
2. Check imediato — funciona 95%+ das vezes (sem polling)
3. Fallback poll 500ms — captura edge cases (MT5 reformatando valor)
Descoberta (S124): WM_SETTEXT sozinho causa "Invalid Volume" — MT5 precisa de WM_CHAR real pra validar.
| Guard | Threshold | Acao se violar |
|---|---|---|
| Volume Guard | \|actual - expected\| / expected > 10% |
ACK = FAILED, servidor fecha ambos lados (delay 61-90s) |
| SL/TP Readback | Tolerancia dinamica por simbolo | ACK = FAILED com diagnostico |
Buffer circular das ultimas 15 linhas de log significativas. Enviado no heartbeat campo last_logs.
Exemplos:
"OPEN OK #123456 XAUUSD BUY vol=0.10 250ms [S1-S10]"
"MODIFY READBACK FAIL #123456 SL=2049.5(tgt=2048.5)"
"OTT Deal ENTRY_OUT SL posId=123456 P&L=150.50 reason=SL"
"EQUITY FLOOR BREACHED: 89850.00"
Usado pra diagnostico remoto sem acesso ao terminal MT5.
Cada passo da execucao GUI eh logado com timing:
| Step | Acao | Timing tipico |
|---|---|---|
| S1 | CloseAllDialogs | 5ms |
| S2 | F9 sent | 1ms |
| S3 | Dialog appears (WaitForDialog) | 50-250ms |
| S4 | SelectSymbol | 50-150ms |
| S5 | SubDialog visible | 10-50ms |
| S6 | Controls found (hVol, hSL, hTP) | 5ms |
| S6b | QuickFlush vol | 10ms |
| S7 | Volume set (TypeText) | 100-200ms |
| S8 | SL set | 100-200ms |
| S9 | TP set | 100-200ms |
| S10 | VOL validation (final check) | 5ms |
Trace acumulado em g_gui_traceLog (pipe-separated), enviado nos remote logs.
Servidor envia SL/TP raw (precos absolutos). EA aplica buffer localmente:
| Input | Default | Descricao |
|---|---|---|
InpSLBuf_Metals |
100 pips ($1.00 XAUUSD) | Buffer SL metais |
InpTPBuf_Metals |
0 | Buffer TP metais (sem buffer) |
InpSLBuf_Forex |
0 | Buffer SL forex |
InpTPBuf_Forex |
0 | Buffer TP forex |
InpSLBuf_Default |
0 | Buffer outros |
InpBufferMode |
PIPS | PIPS (pontos fixos) ou PRICE (absoluto) |
Logica: Buffer adicionado ao SL (margem extra contra slippage). TP geralmente sem buffer.
| Aspecto | Detalhe |
|---|---|
| Input | InpEquityFloor (double, 0=desabilitado, ex: 90000) |
| Check | OnTimer + PollAndExecuteSignals |
| Trigger | AccountInfoDouble(ACCOUNT_EQUITY) < equity_floor |
| Efeito | g_floorBreached = true → EA para TUDO (OPEN/MODIFY/CLOSE rejeitados) |
| Recovery | NAO tem — one-way (feature de seguranca, nao bug) |
| Complementa | Server-side equity floor check (redundancia) |
| Aspecto | Detalhe |
|---|---|
| Inputs | InpRolloverEnabled (bool), InpRolloverSwapHourUTC (int, default 22), InpRolloverMinBefore (60), InpRolloverMaxBefore (30), InpRolloverClockDriftMax (60 seg) |
| Check | Rollover_Check() chamado no OnTimer apos EquityFloor_Check() |
| Janela ativa | [swap - min_before, swap - max_before] — default [21:00, 21:30] UTC |
| Sorteio | DJB2 hash (account_login + ticket + UTC_date) % window_sec via Rollover_ComputeOffsetSec() — deterministico, sem persistencia. Mesma posicao mesmo dia = mesmo horario |
| Execucao | Rollover_TryClose() usa GUI Win32 (F9) — mesmo padrao de EquityFloor_Check |
| Retry | Rollover_ScheduleRetry() + Rollover_ProcessRetries() — jitter 15s/30s/45s, max 3 tentativas |
| Deduplicacao | Rollover_AlreadyDone() + Rollover_MarkDone() via g_rolloverDoneTickets[] — evita reabrir dialog no mesmo ticket |
| Clock drift | Se |TimeGMT() - rollover_server_time| > drift_max: desabilita camada 1 + WS rollover_drift — servidor cobre em T-30 (camada 2) |
| Eventos WS | rollover_close_ok, rollover_close_failed (apos 3 fails), rollover_drift |
| Fallback | Servidor (check_rollover_fallback em server/af/signals.py) acorda em T-30 se pair ainda executing + envia Telegram CRITICAL |
| SSoT | specs/rollover-dupla-camada.md (desenho), specs/af-rules-whitepaper.md §E9 (regra) |
Anti-pattern nota: sorteio eh POR POSICAO (nao por par/pool) — evita 5 fechamentos correlatos que prop firm identifica como carimbo de robo.
Compilar: bash compile.sh [LinniuC|RandomTrader|RiskCalculator]
Deploy completo: Usar bash deploy_ea.sh (NUNCA copiar .ex5 manualmente). O script: compila, copia .ex5 Terminal→Dropbox, sincroniza pra VPS.
compile.sh: Detecta worktree via git rev-parse (S301), define DBX apropriado, sincroniza .mqh do $DBX/Include/CopyTrade/ -> $MQL5/Include/CopyTrade/ antes de compilar, chama MetaEditor CLI, copia .ex5 de volta pro $DBX.
Terminal MT5: C:\Users\mrodr\AppData\Roaming\MetaQuotes\Terminal\53785E099C927DB68A545C249CDBCE06\MQL5
Include/CopyTrade: Pasta REAL (S301+) — antes era junction apontando pro Dropbox/main, mas isso vazava arquivos da worktree pro main local quando alguem editava .mqh fora do main. Agora cada bash compile.sh sincroniza explicitamente do $DBX (worktree-aware) pro Terminal.
O sistema tem dois fluxos de deploy distintos com riscos diferentes:
deploy-canonico.sh (servidor) |
deploy_ea.sh (EA) |
|
|---|---|---|
| O que sobe | Codigo Python do servidor | Binario .ex5 do EA |
| Origem do codigo | Commits do git (git push) |
Arquivo .mq5 no disco (working dir) |
| Pra onde vai | VPS (FastAPI restart automatico) | VPS + auto-update na pool de N EAs |
| Automatico? | SIM, via post-commit hook a cada commit | NAO, precisa rodar manualmente |
| Precisa guard contra dirty? | NAO — git push so envia commits, dirty no working dir fica pra tras | SIM — deploy_ea.sh compila o .mq5 no disco, dirty vira binario na pool |
Por que a assimetria eh deliberada:
- Compilar EA exige MetaEditor (Windows GUI), nao roda em CI
- Pool baixa .ex5 em coordenacao — erro propaga pra todas as N contas de uma vez
- Quality gate proposital: passo manual forca validacao no sandbox antes de propagar
- Server restart eh trivial (segundos), reverter eh facil; EA deploy mexe com ordens reais em 12 mesas prop
Implicacao operacional: depois de git commit no main, o servidor ja foi atualizado em background. Mas o EA so atualiza quando voce explicitamente roda bash deploy_ea.sh (com guard que verifica commit limpo + avisa se rodando de worktree).
Quando trabalhar numa worktree paralela (criada via Claude Desktop ou bash scripts/paralelo.sh create <nome>):
| Passo | Comando | Efeito |
|---|---|---|
| 1. Editar .mq5/.mqh | direto na worktree | edits ficam isolados na worktree |
| 2. Compilar | bash compile.sh (de dentro da worktree) |
detecta worktree, sincroniza .mqh da worktree -> Terminal MT5, compila |
| 3. Testar | restart EA no MT5 | EA roda codigo da worktree, isolado do main |
| 4. Deploy pra VPS | bash deploy_ea.sh (de dentro da worktree) |
guard recusa se ha mudancas nao-commitadas; senao compila + SCP + EaVersion + auto-update na pool |
| 5. Commit | git commit na worktree |
fica em claude/<nome> ou paralelo/<nome> |
| 6. Merge pro main (OBRIGATORIO) | git merge claude/<nome> no main local |
post-commit auto-empurra pra VPS |
Risco se pular passo 6: pool roda binario da worktree, mas codigo fonte fica SO na branch. Proximo bash deploy_ea.sh rodado do main compila codigo DIFERENTE/ANTIGO -> regressao silenciosa na pool.
Mitigacao S301++ (commit 125da3f): deploy_ea.sh agora tem guard:
- Aborta se ha mudancas tracked nao-commitadas (forca commit antes de deploy)
- Avisa (nao bloqueia) se rodar de worktree, lembrando do merge pos-validacao
- Override emergencia: DEPLOY_DIRTY_OK=1 bash deploy_ea.sh
Regra: SEMPRE incrementar EA_BUILD ao modificar EA ou qualquer .mqh. Formato: MAJOR.MINOR.PATCH.
CUIDADO: "3.9" > "3.10" em string compare! Sempre usar 2+ digitos: "3.09"
Verificar versao: Log inicializacao | Heartbeat ea_version | Dashboard aba Contas | API GET /accounts
Mixed versions no mesmo grupo: Heartbeat intervals diferentes (WS vs polling), bugs corrigidos em versoes diferentes, ghost WS connections. Recomendacao: Manter TODAS as contas do mesmo grupo na MESMA versao.
CRITICO: Auto-update NUNCA deve rodar durante trade ativo — EA verifica g_se_hasOpenPositions.
common.ini campo ProfileLast (UTF-16LE) pra detectar qual profile esta ativo. Injeta EA em 1 chart do profile ativo, remove dos extrasupdate_attempts <= 5, depois paraUPDATE accounts SET update_attempts=0 WHERE id=Xlstrip('b') no servidorWS Reconnect (v3.77.0+, S253):
- Backoff exponencial: 3s → 6s → 12s → 24s → 48s → 60s (cap 1min). Antes v3.76-: 10-300s (cap 5min) — WS raramente vencia HTTP em reconnect
- Jitter por conta: account_num % 15 (0-14s). Antes v3.76-: % 7 (0-6s). Spread maior anti-storm com 13 EAs simultaneos
- Flush parcial: _flush_pending_signals usa continue em erro (S253) — antes break deixava pendings 4o/5o orfaos ate proximo reconnect
- Health check 60s: se connected=true mas sem msgs -> reset
- Fallback: 3x WS fail -> HTTP polling (2-5s adaptive). WS retenta cada 120s
- Constantes em WinHttpWS.mqh: WS_RECONNECT_BASE_SEC=3, WS_RECONNECT_MAX_SEC=60, WS_RECONNECT_JITTER_MAX_SEC=15
Retomada apos reconexao (server-side, ea_ws.py):
- _flush_pending_signals (linha 600): ao EA reconectar + autenticar, servidor busca PendingSignal da conta com resolved=False e expires_at nao vencido -> re-envia todos via WS. Log [WS-FLUSH] Sent N pending signals to account X. TTL = PENDING_SIGNAL_TTL_SECONDS=60s (sinal velho = pending_expired)
- _detect_missing_positions (linha 647): compara posicoes reportadas no heartbeat vs TradeLink is_closed=False. Ticket sumiu -> cria CLOSE signals pros pares (sibling accounts) automaticamente. Grace 30s pos-abertura pra evitar falso positivo
- Ghost connection (ea_ws.py:59-66): EA reconecta -> servidor fecha WS antiga com code 1000 reason "replaced". Evita 2 WS abertas mesma conta disputando sinais
- Auth: 2 modos — query params ou 1a mensagem JSON {"type":"auth"}. Timeout 10s. Version gate MIN_WS_VERSION=3.0.4
- Flush ACK (v3.76.0+, "tick cinza"): EA envia {"type":"flush_ack","signal_id":N} imediatamente ao receber signal via WS (ANTES de executar). Servidor handler _handle_ws_flush_ack marca pending_signals.ws_received_at = now. Separado de resolved=True (tick azul = ACK de execucao). Permite distinguir "nao chegou" de "chegou mas nao executou". EAs <3.76.0 nao emitem flush_ack — coluna fica NULL e polling HTTP continua sendo rede de seguranca
| Codigo | Nome | Causa | Fix |
|---|---|---|---|
| 10004 | REQUOTE | Preco mudou | Retry com slippage maior |
| 10006 | REQUEST_REJECTED | Broker rejeitou | Checar restricoes da conta |
| 10014 | INVALID_VOLUME | Lote invalido | Checar min/max/step do simbolo |
| 10016 | INVALID_STOPS | SL/TP invalido | Distancia minima, checar invert |
| 10017 | TRADE_DISABLED | Trading off | Horario, permissoes, AlgoTrading |
| 10018 | MARKET_CLOSED | Mercado fechado | Esperar abertura |
| 10019 | NOT_ENOUGH_MONEY | Sem margem | Balance/leverage insuficiente |
"EA nao faz nada": Checar aba Experts no MT5. Se nenhum log -> EA pode estar em loop ou OnTimer nao esta sendo chamado.
new/delete/nullptr/templates/STLStringLen/StringFind/StringSubstr (nao .length())#ifndef/#define/#endif obrigatorio em todo .mqhProp firms detectam ordens de EA (magic number, filling flags, deal comment). GUI automation cria ordens que parecem MANUAIS (F9 dialog).
CTrade, OrderSend(), PositionClose() ou OrderModify()<Trade\Trade.mqh> no GUIExecution.mqhuser32.dll, FindWindowExW, SendMessageW, PostMessageWDescoberta (S73): EA usava OrderSend desde v3.12.0 disfarçado como "fill mode fix" sem usuario saber.
| Campo | OPEN | MODIFY | CLOSE | Descricao |
|---|---|---|---|---|
status |
X | X | X | FILLED, FAILED, SKIPPED |
ticket |
X | X | X | Ticket local do broker |
error_msg |
X | X | X | Mensagem se FAILED |
open_price |
X | X | — | Preco de abertura/modificacao |
sl |
X | X | — | SL aplicado |
tp |
X | X | — | TP aplicado |
volume |
X | — | — | Volume executado |
channel |
X | X | X | ws_push ou http_poll |
latency_ms |
X | X | X | Tempo de execucao GUI (ms) |
deal_price |
— | — | X | Preco de fill exato do deal |
profit |
— | — | X | DEAL_PROFIT + COMMISSION + SWAP |
Quando OrderCalcProfit() falha (XAUUSD durante rollover, exoticos), EA usa formula manual:
profit = (price_diff) * volume * pip_value. Garante que AF tem dados pra calculo de risco mesmo em periodos com bugs do broker.
EA le key de MQL5/Files/copytrade_key.txt no OnInit. Prioridade: FILE (se len >= 10) > INPUT. Whitespace trimmed.
| Versao | Feature principal |
|---|---|
| 3.25.0 | WS Reconnect backoff (5s→15s→30s) |
| 3.29.3 | OnTradeTransaction como primario pra ticket detection |
| 3.32.0 | Volume/SL/TP safety guards (abort on mismatch) |
| 3.34.0 | GUI-TRACE 50 logs + LNC_LogRemote buffer |
| 3.39.0 | OTT Fast Close via WS |
| 3.44.0 | Sync barrier (WM_NULL) em SetEditText |
| 3.49.0 | TypeText (WM_CHAR) como primario pra GUI input |
| 3.48.0 | MODIFY diagnostic (g_gui_modifyDiag) + readback validation |
| 3.58.0 | Buffer SL no EA only, servidor envia SL/TP raw |
| 3.65.0 | Equity Floor Circuit Breaker |
| 3.72.0 | Rollover Dual-Layer — camada 1 EA primary (TODO #152) |
| 3.76.0 | WS flush_ack "tick cinza" (S251/S252) — EA confirma recepcao via WS |
| 3.77.0 | WS backoff tuning (S253 Onda 1): 10-300s → 3-60s, jitter 7→15, break→continue |
| 3.78.0 | WS resume semantics (S253 Onda 2): EA envia last_signal_id no auth, servidor filtra flush. _detect_missing_positions com paridade HTTP/WS + advisory lock |
| 3.79.0 | Onda 3: zombie detector (g_timerTickCount no HB + alerta Telegram) + ACK queue 20→100 + dump persistente em disco |
wss:// nao funciona em WinHttpCrackUrl -> usar https:// + secure=trueStatus: ACTIVE | Ultima revisao: S221 (2026-04-13) — drift fixes: --host 127.0.0.1 (nao 0.0.0.0), PG 16 (nao 15), js_error_reporter movido pra endpoints [allow-spec] | Drift S294 (2026-04-25): apos S288-S289 main.py absorveu 3 features novas (Market Hours Guard Camadas 1/4 — ver specs/af-market-hours-guard.md, Correlation ID end-to-end HTTP middleware, Exception Alerter Telegram com dedup). telegram_alerts.py absorveu Telegram dry-run + AfAuditLog (sim-e2e-real F2). Stack/nginx/systemd/PG/cron/rollback inalterados. Cron VPS atual (S293 D''): backup_daily 03h, healthcheck_v2 /2, monitor /5, error-monitor /30 (sem LLM, S293), build-context, quality_monitor, audit-export. daily-audit + weekly-research REMOVIDOS S293. Drift S354 (2026-05-19):* post-receive agora sincroniza
scripts/(causa-raiz drift Abr-25 — crons server-path rodavam código congelado); 7 crons versionados ganharam liveness marker (trap EXIT→/var/log/copytrade-cron-*-last-success.txt) que torna E23 executável. Cron VPS atual = 13 jobs (ver seção Cron). [allow-spec]SSoT para: Stack VPS, deploy, nginx, systemd, backup, rollback, Telegram, PostgreSQL
Consolida:memory/server-infra.md+.claude/knowledge/copytrade-ops.md+.claude/knowledge/copytrade-server.md+.claude/knowledge/copytrade-rollback.md
Internet -> Cloudflare (CDN/WAF) -> nginx (SSL linniuc.com) -> uvicorn 127.0.0.1:8000 (1 worker) -> FastAPI
|
PostgreSQL 16
ssh [email protected]/opt/copytrade-server/app/curl localhost:8000/api/...https://linniuc.com/api/ (porta 8000 NAO exposta)[Service]
User=copytrade
WorkingDirectory=/opt/copytrade-server
ExecStart=/opt/copytrade-server/venv/bin/gunicorn app.main:app \
--bind 127.0.0.1:8000 --workers 1 \
--worker-class uvicorn.workers.UvicornWorker \
--timeout 120 --graceful-timeout 30 --keep-alive 5 \
--access-logfile - --error-logfile - --log-level info
Restart=always | RestartSec=5
1 worker (single-process). Restart limit: 5 em 10s, depois "failed".
Drift fix S373 (2026-05-23): ExecStart real usa gunicorn (gerenciador de processos que supervisiona o worker — reinicia no crash, mata se travar >120s) com worker uvicorn (
UvicornWorker), naouvicorndireto como dizia antes. 1 worker, bind so em localhost. Verificado viasystemctl cat copytrade. [allow-spec]
systemctl restart copytrade # restart
systemctl reset-failed copytrade # limpar failed
journalctl -u copytrade -f # logs real-time
journalctl -u copytrade --since '5m ago' --no-pager # ultimos 5min
Config: /etc/nginx/sites-enabled/copytrade. HTTP->HTTPS redirect. www->non-www redirect.
SSL: /etc/ssl/linniuc.com.pem + .key. Certbot auto-renew.
WS: proxy_http_version 1.1, upgrade headers, timeout 600s.
| Erro | Causa | Fix |
|---|---|---|
| 502 | Uvicorn down | systemctl restart copytrade |
| 504 | Request >60s | Query lenta ou endpoint travado |
nginx -t && systemctl reload nginx # testar + reload sem downtime
certbot certificates # validade SSL
| Task | Intervalo | Funcao |
|---|---|---|
| offline_checker | 30s | Detecta EAs offline >90s, Telegram alert |
| drawdown_checker | 5min | Alerta drawdown por tipo conta |
| daily_report | 23:55 UTC | Relatorio diario Telegram |
| periodic_cleanup | 15min | Limpa heartbeats antigos, reconcilia trade_links orfaos, resolve pending expirados |
| auto_close_pendings | 15s | Auto-close stale pending signals (sinais nao consumidos) |
| initial_cleanup | 30s apos start | Resolve pending expirados na inicializacao |
| tv_price_ws | continuo | WebSocket TradingView precos (watchdog 45s) |
| telegram_bot | continuo | Polling comandos Telegram |
| af_scheduler | continuo | Orquestracao AutoFund — plano diario, execucao de pares |
| af_scheduler Block 7 | ~5s (dentro do scheduler) | Swap close checker — fecha pares antes do rollover |
| af_scheduler Block 8 | ~5s (dentro do scheduler) | MODIFY fallback — re-triggers stale modify_scheduled >3min (S154) |
| af_scheduler Block 9 | ~5s (dentro do scheduler) | Alert latch timeout — publica msg Telegram parcial se 2o ACK nao chegou em 10s (S258) |
| af_scheduler DST check | ~1h (dentro do scheduler) | Auto-adjust swap_hour_utc: 21 (verao) / 22 (inverno) (S154) |
| af_daily_report | 22:00 UTC | Relatorio AF diario via Telegram |
Endpoints auxiliares (nao background task):
- POST /api/js-error (main.py:835-854) — erros JS do dashboard vao pro Telegram (max 50/uptime via contador _JS_ERROR_MAX, dedup). Nao eh background task; eh endpoint HTTP normal chamado pelo window.onerror do dashboard.
Anti-spam Telegram (S389): o agrupador de rajada (burst aggregator em telegram_alerts.py) agora age SO em erros/riscos — alertas de status (EA online/offline/atualizado/rollout/restart/login ok/reconcile ok) sempre chegam individuais. Erros comprimidos sao resgataveis via comando /erros no bot. SSoT: specs/telegram-antispam-so-erros.md.
0 3 * * * backup_daily.sh # Backup diario 3h UTC (VPS-only)
*/2 * * * * healthcheck_v2.sh # Watchdog: auto-restart se cair (VPS-only)
*/5 * * * * monitor.sh # Monitor proativo + Telegram (VPS-only)
0 * * * * build-context.sh # Contexto pre-compilado do bot
*/30 * * * * error-monitor.sh # Erros journalctl -> Telegram (sem LLM)
*/15 * * * * quality_monitor.py # Quality monitor unificado
*/30 * * * * watchdog.py # Watchdog quality
30 3 * * * cron/purge_signal_events.sh # Retention signal_events 90d
0 * * * * cron/health_audit.sh # Traffic por endpoint -> alerta
0 4 * * * cleanup_login_attempts.sh # Purga login_attempts >30d
45 3 * * * cron/audit-export.sh # Export audit .csv.gz (6 anos)
0 12 * * * vps/drift-detect.sh # Detecta edicao direta VPS
0 10 * * 0 spec-freshness-alert.sh # Freshness specs (VPS-only)
Deploy de scripts/ (S354): post-receive sincroniza scripts/ →
/opt/copytrade-server/scripts/ (rsync SEM --delete — preserva VPS-only
como spec-freshness-alert.sh). Antes (até Abr-25) só server/static/docs
eram rsync'd → crons server-path rodavam código congelado. Crons chamados de
/opt/copytrade-code/scripts/ (build-context, error-monitor, drift-detect)
sempre foram frescos (checkout direto do hook).
Liveness markers E23 (#257): os 7 crons versionados gravam epoch UTC em
/var/log/copytrade-cron-<name>-last-success.txt via trap EXIT (só em exit
0). GET /api/health/cron-status lê isso → estação E23 da bateria EA Tester
sai de WARN permanente (available=false) para executável.
Alerta proativo de cron stale (#321, S378+): antes, a estação E23 só pegava
um cron caído manualmente (rodar a bateria) — drift-detect ficou DOWN 6 dias
sem ninguém ver. Agora uma task de fundo no próprio servidor (schedule_cron_stale_check
em server/routes/health_checks.py, disparada no lifespan do main.py) checa
o cron-status a cada CRON_STALE_CHECK_INTERVAL_SEC (6h) e manda Telegram por
cron stale, com dedup por nome via send_telegram_alert(category="cron_stale",
dedup_key=<nome>, cooldown=24h) (≤1 alerta/cron/dia). Decisão de design: mora
no servidor (sempre-vivo), NÃO num cron — um cron vigiando crons teria o MESMO
ponto único de falha que isto quer eliminar ("quem vigia o vigia"); se o servidor
cair, tudo já alerta. Função pura testável stale_crons_for_alert(status) (filtra
stale=True; available=false → [], degradação graciosa). Sobrevive a restart
(cooldown in-memory). Limitação conhecida: um cron que nunca gravou marcador é
invisível (sem marcador = fora do mapa); pega o caso real "rodou antes, parou agora"
(marcador envelhece > 24h).
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.
Deploy/mudanca + sistema quebrou?
|
+-- Servidor 502? -> §1 Rollback servidor
+-- DB corrompido? -> §2 Restore DB
+-- EA crasha? -> §3 Rollback EA (ver specs/ea-architecture.md)
+-- Config errada? -> §4 Revert config
+-- Tela branca? -> §5 Rollback frontend (ver specs/dashboard.md)
§1 Servidor: cp arquivo.py.bak arquivo.py && systemctl restart copytrade. Sem .bak: git show HEAD~1:scripts/server/ARQUIVO.py.
§2 DB: systemctl stop copytrade && psql < BACKUP.sql && systemctl start copytrade. Parcial: pg_restore -t TABELA.
§4 Config: ct_update_account(account_id=ID, field="group_id", value="GRUPO") ou DB direto.
Checklist pos-rollback: Servidor ativo? Logs sem erros? EAs reconectaram? Posicoes intactas? Dashboard funcional?
Pool: 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.
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 🔍.
/api/sim/* e /api/test/* piso interno 1500/min. Login 30/min/IP (50 localhost). Toda resposta /api/* traz RateLimit-Limit/Remaining/Reset; o 429 traz Retry-After (>=1s) — cliente se auto-regula (dashboard apiFetch re-tenta leitura honrando Retry-After+jitter, nunca escrita; bateria idem). Relogio monotonic (imune a NTP). Estouro sustentado de uma chave (>=20/300s) -> alerta Telegram debounced. in-memory janela-deslizante (CF-Connecting-IP ou TCP source — anti-spoof). Spec: specs/api-rate-limit-sweetspot.md. TODO #322: token bucket futuro gated em metrica.Claude Code (Win11) --stdio--> copytrade_mcp.py (FastMCP 3.1, Python 3.14) --HTTPS--> linniuc.com/api/*
Auth JWT auto-renovacao. Cache TTL 15s/30s. 15 tools read-only.
| Script | Funcao |
|---|---|
deploy.sh |
Deploy seguro: backup -> syntax -> import -> restart -> health -> auto-rollback |
healthcheck_v2.sh |
Watchdog: servico, HTTP, disco, heartbeat age, orphan links |
monitor.sh |
Proativo: failed ACKs, pending, stale, orphans, disco, DB size |
backup.sh / backup_daily.sh |
Backup manual / diario |
restore.sh |
Restaura backup |
Adicionar: DB insert ou Dashboard Admin > Contas > Adicionar. Instalar EA, configurar key.
Remover: Fechar posicoes -> SET is_active=false -> remover EA do chart. NAO deletar do DB.
| Preciso de... | Comando |
|---|---|
| Disco | df -h / |
| RAM | free -h |
| CPU | top -bn1 | head -5 |
| Logs | journalctl -u copytrade --since '5m ago' |
| SSL | certbot certificates |
| Nginx | nginx -t && systemctl status nginx |
| Firewall | ufw status (portas: 22, 80, 443) |
| Clock | timedatectl status |
| DB size | SELECT pg_size_pretty(pg_database_size('copytrade')); |
http://5.161.104.126:8000 -> porta NAO expostaStatus: ACTIVE | Ultima revisao: S233 (2026-04-14) — drift flush: S11 closePair #160 fix, test accounts toggle, signal timeline modal, pool-paused timer freeze, historical chairs P53-safe [allow-spec] | Drift S294 (2026-04-25): apos S256-S289 — Tools tab ganhou Simulacao E2E sub-pill (S277-S280, ver specs/sim-e2e.md), market_status badge S288 em af_hedge.js, swap_after_buffer_minutes input no form AF Pool, error feedback closePair/closeAllPool, Health Audit widget Sistema>Saude. Estrutura JS de namespace CT. + wsEvents inalterada. [allow-spec] | S311 (2026-04-29): classificacao PASSOU/BREACH em overview.js + af_hedge.js agora consome
prop_rules.target_pct/max_ddda API (era hardcode 8%/10%/100k duplicado). Ver secao "Classificacao de Contas Prop". [allow-spec] | S344 (2026-05-12): 3 bugs descobertos em wsEvents handlers via validacao visual Playwright. Fixes deployados: (a) handler 1 nao re-renderiza em modify_scheduled/modify_done (preserva badge cronometro), (b) setInterval do countdown agora tem ref pra clearInterval em modify_done, (c) catalogo de armadilhas em.claude/knowledge/dashboard-handler-traps.md. Ver secao "Bugs S344 — MODIFY badge handlers". [allow-spec] | S391 (2026-05-30):* secao "Ultima Rodada" (rounds encerrados) em af_hedge.js agora agrupa cards por tipo (Concluidas x Nao executadas), espelhando o S388 da secao ativa — cada sub-grid com altura uniforme, mata o buraco branco entre card alto (CONCLUIDO) e baixo (TIMEOUT/PAUSA ROLLOVER). Helper purosplitLastRoundByTypeem af_hedge_logic.js (testado em vitest). Cabecalho de grupo so quando ha 2+ classes. Verspecs/af-last-round-group-by-type.md. [allow-spec]SSoT para: Frontend SPA, tabs, deploy, armadilhas, XSS, temas
Consolida:memory/dashboard-features.md+.claude/knowledge/copytrade-dashboard.md+memory/dashboard-sidebar-notes.md
/opt/copytrade-server/static/ (subpastas: css/, js/)https://linniuc.comlocalStorage.ct_token (NAO jwt!)access_token (NAO token)| Arquivo | Responsabilidade |
|---|---|
index.html |
Layout principal, sidebar, modais (~500 linhas) |
api.js |
Estado global (window.CT), API wrappers, helpers |
app.js |
Init, auth, tabs, toasts (~1475 linhas) |
websocket.js |
WS client, reconnect, wsEvents event emitter |
overview.js |
Tab Overview (cards equity, grupos) |
accounts.js |
Tab Contas (CRUD, remote settings) |
positions.js |
Tab Posicoes (close/modify individual) |
admin.js |
Tab Admin (cleanup, debug) (~800 linhas) |
remote_settings.js |
Modal Remote Settings (6 buffers) |
delay.js |
Delay Analysis (stats + tabela) |
ea_update.js |
Auto-update UI (upload, release, rollback) |
health.js |
Tab Saude (diagnostics, logs) |
roadmap.js |
Kanban roadmap |
af_hedge.js |
AF Hedge tab (pair battles, pool config, force execute, history, debug mode, swap countdown, compact timeline) (~3860 linhas) |
af_hedge_logic.js |
Helper functions para operacoes AF hedge (~150 linhas) |
journal.js |
Tab Sistema > Journal (diario de trades, CSV export UTF-8, filtros) (~360 linhas) |
prop_firms.js |
Tab 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.py → scripts/hedge/dashboard.py → simulator.py → rules.py. Deploy necessario pra atualizar regras. |
styles.css |
Estilos globais + componentes extraidos (~3100 linhas) |
themes.css |
Overrides de temas (OBRIGATORIO) |
CT.*Todo estado compartilhado vive em window.CT = {} (definido em api.js). Variáveis: CT.token, CT.accounts, CT.allSignals, CT.equityChart, CT.chartHours, CT.refreshInterval, CT.notifSound, CT.ws, CT.wsHealthy, CT.syncingAccounts, CT.syncConfirmedAt, CT.afCurrentPool, CT.afPools, etc. Código novo DEVE usar CT.* — nunca criar variáveis globais soltas.
wsEventsWebSocket handlers registram-se via wsEvents.on('event', fn) em vez de monkey-patching handleWS. Definido em websocket.js. Eventos: heartbeat, signal, ack, reverse, login_failed, settings_synced, alert, account_new, af_pair_update. Módulos que escutam: af_hedge.js (af_pair_update), remote_settings.js (heartbeat), websocket.js (signal, ack, account_new, login_failed, heartbeat).
Todo CSS vive em styles.css e themes.css. NUNCA injetar CSS via JavaScript (document.createElement('style')). Seções em styles.css: base + Simulator + Delay Analysis + Remote Settings + Modify Modal + ACK Spinner + AF Hedge (battle cards, fighter panels, insight cards) + Journal + Prop Firms.
af_hedge.jsgetAccountDisplay() — busca nome completo + cor do dono de CT.accounts (alinhado com Visao Geral)cadastros.js. Hash legacy /tools/propfirms/cadastros/propfirms (window 30d).Atalhos: Alt+1..5 (tabs), R (refresh), Esc (fechar modal), ? (ajuda)
Backwards compat: switchTab('signals') redireciona automaticamente para Trading > Sinais
Browser MCP (unico — S160):
| Ferramenta | Melhor para | Config Brave |
|---|---|---|
| Playwright MCP | Automacao, snapshots, screenshots, debug | --executable-path pro Brave, --isolated, --image-responses omit |
Login: Usar conta test_runner (NAO admin). Credenciais em .secrets.local e tests/test-helpers.js.
- Campos: #loginUser + #loginPass (submit com \n no campo senha)
- Apos login: #loginPage some e #dashboard aparece
Exemplo @playwright/cli:
playwright-cli open https://linniuc.com --headed # abre Brave
playwright-cli fill e8 "test_runner" # usuario (ref do snapshot)
playwright-cli fill e11 "PW_test_runner_2026" # senha
playwright-cli click e12 # botao Entrar
playwright-cli screenshot # salva PNG em .playwright-cli/
playwright-cli close # fecha browser
Navegacao — SEMPRE usar eval (NAO clicar):
// Tabs principais (div.nav-tab, NAO sao buttons)
switchTab('overview') // Visao Geral
switchTab('tournament') // AF Hedge (S320: data-tab="tournament" eh naming legacy AF — backend tournament removido, frontend ID mantido por compat com af_hedge.js + app.js + overview.js)
switchTab('trading') // Trading
switchTab('system') // Sistema
switchTab('tools') // Tools
// Sub-tabs (div.sub-pill)
switchSub('tools', 'roadmap') // Tools > Roadmap
switchSub('tools', 'propfirms') // Tools > Prop Firms
switchSub('tools', 'simulator') // Tools > Simulador
switchSub('trading', 'signals') // Trading > Sinais
switchSub('trading', 'orders') // Trading > Ordens
switchSub('trading', 'positions') // Trading > Posicoes
Regras browser:
- Browser eh Brave, nao Chrome. SEMPRE rodar Brave-Debug.bat antes do DevTools MCP
- new_tab com URL no payload (NUNCA vazio + navigate separado)
- list_tabs ANTES de close_tab
- NAO usar tab principal do usuario — sempre nova tab
- Usar test_runner pra login automatizado, NUNCA admin
// 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);
3 modos: % Balance, % Equity, USD fixo. Formula: riskMoney / (slDist / tickSize * tickValue)
Lista completa (definida em websocket.js via wsEvents):
- heartbeat (balance/equity/positions)
- signal (novo signal)
- ack (confirmacao — MODIFY/CLOSE/OPEN)
- ea_status (online/offline)
- af_pair_update (critico AF — usado por af_hedge.js pra live updates de pares)
- reverse, login_failed, settings_synced, account_new, alert (auxiliares)
SSoT: Tabela prop_firms no DB. Backend enriquece cada conta no payload /api/accounts com prop_rules (objeto com target_pct, max_dd, daily_dd, equity_floor, target_usd, etc) ja ajustado por phase (F1 vs F2). Codigo: server/routes/accounts.py funcao _get_prop_rules.
Regra (overview.js + af_hedge.js):
// Frontend NUNCA reimplementa lookup de prop firm. Usa o que vem da API:
var baseSize = parseInt((a.prop_size || '100k').replace('k','')) * 1000 || 100000;
var targetPct = (a.prop_rules && a.prop_rules.target_pct) || 8; // fallback so se prop_rules vier null
var maxDdPct = (a.prop_rules && a.prop_rules.max_dd) || 10;
var target = baseSize * (1 + targetPct / 100);
var ddFloor = baseSize * (1 - maxDdPct / 100);
var isDead = equity < ddFloor; // BREACH (perdeu mais que max_dd)
var isPassed = equity >= target; // PASSOU (alcancou target_pct)
// senao: em progresso (visivel em "Contas Prop", nao em "Inativas (N)")
Anti-pattern (S311 fix): dicionario hardcoded JS tipo _propTargets = {'FTMO Swing':10, ...} ou 100000 hardcoded como baseSize. Duplica info do DB e bugifica ao adicionar prop firm nova ou conta de tamanho diferente. Smoking gun original: card mostrava "Meta 10%" (correto, do prop_rules) mas tag "PASSOU" (errado, do hardcode 8%) na mesma tela.
Filtro "Inativas (N)": Conta com isDead || isPassed cai aqui. Default escondido (_ovShowInactive = false). Checkbox toggle revela.
data-theme no <html>: original, apple, cyberpunk, pinkpurple, figma, brutalistdata-mode: dark, lightthemes.css, overrides via [data-theme="xxx"]--bg-card: #1a1a2e, --accent: #00d4aa, botoes #7c4dffRegra: NUNCA inserir dados da API via innerHTML sem escapeHtml() de api.js.
Status S82: 11 pontos XSS encontrados/corrigidos. Pendentes: positions.js (data-ticket), remote_settings.js (rsModalAccountId)
# 1. Copiar
scp app.js admin.js styles.css [email protected]:/opt/copytrade-server/app/static/
# 2. Cache bust (OBRIGATORIO)
ssh [email protected] "sed -i 's/v=[0-9]*/v=$(date +%Y%m%d%H)/' /opt/copytrade-server/app/static/index.html"
# 3. NAO precisa restart (nginx serve static files)
# 4. Verificar em aba anonima
Tela branca: F12 -> Console -> erro JS | Network -> app.js (200 vs 404) | Se erro -> const TDZ ou fetch sem try/catch
Dados nao atualizam: Network -> requests sendo feitas? | setInterval limpo? | WS desconectou?
Layout quebrado: Elements -> CSS | styles.css carregou? | media queries
| Armadilha | Fix |
|---|---|
const TDZ |
Declarar ANTES de usar em template literals |
| Fetch sem try/catch | SEMPRE envolver fetch |
| Login redirect loop | Limpar ct_token, recarregar |
| innerHTML XSS | SEMPRE usar escapeHtml() |
apiPost() retorna {ok, data} |
Acessar resp.data, checar resp.ok |
apiFetch timeout 8s |
S255: retry silencioso 1x em 1s em GET/HEAD se 502/503/504/AbortError (absorve restart do backend em deploys). POST/PUT/DELETE nao retry (nao-idempotente). Endpoints CPU-bound reais (simulador/export) ainda devem usar fetch() direto com timeout custom |
| CSS var inexistente | Consultar themes.css. Validos: --bg-card, --text-primary, --text-muted, --border |
Tabs fora do .main |
TODA tab-content DEVE estar dentro de div.main |
| Cache bust no deploy | Sem ?v= atualizado, browser serve versao antiga |
| themes.css removido | Quebra todos os temas. NUNCA remover |
| Chart.js resize infinito | Canvas DEVE estar em div com position:relative;height:Xpx |
~~showToast vs toast~~ |
CORRIGIDO S92. ea_update.js agora usa toast() de api.js |
| Validacao em browser separado | NUNCA usar browser principal do usuario |
| Mudanca | Arquivo | Nota |
|---|---|---|
| S11 #160 closePair | af_hedge.js:480-535 |
afConfirm refatorado: Promise com handlers escopados (sem singleton _afConfirmCb). Cada chamada tem settled local + cleanup(val), listener Escape/Enter, resolve direto. Corrige trava em double-click ou re-entrada por WS refresh. closePair(pairId, btn) agora recebe btn explicito (nao mais window.event). Teste Playwright: tests/af_confirm_modal.spec.js (regressao reentrante) |
| Test accounts toggle | index.html:688-700, accounts.js |
Checkbox "Mostrar contas de teste" + banner amarelo quando ligado. Separa visao normal de sandbox |
| Signal timeline modal | signal_timeline_modal.js, af_hedge.js botao showTradeTimeline |
Novo JS carregado em index.html, botao "Timeline" nos cards live/completed |
| Pool-paused timer freeze | af_hedge.js:1298-1360 |
Cronometros usam refMs = paused_at || Date.now(). Quando pool pausada, nenhum timer avanca. Inclui getStatusInfo(p, refMs) e isTimerExpired com mesma referencia |
| sl_buffer em cards live | af_hedge.js:1403-1410 |
rd.sl_buffer_offset injetado em posA/posB pra mostrar indicador "+" no side P&L ao vivo (nao so historico) |
| Historical chairs P53-safe | af_hedge.js:2638-2720 |
Chairs com status in ('passed','dead') e sem account_id ficam como _isHistorical, renderizam "Saldo final (historico)" em vez de "Aguardando sync MT5" |
| Add chair modal prefill | af_hedge.js:3148-3160 |
_populateAddChairFilters agora le require_type/firm/phase/size do pool config e pre-seleciona filtros |
| getAccountDisplay 3o arg | af_hedge.js:268-290 |
Novo parametro fallbackOwner pra color lookup quando Account tem nome generico (Conta #N). Usa prop_name + "F<phase>" + owner como fallback de display |
// Login (NAO tem #loginBtn!)
document.querySelector('.login-box .btn-primary')
// Sidebar
document.querySelector('.sidebar')
// Tabelas: #positionsTable, #signalsTable, #accountsTable
// Modais: #editModal
// Sidebar mode: localStorage "layoutSidebar" (NAO "dashboardLayout")
// Theme: #themeSelector > #themePanel
Status: ACTIVE | Ultima revisao: S151 (2026-03-29)
SSoT para: Metodologia debug, checklists, ferramentas, race conditions
Consolida:memory/debugging-playbook.md+.claude/knowledge/copytrade-debug.md+.claude/knowledge/debug-sistematico.md
Relacionados:specs/signal-lifecycle.md,specs/invert-rules.md,memory/business-constants.md
Sem root cause = fix aleatorio. Debugging sistematico: ~95% 1st-time fix. Random fixes: ~40%.
Isolar ANTES de consertar. Entender o que acontece, NAO "mudar e ver se melhora".
1. REPRODUZIR -> Confirmar que o problema existe agora (nao assumir)
2. LOCALIZAR -> Qual camada? EA / Servidor / Dashboard / Banco / Rede
3. INSTRUMENTAR -> Coletar dados: logs, queries, screenshots
4. ISOLAR -> 1 hipotese testavel por vez
5. CORRIGIR -> Fix cirurgico, minimo necessario
6. VALIDAR -> Confirmar fix funciona E nao quebrou mais nada
7. DOCUMENTAR -> Atualizar known-issues.md e changelog.md
1. QUE DIA/HORA? -> FDS? Feriado? Fora de sessao?
- Forex: seg 00:00 -> sex 22:00 UTC. FECHADO sab/dom
- Metals (XAUUSD): similar Forex
- Crypto: 24/7 mas brokers podem ter manutencao
- Indices: sessoes especificas, nao 24h
2. QUAL BROKER? -> Exness (streaming parcial) vs Capital Point (corta tudo)
3. QUAL SIMBOLO? -> Cada simbolo tem sua sessao
4. CONTROLAR VARIAVEIS -> Se 2 contas diferem: versao EA? Broker? Grupo? PC?
Correlacao != Causalidade
5. SO DEPOIS -> Se contexto nao explica, AI SIM investigar codigo
Regra de ouro: Contexto simples (mercado fechado, broker offline) eh MAIS provavel que bug.
1. IMPACTO PRIMEIRO -> open_positions: quem tem posicao aberta?
-> trade_links is_closed=false orfaos?
-> Posicao SEM par = DESYNC ATIVO
2. DEPOIS SINTOMA -> debounce, logs, spam
3. NUNCA reportar "zero posicoes" sem conferir TODAS as contas
git log -5 <arquivo> — commits recentes?CopyTrade-specific:
- [ ] Broker market hours? Terminal MT5 conectado? VPS SSH reachavel? DB connection?
1. Conta abre trade -> SnapshotEngine detecta -> POST /api/signals
2. Servidor cria Signal + TradeLink(origin) + PendingSignal
3. Destino poll -> GUIExecution abre -> POST ack (FILLED)
4. Servidor cria TradeLink(slave) -> resolve PendingSignal
Timing: Poll 1-3s, GUI 3-7s, ACK imediato (retry 3x). Total: ~5-10s.
g_processedSignalIds[] 100 IDs circularRoda a cada 15min na VPS (scripts/quality/quality_monitor.py). Market-aware (weekday/weekend thresholds). Dedup 1h.
| Check | Severidade | O que detecta |
|---|---|---|
| same_sign_pnl | CRITICAL | Ambos lados do par AF lucraram (impossivel em hedge) |
| duplicate_af_trades | CRITICAL | Trade duplicado no mesmo par AF |
| net_exceeds_risk | HIGH | Custo liquido do par excede risco planejado |
| stale_heartbeat | HIGH | EA sem heartbeat > threshold (15min dia util, 30min FDS) |
| phantom_positions | HIGH | Posicao no broker sem TradeLink correspondente (>30min) |
| signal_without_ack | HIGH | Signal distribuido sem ACK em >5min |
| ghost_positions | MEDIUM | TradeLink aberto mas posicao nao existe no broker (>10min) |
| orphan_trade_links | MEDIUM | TradeLink orfao (>24h sem par) |
| pnl_suspect_unresolved | MEDIUM | Par P&L suspeito sem resolucao |
Reconcile (reconcile.py): Detecta phantoms, ghosts, e P&L drift. Roda standalone ou via cron.
Alertas: CRITICAL/HIGH → Telegram imediato. MEDIUM → log only.
CLOSE antes de OPEN ACK: PendingSignal bloqueia CLOSE enquanto OPEN pendente. Se expirou (>60s) -> orphan checker pega.
MODIFY antes de FILL: EA ignora MODIFY (posicao nao existe). SL/TP reconciliation no heartbeat corrige (debounce 30s).
Duplicate ACK: Servidor usa idempotency — segundo ACK com mesmo signal_id = ignora.
| Ferramenta | Quando usar |
|---|---|
ct_trace(trade_group_id) |
Rastrear um trade especifico (timeline completa) |
ct_signals(hours, limit) |
Ver sinais recentes |
ct_positions |
Posicoes abertas de todas contas (detectar desync) |
ct_delay |
Latencia entre pares |
ct_diagnostics |
Telemetria EA (uptime, erros/h, ultimo erro) |
ct_errors(hours) |
Erros recentes |
Trace completo: Identificar trade_group_id (dashboard/DB) -> ct_trace -> procurar gaps (signal sem ACK, ACK FAILED).
tasklist | grep terminal64SELECT * FROM heartbeats WHERE account_id=X ORDER BY created_at DESC LIMIT 3powershell Get-Content ...Logs/YYYYMMDD.log -Encoding Unicode -Tail 50Orfaos: WHERE is_closed=false AND created_at < NOW() - INTERVAL '1h'
Duplicados: GROUP BY trade_group_id HAVING COUNT(*) > 4
signal_acks: error_msg (NAO error_message, NAO error)heartbeats: created_at (NAO last_seen). Ultimo HB: ORDER BY created_at DESC LIMIT 1trade_links: is_origin (NAO role)access_token (NAO token)| Dado | Dashboard | Diferenca possivel |
|---|---|---|
| Posicoes | open_positions (heartbeat snapshot) | Cache browser (refresh) |
| Equity | Ultimo heartbeat | Se stale, mostra dado antigo |
| EAs online | last_seen < 90s | Poll interval |
Arquivos P0 (afeta TUDO): signals.py, ea_ws.py, heartbeat.py, settings.py, main.py.
Mudanca -> backup + teste local + deploy horario seguro + monitorar 5min.
| Ferramenta | Comando |
|---|---|
| Logs EA | powershell Get-Content ...Logs/YYYYMMDD.log -Encoding Unicode -Tail N |
| Logs servidor | ssh root@... "journalctl -u copytrade --since '5 min ago' --no-pager" |
| DB query | ssh root@... 'PGPASSWORD=... psql -U copytrade_user -d copytrade -c "..."' |
| Health | curl -s https://linniuc.com/api/health |
Status: ACTIVE | Ultima revisao: S230 (2026-04-14) — revisado, mudancas em
signals.pyeGUIExecution.mqhdesde S194 sao de observabilidade (EventLog S228 C10-C14, Fase F simetria Modify/Close 3.73.6, Telegram alerts ack_failed, drift sync). Nenhuma mudanca toca logica de inversao. Verspecs/ea-observability.md.
Versao: 1.2
Data: 2026-04-14
SSoT para: Logica exata de inversao de sinais no CopyTrade
Analogia: Invert e como um espelho. Quando o master abre BUY, o slave invertido abre SELL. O SL e TP tambem trocam de lugar (o que era protecao vira alvo, e vice-versa).
Por que existe: No hedge, um lado PRECISA ser o oposto do outro. Invert automatiza isso.
| Modo | Quem faz | Quando |
|---|---|---|
Online (InpOnlineMode=true) |
Servidor | Ao criar o Signal para o peer. EA recebe JA invertido. |
Local (InpOnlineMode=false) |
EA | Ao ler o sinal, antes de executar. |
No modo online, o EA slave NAO aplica inversao — o sinal ja chega invertido.
INVERTE se: peer.invert != master.invert
| Master invert | Peer invert | Resultado |
|---|---|---|
| false | false | Copia normal |
| false | true | Inverte |
| true | false | Inverte |
| true | true | Copia normal |
Dois EAs com
invert=trueno mesmo grupo copiam entre si SEM inverter. So inverte quando os flags sao DIFERENTES.
INVERTE se: account.invert == true
No broadcast nao ha conta de origem, entao a condicao e simplesmente
if acct.invert.
funcao inverter(direction, sl, tp):
direction = "SELL" se era "BUY", "BUY" se era "SELL"
sl, tp = tp, sl // swap simples
sl_distance, tp_distance = tp_distance, sl_distance // swap
MASTER abre: BUY XAUUSD, SL = 2000, TP = 2100
SLAVE recebe: SELL XAUUSD, SL = 2100, TP = 2000
(era TP) (era SL)
signals.py:78-86)def _invert_direction(direction: str) -> str:
return "SELL" if direction == "BUY" else "BUY" if direction == "SELL" else direction
def _invert_sl_tp(sl: float, tp: float, invert: bool):
if invert:
return tp, sl # swap simples
return sl, tp
GUIExecution.mqh:1527-1531)void GUIExecution_ApplyInversion(string &direction, string &sl, string &tp)
{
direction = (direction == "BUY") ? "SELL" : "BUY";
string tmp = sl; sl = tp; tp = tmp;
}
| Tipo | Direction inverte? | SL/TP inverte? | Distancias invertem? |
|---|---|---|---|
| OPEN | Sim | Sim | Sim |
| MODIFY (EA-to-EA) | Sim (por consistencia) | Sim | Nao (MODIFY nao tem distancias) |
| MODIFY (broadcast) | Nao (direction vazia) | Sim | Nao |
| CLOSE | Nao | Nao | Nao |
CLOSE nao inverte NADA. O servidor usa o
local_ticketdo TradeLink para saber qual posicao fechar. Direction/SL/TP sao zerados no CLOSE.
Quando InpOnlineMode=false e invert=true:
| Tipo | Comportamento |
|---|---|
| OPEN | GUIExecution_ApplyInversion(direction, sl, tp) — inverte tudo |
| MODIFY | Passa dummyDir = "" — so SL/TP sao swapados, direction nao muda |
| CLOSE | Nenhuma inversao |
# accounts.py:147-148
if data.invert != account.invert:
_check_open_positions(db, account_id, "invert")
# Lanca HTTP 409 se conta tem posicoes abertas
NAO e possivel mudar o flag
invertde uma conta que tem posicoes abertas. Fechar todas antes.
| Local | Campo | Tipo |
|---|---|---|
| PostgreSQL | accounts.invert |
Boolean, default False |
| EA input | InpInvertSignal |
bool, default false |
| EA runtime | g_sm_settings.invert |
Sobrescrito pelo servidor via SettingsManager_Sync() |
O servidor e a fonte da verdade. O EA sincroniza o valor no heartbeat.
| # | Armadilha | Detalhe |
|---|---|---|
| G1 | broadcast-modify: enviar valores originais | Dashboard deve enviar SL/TP para a posicao ORIGINAL (nao invertida). O servidor faz o swap. Enviar valores ja invertidos = dupla inversao = errado. |
| G2 | Validacao de SL/TP apos swap | BUY: SL < preco, TP > preco. SELL: SL > preco, TP < preco. Apos swap, os valores continuam validos para a direcao oposta porque o que era SL do BUY (abaixo) vira TP do SELL (tambem abaixo). |
| G3 | Partial close | Detectado como MODIFY com close_reason="PARTIAL_CLOSE". Inversao identica a MODIFY normal — sem tratamento especial. |
| G4 | Modo local sem protecao | Se EA offline e invert=true, inverte localmente sem validacao do servidor. Se flag estiver errado no input = trades invertidos errado. |
Status: ACTIVE | Ultima revisao: S230 (2026-04-14) — revisado, mudancas em
ea_ws.pydesde S173 sao de observabilidade (Telegram alerts ack_failed, Fase E S228) e NAO afetam o fluxo de auto-update. Logica inalterada. Verspecs/ea-observability.md.
Versao: 1.3
Data: 2026-04-14
SSoT para: Fluxo completo de auto-update do EA (upload → staged rollout → restart)
Relacionado: specs/settings-sync-guardian.md — Settings Sync Guardian (v3.66.0, WS push + verify) complementa o auto-update com sync de configuracoes em tempo real
Analogia: E como atualizar um app no celular. O operador faz upload da nova versao, o sistema distribui primeiro pros "beta testers" (contas early), e so depois libera pro resto (contas stable). O EA se auto-atualiza, reinicia o MT5, e volta a operar.
OPERADOR SERVIDOR EA
───────── ───────── ────
1. Upload .ex5 via Dashboard → 2. Salva + SHA256 → ea_versions(pending)
3. Clica "Release" → 4. stage=testing → desired_version nas early
5. Background monitor (30min)
6. Heartbeat HTTP → update_available=true
7. Posicoes=0? → Download + Verify + Launch
8. Gera PS1, mata MT5, copia .ex5, reinicia
9. Proximo HB: versao nova → limpa desired
10. Contas early atualizaram →
stage=stable → desired nas stable
11. Contas stable: mesmo fluxo (6-9)
AU_IDLE ──[update_available=true]──→ posicoes=0? ──sim──→ AU_DOWNLOADING
↑ | |
| nao [HTTP GET .ex5]
| AU_WAITING |
| (max 30min) AU_VERIFYING
| | [SHA256 + size]
+────[timeout 30min]─────────────────+ |
AU_LAUNCHING
[gera PS1, ShellExecute]
|
AU_DONE
[Sleep(2s) + ExpertRemove]
| Estado | Descricao |
|---|---|
AU_IDLE (0) |
Esperando update |
AU_WAITING (1) |
Update disponivel, aguardando 0 posicoes |
AU_DOWNLOADING (2) |
Baixando .ex5 |
AU_VERIFYING (3) |
Verificando SHA256 + tamanho |
AU_LAUNCHING (4) |
Gerando PS1 + lancando PowerShell |
AU_DONE (5) |
Script lancado, EA se mata |
AU_ERROR (-1) |
Erro — retry no proximo ciclo |
AU_LOCKED (-2) |
Outro EA no mesmo terminal ja esta atualizando |
Resposta do POST /api/heartbeat inclui:
{
"update_available": true,
"update_version": "3.25.0",
"update_hash": "1869006665f4eae46...",
"update_size": 337958,
"update_url": "/api/ea/download/3.25.0",
"update_force": false
}
Mensagem tipo "update" com version + hash + size + url. EA monta fakeHB e chama mesma funcao.
{"type": "update", "version": "3.65.0", "hash": "abc123...", "size": 337958, "url": "/api/ea/download/3.65.0"}
Nota: WS push NAO inclui
update_force(sempre assume force=false). Se force necessario, usar heartbeat HTTP.
Armadilha: Com WS ativo, HTTP roda so a cada 120s. Update pode demorar ate 2min pra ser detectado via HTTP.
Se conta NAO tem desired_version definido, o heartbeat promove automaticamente para a versao mais nova ELEGIVEL — sem acao do operador ("pegar o trem"). O conjunto de stages elegiveis depende do tipo da conta:
| Tipo de conta | Stages elegiveis | Pega build testing? |
|---|---|---|
Pool / real (is_test=false) |
stable (+is_stable=true) |
NAO |
Sandbox dogfood (is_test=true, nao-sim, nao-stress) |
testing, stable, completed |
SIM (S378) |
S378 — Dogfood ring: a bancada de dev (sandbox is_test) recebe automaticamente o build MAIS NOVO, mesmo em testing, igual ao "dogfood" da industria (no interno auto-instala o experimental, distinto do canary que vai pra subset de usuarios reais). Isso permite que a bancada e o teste "Validar tudo" exercitem o build novo sem o operador ter que promover pra stable. INV-LEAK: conta nao-dogfood NUNCA pega testing (property-tested em test_dogfood_catchup_s378.py).
Implementado em heartbeat_helpers.py (pick_catchup_version + is_dogfood_account + eligible_catchup_stages), chamado pelo _compute_update_info. Guard espelha can_force_downgrade (routes/ea_tester.py). Util quando contas novas sao criadas apos um release, e essencial pro vai-volta do teste restart+reconcile.
// MAJOR.MINOR.PATCH — componente por componente
AU_CompareVersions("3.25.0", "3.24.1") → positivo (3.25 > 3.24)
AU_CompareVersions("3.25.0", "3.25.0") → 0 (iguais = nao atualiza)
AU_CompareVersions("3.24.0", "3.25.0") → negativo (downgrade bloqueado)
Downgrade so permitido com force=true (rollback via dashboard).
| Etapa | Detalhe |
|---|---|
| Download | GET /api/ea/download/{version} via WinInet (bypassa restricoes MT5) |
| Arquivo staging | Common/Files/linniuc_update.dat |
| Sanity check | receivedSize >= 1000 bytes E receivedSize == expectedSize |
| SHA256 | CryptEncode(CRYPT_HASH_SHA256, ...) — compara com hash do servidor |
| Sem hash | Fallback: verificacao apenas por tamanho |
| Situacao | Comportamento |
|---|---|
| 0 posicoes | Adquire lock → AU_DOWNLOADING |
| >0 posicoes, force=false | AU_WAITING (sem lock, aguarda ate 30min) |
| >0 posicoes, force=true | Adquire lock → AU_DOWNLOADING (ignora posicoes) |
| Timeout 30min | Volta para AU_IDLE (desiste, tenta no proximo ciclo) |
Lock file so e adquirido quando pronto para baixar. Nao segura o lock durante a espera.
| Aspecto | Detalhe |
|---|---|
| Arquivo | Common/Files/linniuc_update.lock |
| Formato | PID|SYMBOL|TIMESTAMP |
| Lock stale | >5 minutos → removido automaticamente |
| Double-check | Apos criar lock: Sleep(200ms) + re-le pra confirmar que e nosso |
O EA gera dinamicamente Common/Files/linniuc_updater.ps1:
Stop-Process -Id $mt5pid -Force (aguarda ate 20s)LinniuC.ex5 → LinniuC.ex5.bak.mq5.disabled (evita recompilacao)linniuc_update.dat → MQL5/Experts/LinniuC.ex5common.ini (ProfileLast=).chr apenas no profile ativo.chrInpWebAPIKey no .chrStart-Process terminal64.exelinniuc_update.dat + linniuc_update.lockSe erro: Bloco catch restaura
.ex5.bak, remove lock, reinicia MT5.
pending → testing (contas early) → stable (contas stable) → completed
↘ rolled_back
| Fase | Contas | Quando avanca |
|---|---|---|
testing |
Early (ids 3, 4 — TESTE) | Ao clicar "Release" |
stable |
Stable (ids 1, 2, 22-26) | Automatico quando early adotam (monitor 30min) |
rolled_back |
Nenhuma | Manual via dashboard |
# heartbeat.py
if desired_clean == current_build:
# Update concluido! Limpa desired_version
account.desired_version = None
account.update_attempts = 0
elif attempts >= 10:
# Desistir: loop detectado
account.desired_version = None
alert_ea_update_failed(...)
else:
# So conta tentativa se pos_count == 0
# (Com posicoes = EA em AU_WAITING, nao e falha)
if pos_count == 0:
account.update_attempts += 1
| Constante | Valor | Onde |
|---|---|---|
AU_WAIT_TIMEOUT |
30 min | AutoUpdate.mqh:50 |
AU_MAX_RETRIES |
3 | AutoUpdate.mqh:51 |
| Lock expiry | 5 min | AutoUpdate.mqh |
| Check interval | 30s | AutoUpdate.mqh |
| Min file size | 1000 bytes | AutoUpdate.mqh |
| Max tentativas servidor | 10 | heartbeat.py |
| Rollout monitor | 30 min (30s x 60) | ea_update.py |
| Upload max size | 50 MB | ea_update.py |
| Double-check sleep | 200ms | AutoUpdate.mqh |
| Kill MT5 timeout | 20s (10x 2s) | PS1 script |
| Method | Endpoint | Acao |
|---|---|---|
| POST | /api/ea/upload |
Upload .ex5 + version + changelog |
| POST | /api/ea/release/{version}?force= |
Inicia rollout (testing → early) |
| POST | /api/ea/rollback/{version} |
Reverte (limpa desired de todas contas) |
| GET | /api/ea/versions |
Lista versoes com stage/downloads |
| GET | /api/ea/rollout-status |
Status por conta (current vs desired) |
| GET | /api/ea/download/{version} |
Serve .ex5 binario |
| DELETE | /api/ea/versions/{version} |
Remove versao (so pending) |
| Coluna | Tipo | Descricao |
|---|---|---|
| version | varchar | Ex: "3.25.0" |
| file_path | varchar | Caminho fisico no VPS |
| file_hash | varchar | SHA256 |
| file_size | int | Bytes |
| changelog | text | Descricao |
| is_stable | boolean | Promovido? |
| is_active | boolean | Disponivel? |
| rollout_stage | varchar | pending/testing/stable/rolled_back |
| download_count | int | Total downloads |
| uploaded_at | timestamp | Data upload |
Campos em accounts: update_group (early/stable), desired_version (ex: "b3.25.0"), update_attempts (0-10)
| # | Armadilha | Status |
|---|---|---|
| A1 | Prefixo "b" em desired_version causa loop infinito |
Corrigido (fix_update_loop.py) |
| A2 | SHA256 nunca verificado (antes 3.10.1) | Corrigido |
| A3 | Download parcial aceito | Corrigido |
| A4 | Loop cross-session (retryCount nao persiste) | Corrigido (AU_SaveFailedVersion + cooldown 1h) |
| A5 | Race condition .chr (multiplos EAs) | Corrigido (lock + profile ativo) |
| A6 | API key perdida no .chr apos update | Corrigido (PS1 corrige) |
| A7 | WS push nao inclui update_force |
Comportamento: assume force=false |
| # | Mudanca | Estado atual | Desejado |
|---|---|---|---|
| M1 | Pular staged rollout | Release faz early→stable automatico | Release direto pra todos. Staged so quando solicitado |
| M2 | Atualizar com posicoes abertas | Espera 30min por 0 posicoes | Atualiza direto. Usuario avisa se update afeta ordens |
| M3 | Notificar falha do PS1 | Restaura backup silenciosamente | Enviar Telegram alert de falha |
| M4 | Lock por terminal | Lock global (bloqueia todos os terminais) | Lock por terminal (paralelo entre terminais diferentes) |
| M5 | Retry com backoff crescente | Retry a cada heartbeat (~30s) sem backoff | Backoff: 1min → 5min → 15min → 30min entre tentativas |
Ultima revisao: S372 (2026-05-23) — DROP COLUMN
accounts.prop_firm(era copia stale;Account.prop_firmagora@hybrid_propertyque derivaprop_firm_id->prop_firms.name). Anterior S294 (2026-04-25) — drift bookkeeping apos S288-S293 em server/models.py: schema base inalterado, snapshot prod emmemory/prod-schema.jsoncontinua autoritativo. Validate via hook schema-drift-check em todo commit. Mudancas absorvidas: senha plain auto-delete 30d (security S272), modelo S267 sim-e2e (AfRound/AfPair/AfTrade ja documentados em outras specs). PG 15 -> 16 ja drift-fixed em S221. [allow-spec]
PGPASSWORD=(ver .secrets.local) psql -U copytrade_user -h localhost -d copytrade
Contas MT5 registradas. Cada EA autentica via api_key.
| Coluna | Tipo | Default | Descricao |
|--------|------|---------|-----------|
| id | serial PK | auto | |
| name | varchar | | Nome amigavel |
| broker | varchar | | Nome da corretora |
| account_num | bigint UNIQUE | | Numero da conta MT5 |
| api_key | varchar | | Chave individual do EA |
| group_id | varchar | | Grupo de copy (ex: Grupo01, TESTE) |
| invert | boolean | | BUY<>SELL, SL<>TP |
| is_active | boolean | | EA ativo? |
| is_deleted | boolean | false | Soft delete |
| deleted_at | timestamp | | |
| created_at | timestamp | | |
| pool | varchar | '' | Pool de torneio |
| max_risk_pct | float | 2.0 | Risco maximo % |
| poll_interval | int | 3 | Segundos entre polls |
| sl_buffer | float | 0.0 | Buffer SL legado |
| tp_buffer | float | 0.0 | Buffer TP legado |
| sl_buffer_metals | float | 0.0 | Buffer SL metais |
| tp_buffer_metals | float | 0.0 | Buffer TP metais |
| sl_buffer_forex | float | 0.0 | Buffer SL forex |
| tp_buffer_forex | float | 0.0 | Buffer TP forex |
| sl_buffer_default | float | 0.0 | Buffer SL default |
| tp_buffer_default | float | 0.0 | Buffer TP default |
| buffer_mode | varchar | 'PIPS' | PIPS ou PRICE |
| settings_dirty | boolean | false | Pendente sync no EA |
| mt5_server | varchar | '' | Servidor MT5 reportado |
| account_type | varchar | 'undefined' | undefined/prop/normal/bonus (v2.71: default changed) |
| account_phase | varchar | '' | F1/F2/Funded/'' |
| bonus_pct | float | 0.0 | % bonus |
| prop_firm_id | int FK | NULL | S361 G.2 / S372: FK pra prop_firms.id (ON DELETE RESTRICT) — fonte UNICA do nome da mesa. Coluna copia prop_firm DROPADA em S372 (era stale apos rename); Account.prop_firm virou @hybrid_property que deriva prop_firm_obj.name (live). CHECK accounts_prop_firm_id_required_chk exige NOT NULL EXCETO se is_deleted=true OU is_test=true. Indice idx_accounts_prop_firm_id |
| prop_size | varchar | '' | 10k/25k/50k/100k/200k |
| prop_steps | varchar | '' | DEPRECATED S361: ainda existe pra compat legacy (af.py:608/3011/3035 + 5 callsites). Backfilled via FK lookup prop_firms.steps. DROP fisico pendente em TODO MED-04 (limpar refs Python primeiro) |
| update_group | varchar | 'stable' | early/stable (auto-update) |
| desired_version | varchar | | Versao desejada pra auto-update |
| update_attempts | int | 0 | Tentativas de update (max 5) |
| terminal_build | int | NULL | Build do MT5 (enviado 1x por sessao no HB). Mudanca dispara alerta Telegram (S265) |
| last_heartbeat_at | timestamp | NULL | Ultimo HB recebido (WS ou HTTP), atualizado SEM throttle (S314.3 Bug 3 fix Opcao C). Tabela heartbeats continua throttled em 30s. Helper is_account_alive() em account_filters.py. Spec: specs/heartbeat-last-seen.md |
Sinais de trade (OPEN/MODIFY/CLOSE). Criados pelo EA ou broadcast do dashboard.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| group_id | varchar | Grupo destino |
| action | varchar | OPEN/MODIFY/CLOSE |
| ticket | bigint | Ticket MT5 de referencia |
| symbol | varchar | BTCUSD, EURUSD etc |
| direction | varchar | BUY/SELL |
| volume | float | Lotes |
| price | float | Preco de abertura |
| sl | float | Stop Loss |
| tp | float | Take Profit |
| source_account | int FK | Conta que originou |
| created_at | timestamp | |
| expires_at | timestamp | TTL |
| origin | varchar | 'ea' ou 'dashboard' |
| trade_group_id | varchar | UUID agrupador de trades relacionados |
| sl_distance | float | Distancia SL em preco |
| tp_distance | float | Distancia TP em preco |
| signal_channel | varchar(20) | Canal de envio (ws/poll/af/group_copy) |
| detection_method | varchar(30) | Metodo de deteccao (ex: af_push_modify, af_engine, group_copy, OTT, SE) |
| close_reason | varchar(20) | Motivo do close |
| queued_duration_ms | int | Tempo na fila (ms) |
| server_received_at | timestamp | Quando servidor recebeu |
| server_processing_ms | int | Tempo de processamento servidor (ms) |
| signal_source | varchar(16) | S219: 'manual' (default) ou 'test' |
| trace_id | varchar(32) | S224: W3C-inspired trace identifier (nullable pre-migration) |
| is_retroactive | boolean | S300: TRUE = signal CLOSE retroativo via historico MT5 (reconcile pos-offline). Index parcial UNIQUE(source_account, ticket, action) WHERE is_retroactive=TRUE garante idempotency (R6 da spec) |
Confirmacao de execucao de sinais pelos EAs.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| signal_id | int FK | Sinal confirmado |
| account_id | int FK | Conta que executou |
| status | varchar | ORIGIN/FILLED/FAILED/SKIPPED |
| local_ticket | bigint | Ticket local criado |
| error_msg | text | Mensagem de erro se FAILED |
| executed_at | timestamp | |
| open_price | float | Preco real de abertura |
| applied_sl | float | SL aplicado |
| applied_tp | float | TP aplicado |
| actual_volume | float | Volume real executado |
| receive_channel | varchar(20) | Canal de recebimento (ws/poll) |
| gui_duration_ms | int | Tempo de execucao GUI (ms) |
| total_delay_ms | int | Delay total sinal->execucao (ms) |
| trace_id | varchar(32) | S224: carrega mesmo trace_id do signal pai (nullable pre-migration) |
| ea_received_at_ms | bigint | S326: epoch ms (relogio do EA) de quando o EA recebeu o signal. Buffer circular em WebBridge.mqh + helper RecordSignalReceived. Permite medir latencia EA-side vs server-side |
| bid_at_request | float | S336 exec-slippage: bid no momento que EA pediu cotacao (pre-OrderSend). Base pra exec_slip_pts |
| ask_at_request | float | S336 exec-slippage: ask no momento que EA pediu cotacao (pre-OrderSend) |
| exec_slip_pts | float | S336 exec-slippage: slippage de execucao em pontos (open_price vs bid/ask_at_request). Mede slippage do broker no fill |
| pipeline_slip_pts | float | S336 exec-slippage: slippage de pipeline em pontos (preco do signal vs bid/ask_at_request). Mede atraso sinal->EA pedir cotacao |
Event sourcing do ciclo de vida de cada signal (append-only, PARTITIONED BY RANGE(ts_server) daily, 90d retention).
Timeline 1-click: SELECT * FROM signal_events WHERE signal_id=X ORDER BY seq_num.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | bigserial PK composto | Parte do PK (id + ts_server exigido pela particao) |
| signal_id | int FK signals | Signal dono do evento |
| trace_id | varchar(32) | W3C-inspired 128-bit hex, nullable pra backward-compat |
| account_id | int | Conta (nao FK pra permitir conta deletada) |
| event_type | varchar(32) | signal_created, ea_received, gui_s1_ok..s11_ok, ack_sent, ack_failed |
| seq_num | smallint | Per-signal 1,2,3... ordem dentro do signal |
| ts_ea | timestamptz | Timestamp origem no EA (nullable) |
| ts_server | timestamptz PK composto | Quando chegou no servidor (chave de particao) |
| payload | jsonb | Contexto rico (error, duration, SL/TP aplicado, etc) |
Indexes (propagados automaticamente pras particoes):
- (signal_id, seq_num) — timeline query principal
- trace_id partial WHERE IS NOT NULL — correlacao cross-signal
- (event_type, ts_server) — stats por tipo no periodo
UNIQUE constraint (idempotencia R7): (signal_id, event_type, seq_num, ts_server) — permite retry ON CONFLICT DO NOTHING.
Particionamento: 7 particoes pre-criadas (CURRENT_DATE..+6d) + signal_events_default safety net. Cron scripts/cron/purge_signal_events.sh cria ahead + DROP >90d + VACUUM.
Vinculo entre trades copiados. Permite propagar CLOSE/MODIFY.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| trade_group_id | varchar | UUID do grupo |
| signal_id | int FK | Sinal que criou |
| account_id | int FK | Conta dona |
| local_ticket | bigint | Ticket MT5 |
| is_origin | boolean | true = quem originou |
| is_closed | boolean | false | Trade fechado? |
| created_at | timestamp | |
| closed_at | timestamp | |
| close_price | float | Preco de fechamento |
| profit | float | P&L |
| close_provisional | boolean NOT NULL default false | S267: fechamento via snapshot do ultimo HB (peer estava offline). Removido quando ACK WS traz deal_price/deal_profit reais (Fase B Caso 1). |
Indexes: idx_tl_tgid, idx_tl_account_ticket, idx_tl_close_provisional (parcial: WHERE close_provisional = true — S267)
Fila de sinais aguardando execucao. TTL default 60s (PENDING_SIGNAL_TTL_SECONDS). TTL 86400s (24h) quando awaiting_peer_return=True (S267 PENDING_TTL_PEER_OFFLINE).
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | Conta destino |
| symbol | varchar | |
| direction | varchar | BUY/SELL |
| volume | float NOT NULL | |
| signal_id | int FK | |
| trade_group_id | varchar | |
| expires_at | timestamp NOT NULL | TTL 60s default, 24h se awaiting_peer_return |
| resolved | boolean | false (true = ACK de execucao recebido) |
| ws_received_at | timestamp | EA flush_ack via WS (NULL = sem confirmacao, polling HTTP cobre) |
| awaiting_peer_return | boolean NOT NULL default false | S267: true no CLOSE origin='ea' quando peer stale. TTL 24h. Cleanup main.py Block 11. |
| created_at | timestamp | |
Indexes: idx_ps_account_symbol, idx_ps_tgid, idx_ps_ws_ack_missing (parcial: ws_received_at IS NULL AND resolved=false)
Anti-echo para MODIFY/CLOSE. TTL 10 segundos.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| trade_group_id | varchar | |
| action | varchar | MODIFY/CLOSE |
| expires_at | timestamp | TTL 10s |
| created_at | timestamp | |
Snapshot de posicoes abertas, atualizado via heartbeat.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | |
| ticket | bigint | |
| symbol | varchar | |
| direction | varchar | |
| volume | float | |
| open_price | float | |
| current_price | float | |
| sl | float | |
| tp | float | |
| profit | float | |
| pips | float | |
| swap | float | |
| magic | bigint | |
| open_time | varchar | |
| updated_at | timestamp | |
| tick_value | float | Valor do tick |
| tick_size | float | Tamanho do tick |
| spread | float | Spread em valor de preco |
| commission | float | Comissao total dos deals |
| profit_at_sl | float | P&L projetado se bater SL (OrderCalcProfit) |
| profit_at_tp | float | P&L projetado se bater TP (OrderCalcProfit) |
Heartbeats dos EAs (balance, equity, posicoes). Tabela grande (~20MB).
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | |
| balance | float | |
| equity | float | |
| margin | float | |
| free_margin | float | |
| positions | int | Quantidade |
| server_time | varchar | Hora do servidor MT5 |
| ea_version | varchar | Build do EA |
| created_at | timestamp | |
| applied_settings | jsonb | Settings aplicados pelo EA |
Logs persistidos dos EAs. TTL 7 dias.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | |
| level | varchar | 'INFO' |
| message | text | |
| created_at | timestamp | now() |
Versoes do EA para auto-update.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| version | varchar | Ex: 3.3.4 |
| file_path | varchar | Caminho do .ex5 |
| file_hash | varchar | SHA256 |
| file_size | int | |
| changelog | text | |
| is_stable | boolean | |
| is_active | boolean | |
| rollout_stage | varchar | pending/early/stable |
| download_count | int | |
| uploaded_at | timestamp | |
| stable_at | timestamp | |
| force_update | boolean | false | Forcar update mesmo com posicoes abertas |
Snapshots periodicos de equity por conta.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | |
| balance | float | |
| equity | float | |
| margin | float | |
| free_margin | float | |
| positions | int | |
| snapshot_at | timestamp | now() |
Info de simbolos reportada pelos EAs (tick_value, tick_size, etc).
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | |
| symbol | varchar | |
| tick_value | float | |
| tick_size | float | |
| contract_size | float | |
| digits | int | |
| point | float | |
| volume_min | float | 0.01 |
| volume_max | float | 100.0 |
| volume_step | float | 0.01 |
| spread | float | Spread atual |
| bid | float | Preco bid |
| ask | float | Preco ask |
| swap_long | float | Swap compra |
| swap_short | float | Swap venda |
| updated_at | timestamp | now() |
Lista de pendencias do projeto (fonte unica de verdade). Kanban do site e todos.md sao "janelas" pra esta tabela.
| Coluna | Tipo | Default | Descricao |
|--------|------|---------|-----------|
| id | serial PK | auto | |
| title | varchar(200) | | Titulo da tarefa |
| description | text | '' | Descricao detalhada |
| priority | varchar(10) | 'media' | alta/media/baixa |
| status | varchar(10) | 'backlog' | backlog/next/doing/done |
| tags | text[] | '{}' | Tags (ex: bug, feature, infra) |
| created_at | timestamp | now() | |
| updated_at | timestamp | now() | |
| completed_at | timestamp | null | Preenchido quando status=done |
Prop firms com regras completas. SSoT para AF engine e classificacao de contas.
| Coluna | Tipo | Default | Descricao |
|--------|------|---------|-----------|
| id | serial PK | auto | |
| name | varchar(100) UNIQUE | | Nome Desafio (label UI S370): identidade unica da mesa (FTMO Swing, FundedNext). Chave de busca lower(trim(name)) + exibicao. So o rotulo da UI mudou (Nome -> Nome Desafio); coluna name inalterada |
| company | varchar(100) NOT NULL | '' | S362: Empresa (companhia, ex: "FTMO"). R7 pareia comparando company, nao name — 2 desafios da mesma Empresa nunca pareiam num hedge. Backfill company=name. CRUD obrigatorio (datalist) |
| steps | varchar(20) | '2-step' | S361 G.2: renomeada de default_steps. CHECK constraint prop_firms_steps_chk enforced em {'1-step','2-step','3-step','instant','sem-fase'} |
| phases | JSON | [] | Fases disponiveis (F1, F2, Funded) |
| sizes | JSON | [] | Tamanhos disponiveis (10k, 25k, etc) |
| is_builtin | boolean | false | Se veio pre-cadastrada (seed) |
| created_at | timestamp | now() | |
| price | int | 0 | Preco USD (referencia 100k) |
| leverage | varchar(20) | '' | Alavancagem (ex: 1:100) |
| max_dd | float | 10.0 | Max DD % (estatico, base saldo inicial) |
| daily_dd | float | 5.0 | Daily DD % |
| daily_dd_type | varchar(10) | 'equity' | 'equity' ou 'balance' |
| target_f1 | float | 8.0 | Meta F1 % |
| target_f2 | float | 5.0 | Meta F2 % |
| min_days_f1 | int | 0 | Dias minimos trading F1 |
| min_days_f2 | int | 0 | Dias minimos trading F2 |
| min_profit_days | int | 0 | Dias lucrativos obrigatorios |
| max_risk | int | 5000 | Risco operacional por trade USD (100k ref) |
| daily_dd_op | int | 4500 | DD operacional diario USD (100k ref) |
| news_eval | varchar(100) | '' | Restricoes noticias avaliacao |
| news_funded | varchar(100) | '' | Restricoes noticias funded |
| limitation | varchar(200) | '--' | Limitacoes especiais |
| has_200k | boolean | false | Tem conta 200k? |
| is_active | boolean | true | Ativa no sistema? |
| is_af_eligible | boolean | true | Elegivel pro AF engine? (auto: risk>=2500 AND dd>=10) |
| updated_at | timestamp | null | Ultima atualizacao |
Historico de resets de classificacao de conta.
| Coluna | Tipo | Default | Descricao |
|--------|------|---------|-----------|
| id | serial PK | auto | |
| account_id | int FK | | Conta resetada |
| reset_at | timestamp | now() | Quando |
| pool_id | int | null | Pool de onde saiu |
| pool_name | varchar(100) | '' | Nome da pool (snapshot) |
| previous_type | varchar(20) | '' | Tipo anterior |
| previous_firm | varchar(100) | '' | Prop firm anterior |
| previous_phase | varchar(20) | '' | Fase anterior |
users — Usuarios do dashboard (username, password_hash, display_name, is_active)
user_sessions — Sessoes ativas (username, ip, user_agent, login_at, last_activity, request_count)
login_attempts — Tentativas de login (username_tried, password_tried, ip, success, fail_reason)
page_visits — Visitas a paginas (ip, path, user_agent, username, visited_at)
audit_log — Log de acoes (user, action, detail, created_at)
ip_whitelist — IPs permitidos (ip_address, description, is_active)
af_pools — Pools AF v2 (name, mode varchar(20) default 'live' LEGACY, validated bool default false, symbol, status, spread_pct, trade_interval_sec, trade_timeout_sec, group_id, config JSONB, created_at, updated_at). S170: mode é legacy (código não lê mais). validated controla se pool pode operar.
af_pool_accounts — Contas no pool com prop atribuida (pool_id FK, account_id FK nullable UNIQUE (uq_account_one_pool — 1 conta = 1 pool), prop_name, prop_config JSONB, phase, virtual_balance, profitable_days, trading_days, status active/dead/funded/paused/passed, chair_number, transition_at, daily_pnl, daily_pnl_peak, direction_history JSONB default '[]' — ultimas 10 direcoes executadas pro anti-OSB, created_at, updated_at). S207: prop_config agora inclui snapshot: owner, account_name, account_type, original_group_id. Cadeiras passed/dead com account_id=NULL usam prop_config pra display historico (nome, cor, badge). virtual_balance serve como saldo congelado.
af_rounds — Rodadas de trading (pool_id FK, round_number, status pending/executing/completed/failed, pairs_count, started_at, completed_at, summary JSONB, config_snapshot JSONB nullable — S306 ADR-0011: snapshot imutavel do pool.config quando round foi criado; lida via app.af.config_helper.get_round_config(round, pool) durante execucao; NULL em rounds legacy → fallback pra pool.config; created_at)
af_pairs — Pares/batalhas dentro de rodada (round_id FK, pool_id FK, account_a_id FK, account_b_id FK CHECK(a != b — chk_pair_diff_accounts), risk_usd, risk_detail JSONB, symbol, direction_a, volume, sl_price/tp_price colunas SQL legadas — guardam half-width do colchao (distancia em points, NAO preco absoluto); Python/API/dashboard acessam como sl_distance/tp_distance via alias no model AfPair (S257); precos absolutos REAIS ficam em af_trades.sl_price/tp_price, scheduled_at, status, winner_pool_account_id FK, profit_a, profit_b, spread_cost, created_at, completed_at)
af_trades — Trades individuais (pair_id FK, pool_account_id FK, account_id FK, signal_id FK, direction, volume, sl/tp/open/close price, profit, status, local_ticket bigint)
af_audit_log — Log imutavel INSERT-only (pool_id FK, action, entity_type, entity_id, detail text)
af_dead_letters — Dead Letter Queue: sinais AF que falharam (pool_id FK, pair_id, account_id, prop_name, failure_type varchar(50), reason text, attempts int, context JSONB). Tipos: timeout, margin, rsafe, invert, e7_exhausted, gui_fail
symbol_presets — Presets de configuracao por simbolo (id PK, symbol varchar(20) UNIQUE, config JSONB, spread_pct float, created_at, updated_at). Campos preset: sl_min/max, dz_min/max, rsafe2_price_gate, modify_margin_min/max, push_buffer_usd. Defaults hardcoded em server/af/presets.py (XAUUSD, BTCUSD, _default).
Diario de bordo: cada evento na vida de uma posicao. Append-only, cleanup 90 dias.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | bigserial PK | |
| created_at | timestamptz | Quando o evento foi registrado |
| event_type | varchar(30) | OPEN, MODIFY, CLOSE, PROPAGATE, ACK |
| account_id | int FK accounts | Conta envolvida |
| ticket | bigint | Ticket MT5 |
| trade_group_id | varchar(64) | Trade group (ex: TG_xxx, AF_P123) |
| signal_id | int | Signal que gerou o evento |
| origin | varchar(40) | ea, server_cleanup, server_auto_close, ws_detect, heartbeat_gone, admin_reconcile, af_orphan_cleanup |
| close_reason | varchar(30) | Motivo do close (se aplicavel) |
| price | float | Preco da operacao |
| profit | float | P&L capturado |
| volume | float | Volume em lots |
| context | jsonb | Dados extras (symbol, direction, reason, etc) |
Indices: trade_group_id, (account_id, created_at), (event_type, created_at), created_at
Endpoints: GET /api/af/trade-events/pair/{pair_id}, GET /api/af/trade-events/account/{account_id}, GET /api/af/trade-events/{trade_group_id}
Append-only. Todo evento telemetrico do EA aterriza aqui. Dedupe via event_id UNIQUE. Retencao 30 dias (D5).
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| event_id | varchar(64) UNIQUE | ID deterministico gerado pelo EA (hash position_id+deal_time+symbol) |
| trace_id | varchar(32) | W3C Trace Context 32-hex. Auto-enriquecido pelo server se ausente (I12) |
| account_id | int FK accounts NULL | Conta relacionada (nullable pra eventos globais) |
| event_type | varchar(32) | signal_received, order_sent, order_rejected, gap_alert, etc |
| ts_broker | timestamp | Quando o EA observou o evento no broker |
| ts_received | timestamp | Quando o servidor recebeu (auto-preenchido) |
| gap_ms | int | ts_received - ts_broker em ms. late=true se > 5000 (I1) |
| payload | jsonb | Dados do evento (slip_pts, latency_ms, spread_pts, etc) |
| late | bool | gap_ms > 5000ms (R1) |
| clock_offset_ms | int NULL | Offset de clock reportado pelo EA |
| ea_instance_id | varchar(64) NULL | Identificador da instancia do EA |
| created_at | timestamp | Insert time |
Indices: trace_id, account_id, event_type, ts_received, (account_id, ts_received DESC)
Endpoints: POST /api/telemetry/events, GET /api/telemetry/trace/{trace_id}, GET /api/telemetry/anomalies, GET /admin/health
Worker queue simples via Postgres NOTIFY/LISTEN. Trigger trg_event_outbox_notify dispara pg_notify('event_outbox_new', id) em cada insert.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| event_stream_id | int FK event_stream | Evento original |
| published | bool | Worker marca true apos processar |
| attempts | int | Retries do worker |
| last_error | text NULL | Ultima msg de erro se worker falhou |
| created_at | timestamp | |
| published_at | timestamp NULL | Quando worker marcou published |
Indices: (published WHERE published=false) partial, event_stream_id
telemetry_ro (S243 — I7 enforcement mecanico)Role Postgres NOLOGIN com SELECT only em signals, signal_acks, open_positions, af_trades, event_stream, event_outbox. INSERT/UPDATE/DELETE revogados explicitamente. Garante que pipeline de telemetria nao corrompe tabelas de trading mesmo com bug de codigo (I7). Worker e dashboard de telemetria DEVEM usar essa role.
O que eh: check rapido pra confirmar que o cenario
modify_dw_stress_real
esta funcionando ponta-a-ponta no servidor real (nao em mock).Quando rodar: depois de mexer em
signals.py(handler MODIFY),runner.py
(path do scenario),vea_client.py(simulador do EA), ou no caminho de
near_death/near_targetdaengine.py. Tambem antes de bater o branch
em prod.Tempo: ~20s pra rodar os 2 quadrantes (death + target).
Imagina que duas contas estao no hedge — uma comprando, outra vendendo
o mesmo par. Quando uma delas chega muito perto da morte (saldo prox.
do piso de DD) ou muito perto da meta (saldo perto do target), o
sistema deve ajustar o SL/TP dessa conta automaticamente, sem mexer
no lado oposto.
O cenario forca esse estado de propostito:
Em ambos os casos: o outro lado do hedge nao pode ser tocado (regra
P223 — invariante hedge zero-sum).
Pre-condicoes:
- VPS copytrade rodando + TEST_INJECTION_ENABLED=true no env
- JWT admin valido
- Nenhum outro run SIM em flight
Disparar os 2 quadrantes via Bash:
TOKEN=$(curl -s -X POST https://linniuc.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"..."}' | jq -r .access_token)
for SUB in death target; do
curl -sS -X POST https://linniuc.com/api/sim/scenarios/modify_dw_stress_real \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"subtype\":\"$SUB\",\"auto_cleanup\":true,\"timeout_sec\":180,\"seed\":0}" \
| jq '{status, pass: .result.pass, foco_USD: .result.foco_USD, elapsed_sec}'
done
Resposta esperada (cada chamada):
- HTTP 200 em ~9s
- status: "done", pass: true
- foco_USD entre 54 e 56
| O que checa (leigo) | Esperado | Death (S340) | Target (S340) |
|---|---|---|---|
| Veredicto unico — missao toda OK? | True | True ✅ | True ✅ |
| Perda/ganho do trade-alvo no range (cushion/dist $50 + margin random 4-6) | $54-$56 | $54.90 ✅ | $55.44 ✅ |
| Booleano alvo (cruza com outras regras) | True | True ✅ | True ✅ |
| Outro lado do hedge ficou parado (diff < $5) | True | diff $0.39 ✅ | diff $1.81 ✅ |
| Almofada aleatoria anti-robotica dentro do range pool config | $4-$6 | $5.11 ✅ | $5.53 ✅ |
| HB injection — empurrou saldo forcado nas 2 contas | 2/2 sucessos | 2/2 ✅ | 2/2 ✅ |
MODIFY dispara — gate near_* ativa e handler executa antes do timeout |
<60s | <1s ✅ | <1s ✅ |
| Resposta sincrona antes do nginx fechar (504) | <60s | 8.78s ✅ | 9.05s ✅ |
| Cleanup auto — contas/pool de teste apagadas | 0 residual | 0 ✅ | 0 ✅ |
| Telegram dry_run — alerts em modo ensaio | 2 por run | 2 ✅ | 2 ✅ |
| Sem quebra nos EAs reais da pool 20 pos-deploy | sem novos erros 2h | so 502s transient ✅ | idem ✅ |
pass: false com error: "MODIFY nao disparou em 60s"Significa que o gate near_death/near_target nao ativou. Causas comuns:
WS_HB_DB_INTERVAL=30s esta ativo. Solucao: usar force_http=True_send_hb_now (replica caminho que EA real usa em WS-down)._real_balance ficou stale — virtual_time avancou muito no SimClock_sim_tick apos HB inject nomax_dd diferente de 10% — cushion = balance - dd_floorprop_config da pool SIM.Run levou mais de 60s. Provavel timeout do poll MODIFY (60s wait). Roda em
~9s pos-fix S340. Se passar de 30s, investigar logs do servidor.
pair_id nao criadoEngine recusou o par. Olhar logs do _af_scheduler — provavel margin
validation, swap hour ou trading window (todos devem estar bypassados pela
config SIM).
heartbeat_interval divergente passam despercebidos.**result: se o dict de result ja temX=... explicito alem do **result. Exceptionforce_http=True): WS path temSpec tecnica completa: sim-e2e-real-S281.md (regras R1-R8 do real_path).
Pattern P224 (KnowledgeHub): "VEA deve replicar EA real — comparar
payload MQL5 + thresholds server-side antes de approval de review E2E".
[allow-spec]
O que este guia responde: "quando eu rodo o simulador no modo real, o que ele de fato faz,
passo a passo?" — em linguagem de quem opera, não de quem programa.
O real path é o simulador dirigindo o servidor de verdade (o mesmo agendador, o mesmo
motor de hedge, o mesmo ciclo de vida das contas que rodam em produção) — só que com contas
de mentira marcadas como simulação e um EA virtual (um programinha em Python, o "VEA")
fazendo o papel do EA: confirmar ordens e mandar batimento de saldo. Nada é mock degradado: é
linha real de banco com etiqueta is_simulator=true. Por isso o sim pega bug de servidor que
um teste matemático nunca pegaria — porque é o servidor REAL reagindo.
Analogia: é um ensaio geral de teatro no palco de verdade, com o elenco de verdade e a
peça de verdade — só os atores principais (os EAs) são dublês que seguem o roteiro.
| Peça | O que é |
|---|---|
| Gatilho | POST /api/sim/scenarios/... (só liga com TEST_INJECTION_ENABLED=true). Em produção a flag é false → os endpoints dormem (403/404). |
| Servidor local | python scripts/run_sim_local.py sobe um servidor próprio em :8001 com a flag ligada + Postgres local de teste (copytrade_test:5433). |
| EA virtual (VEA) | server/sim/vea_client.py — conecta no WebSocket real como ea_version="SIM_v1", escuta sinais e responde (ACK + heartbeat). |
| Mesas / contas / pool | linhas reais marcadas: contas is_simulator=true, pool com prefixo SIM_, mesas prop [SIM] .... |
Regra de ouro: a etiqueta is_simulator só serve pra (a) esconder do dashboard real e
(b) limpar depois. Nunca entra na lógica de comportamento — a mesa SIM resolve, pareia e
calcula risco IDÊNTICO a uma real.
[classificar]
│ conta nasce avulsa → escolhe mesa+fase+dono → ganha #N + ciclo de desafio
▼
[criar pool + sentar nas cadeiras]
│ N contas viram cadeiras (AfPoolAccount). Empresas distintas = pode parear (R7)
▼
[subir os VEAs] ── cada conta ganha 1 EA virtual que autentica no WebSocket
▼
┌────────────────── LOOP de rodadas ──────────────────┐
│ batimento de saldo (heartbeat) de todas as contas │
│ ▼ │
│ agendador REAL pareia + abre o par (sinais canônicos)│
│ ▼ │
│ VEA confirma a ordem (ACK) │
│ ▼ │
│ fecha o par zero-sum (um ganha o que o outro perde) │
│ ▼ │
│ ciclo de vida decide: passou de fase? morreu? ───────┼──► [transições]
│ ▼ │
│ recompra: morto/funded → nasce substituto T+1 │
└───────────────────────────────────────────────────────┘
▼
[resumo da run] ── passes, mortes, substitutos, invariantes por rodada
Mapeando cada estação pra função real do servidor (pra quem quiser cavar):
| Estação | Função real | O que prova | O que pode quebrar |
|---|---|---|---|
| Classificar | sync_classify_cycle (routes/accounts.py) |
#N temporal + account_challenges aberto + nome montado |
#N errado; ciclo órfão; vínculo conta-filha (S385) |
| Criar pool | create_sim_pool (sim/af_tick_real.py) |
pool SIM_* + N contas is_simulator + cadeiras |
capacidade; mesa não resolve por nome |
| Parear | agendador real + pairing (R7) |
nunca parear 2 da mesma Empresa | pareamento trava se poucas Empresas distintas |
| Abrir + ACK | sinais canônicos + VEA | sinal chega, EA virtual confirma | VEA não confirma; sinal perdido |
| Fechar zero-sum | process_trade_result |
P&L do par soma zero | assimetria SL/TP; slippage |
| Passar de fase | check_passes → create_opc3_f2_substitutes (sim/runner.py) |
F1 passa cria conta F2 NOVA — a velha fica passed, NUNCA muta |
promoção que muta conta velha (modelo errado de prop firm) |
| Morrer por DD | check_deaths (af/lifecycle.py) |
saldo < piso da mesa → morta + saldo congelado | morte não reconhecida; $NaN no card (S374) |
| Recompra | recycle intent + apply_recycle_batch |
morto/funded → substituto novo T+1 | substituto não nasce; substituto duplicado |
Em produção a conta morre por DD de duas formas — e desde S378 #8 o sim cobre as duas:
check_deaths): no fim da rodada, se o saldo fechado cruzou o pisopiso = tamanho × (1 − max_dd/100)), a conta é marcada morta e o saldo é congelado.check_deaths). É o que odd_recognition valida._handle_equity_breach, routes/ea_ws.py): o EAequity_floor_breach. É o gatilho REAL e mais rápido em produção (foi a origem do$NaN do S374). Desde S378 #8 o VEA cobre essa via também: novo métodoreport_equity_breach() emite o mesmo frame WS que o EA real. Cenário dedicado:dd_breach_intratrade — sobe VEA conectado, ele emite o frame, o handler real marcadead + congela saldo + grava AuditLog.Os DOIS pisos têm que bater: o que o EA recebe (get_equity_floor, via FK da mesa) ==
o que o motor usa pra declarar morte (_max_dd_val, via snapshot na cadeira). Se divergirem
(snapshot velho vs mesa viva), é exatamente a classe de bug "DD não-reconhecido".
| Endpoint | O que exercita |
|---|---|
POST /api/sim/scenarios/classify_cycle |
Classificação: nascer→#1, reset→#2, vínculo conta-filha S385 (consume-once) |
POST /api/sim/scenarios/dd_recognition |
Gatilho A (via SALDO): reconhecimento de DD por perfil de mesa (8%/10%/5%): pisos batem, mesmo saldo morre na apertada e sobrevive na larga, congela, daily cap por perfil |
POST /api/sim/scenarios/dd_breach_intratrade |
Gatilho B (via EQUITY ao vivo): VEA conectado emite frame equity_floor_breach → handler real marca dead + congela saldo + grava AuditLog (S378 #8) |
POST /api/sim/scenarios/modify_dw_stress_real |
MODIFY perto da morte/alvo (E2E real com VEA + agendador) |
POST /api/sim/scenarios/lifecycle_full_recycle |
Ciclo de vida completo: pool→rodadas→F2→mortes→recompras (heatmap/timeline/invariantes) |
Rede de segurança automatizada (rodam no sqlite rápido + Postgres real):
test_dd_recognition_S378.py, test_dd_breach_intratrade_S378.py,
test_classify_cycle_sim_S378.py, test_journey_2step_S378.py
(jornada inteira numa tacada + modos de falha).
# 1. sobe o servidor local com o sim ligado (Postgres copytrade_test migrado)
python scripts/run_sim_local.py # uvicorn :8001 + VEA aponta de volta pra :8001
# 2. dispara uma estação (token admin no servidor local)
curl -sS -X POST http://127.0.0.1:8001/api/sim/scenarios/dd_recognition \
-H "Authorization: Bearer $TOKEN" | jq '{all_passed, cross_profile}'
Pré-requisito do ambiente: o Postgres local copytrade_test precisa estar com as migrações
em dia (ex: S382 colunas de estilo de DD/news, S385 vínculo conta-filha) — senão qualquer query
de mesa quebra. Produção já tem; o local de dev pode ficar pra trás. Aplicar as migrações faltantes
de server/migrations/ antes de rodar.
source=test nunca toca a pool real (etiquetas + filtros).auto_cleanup=true (default) apaga pool/contas/sinais ao fim; manual viaDELETE /api/sim/cleanup. Cada cenário limpa suas fixtures por faixas de ID reservadas..git do projeto vive no Dropbox e embaralha sob sync de outragit worktree NÃO isola disso (só os arquivos). Pra trabalho paralelo de verdade, usargit clone fora do Dropbox.SSoT técnica: specs/sim-e2e-real-S281.md (endpoints + padrões anti-flake) +
specs/sim-cockpit-validacao-completa-S378.md (cockpit S378). Este guia é o mapa leigo do fluxo.
Status: ACTIVE | Ultima revisao: S348+1 (2026-05-15) | Documento vivo — atualizar conforme novas features entram.
O CopyTrade tem 2 camadas de logica:
Cada feature/funcao vive em uma das 3 categorias: so EA, so Site, ou dupla camada (defesa em profundidade — EA + Site cobrindo a mesma coisa, com responsabilidades complementares).
A regra geral: se um bug naquela camada pode causar PERDA REAL ou VAZAMENTO PRA PROD, criar dupla camada. Senao, single-layer eh aceitavel.
Total: 25 estacoes, ~115s tipico. Distribuicao por camada: EA-only 4, SITE-only 9, DUPLA 12.
| # | Estacao | Camada | Onde vive | Vale dupla camada? | Por que |
|---|---|---|---|---|---|
| 1 | HB shape | DUPLA | EA envia HB, Site recebe + valida campos | ja eh | EA reporta saude, Site consolida e alerta se stale |
| 2 | Sandbox limpa pre-flight | DUPLA | Site dispatch CLOSE, EA executa | ja eh | Limpeza so funciona com ambos |
| 3 | Olheiro SL/TP | SITE | Validacao SL/TP no servidor | nao | Dominio do servidor. EA so executa |
| 4 | Run-all OPEN/MODIFY/CLOSE | DUPLA | Site cria signal, EA executa via GUI | ja eh | Cadeia inteira do ciclo de vida |
| 5 | Invert (BUY→SELL + audit + SL/TP trocados) | DUPLA | Site insere signal_inversions, EA aplica GUIExecution_ApplyInversion |
ja eh | Audit no Site, execucao no EA |
| 6 | Symbol resolve | EA | SymbolResolver.mqh |
nao | Broker eh dominio do EA |
| 7 | Buffers SL/TP multi-asset | EA | _apply_buffers() no EA |
nao | Calculo proximo do tick |
| 8 | Mini-stress 3 contas | DUPLA | Site spawn_stress_pool, EAs executam paralelo |
ja eh | Pool + broadcast + execucao em massa |
| 9 | 6 cenarios de erro (M1 SWAP: antes de WS kill) | DUPLA | volume_inv valida no Site; margin/sym_fake erra no EA | ja eh | Cada preset testa camada diferente |
| 10 | WS fallback HTTP poll | DUPLA | EA reconecta canal, Site responde HTTP | ja eh | Canal eh propriedade compartilhada |
| 11 | Reconcile pos-restart EA (opt-in) | DUPLA | Site mantem fila, EA puxa pos-reboot | ja eh | Fila no Site, replay no EA |
| 12 | Equity floor breach + reset | DUPLA | _af_runtime_validator no Site + EquityFloor_Check no EA |
ja eh | EA defende mesmo offline; Site valida + reseta |
| 13 | Auto-update simulado | DUPLA | Site PATCH desired_version, EA detecta + reporta |
ja eh | Site agenda, EA executa |
| 14 | Telemetria 10 campos | DUPLA | Site coleta timeline, EA emite eventos | ja eh | Eventos no EA, agregacao no Site |
| 15 | Rollover diario antes do swap | DUPLA (ja aprovada em specs/rollover-dupla-camada.md) |
EA primary + Site fallback | ja eh | Critico: se Site cair entre T-60 e T-30, EA fecha sozinho |
| 16 | Isolamento de grupo | SITE | Filtragem target_group_id via API key da conta |
nao (recalibrado) | Single-layer forte. Dupla seria nice-to-have, nao urgente |
| 17 | Cap de lote (max_lots) | SITE | Servidor valida lots antes de despachar | nao (recalibrado, ver TODO) | Verificar se servidor ja valida. Se nao, dupla camada vale |
| 18 | Validacao payload (timestamp futuro, JSON quebrado, null bytes) | SITE | Pydantic + sanitizacao explicita | nao | EA recebe estrutura ja sanitizada |
| 19 | MODIFY queue overflow (>4) | EA | MOD_VERIFY_QUEUE_SIZE=4 no EA |
nao | Fila eh design interno do EA |
| 20 | Race OPEN+CLOSE 50ms | DUPLA | Lifecycle state machine no Site + g_busy no EA |
ja eh | Site sequencia, EA respeita flag |
| 21 | Replay ACK antigo | DUPLA | idempotency_unique index (server) + g_processedSignalIds[100] ring buffer (EA, LinniuC.mq5:162) |
ja eh (auditado S354) | Dedup signal_id 2 camadas. Replay >100 sinais atras: so server pega |
| 22 | Signal source=test em conta prod | SITE | R3 do sim-e2e: signal_source='test' NUNCA em conta is_test=false |
nao (recalibrado) | Single-layer suficiente. Dupla nice-to-have |
| 23 | Cron health pre-flight (NOVO) | SITE | 5 crons rodaram <24h? Disco OK? | nao | Pura infra servidor. Executável desde #257 (S354): 7 crons gravam liveness marker /var/log/copytrade-cron-*-last-success.txt; GET /api/health/cron-status lê. Antes era WARN permanente. |
| 24 | AF runtime pre-flight (NOVO) | SITE | Sem round stuck >30min, pending <50, par offline OK | nao | Estado AF eh servidor |
| 25 | OnTradeTransaction crash test (NOVO) | EA | positionId=0 fake: EA nao crasha, reporta erro |
nao | Robustez interna EA |
| Onde | O que valida | Camada |
|---|---|---|
| E1 HB shape | ea_version bate com server_version (/api/health) — detecta deploy mismatch |
DUPLA |
| E4 run-all | Ticket aparece em _ticketListPanel (capitulo C1 multi-ticket selector) |
EA |
| E4 run-all | signal_lifecycle transicao PENDING→DISPATCHED→ACKED→RESOLVED em ordem |
SITE |
| E4 run-all | ACK retornou com target_account_id=3 (defesa anti-vazamento) |
DUPLA |
| E4 run-all | Fila MODIFY verify 5s pos-MODIFY foi processada (g_modVerifyQueue) |
EA |
| E4 run-all | _af_runtime_validator confirma estado consistente pos-trade (detecta EA inert 15.6/15.9) |
DUPLA |
| E5 invert | SL e TP trocados de fato no broker (nao so direction) — pos-G11 auditoria EA | EA |
| E12 equity floor | Pos-breach + restore, EA volta a operar (floor_reset funcionou) | EA · forçar breach com segurança = spec ea-tester-e12-e15-force-breach-S354.md (F11, aguarda execução) |
| E14 telemetria | WS push reload UI tabela <2s | SITE |
| Pre-flight global | localStorage.ct_token valido + >5min restante; is_test=true na conta 3 |
SITE |
signal_dispatch.py passa volume direto;af/signals.py:76 calculate_volume tem piso max(lots,0.01) mas SEMrisk_usd jaaf/validator.py:333+ e af/audit.py:180+min(lots, MAX_LOT) em calculate_volume como cinto-e-risk_usd. Registrado, nao bloqueia.calculate_volume agoravolume_max opcional e aplica min(lots, MAX_LOT) — MAX_LOT =volume_max do broker quando valido, senao MAX_LOT_FALLBACK=100.0sym_info.get("volume_max"). Teste-primeiro: test_calculate_volume_cap.pyg_processedAcks mas o real (e correto) eh dedup deLinniuC.mq5:162-164 g_processedSignalIds[100] ring buffer +LNC_IsSignalProcessed/MarkSignalProcessed (:425-443), aplicado no:800-801) e no poll (:1305-1309). Server idempotency_uniqueNova feature precisa rodar:
├── So no MT5 (broker, GUI, simbolo)? → EA-only
├── So no servidor (orquestracao, AF, audit, lifecycle, telegram)? → Site-only
├── Ambos (signal, ACK, settings sync)? → Dupla camada NATURAL
└── Tem risco de PERDA REAL ou VAZAMENTO se 1 camada falhar? → Dupla camada OBRIGATORIA
Lista de criterios pra dupla camada obrigatoria:
Esta aba /guide#camadas-ea-vs-site eh fonte da verdade do mapa de camadas. Sempre que adicionar/remover feature da bateria de validacao, ou descobrir que algo precisa virar dupla camada, atualizar:
specs/camadas-ea-vs-site.md (este arquivo)bash scripts/guide-update-and-verify.sh camadas-ea-vs-site — script automatiza:/guide pra confirmar render OKspecs/rollover-dupla-camada.md — modelo de dupla camada ja implementadospecs/ea-tester-bateria-completa.md — bateria 22 estacoes "Validar completo"specs/ea-tester-adversarial-completo.md — 42 vetores adversariais que motivaram E16/E17/E22specs/sim-e2e-real-S281.md — Simulador E2E (camada irma que valida o cerebro do servidor)Derivado automaticamente das fases de cada
specs/*-PLAN.md. Sem marcacao manual. Estado vem de git + ledgers de review/validate + todos. Fingerprint de conteudo:5be4fe43.
Legenda: · Planejado · ~ Em obra · + Construido · R Revisado · ✓ Provado · 🔴 Bloqueado (flag ortogonal)
| Fase | Estado | Descricao |
|---|---|---|
| F1 | + Construido | Trace emitter (per-brainstorm jsonl + model_version) |
| F2 | + Construido | Emissores forte (0f/1/4/5/6/7b) + fraco (2/3) |
| F3 | + Construido | Conformance check PreToolUse /plan (tier-aware + model move) |
| F4 | + Construido | Retry cap + fail-safe + SessionStart cleanup |
| F5 | + Construido | Wiring settings.json + dogfood S349 + teste negativo |
| F6 | + Construido | Post-impl: code-review LOOP P228 + validate |
| Fase | Estado | Descricao |
|---|---|---|
| F1 | R Revisado | RED tests T1-T4 |
| F2 | R Revisado | E1-E4 (HB, cleanup, Olheiro, run-all) |
| F3 | R Revisado | E5-E7 (invert, symbol, buffers) + SA-3 |
| F4 | R Revisado | E9 6 cenarios erro + E10 WS fallback |
| F5 | R Revisado | E12 equity_floor + E13 auto-update |
| F6 | R Revisado | E14 + reconcile + cancel/beforeunload |
| F7 | R Revisado | toggles + Telegram opt-in + P228 LOOP |
| F8 | + Construido | Multi-aba lock BroadcastChannel + polish |
| F9 | + Construido | E15 Rollover + E22 prod + SA-1..SA-4 |
| F10 | ~ Em obra | E16-E21 + SA-5..SA-8 |
| F11 | ~ Em obra | E23-E25 + 3 endpoints servidor + SA-9/SA-10 |
| Fase | Estado | Descricao |
|---|---|---|
| F1 | ✓ Provado | Contract parser |
| F2 | ✓ Provado | Git-derived states |
| F3 | ✓ Provado | Review marker reader |
| F4 | ✓ Provado | Validate-ledger reader |
| F5 | ✓ Provado | Blocked flag from todos |
| F6 | ✓ Provado | Aggregator + cache + CLI |
| F7 | ✓ Provado | Contract migration + reality check |
| F8 | R Revisado | Publish /guide (P243-aware) |
| F9 | ✓ Provado | 4 hooks wiring |
| F10 | ~ Em obra | Post-impl: code-review LOOP + validate |
Status: ACTIVE | Criado: S362 (2026-05-22) | Atualizado: S364 (#E6 reset unificado + #297 piso diário) | Documento vivo — atualizar quando uma camada mudar de lado.
Rodar a pool tem 4 camadas. Pensa num restaurante:
O simulador eh uma cozinha de treino: usa o MESMO chef (receita identica),
mas NAO tem gerente (roda os pratos que voce pedir, sem horario nem limite) e usa
um cozinheiro de mentira (EA-fantasma / VEA). A "conta do dia" (reset) ja foi unificada
no treino canonico (S363 #E6) — ele agora le a mesma folha do restaurante; so os treinos
antigos (matematico / validator) ainda usam papelzinho separado.
Principio que decide compartilhar vs fingir:
- E uma DECISAO/REGRA de negocio (parear, risco, direcao, morte/pass, cap de DD,
quando zerar o contador)? -> logica unica, compartilhada.
- E uma ENTRADA DO MUNDO (heartbeat, saldo do broker, relogio, execucao no MT5)?
-> fingida no simulador de proposito (rapido + deterministico + sem efeito real).
| # | Camada | O que faz | Pool real (producao) | Simulador | Compartilha? |
|---|---|---|---|---|---|
| 1 | Gerente (agendador) | Decide QUANDO abrir batalha: mercado aberto? teto de rodadas/dia? intervalo passou? ja tem uma rolando? | _run_af_scheduler (main.py) + check_daily_round_limit (constants.py) |
NAO tem — roda N rodadas direto (rounds=N) |
NAO — so producao. Sim substitui pelo proprio loop |
| 2 | Chef (engine) | Quem joga contra quem, quanto apostar, pra que lado, quem morreu/passou, quanto ainda pode perder no dia + se a migalha do dia nao vale trade (piso #297) | server/af/engine.py (pair_accounts, calc_risk, _choose_direction, find_dead/passed, _daily_dd_remaining, piso min_daily_risk_pct em _individual_risk) |
usa os MESMOS (sim passa daily_floor_active) |
SIM — fonte unica |
| 3 | Cozinheiro (execucao) | Faz a ordem acontecer + espera "fiz" + atualiza saldo | EA real (GUI Win32 no MT5) | EA-fantasma (VEA) ou so anota o resultado (random walk) | NAO — fingido de proposito |
| 4 | Conta do dia (reset) | Zera a perda acumulada do dia no horario de virada (swap) | reset_daily_pnl (lifecycle.py) + helpers (constants.py) |
canonico (runner) consome o MESMO reset_daily_pnl (S363 #E6); math (harness.py) / validator.py / af_tick_real.py mantem reset proprio |
SIM no canonico (S363) — math/validator fora de escopo |
| Entrada | Pool real | Simulador finge como |
|---|---|---|
| "A conta esta viva agora?" (heartbeat) | pinga o EA e espera resposta (fail-closed) | conta-fantasma sempre responde / pula o ping |
| Saldo real no broker | le do que a conta reporta (sync_real_balances) |
saldo inventado do resultado da batalha |
| Abrir ordem no MetaTrader | manda o robo clicar (GUI Win32) | so registra "fiz" (VEA) ou calcula resultado direto |
| Que horas sao / mercado aberto | relogio real + is_market_open / janela de swap |
relogio virtual (SimClock) — roda quando quiser |
| Quando comecar a proxima batalha | espera intervalo + checa teto + se ja tem uma rolando | dispara em sequencia, sem esperar |
Por que o teto de rodadas/dia (
max_rounds_per_day) nao vale no sim: ele mora na
camada 1 (gerente), nao na camada 2 (chef). O simulador nao usa gerente, entao nunca
le esse teto — independente de ter valor ou nao na config. So producao enforca.
reset_daily_pnl (com o toggle do #290 — reseta 1x/dia no swaprunner._create_round_and_wait)reset_daily_pnl de producao, com o relogio virtual (SimClock) comonow — abordagem "consumo, nao copia". Fecha o gap E6: o sim canonico ja reproduz fielmentespecs/unify-daily-reset-rule-S363.md.harness.py matematico,validator.py fuzz, af_tick_real.py deprecated) mantem reset proprio — testam matematica_daily_dd_remaining) cair abaixo demin_daily_risk_pct (default 0.5% do tamanho da conta), a conta abstem (descansa) —_individual_risk (engine) → o sim consome igual (passa o booleanodaily_floor_active). Regra unica: reusa a decisao do reset_daily_pnlfloor_active = temporal_rules_on(pool) and not did_reset). Dormente ate ligar o toggle./guide#piso-risco-dia. Spec: specs/min-daily-risk-floor-S364.md.specs/temporal-rules-toggle-per-pool-S362.md (#290),specs/unify-daily-reset-rule-S363.md (#E6).Chef (engine) = compartilhado (inclui o piso de risco diario #297). Gerente (agendador +
teto de rodadas) e cozinheiro (execucao) = so producao, o sim substitui. Reset do dia: regra
unica ENTREGUE no sim canonico (S363 #E6) — math/validator ainda separados de proposito.
Esta aba /guide#camadas-pool-vs-simulador eh referencia rapida das camadas. Quando uma
peca mudar de lado (ex: a regra unica do reset for implementada), atualizar:
specs/camadas-pool-vs-simulador.md (este arquivo)bash scripts/guide-update-and-verify.sh camadas-pool-vs-simulador (commit + push + verify render)specs/temporal-rules-toggle-per-pool-S362.md — #290 toggle de regras temporais (camada 4)specs/unify-daily-reset-rule-S363.md — #E6/#296 regra unica do reset (sim canonico consome prod)specs/min-daily-risk-floor-S364.md + /guide#piso-risco-dia — #297 piso de risco minimo diario (camada 2)specs/camadas-ea-vs-site.md — outra divisao (EA vs Servidor)specs/af-rules-whitepaper.md — as regras do chef (engine)specs/sim-e2e-real-S281.md — o simulador E2E real (VEA)Status: ATIVO (dormente até o "Daily DD" ser ligado na pool) | Criado: S364 (2026-05-23) | #297
Cada conta tem um tanque de gasolina do dia: quanto ela ainda pode perder hoje antes de
estourar o limite diário da mesa (o "Daily DD"). Cada mesa enche esse tanque com um tamanho
diferente (umas 5%, outras 3%, outras 4% do valor da conta).
O Piso de Risco Diário é a marca de "reserva" no tanque: quando a gasolina do dia cai
abaixo dessa marca, a conta encosta o carro (não abre mais trade) e descansa até amanhã,
quando o tanque enche de novo.
Por que existe: sem o piso, quando sobra muito pouco no tanque, o sistema ainda tentaria
abrir o menor trade possível (0,01 lote) — e esse trade mínimo pode custar MAIS que o que
sobrou, furando o próprio limite diário que deveria respeitar. O piso para antes disso.
No modal Config da pool (aba AF Hedge), ao lado do toggle "Daily DD", tem
o campo "Piso Risco/Dia (%)":
"Não vale abrir o 2º trade do dia se o que a conta ainda pode arriscar hoje virou migalha
(< 0,5%) — ela descansa e tenta amanhã com o tanque cheio."
Status: ACTIVE | Criado S371 (2026-05-23) | Fonte: GUIExecution.mqh + telemetria signal_events
O EA não usa API de trade (OrderSend/CTrade). Ele age como um humano clicando na
janela do MT5 — abre o diálogo de ordem (tecla F9), digita volume/SL/TP e clica
Comprar/Vender/Fechar, via automação Win32 (user32.dll). Motivo: mesas prop
detectam EA via API; cliques "parecem manuais". Toda ação passa por uma trava
única (GUI lock) — só uma ação por vez, nunca dois cliques concorrentes.
| Modo | Input do EA | O que faz | Em produção (pool) |
|---|---|---|---|
| Visual | InpVisualMode |
Traz a janelinha pra frente + pausa 400ms antes de cada clique, pra um humano VER o EA operando ao vivo | OFF (ninguém olhando) → sem pausa, mais rápido |
| Log detalhado | InpDebugLog / coluna accounts.debug_log (server-push, S371) |
Grava o passo-a-passo [GUI-TRACE] de cada etapa |
OFF (senão enche o log) — liga por conta pelo painel só pra debug |
| Debug GUI | InpGUIDebug |
Mostra as tripas do diálogo (combo, leitura de campos, settle, readback) | OFF — só investigação pesada |
O debug_log agora é ligável por conta direto pelo painel (sem mexer no gráfico
do PC daquela conta): PUT /api/accounts/{id}/settings {"debug_log": true} → o EA
aplica em runtime no próximo sync (P310).
A Exness mostra um aviso de alavancagem antes de cada trade. O EA lê o texto do
aviso (varre os campos de texto da janelinha) e decide:
- Texto de aviso normal ("alavancagem", "risco", "confirma") → confirma e segue.
- Texto de ERRO ("saldo insuficiente", "rejeitado", "off quotes", "inválido") →
aborta e marca a ordem como FALHA — não finge que deu certo.
| Ação | Cliques de trade | Etapas principais | Tempo típico |
|---|---|---|---|
| Abrir | 1 (Comprar OU Vender) | F9 → selecionar símbolo → sub-diálogo → setar volume → SL → TP → clicar direção → confirmar popup | ~1,0–1,3s |
| Modificar | 2 (abrir painel "Modificar a Posição" + Modificar) | abrir diálogo da posição → painel modificar → setar SL/TP → Modificar → confirmar | ~0,5s |
| Fechar | 1 (Fechar #ticket… a mercado) | abrir diálogo da posição (duplo-clique na linha) → clicar Fechar do ticket certo → confirmar popup → detectar fechamento | ~0,7s |
As esperas são event-driven (sai assim que o resultado chega; marca 0ms quando
rápido). A seleção de símbolo dá 0ms quando já está no símbolo certo (não redigita à
toa). O maior pedaço do Abrir é confirmar o popup do broker + fechar a janelinha
(~400ms + ~250ms). Nenhum clique de trade redundante.
| Trava | Quando dispara | Resultado |
|---|---|---|
| Erro do broker | Popup com texto de erro (rejeitado/insuficiente/off quotes) | Aborta + ACK FALHA (não finge sucesso) |
| Volume não entra | Campo de volume diverge do alvo após retries | ABORTA com ZERO ordens (nunca abre com tamanho errado) |
| Delta de posição | Após abrir, se não apareceu EXATAMENTE +1 posição (veio 0 ou 2) | ACK FALHA (pega anomalia / posição manual concorrente) |
| Fechar ticket certo | Sempre (S371 fix) | Casa o botão Fechar pelo número do ticket no texto, nunca o 1º botão genérico → não fecha a oposta por engano |
| GUI lock | Sempre | Uma ação GUI por vez; auto-libera se travar >5min (sentinela) |
Onde a correção do PAR mora: quem fica comprado vs vendido é decidido e blindado
no servidor (motor AF + hedge guards), NÃO no EA — o EA só executa o sinal que
chega. Não há trava EA-side de "par errado" porque o EA não conhece o par.
| Aspecto | Status | Nota |
|---|---|---|
| Precisão de cliques | ✅ Preciso | 1–2 cliques de trade por ação, sem redundância |
| Performance | ✅ Otimizado | Esperas event-driven; OPEN ~1s, MODIFY ~0,5s, CLOSE ~0,7s |
| Fechar ticket correto | ✅ Corrigido S371 | Bug "fechar 1 fechava as 2 opostas" resolvido (casar botão por ticket). Ver ea-gui-close-opposite-double-INVESTIGATION.md |
| Telemetria | ✅ Refinada | ms por etapa em signal_events (gui_s1..s12 / m1..m6 / c1..c3) + dump de controles (GUI_DumpButtons, debug-gated) |
| Detecção de erro do broker | ✅ Ativa | Lê popup, distingue erro de confirmação |
| Rede 2-opostas (server-side) | ✅ Ativa S371 | Alerta Telegram se uma conta tiver BUY+SELL mesmo símbolo abertos (pré-condição do bug raro) |
Validação ao vivo S371 (sandbox id=3, b3.91.0): fechar 1 de 2 opostas deixou a
outra intacta (antes caíam as 2); fechar sem oposta limpa normal. Fix sandbox-only
até aqui — rollout fleet (deploy_ea.sh, 12 contas) sob aprovação.
Quando uma rodada começa, algumas contas podem ficar de fora — por exemplo o
computador de um dos PCs caiu e os EAs ficaram offline. A rodada abre com menos
duplas do que poderia. O Raio-X da rodada mostra quem ficou de fora e por quê;
o Rechamar Contas deixa você trazer de volta quem voltou, com 1 clique, sem
mexer nas batalhas que já estão rolando.
Analogia: o ônibus de alguns jogadores quebrou e a partida começou com poucas
duplas. Quando o ônibus chega, aparece um aviso "os atrasados chegaram — quer
incluir?". Você clica e o sistema monta duplas novas na mesma partida.
No card da rodada (aba AF Hedge) aparece a faixa "Raio-X da rodada · X de Y
em batalha · Z de fora". Abrindo, você vê cada conta de fora com o motivo em
linguagem clara e um ícone do "balde":
O servidor fica de olho: quando 2 ou mais contas transitórias voltam a dar
sinal de vida (ou fecham a posição que tinham aberta) e dá pra formar pelo menos
uma dupla de empresas diferentes, aparece a faixa "🔄 N conta(s) voltaram"
com o botão Rechamar N contas. Você também recebe o aviso na hora (notificação
instantânea no painel).
Só aparece enquanto a rodada está em andamento. Rodada encerrada não rechama.
Ao clicar, o sistema:
Toda dupla criada por rechamada ganha o selo 🔄 RECHAMADO no card. É
transparência: ela entrou depois do início da rodada, então o lucro/prejuízo
dela pode ser diferente das duplas que abriram no começo.
Status: ATIVO (no ar desde EA 3.101.0) | Criado: S392 (2026-05-30) | #352
Antes de abrir um par de hedge, o sistema precisa de um preço de referência pra colocar os
freios (stop loss e take profit) no lugar certo. Se esse preço estiver velho, travado ou
esquisito, abrir o par é como estacionar de olhos vendados — os freios saem tortos e a
corretora recusa calada. Foi o que aconteceu em 29/05: 6 contas tentaram abrir num preço
congelado e falharam todas.
Agora o sistema confere o preço antes e, se não confia, não abre e te avisa o motivo
num selo no card da batalha — em vez de abrir torto.
No card do par, quando ele está esperando em vez de abrir, aparece um selo com o motivo em
português (passe o mouse pra ler a explicação completa no balãozinho):
Nunca aparece código interno no selo — só português claro.
Se passar em tudo: abre normal, idêntico ao de sempre (zero diferença no dia bom). Só barra
quando o preço é ruim.
O selo só tira aberturas ruins — nunca adia uma boa sem motivo. Num hedge, abrir um lado com
preço errado quebra o espelho e gera perda que o outro lado não compensa. Recusar protege as
duas pernas igualmente.
"Se o preço pra abrir o par não é confiável, o sistema não abre, espera o próximo bom, e te diz
o porquê no card — em vez de abrir torto e a corretora recusar calada."
Status: ATIVO (no ar desde EA 3.103.0) | Criado: S392 (2026-05-30)
Toda madrugada a corretora "vira o dia" e cobra um juro de rolagem (swap) de quem ficou com
posição aberta. Pra evitar essa cobrança (e o risco de o preço dar um salto na virada), o
sistema fecha os pares de hedge um pouco antes dessa hora.
A novidade (build 3.103): o robô agora sabe sozinho que horas são essas — ele calcula pelo
padrão de Nova York, ajustando o horário de verão automaticamente. Então mesmo se o site
cair, o robô fecha na hora certa por conta própria.
Importante: isso não muda stop loss, take profit, direção ou tamanho da ordem. Só afeta
quando fechar pela virada de juros (e passa pela trava de hedge — fecha as duas pernas
juntas).
"O robô fecha o par antes da cobrança de juro da madrugada, sabendo a hora sozinho mesmo com o
site fora — e sem nunca matar um par recém-aberto por engano."
Status: ATIVO (no ar desde EA 3.99.0 / servidor S388) | Criado: S392 (2026-05-30) | Admin
O painel ao vivo só guarda as últimas 15 linhas do que o robô faz. Numa rajada (abrir um par
= ~12 cliques, fechar = ~4) isso estoura na hora e some antes de você ver. Quando um problema
já aconteceu, esse resumo curto não conta a história toda.
Agora dá pra pedir o diário completo do dia (o log inteiro do MT5) de qualquer conta —
até das máquinas remotas — pra investigar com calma depois.
Funciona pela mesma conexão robô↔servidor que já existe, então alcança as contas dos outros PCs
também.
Quando um par fez algo estranho e o painel ao vivo já perdeu as linhas — ex: um par que fechou em
segundos, uma ordem que falhou, um clique que não saiu. O diário completo tem tudo, sem janela e
sem perda por timing.
"Quando o painel ao vivo já apagou as linhas, peça o diário completo do dia daquela conta (até
das remotas) e investigue o que aconteceu com calma."