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, Mahoraga 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 |
|---|---|---|
| /quality-gates | Pre-deploy, "validar" | ~70% |
| /validate-loop | "testar", "validar" | ~70% |
| /verification | "deploy feito", "testes passaram" | ~70% |
| /brainstorming | "design", "trade-off" | ~70% |
| /debug-sistematico | "debug", "mesmo erro" | ~70% |
| /learn | Licao descoberta | ~90% (CLAUDE.md) |
| /compound | Fim de sessao | ~90% (CLAUDE.md) |
| /commit | Commitar | Manual |
| /deploy | Deploy VPS | Manual |
| /status | Diagnostico | Manual |
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: S221 (2026-04-13) — adicionado EC-Test: variante de injecao de sinais de teste (Phase C, signal_source=test, is_test isolation)
Versao: 2.1
Data: 2026-03-31
Status: v2.1: Adicionado EA remote logs pipeline, mapa de funcoes EA/servidor, ACK validation V1-V4, reconciliation pos-trade, grupo-only routing (S1015).
SSoT para: Ciclo completo de vida de um sinal — da deteccao ate o trade_link
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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
X-API-Key → verify_api_key)actiontrade_group_id (UUID)sl_distance = abs(price - sl), tp_distance = abs(tp - price)Signal com origin="ea", expires_at = now + 1minSignalAck com status="ORIGIN" para conta masterTradeLink com is_origin=True para conta mastergroup_id (exceto master)peer.invert != account.invert → aplica inversao (ver specs/invert-rules.md)Signal + PendingSignal (TTL 60s)ea_ws_manager.notify_account(peer.id, "new_signal")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)Signal + PendingSignal com ticket = local_ticket do peerTradeLink 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, ignoraSignal + PendingSignalSuppressMarker com TTL 10s para o trade_group_id (anti-echo servidor)log_trade_event(event_type="CLOSE", close_reason, profit, close_price)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.
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) |
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)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 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 |
| 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) |
WebBridge_PostReverse() |
POST /api/reverse |
Emergency reverse |
| 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: 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 | NUNCA colocar 2 contas da MESMA prop uma contra a outra. Se nao tem par de prop diferente, conta espera. SEM FALLBACK. | Prop detecta hedge = ban |
| R8 | So mesma fase | F1 vs F1, F2 vs F2, Funded vs Funded. Nunca misturar fases. | Risco inconsistente entre fases |
| R12 | Max 6 contas/prop | No maximo 6 contas por pessoa na mesma prop firm. | Regra da prop |
| D6 | Parear por distancia | Ordenar contas por distancia ao target. Parear vizinhos (adjacentes). | Desperdicar trades pareando longe |
| D7 | Impar = 1 ociosa | Se N eh impar, 1 conta descansa no dia. Normal. | N/A |
| # | Regra | Detalhe | Se violar |
|---|---|---|---|
| R2 | F1 max = $2.500 | Na fase 1 (Challenge), risco maximo = $2.500 por trade (2.5% de $100k). | Ban pela prop |
| R3 | F2 max depende da prop | Props COM profit days (5ers, City): max $2.000 (2%). Props SEM profit days: max $2.500 (2.5%). | Ban ou trades insuficientes pra profit days |
| R4 | Smart risk | Nao apostar mais que precisa pra fechar. Se falta $800: aposta = ceil($800 / (1 - 0.02)) = $816. Minimo entre teto e distancia. | Overshoot = dinheiro jogado fora |
| R5 | Look-ahead (zona morta) | Antes de apostar, simula resultado. Se deixaria distancia OU colchao entre $0 e $500: reduz o risco. | Conta fica "quase la" com micro-trades inuteis |
| R5b | Spread compensation | No ULTIMO trade (distance <= max_risk + $150): permite risk = ceil(distance/(1-spread)), mesmo que ultrapasse max_risk interno. Safety cap = max_risk + $150: Funding Pips cap=$2,650 (2.65%, margem $350 pro hard breach 3%). Props com max_risk=$5k: cap=$5,150 (nunca atinge). Seguro com qualquer spread. Look-ahead do OPONENTE continua ativo normalmente. | 1 dia a mais por fase |
| R6 | Death trade | Colchao < max risk efetivo = modo death trade. Risk = colchao (aposta o que tem, SL/TP cabe na vida restante). Look-ahead nao protege. Ainda limitado pelo oponente (smart risk). Motivo: apostar mais que o colchao = SL ultrapassa piso DD = conta morre intraday mesmo "ganhando". | Sangrar devagar numa conta moribunda |
| # | 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: max_risk >= $2.500 AND max_dd >= 10%
| # | Prop | Preco | F1% | F2% | Max Risk | Profit Days |
|---|---|---|---|---|---|---|
| 1 | The 5%ers | $545 | 8 | 5 | $5.000 | 3 lucrativos |
| 2 | FTMO Swing | $635 | 10 | 5 | $5.000 | nao |
| 3 | BrightFunded | $582 | 8 | 5 | $5.000 | nao |
| 4 | City Traders | $689 | 10 | 5 | $5.000 | 3 lucrativos |
| 5 | FundedNext | $549 | 8 | 5 | $5.000 | nao |
| 6 | Funding Pips | $529 | 8 | 5 | $2.500 | nao |
12 cadeiras = CLEAN_PROPS x 2 (2 contas por prop)
Excluidas:
- Alpha Pro: max_risk $1.500 < $2.500 base
- FTP Classic: max_dd 8% < 10%
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: Mais fraco manda (P57) | desired_loss/desired_gain limitados a min(cushion, risk_usd) + margin. Margin ($2-10) preservada pra garantir cruzamento do floor/target (sem margem, conta pode empatar exatamente no limite). NUNCA usar cushion individual se trade foi dimensionado pelo risco do PAR. |
| F4: Sanidade | Shift maximo de SL/TP = 2x distancia original. Rejeita ajustes extremos |
| F5: Limite original | Risco pos-MODIFY nao pode exceder risco original do par |
| F6: Margem minima | Skip MODIFY se par ja esta dentro de margin_max do target |
| F7: Buffer SL | MODIFY SL desconta sl_buffer ($1.00 XAUUSD) da distancia — EA aplica buffer ao SL inclusive no MODIFY, entao sl_dist = desired/vol - buffer. Sem isso, perda real = desired + buffer_cost. So aplica pra SL (death), nao TP (target). |
Fluxo:
Ambos FILLED → Detecta push zone → Delay 30-120s → Calcula novo SL/TP
→ F1 (duplicata?) → F2/F3 (mais fraco) → F4 (sanidade) → F5 (limite)
→ F6 (ja proximo?) → Cria sinais MODIFY → EA aplica via GUI
| # | 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 (XAUUSD: $10, BTCUSD: $50). Se nao moveu: retry a cada 5min, ate 60x (rsafe2_max_retries). Se fim da janela (12:00 no timezone do pool, via trading_tz_offset) OU max retries: skip (terminal). |
>= $10 USD (XAUUSD), $50 (BTCUSD) | Prop ve mesmo preco de entrada |
| R-SAFE3 | Lote != sibling | Lote resultante DEVE diferir do lote do sibling. Se igual: re-rolar SL/TP distance ate lote divergir. Hard check APOS todo calculo de risco. Lote = risk / (SL_distance * point_value) — SL/TP distance eh o motor principal de divergencia de lote. | >= 0.05 lots | Prop ve mesmo volume |
| R-SAFE4 | Risco variavel | DESATIVADO (S172). Reduzia risco em 20% quando siblings tinham risco < $100 diferenca. Removido: SL distance aleatorio ja cria variacao suficiente. | ~~< $100~~ | ~~P&L~~ |
| R-SAFE5 | SL/TP nivel absoluto != sibling | O NIVEL DE PRECO do SL/TP (nao a distancia) deve diferir >= $10 do sibling. Garante que closes acontecem em precos diferentes. Se nao cabe: skip a conta (sibling que ja executou fica OK). | >= $10 USD nivel absoluto | Prop ve close correlacionado |
| R-SAFE6 | Horario variavel (graph coloring) | Usa graph coloring pra atribuir slots a pares same-prop. rsafe_gap_min (1h), rsafe_gap_max (2h) — gap randomizado por par. Jitter 5-55s por par dentro do slot (anti-robotico). Ordem dos slots randomizada a cada rodada. Janela definida por trading_start/trading_end e trading_tz_offset do pool. |
Janela configuravel, min ~1-2h | Prop detecta padrao temporal |
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?
SIM -> executa (com jitter R-SAFE6)
NAO -> re-agenda +5min, incrementa retry counter
Fim da janela (12:00 no timezone do pool, via trading_tz_offset)
OU max retries (60, configurable):
-> Skip terminal (status = skipped_rsafe)
Gates por simbolo (presets.py): XAUUSD: $10 | BTCUSD: $50 | Default: $10
Configuravel por pool: rsafe2_price_gate no config JSONB (override via API ou dashboard)
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 / sl_max |
$25 / $50 | $400 / $800 |
dz_min / dz_max |
$500 / $1000 | $2000 / $4000 |
rsafe2_price_gate |
$10 | $200 |
push_buffer_usd |
$100 | $100 |
spread_pct |
2% | 2% |
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) |
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 |
Status: ACTIVE | Ultima revisao: S194 (2026-04-06)
SSoT para: Modulos EA, compilacao, versionamento, erros MT5, armadilhas
Versao: 2.2 (EA v3.69.0, atualizado S194 — 2026-04-06). Desde 2.1: WebBridge usa Digits() no DoubleToString, MODIFY-VERIFY queue, heartbeat async retry, mensagens legiveis.
Consolida:memory/ea-modules.md+.claude/knowledge/copytrade-ea.md
Relacionados:specs/invert-rules.md(inversao),specs/auto-update-flow.md(auto-update),specs/settings-sync-guardian.md(sync settings),memory/business-constants.md(constantes)
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) |
| GUIExecution.mqh | 1156 | g_gui_ | GUI automation Win32. OPEN=F9, MODIFY=ListView+DBLCLK, CLOSE=ListView+Fechar. Lock file anti-conflito |
| SnapshotEngine.mqh | 417 | g_se_ | Detecta OPEN/MODIFY/CLOSE. Compare snapshots -> enfileira. Cooldowns (CLOSE 0.5s, MODIFY 1s, OPEN 0.5s — OPEN guard via SE_SuppressTicket(newTicket, 5s)). SE_MAX_RETRIES=5: sinal descartado apos 5 falhas. SE_MAX_QUEUE=200 |
| SymbolResolver.mqh | 401 | g_sr_ | 3 camadas: Override -> Cache normalizado -> Alias (30 hardcoded). Buffers por classe |
| SettingsManager.mqh | 399 | g_sm_ | Cascata: SERVER -> LOCAL+JSON -> LOCAL -> OFFLINE. Persiste em Common/Files/. Init nunca falha. v3.66.0: Settings Sync Guardian — WS push notifica EA de mudancas, EA verifica e aplica (ver specs/settings-sync-guardian.md) |
| PanelDisplay.mqh | 440 | g_pnl_ | Painel Apple Finance Dark. Rounded corners, semi-transparent. Throttle 2s |
| AutoUpdate.mqh | 662 | g_au_ | States: IDLE->WAITING->DOWNLOADING->VERIFYING->LAUNCHING->DONE. Lock file anti-race. PS1 profile-aware |
| EventLog.mqh | 294 | g_el_ / EL_ | v3.73.2 (Fase C obs): Buffer per-signal de eventos de observabilidade (struct EL_Event, max 500 com FIFO overflow + WARN). EL_Emit() persiste cada evento em MQL5/Files/signal_events_pending.jsonl (append) — R3 zero-perda em crash. EL_LoadPersistent() em OnInit recupera buffer pos-crash. EL_FlushBulk() em OnTimer envia via POST /api/events (throttle 5s, batch 50, idempotente no servidor via UNIQUE). EL_RewritePersistFile + EL_BuildBulkJson auxiliares. Auto seq_num per-signal via scan linear. Sem call sites de emit ainda — integracao em WebBridge_SendAck/GUIExecution S1-S11/Poll fica em C10-C14. Spec: specs/ea-observability.md |
| WinInetHTTP.mqh | 325 | -- | Conexao persistente, request por chamada, leitura em chunks 8KB. Sem whitelist MT5 |
| WinHttpWS.mqh | 391 | -- | WebSocket async via DLL (MQL5 tem sockets TCP nativos mas sincrono/bloqueante; DLL da async + WS pronto + sem whitelist). Auth via 1a mensagem JSON. Fallback: HTTP polling sempre ativo |
| asyncwebsocket.mqh | 502 | -- | DLL wrapper para WS async |
| asyncwinhttp.mqh | 233 | -- | DLL wrapper para WinHTTP async |
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 |
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: Copia .mq5 Dropbox→Terminal, compila via MetaEditor CLI, copia .ex5 de volta pro Dropbox.
Terminal MT5: C:\Users\mrodr\AppData\Roaming\MetaQuotes\Terminal\53785E099C927DB68A545C249CDBCE06\MQL5
Junction: MQL5\Include\CopyTrade\ -> Dropbox (editar no Dropbox, MetaEditor le direto)
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.25.0+):
- Backoff: imediato -> 5s -> 15s -> 30s (max)
- Health check 60s: se connected=true mas sem msgs -> reset
- Fallback: 3x WS fail -> HTTP polling (2-5s adaptive). WS retenta cada 120s
| 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) |
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]
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
ExecStart=uvicorn app.main:app --host 127.0.0.1 --port 8000
Restart=always | RestartSec=5
WorkingDirectory=/opt/copytrade-server
1 worker (single-process). Restart limit: 5 em 10s, depois "failed".
systemctl restart copytrade # restart
systemctl reset-failed copytrade # limpar failed
journalctl -u copytrade -f # logs real-time
journalctl -u copytrade --since '5m ago' --no-pager # ultimos 5min
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 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.
0 3 * * * backup_daily.sh # Backup diario 3h UTC
*/2 * * * * healthcheck_v2.sh # Watchdog: auto-restart se cair
*/5 * * * * monitor.sh # Monitor proativo + Telegram alert
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 🔍.
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]
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) |
tournament.js |
Tab Torneio (pairings, hedge) |
admin.js |
Tab Admin (cleanup, debug) (~800 linhas) |
remote_settings.js |
Modal Remote Settings (6 buffers) |
delay.js |
Delay Analysis (stats + tabela) |
ea_update.js |
Auto-update UI (upload, release, rollback) |
health.js |
Tab Saude (diagnostics, logs) |
roadmap.js |
Kanban roadmap |
af_hedge.js |
AF Hedge tab (pair battles, pool config, force execute, history, debug mode, swap countdown, compact timeline) (~3860 linhas) |
af_hedge_logic.js |
Helper functions para operacoes AF hedge (~150 linhas) |
journal.js |
Tab Sistema > Journal (diario de trades, CSV export UTF-8, filtros) (~360 linhas) |
prop_firms.js |
Tab Tools > Prop Firms (tabela comparativa) (~270 linhas) |
risk_calc_v2.js |
Risk Calculator V2 (3 modos: %Bal, %Eq, USD) (~980 linhas) |
simulator.js |
Monte Carlo AF v2 (IIFE) (~880 linhas). Backend: VPS routes/simulator.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)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') // Torneio
switchTab('trading') // Trading
switchTab('system') // Sistema
switchTab('tools') // Tools
// Sub-tabs (div.sub-pill)
switchSub('tools', 'roadmap') // Tools > Roadmap
switchSub('tools', 'propfirms') // Tools > Prop Firms
switchSub('tools', 'simulator') // Tools > Simulador
switchSub('trading', 'signals') // Trading > Sinais
switchSub('trading', 'orders') // Trading > Ordens
switchSub('trading', 'positions') // Trading > Posicoes
Regras browser:
- Browser eh Brave, nao Chrome. SEMPRE rodar Brave-Debug.bat antes do DevTools MCP
- new_tab com URL no payload (NUNCA vazio + navigate separado)
- list_tabs ANTES de close_tab
- NAO usar tab principal do usuario — sempre nova tab
- Usar test_runner pra login automatizado, NUNCA admin
// 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)
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 |
Endpoints lentos: usar fetch() direto com AbortController |
| CSS var inexistente | Consultar themes.css. Validos: --bg-card, --text-primary, --text-muted, --border |
Tabs fora do .main |
TODA tab-content DEVE estar dentro de div.main |
| Cache bust no deploy | Sem ?v= atualizado, browser serve versao antiga |
| themes.css removido | Quebra todos os temas. NUNCA remover |
| Chart.js resize infinito | Canvas DEVE estar em div com position:relative;height:Xpx |
~~showToast vs toast~~ |
CORRIGIDO S92. ea_update.js agora usa toast() de api.js |
| Validacao em browser separado | NUNCA usar browser principal do usuario |
| 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
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. |
| G5 | reverse_signal e legado |
Endpoint antigo que cria CLOSE simples sem inversao nem trade_links. Nao usar no fluxo principal. |
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, heartbeat response automaticamente promove para a versao stable mais recente. Sem acao do operador — "pegar o trem" automaticamente.
Implementado em heartbeat.py:740-763. Util quando contas novas sao criadas apos um release.
// 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 |
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 | varchar | '' | Nome da prop firm (FTMO, etc) |
| prop_size | varchar | '' | 10k/25k/50k/100k/200k |
| prop_steps | varchar | '' | 1-step/2-step/3-step |
| update_group | varchar | 'stable' | early/stable (auto-update) |
| desired_version | varchar | | Versao desejada pra auto-update |
| update_attempts | int | 0 | Tentativas de update (max 5) |
Sinais de trade (OPEN/MODIFY/CLOSE). Criados pelo EA ou broadcast do dashboard.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| group_id | varchar | Grupo destino |
| action | varchar | OPEN/MODIFY/CLOSE |
| ticket | bigint | Ticket MT5 de referencia |
| symbol | varchar | BTCUSD, EURUSD etc |
| direction | varchar | BUY/SELL |
| volume | float | Lotes |
| price | float | Preco de abertura |
| sl | float | Stop Loss |
| tp | float | Take Profit |
| source_account | int FK | Conta que originou |
| created_at | timestamp | |
| expires_at | timestamp | TTL |
| origin | varchar | 'ea' ou 'dashboard' |
| trade_group_id | varchar | UUID agrupador de trades relacionados |
| sl_distance | float | Distancia SL em preco |
| tp_distance | float | Distancia TP em preco |
| signal_channel | varchar(10) | Canal de envio (ws/poll) |
| detection_method | varchar(10) | Metodo de deteccao |
| close_reason | varchar(20) | Motivo do close |
| queued_duration_ms | int | Tempo na fila (ms) |
| server_received_at | timestamp | Quando servidor recebeu |
| server_processing_ms | int | Tempo de processamento servidor (ms) |
| signal_source | varchar(16) | S219: 'manual' (default) ou 'test' |
| trace_id | char(32) | S224: W3C-inspired trace identifier (nullable pre-migration) |
Confirmacao de execucao de sinais pelos EAs.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| signal_id | int FK | Sinal confirmado |
| account_id | int FK | Conta que executou |
| status | varchar | ORIGIN/FILLED/FAILED/SKIPPED |
| local_ticket | bigint | Ticket local criado |
| error_msg | text | Mensagem de erro se FAILED |
| executed_at | timestamp | |
| open_price | float | Preco real de abertura |
| applied_sl | float | SL aplicado |
| applied_tp | float | TP aplicado |
| actual_volume | float | Volume real executado |
| receive_channel | varchar(20) | Canal de recebimento (ws/poll) |
| gui_duration_ms | int | Tempo de execucao GUI (ms) |
| total_delay_ms | int | Delay total sinal->execucao (ms) |
| trace_id | char(32) | S224: carrega mesmo trace_id do signal pai (nullable pre-migration) |
Event sourcing do ciclo de vida de cada signal (append-only, PARTITIONED BY RANGE(ts_server) daily, 90d retention).
Timeline 1-click: SELECT * FROM signal_events WHERE signal_id=X ORDER BY seq_num.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | bigserial PK composto | Parte do PK (id + ts_server exigido pela particao) |
| signal_id | int FK signals | Signal dono do evento |
| trace_id | char(32) | W3C-inspired 128-bit hex, nullable pra backward-compat |
| account_id | int | Conta (nao FK pra permitir conta deletada) |
| event_type | varchar(32) | signal_created, ea_received, gui_s1_ok..s11_ok, ack_sent, ack_failed |
| seq_num | smallint | Per-signal 1,2,3... ordem dentro do signal |
| ts_ea | timestamptz | Timestamp origem no EA (nullable) |
| ts_server | timestamptz PK composto | Quando chegou no servidor (chave de particao) |
| payload | jsonb | Contexto rico (error, duration, SL/TP aplicado, etc) |
Indexes (propagados automaticamente pras particoes):
- (signal_id, seq_num) — timeline query principal
- trace_id partial WHERE IS NOT NULL — correlacao cross-signal
- (event_type, ts_server) — stats por tipo no periodo
UNIQUE constraint (idempotencia R7): (signal_id, event_type, seq_num, ts_server) — permite retry ON CONFLICT DO NOTHING.
Particionamento: 7 particoes pre-criadas (CURRENT_DATE..+6d) + signal_events_default safety net. Cron scripts/cron/purge_signal_events.sh cria ahead + DROP >90d + VACUUM.
Vinculo entre trades copiados. Permite propagar CLOSE/MODIFY.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| trade_group_id | varchar | UUID do grupo |
| signal_id | int FK | Sinal que criou |
| account_id | int FK | Conta dona |
| local_ticket | bigint | Ticket MT5 |
| is_origin | boolean | true = quem originou |
| is_closed | boolean | false | Trade fechado? |
| created_at | timestamp | |
| closed_at | timestamp | |
| close_price | float | Preco de fechamento |
| profit | float | P&L |
Fila de sinais aguardando execucao. TTL 5 minutos (300s).
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| account_id | int FK | Conta destino |
| symbol | varchar | |
| direction | varchar | BUY/SELL |
| volume | float | |
| signal_id | int FK | |
| trade_group_id | varchar | |
| expires_at | timestamp | TTL 5 min (300s) |
| resolved | boolean | false |
| created_at | timestamp | |
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 (FTMO Swing, FundedNext, etc) |
| default_steps | varchar(20) | '2-step' | 1-step/2-step/3-step |
| phases | JSON | [] | Fases disponiveis (F1, F2, Funded) |
| sizes | JSON | [] | Tamanhos disponiveis (10k, 25k, etc) |
| is_builtin | boolean | false | Se veio pre-cadastrada (seed) |
| created_at | timestamp | now() | |
| program | varchar(100) | '' | Nome do programa (ex: High Stakes Challenge) |
| price | int | 0 | Preco USD (referencia 100k) |
| leverage | varchar(20) | '' | Alavancagem (ex: 1:100) |
| max_dd | float | 10.0 | Max DD % (estatico, base saldo inicial) |
| daily_dd | float | 5.0 | Daily DD % |
| daily_dd_type | varchar(10) | 'equity' | 'equity' ou 'balance' |
| target_f1 | float | 8.0 | Meta F1 % |
| target_f2 | float | 5.0 | Meta F2 % |
| min_days_f1 | int | 0 | Dias minimos trading F1 |
| min_days_f2 | int | 0 | Dias minimos trading F2 |
| min_profit_days | int | 0 | Dias lucrativos obrigatorios |
| max_risk | int | 5000 | Risco operacional por trade USD (100k ref) |
| daily_dd_op | int | 4500 | DD operacional diario USD (100k ref) |
| news_eval | varchar(100) | '' | Restricoes noticias avaliacao |
| news_funded | varchar(100) | '' | Restricoes noticias funded |
| limitation | varchar(200) | '--' | Limitacoes especiais |
| has_200k | boolean | false | Tem conta 200k? |
| is_active | boolean | true | Ativa no sistema? |
| is_af_eligible | boolean | true | Elegivel pro AF engine? (auto: risk>=2500 AND dd>=10) |
| updated_at | timestamp | null | Ultima atualizacao |
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)
pairings — Pareamentos de torneio (pool, round_num, account_a/b, balance_a/b, diff_pct, status, bracket_id, match_label, trade_group_id, winner_account_id, profit_a, profit_b, started_at, match_duration_s)
tournament_brackets — Brackets de torneio (name, pool, status, config JSONB, account_ids INT[], bracket_structure JSONB, current_round, matches_today, last_match_date, champion_account_id, created_at, completed_at)
ip_whitelist — IPs permitidos (ip_address, description, is_active)
Metricas detalhadas de cada batalha de torneio — spread, swap, slippage, drawdown.
| Coluna | Tipo | Descricao |
|--------|------|-----------|
| id | serial PK | |
| pairing_id | int FK pairings | Pareamento relacionado |
| bracket_id | int | Bracket do torneio |
| match_label | varchar(50) | Label do match |
| symbol | varchar(20) | Simbolo (XAUUSD etc) |
| direction | varchar(10) | BUY/SELL |
| broker_a/b | varchar(100) | Corretoras |
| volume | float | Volume em lotes |
| open_price_a/b | float | Preco abertura conta A/B |
| close_price_a/b | float | Preco fechamento conta A/B |
| intended_sl/tp | float | SL/TP planejados |
| actual_sl_a/tp_a | float | SL/TP reais aplicados |
| profit_a/b | float | P&L por conta |
| swap_a/b | float | Swap por conta |
| net_pnl | float | P&L liquido combinado |
| spread_at_open/close | float | Spread no momento de abertura/fechamento |
| slippage_open_a/b | float | Slippage na abertura |
| slippage_close_a/b | float | Slippage no fechamento |
| started_at/ended_at | timestamp | Inicio/fim do match |
| duration_s | int | Duracao em segundos |
| close_trigger | varchar(20) | O que causou o close (sl/tp/timeout) |
| max_dd_a/b | float | Drawdown maximo |
| max_floating_loss_a/b | float | Perda flutuante maxima |
| balance_a/b_before/after | float | Balance antes/depois |
| risk_pct | float | Risco % |
| risk_money | float | Risco $ |
| latency_open/close_a/b | float | Latencia sinal→execucao (s) |
| commission_a/b | float | Comissao por conta |
| spread_broker_a/b | float | Spread do broker |
| winner_account_id | int | Conta vencedora |
| winner_reason | varchar(50) | Motivo da vitoria |
| created_at | timestamp | |
af_pools — Pools AF v2 (name, mode varchar(20) default 'live' LEGACY, validated bool default false, symbol, status, spread_pct, trade_interval_sec, trade_timeout_sec, group_id, config JSONB, created_at, updated_at). S170: mode é legacy (código não lê mais). validated controla se pool pode operar.
af_pool_accounts — Contas no pool com prop atribuida (pool_id FK, account_id FK nullable UNIQUE (uq_account_one_pool — 1 conta = 1 pool), prop_name, prop_config JSONB, phase, virtual_balance, profitable_days, trading_days, status active/dead/funded/paused/passed, chair_number, transition_at, daily_pnl, daily_pnl_peak, direction_history JSONB default '[]' — ultimas 10 direcoes executadas pro anti-OSB, created_at, updated_at). S207: prop_config agora inclui snapshot: owner, account_name, account_type, original_group_id. Cadeiras passed/dead com account_id=NULL usam prop_config pra display historico (nome, cor, badge). virtual_balance serve como saldo congelado.
af_rounds — Rodadas de trading (pool_id FK, round_number, status pending/executing/completed/failed, pairs_count, started_at, completed_at, summary JSONB, created_at)
af_pairs — Pares/batalhas dentro de rodada (round_id FK, pool_id FK, account_a_id FK, account_b_id FK CHECK(a != b — chk_pair_diff_accounts), risk_usd, risk_detail JSONB, symbol, direction_a, volume, sl_price, tp_price, scheduled_at, status, winner_pool_account_id FK, profit_a, profit_b, spread_cost, created_at, completed_at)
af_trades — Trades individuais (pair_id FK, pool_account_id FK, account_id FK, signal_id FK, direction, volume, sl/tp/open/close price, profit, status, local_ticket bigint)
af_audit_log — Log imutavel INSERT-only (pool_id FK, action, entity_type, entity_id, detail text)
af_dead_letters — Dead Letter Queue: sinais AF que falharam (pool_id FK, pair_id, account_id, prop_name, failure_type varchar(50), reason text, attempts int, context JSONB). Tipos: timeout, margin, rsafe, invert, e7_exhausted, gui_fail
symbol_presets — Presets de configuracao por simbolo (id PK, symbol varchar(20) UNIQUE, config JSONB, spread_pct float, created_at, updated_at). Campos preset: sl_min/max, dz_min/max, rsafe2_price_gate, modify_margin_min/max, push_buffer_usd. Defaults hardcoded em server/af/presets.py (XAUUSD, BTCUSD, _default).
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}