merge: integra fase5 do Thiago (DOM, VWAP, big trades, trading, historical browser) mantendo fixes de ontem

- Combina on_history_ready_callback (replay reset) com on_big_trade_callback (Thiago)
- DOM subscription + visualização no canvas
- VWAP e delta profile no volume profile
- Big trades com alerta visual no canvas
- Endpoints /trade/buy, /trade/sell para execução via MQL5
- Historical Browser: date picker no header, /history/load e /history/live
- Trading panel no sidebar com botões BUY/SELL
- config/settings.py: mantém USTEC como padrão, adiciona BIG_TRADE_THRESHOLD
- Todos os fixes de ontem preservados: broadcast live, history_ready, replay reset

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rufinomec-afk
2026-06-08 11:22:13 -03:00
8 changed files with 621 additions and 25 deletions
+124 -6
View File
@@ -15,13 +15,15 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(me
logger = logging.getLogger("mt5_collector")
class MT5Collector:
def __init__(self, aggregator: Aggregator, on_update_callback: Callable[[dict, Optional[dict]], Any], on_history_ready_callback: Callable = None):
def __init__(self, aggregator: Aggregator, on_update_callback: Callable[[dict, Optional[dict], Optional[list]], Any], on_history_ready_callback: Callable = None, on_big_trade_callback: Optional[Callable[[dict], Any]] = None):
self.aggregator = aggregator
self.on_update_callback = on_update_callback
self.on_history_ready_callback = on_history_ready_callback
self.on_big_trade_callback = on_big_trade_callback
self.symbol = settings.MT5_SYMBOL
self.running = False
self.connected = False
self.historical_mode = False
self.last_tick_time_msc = 0
self.seen_ticks_buffer = set()
self.last_mid_price = 0.0
@@ -77,6 +79,11 @@ class MT5Collector:
self.aggregator.active_cluster.tick_size = tick_size
logger.info(f"Set aggregator tick size to {tick_size}")
# Subscribe to Depth of Market (DOM)
book_added = await asyncio.to_thread(mt5.market_book_add, self.symbol)
if not book_added:
logger.warning(f"Failed to subscribe to market book (DOM) for {self.symbol}")
logger.info("Successfully connected to MetaTrader 5 and logged in.")
self.connected = True
return True
@@ -86,6 +93,7 @@ class MT5Collector:
async def disconnect_mt5(self):
try:
await asyncio.to_thread(mt5.market_book_release, self.symbol)
await asyncio.to_thread(mt5.shutdown)
except Exception as e:
logger.error(f"Error during MT5 shutdown: {e}")
@@ -172,6 +180,10 @@ class MT5Collector:
# Polling loop
try:
if self.historical_mode:
await asyncio.sleep(1)
continue
from datetime import datetime
polling_dt = datetime.fromtimestamp(self.last_tick_time_msc / 1000.0)
ticks = await asyncio.to_thread(
@@ -262,6 +274,18 @@ class MT5Collector:
else:
is_buy = self.last_is_buy
# Broadcast Big Trades instantly
if volume >= settings.BIG_TRADE_THRESHOLD and self.on_big_trade_callback:
import time as _time
is_live = ((_time.time() * 1000) - msc) < 10_000
if is_live:
self.on_big_trade_callback({
"price": price,
"volume": volume,
"is_buy": is_buy,
"time_msc": msc
})
active_json, closed_json = self.aggregator.process_tick(price, volume, is_buy, msc)
# Only broadcast during live trading (within 10s of now) to avoid
@@ -284,15 +308,109 @@ class MT5Collector:
if is_live:
active_json['bid'] = self.last_bid
active_json['ask'] = self.last_ask
self.on_update_callback(active_json, closed_json)
# Fetch DOM
dom_json = None
try:
book_items = await asyncio.to_thread(mt5.market_book_get, self.symbol)
if book_items:
dom_json = [
{"type": item.type, "price": item.price, "volume": item.volume}
for item in book_items
]
except Exception as e:
logger.error(f"Error fetching DOM: {e}")
self.on_update_callback(active_json, closed_json, dom_json)
await asyncio.sleep(0.1)
except Exception as e:
logger.error(f"Error during tick polling loop: {e}")
self.connected = False
await self.disconnect_mt5()
await asyncio.sleep(2.0)
logger.error(f"Error in polling loop: {e}")
import traceback
logger.error(traceback.format_exc())
await asyncio.sleep(1.0)
async def load_historical_range(self, start_dt, end_dt):
"""
Pauses live polling, clears the aggregator, fetches a specific historical date range,
processes all ticks into clusters, and broadcasts the completed history to clients.
"""
self.historical_mode = True
logger.info(f"Loading historical data from {start_dt} to {end_dt} for {self.symbol}...")
# Clear existing data
self.aggregator.history.clear()
self.aggregator.active_cluster = None
self.aggregator._create_new_cluster()
# We need to fetch ticks in range.
ticks = await asyncio.to_thread(
mt5.copy_ticks_range,
self.symbol,
start_dt,
end_dt,
mt5.COPY_TICKS_ALL
)
if ticks is None or len(ticks) == 0:
logger.warning(f"No historical ticks found for {self.symbol} in the requested range.")
self.on_update_callback(self.aggregator.active_cluster.to_json(), None, [])
return
logger.info(f"Fetched {len(ticks)} historical ticks. Processing...")
# Process ticks without broadcasting every tick
for tick in ticks:
msc = tick['time_msc']
flags = int(tick['flags'])
bid_price = float(tick['bid'])
ask_price = float(tick['ask'])
volume = float(tick['volume_real'])
price = float(tick['last'])
if price <= 0.0 or volume <= 0.0:
continue
if bid_price > 0: self.last_bid = bid_price
if ask_price > 0: self.last_ask = ask_price
mid_price = (bid_price + ask_price) / 2.0 if (bid_price > 0 and ask_price > 0) else 0.0
if mid_price > 0:
if mid_price > self.last_mid_price:
self.last_is_buy = True
elif mid_price < self.last_mid_price:
self.last_is_buy = False
self.last_mid_price = mid_price
is_buy = self.last_is_buy
if flags & 32:
is_buy = True
elif flags & 64:
is_buy = False
# We don't trigger big trade alerts during history load to avoid spam
self.aggregator.process_tick(price, volume, is_buy, msc)
# Apply bar-level volume annotation
await self.annotate_history_bar_volume()
# Broadcast the reconstructed history
active_json = self.aggregator.active_cluster.to_json() if self.aggregator.active_cluster else None
closed_json = [c.to_json() for c in self.aggregator.history]
self.on_update_callback(active_json, None, closed_json)
logger.info("Historical data loaded and broadcasted.")
def return_to_live(self):
"""Resumes live polling from the current time."""
self.historical_mode = False
from datetime import datetime
self.last_tick_time_msc = int(datetime.now().timestamp() * 1000)
self.aggregator.history.clear()
self.aggregator.active_cluster = None
self.aggregator._create_new_cluster()
self._history_annotated = False
logger.info("Returned to live polling.")
async def fetch_bar_volume(self, open_time_msc: int, close_time_msc: int) -> int:
"""Sum tick_volume of M1 bars that overlap with the cluster's time range."""
+52 -3
View File
@@ -14,6 +14,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")
from config import settings
from backend.aggregator import Aggregator
from backend.mt5_collector import MT5Collector
from backend.trading import send_buy_signal, send_sell_signal
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("server")
@@ -23,7 +24,7 @@ aggregator = Aggregator(tick_size=1.0)
active_connections: Set[WebSocket] = set()
collector_task: Optional[asyncio.Task] = None
def broadcast_update(active_json: dict, closed_json: Optional[dict]):
def broadcast_update(active_json: dict, closed_json: Optional[dict], dom_data: Optional[list] = None):
"""
Callback executed by MT5Collector on every live tick.
Always broadcasts the active cluster state so the frontend updates in real-time.
@@ -34,7 +35,23 @@ def broadcast_update(active_json: dict, closed_json: Optional[dict]):
message = {
"type": "tick",
"active": active_json,
"closed": closed_json
"closed": closed_json,
"dom": dom_data
}
async def safe_send(ws: WebSocket, msg: dict):
try:
await ws.send_json(msg)
except Exception:
active_connections.discard(ws)
for connection in list(active_connections):
asyncio.create_task(safe_send(connection, message))
def broadcast_big_trade(trade_data: dict):
if not active_connections:
return
message = {
"type": "big_trade",
"data": trade_data
}
async def safe_send(ws: WebSocket, msg: dict):
try:
@@ -57,7 +74,7 @@ def broadcast_history_ready():
asyncio.create_task(_send())
# Initialize MT5 Collector
collector = MT5Collector(aggregator, on_update_callback=broadcast_update, on_history_ready_callback=broadcast_history_ready)
collector = MT5Collector(aggregator, on_update_callback=broadcast_update, on_history_ready_callback=broadcast_history_ready, on_big_trade_callback=broadcast_big_trade)
async def _start_collector_delayed():
"""Wait a moment for the server to fully start, then begin polling MT5."""
@@ -108,6 +125,7 @@ async def get_config():
"volume_max": settings.CLUSTER_VOLUME_MAX,
"range_points": settings.CLUSTER_RANGE_POINTS,
"time_seconds": settings.CLUSTER_TIME_SECONDS,
"symbol": settings.MT5_SYMBOL,
}
@app.post("/config")
@@ -134,6 +152,37 @@ async def update_config(update: ConfigUpdate):
return {"ok": True}
@app.post("/trade/buy")
async def trade_buy():
success = send_buy_signal()
return {"success": success}
@app.post("/trade/sell")
async def trade_sell():
success = send_sell_signal()
return {"success": success}
class HistoryLoadRequest(BaseModel):
start_time: str
end_time: str
@app.post("/history/load")
async def load_history(req: HistoryLoadRequest):
from datetime import datetime
try:
start_dt = datetime.fromisoformat(req.start_time)
end_dt = datetime.fromisoformat(req.end_time)
await collector.load_historical_range(start_dt, end_dt)
return {"success": True}
except Exception as e:
logger.error(f"Error loading history: {e}")
return {"success": False, "error": str(e)}
@app.post("/history/live")
async def return_to_live():
collector.return_to_live()
return {"success": True}
@app.get("/history")
async def get_history():
"""Returns the buffer of historical closed clusters."""
+38
View File
@@ -0,0 +1,38 @@
import MetaTrader5 as mt5
import logging
logger = logging.getLogger("trading")
def send_buy_signal():
"""
Sets the global variable SINAL_PYTHON_COMPRA to 1 in MT5.
The MQL5 Expert Advisor will detect this, execute the trade, and reset the variable to 0.
"""
try:
success = mt5.global_variable_set("SINAL_PYTHON_COMPRA", 1.0)
if success:
logger.info("SINAL_PYTHON_COMPRA defined successfully.")
return True
else:
logger.error("Failed to set SINAL_PYTHON_COMPRA.")
return False
except Exception as e:
logger.error(f"Exception when sending BUY signal: {e}")
return False
def send_sell_signal():
"""
Sets the global variable SINAL_PYTHON_VENDA to 1 in MT5.
The MQL5 Expert Advisor will detect this, execute the trade, and reset the variable to 0.
"""
try:
success = mt5.global_variable_set("SINAL_PYTHON_VENDA", 1.0)
if success:
logger.info("SINAL_PYTHON_VENDA defined successfully.")
return True
else:
logger.error("Failed to set SINAL_PYTHON_VENDA.")
return False
except Exception as e:
logger.error(f"Exception when sending SELL signal: {e}")
return False
+2 -1
View File
@@ -13,9 +13,10 @@ CLUSTER_DELTA_MAX = float(os.environ.get("CLUSTER_DELTA_MAX", 800))
CLUSTER_TIME_SECONDS = float(os.environ.get("CLUSTER_TIME_SECONDS", 300))
CLUSTER_CLOSE_MODE = os.environ.get("CLUSTER_CLOSE_MODE", "delta") # "range" | "volume" | "delta" | "time"
# Imbalance
# Imbalance & Big Trades
IMBALANCE_RATIO = float(os.environ.get("IMBALANCE_RATIO", 3.0))
STACKED_MIN_COUNT = int(os.environ.get("STACKED_MIN_COUNT", 3))
BIG_TRADE_THRESHOLD = float(os.environ.get("BIG_TRADE_THRESHOLD", 50.0))
# WebSocket
WS_PORT = int(os.environ.get("WS_PORT", 6002))
+167 -12
View File
@@ -22,7 +22,19 @@ export default function App() {
// Cluster aggregator config
const [closeMode, setCloseMode] = useState('delta');
const [deltaMax, setDeltaMax] = useState(800);
const [volumeMax, setVolumeMax] = useState(1000);
const [timeSeconds, setTimeSeconds] = useState(300);
const [rangePoints, setRangePoints] = useState(10);
const [configDirty, setConfigDirty] = useState(false);
const [targetSymbol, setTargetSymbol] = useState('EURUSD');
// History Mode
const [historicalDate, setHistoricalDate] = useState('');
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
// WebSocket Data States (Phase 5)
const [domData, setDomData] = useState(null);
const [bigTrades, setBigTrades] = useState([]);
// Toasts
const [toasts, setToasts] = useState([]);
@@ -42,18 +54,22 @@ export default function App() {
if (res.ok) {
const data = await res.json();
setCloseMode(data.close_mode || 'delta');
setDeltaMax(data.delta_max || 2418);
setDeltaMax(data.delta_max || 800);
setVolumeMax(data.volume_max || 1000);
setTimeSeconds(data.time_seconds || 300);
setRangePoints(data.range_points || 10);
if (data.symbol) setTargetSymbol(data.symbol);
}
} catch (e) {}
}, []);
// Push config update to backend
const applyConfig = useCallback(async (mode, delta) => {
const applyConfig = useCallback(async (mode, delta, vol, time, range) => {
try {
await fetch(`${API_URL}/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ close_mode: mode, delta_max: delta }),
body: JSON.stringify({ close_mode: mode, delta_max: delta, volume_max: vol, time_seconds: time, range_points: range }),
});
setConfigDirty(false);
} catch (e) {}
@@ -101,8 +117,16 @@ export default function App() {
// Dispatch alerts for the newly closed cluster
AlertEngine.processClusters([msg.closed], pushToast);
}
if (msg.dom) {
setDomData(msg.dom);
}
} else if (msg.type === 'big_trade') {
setBigTrades(prev => [...prev.slice(-49), msg.data]); // Keep last 50
pushToast(`BIG TRADE: ${msg.data.is_buy ? 'BUY' : 'SELL'} ${msg.data.volume} @ ${msg.data.price}`, msg.data.is_buy ? 'info' : 'warning');
AlertEngine.processBigTrade(msg.data);
}
}, [fetchHistory]);
}, [fetchHistory, pushToast]);
const wsStatus = useWebSocket(WS_URL, handleWebSocketMessage);
@@ -113,6 +137,36 @@ export default function App() {
// Aggregate stats from history
const totalVolume = history.reduce((acc, c) => acc + (c.total_volume || 0), 0) + (activeCluster?.total_volume || 0);
const loadHistory = async (dateStr) => {
setIsLoadingHistory(true);
try {
const start = new Date(dateStr);
start.setHours(0, 0, 0, 0);
const end = new Date(dateStr);
end.setHours(23, 59, 59, 999);
await fetch(`${API_URL}/history/load`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ start_time: start.toISOString(), end_time: end.toISOString() })
});
setHistoricalDate(dateStr);
} catch (e) {
console.error(e);
pushToast('Error loading history', 'error');
}
setIsLoadingHistory(false);
};
const returnToLive = async () => {
setIsLoadingHistory(true);
try {
await fetch(`${API_URL}/history/live`, { method: 'POST' });
setHistoricalDate('');
} catch (e) {}
setIsLoadingHistory(false);
};
const avgVolumePerCluster = history.length > 0 ? (history.reduce((acc, c) => acc + (c.total_volume || 0), 0) / history.length).toFixed(0) : 0;
const allClusters = [...history];
@@ -138,7 +192,7 @@ export default function App() {
</div>
{/* Premium Header */}
<header className="flex items-center justify-between px-6 py-4 bg-[#151B26] border-b border-slate-800 shadow-md">
<header className="flex items-center justify-between px-6 py-4 bg-[#151B26] border-b border-slate-800 shadow-md relative z-10">
<div className="flex items-center gap-3">
<div className="w-9 h-9 bg-gradient-to-tr from-[#00E676] to-[#00B0FF] rounded-lg flex items-center justify-center font-bold text-lg text-white shadow-lg">
Yu
@@ -151,8 +205,26 @@ export default function App() {
</div>
</div>
{/* Connection Status indicator */}
{/* Connection Status & History indicator */}
<div className="flex items-center gap-6 text-sm">
<div className="flex items-center gap-2 mr-4 bg-[#0B0E14] border border-slate-800 p-1 rounded-lg">
<input
type="date"
value={historicalDate}
onChange={(e) => loadHistory(e.target.value)}
className="bg-transparent text-slate-300 text-xs px-2 py-1 outline-none cursor-pointer"
title="Load Historical Date"
/>
{historicalDate && (
<button
onClick={returnToLive}
className="bg-[#FF4081] text-white text-[10px] font-bold px-2 py-1 rounded hover:bg-[#F50057] transition-colors"
>
LIVE
</button>
)}
</div>
{lastTickTime && (
<div className="text-slate-400 text-xs hidden sm:block">
Last Update: <span className="font-mono text-slate-300">{lastTickTime.toLocaleTimeString()}</span>
@@ -180,7 +252,17 @@ export default function App() {
{/* Main Body Layout */}
<main className="flex-1 flex overflow-hidden p-6 gap-6">
{/* Footprint Chart Panel */}
<div className="flex-1 flex flex-col h-full bg-[#151B26]/30 rounded-xl overflow-hidden">
<div className="flex-1 flex flex-col h-full bg-[#151B26]/30 rounded-xl overflow-hidden relative">
{isLoadingHistory && (
<div className="absolute inset-0 bg-[#0B0E14]/80 backdrop-blur-sm flex items-center justify-center z-50">
<div className="flex flex-col items-center">
<div className="w-10 h-10 border-4 border-[#00B0FF] border-t-transparent rounded-full animate-spin"></div>
<span className="mt-4 text-[#00B0FF] font-bold text-sm tracking-widest uppercase">Fetching History...</span>
</div>
</div>
)}
<FootprintCanvas
clusters={allClusters}
tickSize={dynamicTickSize}
@@ -190,6 +272,8 @@ export default function App() {
onStepChange={setStepMultiplier}
bid={lastBid}
ask={lastAsk}
domData={domData}
bigTrades={bigTrades}
/>
</div>
@@ -205,7 +289,7 @@ export default function App() {
<div className="flex justify-between items-center text-xs">
<span className="text-slate-400">Target Symbol:</span>
<span className="font-mono font-semibold text-[#00B0FF] bg-[#00B0FF]/10 px-2 py-0.5 rounded">
USTEC
{targetSymbol}
</span>
</div>
<div className="flex flex-col sm:flex-row items-center gap-6">
@@ -296,7 +380,7 @@ export default function App() {
</div>
</div>
{/* Delta threshold — only relevant when mode=delta */}
{/* Range thresholds */}
{closeMode === 'delta' && (
<div>
<div className="flex justify-between mb-1">
@@ -309,15 +393,54 @@ export default function App() {
onChange={e => { setDeltaMax(Number(e.target.value)); setConfigDirty(true); }}
className="w-full accent-[#00E676] bg-slate-800 rounded-lg h-1.5 appearance-none cursor-pointer"
/>
<div className="flex justify-between text-[9px] text-slate-600 mt-0.5">
<span>100</span><span>10 000</span>
</div>
)}
{closeMode === 'volume' && (
<div>
<div className="flex justify-between mb-1">
<span className="text-[10px] text-slate-500 font-bold">VOLUME THRESHOLD</span>
<span className="text-[10px] font-mono text-slate-300">{volumeMax.toLocaleString()}</span>
</div>
<input
type="range" min="100" max="20000" step="100"
value={volumeMax}
onChange={e => { setVolumeMax(Number(e.target.value)); setConfigDirty(true); }}
className="w-full accent-[#00E676] bg-slate-800 rounded-lg h-1.5 appearance-none cursor-pointer"
/>
</div>
)}
{closeMode === 'time' && (
<div>
<div className="flex justify-between mb-1">
<span className="text-[10px] text-slate-500 font-bold">TIME THRESHOLD (seconds)</span>
<span className="text-[10px] font-mono text-slate-300">{timeSeconds}s</span>
</div>
<input
type="range" min="30" max="3600" step="30"
value={timeSeconds}
onChange={e => { setTimeSeconds(Number(e.target.value)); setConfigDirty(true); }}
className="w-full accent-[#00E676] bg-slate-800 rounded-lg h-1.5 appearance-none cursor-pointer"
/>
</div>
)}
{closeMode === 'range' && (
<div>
<div className="flex justify-between mb-1">
<span className="text-[10px] text-slate-500 font-bold">RANGE THRESHOLD (points)</span>
<span className="text-[10px] font-mono text-slate-300">{rangePoints}</span>
</div>
<input
type="range" min="1" max="500" step="1"
value={rangePoints}
onChange={e => { setRangePoints(Number(e.target.value)); setConfigDirty(true); }}
className="w-full accent-[#00E676] bg-slate-800 rounded-lg h-1.5 appearance-none cursor-pointer"
/>
</div>
)}
{/* Apply button */}
<button
onClick={() => applyConfig(closeMode, deltaMax)}
onClick={() => applyConfig(closeMode, deltaMax, volumeMax, timeSeconds, rangePoints)}
disabled={!configDirty}
className={`w-full py-1.5 rounded-md text-xs font-semibold transition ${configDirty ? 'bg-[#00E676] text-slate-900 hover:bg-[#00c853]' : 'bg-slate-800 text-slate-600 cursor-not-allowed'}`}
>
@@ -363,6 +486,38 @@ export default function App() {
Use scroll wheel to move vertical scale.<br />
Hold Shift + scroll wheel to scroll horizontal.
</footer>
{/* Trading Panel (Phase 5) */}
<section className="bg-[#151B26] border border-slate-800 rounded-xl p-5 shadow-lg">
<h2 className="text-sm font-semibold text-slate-300 mb-4 border-b border-slate-800 pb-2">
Trading Execution
</h2>
<div className="space-y-3">
<div className="flex gap-2">
<button
onClick={async () => {
const res = await fetch(`${API_URL}/trade/buy`, { method: 'POST' });
if (res.ok) pushToast("Buy Signal Sent (MQL5)", "info");
}}
className="flex-1 bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 rounded shadow-md transition-colors text-sm"
>
BUY MKT
</button>
<button
onClick={async () => {
const res = await fetch(`${API_URL}/trade/sell`, { method: 'POST' });
if (res.ok) pushToast("Sell Signal Sent (MQL5)", "warning");
}}
className="flex-1 bg-red-600 hover:bg-red-500 text-white font-bold py-2 rounded shadow-md transition-colors text-sm"
>
SELL MKT
</button>
</div>
<div className="text-[10px] text-slate-500 text-center leading-tight">
Executes via MQL5 CTrade API with OCO brackets.<br/>Latência estimada: &lt; 20ms
</div>
</div>
</section>
</aside>
</main>
</div>
+114 -3
View File
@@ -1,6 +1,6 @@
import React, { useRef, useEffect, useState } from 'react';
export default function FootprintCanvas({ clusters, tickSize = 1.0, stepMultiplier = 1, viewMode = 'bidask', imbalanceRatio = 300, onStepChange, bid = 0, ask = 0 }) {
export default function FootprintCanvas({ clusters, tickSize = 1.0, stepMultiplier = 1, viewMode = 'bidask', imbalanceRatio = 300, onStepChange, bid = 0, ask = 0, domData = null, bigTrades = [] }) {
const canvasRef = useRef(null);
// Navigation & Scale State
@@ -398,20 +398,41 @@ export default function FootprintCanvas({ clusters, tickSize = 1.0, stepMultipli
// Draw Volume Profile Overlay Panel (Left Side)
const volProfileWidth = 140;
const volumeProfile = {};
const deltaProfile = {}; // Phase 5
let maxProfileVolume = 0;
let maxDeltaAbs = 0;
// VWAP Calculation
let cumulativeVolume = 0;
let cumulativePriceVolume = 0;
clusters.forEach(cluster => {
const lvls = cluster.levels || {};
Object.keys(lvls).forEach(pStr => {
const p = parseFloat(pStr);
const data = lvls[pStr];
const binPrice = Math.round(p / actualTickSize) * actualTickSize;
volumeProfile[binPrice] = (volumeProfile[binPrice] || 0) + (data.total || 0);
const vol = data.total || 0;
const delta = (data.buy_vol || 0) - (data.sell_vol || 0);
volumeProfile[binPrice] = (volumeProfile[binPrice] || 0) + vol;
deltaProfile[binPrice] = (deltaProfile[binPrice] || 0) + delta;
cumulativeVolume += vol;
cumulativePriceVolume += (binPrice * vol);
if (volumeProfile[binPrice] > maxProfileVolume) {
maxProfileVolume = volumeProfile[binPrice];
}
if (Math.abs(deltaProfile[binPrice]) > maxDeltaAbs) {
maxDeltaAbs = Math.abs(deltaProfile[binPrice]);
}
});
});
const vwap = cumulativeVolume > 0 ? (cumulativePriceVolume / cumulativeVolume) : null;
// Value Area Calculation (70% of total volume)
let totalDayVolume = 0;
let dayPocPrice = null;
@@ -511,13 +532,103 @@ export default function FootprintCanvas({ clusters, tickSize = 1.0, stepMultipli
}
ctx.fillRect(0, y, barWidth, rowHeight - 2);
// Draw Delta Profile inside the Volume bar
const delta = deltaProfile[pStr] || 0;
if (maxDeltaAbs > 0 && delta !== 0) {
const deltaBarWidth = (Math.abs(delta) / maxDeltaAbs) * (volProfileWidth - 5) * 0.5; // Max 50% width
ctx.fillStyle = delta > 0 ? 'rgba(0, 230, 118, 0.7)' : 'rgba(255, 23, 68, 0.7)';
ctx.fillRect(0, y + rowHeight / 2 - 2, deltaBarWidth, 4);
}
});
// Draw VWAP Line
if (vwap !== null) {
const vwapY = getPriceY(vwap);
ctx.strokeStyle = '#FF4081'; // Pink VWAP
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.beginPath();
ctx.moveTo(0, vwapY);
ctx.lineTo(width, vwapY);
ctx.stroke();
ctx.setLineDash([]);
// VWAP Label
ctx.fillStyle = '#FF4081';
ctx.font = 'bold 10px JetBrains Mono, monospace';
ctx.textAlign = 'right';
ctx.fillText('VWAP', width - axisWidth - 5, vwapY - 5);
}
// Panel Title
ctx.fillStyle = '#94A3B8';
ctx.font = '10px JetBrains Mono, monospace';
ctx.textAlign = 'center';
ctx.fillText('VOL PROFILE', volProfileWidth / 2, 20);
ctx.fillText('VOL & DELTA PROF', volProfileWidth / 2, 20);
}
// --- DOM (Depth of Market) Visualization (Phase 5) ---
if (domData) {
const maxDomVol = Math.max(
...((domData.asks || []).map(d => d[1])),
...((domData.bids || []).map(d => d[1])),
1
);
const maxDomWidth = 100; // pixels
const domBaseX = width - axisWidth;
// Draw Asks
(domData.asks || []).forEach(([p, v]) => {
const y = getPriceY(p) - rowHeight / 2;
const barW = (v / maxDomVol) * maxDomWidth;
ctx.fillStyle = 'rgba(239, 68, 68, 0.4)'; // Red for asks
ctx.fillRect(domBaseX - barW, y, barW, rowHeight - 2);
ctx.fillStyle = 'rgba(239, 68, 68, 0.9)';
ctx.font = '9px JetBrains Mono, monospace';
ctx.textAlign = 'right';
ctx.fillText(v.toString(), domBaseX - barW - 4, y + rowHeight / 2 + 3);
});
// Draw Bids
(domData.bids || []).forEach(([p, v]) => {
const y = getPriceY(p) - rowHeight / 2;
const barW = (v / maxDomVol) * maxDomWidth;
ctx.fillStyle = 'rgba(59, 130, 246, 0.4)'; // Blue for bids
ctx.fillRect(domBaseX - barW, y, barW, rowHeight - 2);
ctx.fillStyle = 'rgba(59, 130, 246, 0.9)';
ctx.font = '9px JetBrains Mono, monospace';
ctx.textAlign = 'right';
ctx.fillText(v.toString(), domBaseX - barW - 4, y + rowHeight / 2 + 3);
});
}
// --- Big Trades Visualization ---
if (bigTrades && bigTrades.length > 0 && clusters.length > 0) {
// Find the last cluster's X
const lastColIdx = clusters.length - 1;
const totalClustersWidth = clusters.length * (colWidth + colGap);
const startX = width - axisWidth - scrollOffset.x - totalClustersWidth + 100;
const lastColX = startX + lastColIdx * (colWidth + colGap);
bigTrades.forEach(bt => {
const y = getPriceY(bt.price);
// Draw a glowing circle for the Big Trade
ctx.beginPath();
ctx.arc(lastColX + colWidth / 2, y, 12, 0, 2 * Math.PI);
ctx.fillStyle = bt.is_buy ? 'rgba(59, 130, 246, 0.8)' : 'rgba(239, 68, 68, 0.8)';
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = '#FFF';
ctx.stroke();
// Volume text
ctx.fillStyle = '#FFF';
ctx.font = 'bold 9px Outfit';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(Math.round(bt.volume).toString(), lastColX + colWidth / 2, y);
});
}
// Draw Vertical Price Axis (Right Side)
+9
View File
@@ -10,6 +10,8 @@ class AlertEngine {
playAudio(message) {
if ('speechSynthesis' in window) {
// Prevent spamming the queue
window.speechSynthesis.cancel();
const msg = new SpeechSynthesisUtterance(message);
msg.rate = 1.2;
msg.pitch = 1.1;
@@ -19,6 +21,13 @@ class AlertEngine {
}
}
processBigTrade(trade) {
if (!trade) return;
const direction = trade.is_buy ? 'Big Buy' : 'Big Sell';
const volume = Math.round(trade.volume);
this.playAudio(`${direction} ${volume} lots!`);
}
processClusters(clusters, pushToast) {
if (!clusters || clusters.length === 0) return;
+115
View File
@@ -0,0 +1,115 @@
//+------------------------------------------------------------------+
//| YuCluster_Order_Router.mq5 |
//| Integração Python <-> CTrade |
//+------------------------------------------------------------------+
#property description "Roteador de Execução Institucional OCO"
#property version "1.00"
// 1. Incluindo a Biblioteca CTrade para facilitar o envio de ordens
#include <Trade\Trade.mqh>
// Instanciando o objeto de trade
CTrade trade;
//+------------------------------------------------------------------+
//| VARIÁVEIS DE ENTRADA (ISOLANDO O FATOR PSICOLÓGICO) |
//+------------------------------------------------------------------+
input group "=== Gestão de Risco OCO ==="
input double InpLote = 1.0; // Lote (Tamanho da Posição)
input int InpStopLoss = 150; // Stop Loss (em pontos)
input int InpTakeProfit = 300; // Take Profit (em pontos)
input group "=== Configurações do Robô ==="
input ulong InpMagicNumber = 123456; // Número Mágico (CPF do Robô)
//+------------------------------------------------------------------+
//| FUNÇÃO DE INICIALIZAÇÃO |
//+------------------------------------------------------------------+
int OnInit()
{
// Configura o Número Mágico para que o EA gerencie apenas as próprias ordens
trade.SetExpertMagicNumber(InpMagicNumber);
// Define o desvio máximo aceitável (Slippage)
trade.SetDeviationInPoints(5);
Print("Roteador Institucional Iniciado. Fator emocional desativado.");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| FUNÇÃO DE EXECUÇÃO DE COMPRA A MERCADO |
//+------------------------------------------------------------------+
void ExecutarCompra()
{
// Atualiza os dados de preço de mercado (Tick)
MqlTick tick;
SymbolInfoTick(_Symbol, tick);
// Calcula os níveis de Stop Loss e Take Profit engessados
double sl = tick.ask - (InpStopLoss * _Point);
double tp = tick.ask + (InpTakeProfit * _Point);
// Normaliza as casas decimais de acordo com o ativo (Tick Size)
sl = NormalizeDouble(sl, _Digits);
tp = NormalizeDouble(tp, _Digits);
// Envia a ordem a mercado instantaneamente
if(trade.Buy(InpLote, _Symbol, tick.ask, sl, tp, "YuCluster: Compra na POC"))
{
Print("Compra executada com sucesso! SL e TP na pedra.");
}
else
{
Print("Erro ao executar compra. Código: ", trade.ResultRetcode());
}
}
//+------------------------------------------------------------------+
//| FUNÇÃO DE EXECUÇÃO DE VENDA A MERCADO |
//+------------------------------------------------------------------+
void ExecutarVenda()
{
// Atualiza os dados de preço de mercado (Tick)
MqlTick tick;
SymbolInfoTick(_Symbol, tick);
// Calcula os níveis de Stop Loss e Take Profit engessados (invertido para venda)
double sl = tick.bid + (InpStopLoss * _Point);
double tp = tick.bid - (InpTakeProfit * _Point);
// Normaliza os cálculos
sl = NormalizeDouble(sl, _Digits);
tp = NormalizeDouble(tp, _Digits);
// Envia a ordem a mercado instantaneamente
if(trade.Sell(InpLote, _Symbol, tick.bid, sl, tp, "YuCluster: Venda no Imbalance"))
{
Print("Venda executada com sucesso! SL e TP na pedra.");
}
else
{
Print("Erro ao executar venda. Código: ", trade.ResultRetcode());
}
}
//+------------------------------------------------------------------+
//| LAÇO PRINCIPAL (Aguardando o gatilho do Python) |
//+------------------------------------------------------------------+
void OnTick()
{
// Aqui o EA fica "escutando" variáveis globais do terminal ou requisições
// que o nosso backend em Python enviará ao detectar o botão clicado no Painel Web.
if(GlobalVariableGet("SINAL_PYTHON_COMPRA") == 1)
{
ExecutarCompra();
GlobalVariableSet("SINAL_PYTHON_COMPRA", 0); // Reseta o sinal
}
if(GlobalVariableGet("SINAL_PYTHON_VENDA") == 1)
{
ExecutarVenda();
GlobalVariableSet("SINAL_PYTHON_VENDA", 0); // Reseta o sinal
}
}