algorembrant commited on
Commit
d8bad25
·
verified ·
1 Parent(s): 9fe53cc

Upload 31 files

Browse files
.gitignore ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ lib/
15
+ lib64/
16
+ parts/
17
+ sdist/
18
+ var/
19
+ wheels/
20
+ *.egg-info/
21
+ .installed.cfg
22
+ *.egg
23
+ .venv/
24
+ venv/
25
+ ENV/
26
+
27
+ # Node
28
+ node_modules/
29
+ npm-debug.log*
30
+ yarn-debug.log*
31
+ yarn-error.log*
32
+ .pnpm-debug.log*
33
+ .env-cmdrc
34
+ dist/
35
+
36
+ # Env
37
+ .env
38
+ .env.*
39
+ !.env.example
40
+
41
+ # IDE
42
+ .vscode/
43
+ !.vscode/settings.json
44
+ !.vscode/extensions.json
45
+ .idea/
46
+ *.swp
47
+ *.swo
48
+
49
+ # OS
50
+ .DS_Store
51
+ Thumbs.db
52
+
53
+ # Logs
54
+ *.log
55
+ logs/
56
+
57
+ # Test
58
+ .coverage
59
+ .pytest_cache/
60
+
61
+
62
+ backend/.env
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 callmerem
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <img width="1364" height="689" alt="image" src="https://github.com/user-attachments/assets/0544d213-c0a9-4c0f-9c7e-024823e8101c" />
2
+
3
+
4
+
5
+
6
+ ## Gemini 3 Flash AI Trading Platform - Walkthrough
7
+
8
+ This guide explains how to run and use the autonomous AI trading platform powered by Gemini 3 Flash.
9
+
10
+ ## Prerequisites
11
+ - Python 3.10+ (with .venv activated)
12
+ - Node.js 18+
13
+ - MetaTrader 5 Terminal (installed and running)
14
+
15
+ ## 1. Configuration
16
+
17
+ Ensure your .env file in backend/ has the correct credentials:
18
+
19
+ ```ini
20
+ MT5_LOGIN=YOUR_ID
21
+ MT5_PASSWORD=YOUR_PASSWORD
22
+ MT5_SERVER=YOUR_SERVER
23
+ GEMINI_API_KEY=YOUR_KEY
24
+ ACCOUNT_MODE=demo
25
+ ```
26
+
27
+ ## 2. Starting the Platform
28
+
29
+ **Backend (Python API + Agent)**
30
+
31
+ Open a terminal in `backend/` and run:
32
+
33
+ ```bash
34
+ # Activate venv if needed
35
+ .venv\Scripts\activate
36
+
37
+ #locate
38
+ cd backend
39
+
40
+ #launch backend
41
+ python -m uvicorn main:app --reload --port 8000 --host 0.0.0.0
42
+
43
+
44
+ ```
45
+
46
+ use backend because thats the directory name
47
+
48
+ > You should see: Uvicorn running on http://0.0.0.0:8000
49
+
50
+
51
+ **Frontend (User Interface)**
52
+
53
+ Open a new terminal in frontend/ and run:
54
+
55
+ ```bash
56
+ #locate
57
+ cd frontend_react
58
+
59
+ #launch frontend
60
+ npm run dev
61
+ ```
62
+
63
+ > You should see: Ready in ... at http://localhost:3000
64
+
65
+ ## 3. Using the Platform
66
+ Open the App: Navigate to http://localhost:3000.
67
+ Connection Status: Check the top header. You should see "CONNECTED" (green).
68
+ If "DISCONNECTED", ensure the backend is running and MT5 is open.
69
+ Start the Agent:
70
+ Click the START button in the "Gemini Agent" panel.
71
+ The AI reasoning sidebar will start streaming "thoughts" from Gemini 3 Flash.
72
+ The agent will analyze the custom chart ticks and candles.
73
+ Manual Trading:
74
+ Use the BUY / SELL buttons to place trades manually.
75
+ Set Volume, SL, and TP before trading.
76
+ Open positions appear in the bottom panel.
77
+ ## 4. Verification & Troubleshooting
78
+ Backend Health Check: Visit http://localhost:8000/api/health. It should return {"status": "online", "mt5_connected": true, ...}.
79
+ MT5 Connection: If the bridge fails to connect, ensure "Algo Trading" is enabled in MT5 and the credentials are correct. The backlog will show Connected to MT5.
80
+ Gemini API: If the agent is silent or errors, check your GEMINI_API_KEY.
STRUCTURE.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Project Structure
2
+
3
+ ```text
4
+ GeminiFlash-Trader/
5
+ ├── backend/
6
+ │ ├── .env.example
7
+ │ ├── agent.py
8
+ │ ├── main.py
9
+ │ ├── models.py
10
+ │ ├── mt5_mcp.py
11
+ │ ├── requirements.txt
12
+ │ └── ws_manager.py
13
+ ├── frontend_react/
14
+ │ ├── dist/
15
+ │ │ ├── assets/
16
+ │ │ │ ├── index-BBLjpDlg.js
17
+ │ │ │ └── index-KlXNi27O.css
18
+ │ │ ├── index.html
19
+ │ │ └── vite.svg
20
+ │ ├── public/
21
+ │ │ └── vite.svg
22
+ │ ├── src/
23
+ │ │ ├── assets/
24
+ │ │ │ └── react.svg
25
+ │ │ ├── components/
26
+ │ │ │ ├── AccountBar.jsx
27
+ │ │ │ ├── CandlestickChart.jsx
28
+ │ │ │ ├── PositionPanel.jsx
29
+ │ │ │ ├── ReasoningSidebar.jsx
30
+ │ │ │ └── TradeControls.jsx
31
+ │ │ ├── lib/
32
+ │ │ │ └── useWebSocket.js
33
+ │ │ ├── App.css
34
+ │ │ ├── App.jsx
35
+ │ │ ├── index.css
36
+ │ │ └── main.jsx
37
+ │ ├── .gitignore
38
+ │ ├── eslint.config.js
39
+ │ ├── index.html
40
+ │ ├── package-lock.json
41
+ │ ├── package.json
42
+ │ └── vite.config.js
43
+ ├── .gitignore
44
+ ├── LICENSE
45
+ ├── README.md
46
+ ├── TECHSTACK.md
47
+ └── verify_strict_mode.py
48
+ ```
TECHSTACK.md ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Techstack
2
+
3
+ Audit of **GeminiFlash-Trader** project files (excluding environment and cache):
4
+
5
+ | File Type | Count | Size (KB) |
6
+ | :--- | :--- | :--- |
7
+ | JSX (React) (.jsx) | 7 | 22.6 |
8
+ | Python (.py) | 6 | 34.2 |
9
+ | JavaScript (.js) | 4 | 206.9 |
10
+ | (no extension) | 3 | 1.9 |
11
+ | CSS (.css) | 3 | 28.0 |
12
+ | SVG Image (.svg) | 3 | 7.0 |
13
+ | HTML (.html) | 2 | 1.6 |
14
+ | JSON (.json) | 2 | 98.6 |
15
+ | EXAMPLE (.example) | 1 | 0.4 |
16
+ | Markdown (.md) | 1 | 2.2 |
17
+ | Plain Text (.txt) | 1 | 0.1 |
18
+ | **Total** | **33** | **403.5** |
backend/.env.example ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MetaTrader 5 Credentials
2
+ MT5_LOGIN=your_number
3
+ MT5_PASSWORD=your_password
4
+ MT5_SERVER=your_server
5
+ MT5_PATH=C:\Program Files\MetaTrader 5\terminal64_or_whatever_path
6
+
7
+ # Account Mode: "demo" or "live"
8
+ ACCOUNT_MODE=demo
9
+
10
+ # Gemini API Key
11
+ GEMINI_API_KEY=your_gemini_api_key
12
+
13
+ # Trading Settings, this deppends on the symbols of your broker, im using Exness in my case
14
+ TRADING_SYMBOL=XAUUSDm
15
+ DEFAULT_VOLUME=0.01
16
+ AGENT_INTERVAL_SECONDS=60
backend/agent.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gemini 3 Flash Trading Agent — the 'Antigravity Trader'.
3
+ Analyzes XAUUSDc market data and makes autonomous trading decisions.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ from datetime import datetime
9
+ from typing import Optional
10
+ from dotenv import load_dotenv
11
+
12
+ load_dotenv()
13
+
14
+ # Try importing Google GenAI
15
+ try:
16
+ from google import genai
17
+ from google.genai import types
18
+ GENAI_AVAILABLE = True
19
+ except ImportError:
20
+ GENAI_AVAILABLE = False
21
+ print("[Agent] google-genai not available — running with MOCK agent")
22
+
23
+
24
+ SYSTEM_PROMPT = """You are **Antigravity Trader**, an elite AI trading agent specializing in XAUUSDc (Gold vs USD) trading on MetaTrader 5.
25
+
26
+ ## Your Role
27
+ You analyze real-time market data and make precise trading decisions. You run autonomously, making buy/sell/hold decisions based on price action, candlestick patterns, and market structure.
28
+
29
+ ## Your Trading Rules
30
+ 1. **Risk Management First**: Never risk more than 2% of account balance per trade.
31
+ 2. **Always set SL/TP**: Stop Loss and Take Profit are mandatory. Minimum SL: 50 pips from entry. TP should be at least 1.5x the SL distance (risk-reward ratio).
32
+ 3. **One position at a time**: Don't open a new position if one is already open. You can CLOSE an existing position or HOLD.
33
+ 4. **Market Structure**: Look for support/resistance, trend direction, and key price levels in the candle data.
34
+ 5. **Confidence threshold**: Only trade with confidence >= 0.7. Below that, HOLD or DO_NOTHING.
35
+
36
+ ## Input Data
37
+ You will receive:
38
+ - **Recent candles**: OHLCV data (most recent candles for the timeframe)
39
+ - **Current tick**: Latest bid/ask prices
40
+ - **Account info**: Balance, equity, margin, profit
41
+ - **Open positions**: Currently held positions (if any)
42
+
43
+ ## Output Format
44
+ You MUST respond with ONLY valid JSON (no markdown, no extra text):
45
+ {
46
+ "action": "BUY" | "SELL" | "CLOSE" | "HOLD" | "DO_NOTHING",
47
+ "reasoning": "Your detailed analysis explaining WHY you made this decision. Include what patterns you see, key levels, and your risk assessment.",
48
+ "confidence": 0.0 to 1.0,
49
+ "sl": null or price level for stop loss,
50
+ "tp": null or price level for take profit,
51
+ "volume": null or lot size (e.g. 0.01)
52
+ }
53
+
54
+ ## Action Definitions
55
+ - **BUY**: Open a long position (you expect price to go UP)
56
+ - **SELL**: Open a short position (you expect price to go DOWN)
57
+ - **CLOSE**: Close the current open position
58
+ - **HOLD**: Keep the current position open, no changes
59
+ - **DO_NOTHING**: No position open and no good setup — wait
60
+ """
61
+
62
+
63
+ class TradingAgent:
64
+ """Gemini 3 Flash Agent for autonomous XAUUSDc trading."""
65
+
66
+ def __init__(self):
67
+ self.api_key = os.getenv("GEMINI_API_KEY", "")
68
+ self.model_name = "gemini-2.5-flash"
69
+ self.client = None
70
+ self.decision_history: list[dict] = []
71
+
72
+ if GENAI_AVAILABLE and self.api_key:
73
+ self.client = genai.Client(api_key=self.api_key)
74
+ print(f"[Agent] Gemini client initialized with model: {self.model_name}")
75
+ else:
76
+ print("[Agent] No Gemini API key — using mock decisions")
77
+
78
+ async def analyze(self, candles: list, tick: dict, account: dict,
79
+ positions: list) -> dict:
80
+ """
81
+ Analyze market data and return a trading decision.
82
+ Returns: { action, reasoning, confidence, sl, tp, volume }
83
+ """
84
+ if not self.client:
85
+ # In strict usage, we simply return DO_NOTHING if no AI is available
86
+ return {
87
+ "action": "DO_NOTHING",
88
+ "reasoning": "Gemini API client not initialized (check keys)",
89
+ "confidence": 0.0,
90
+ "sl": None, "tp": None, "volume": None
91
+ }
92
+
93
+ # Build the market context prompt
94
+ recent_candles = candles[-20:] if len(candles) > 20 else candles
95
+ candle_text = self._format_candles(recent_candles)
96
+
97
+ prompt = f"""## Current Market State — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
98
+
99
+ ### Latest Tick
100
+ Bid: {tick.get('bid', 'N/A')} | Ask: {tick.get('ask', 'N/A')}
101
+
102
+ ### Recent Candles (OHLCV, most recent last)
103
+ {candle_text}
104
+
105
+ ### Account Status
106
+ Balance: ${account.get('balance', 0):.2f}
107
+ Equity: ${account.get('equity', 0):.2f}
108
+ Free Margin: ${account.get('free_margin', 0):.2f}
109
+ Current P&L: ${account.get('profit', 0):.2f}
110
+
111
+ ### Open Positions
112
+ {self._format_positions(positions)}
113
+
114
+ ### Recent Decision History
115
+ {self._format_history()}
116
+
117
+ Analyze the market and make your trading decision now."""
118
+
119
+ try:
120
+ response = self.client.models.generate_content(
121
+ model=self.model_name,
122
+ contents=prompt,
123
+ config=types.GenerateContentConfig(
124
+ system_instruction=SYSTEM_PROMPT,
125
+ temperature=0.3,
126
+ max_output_tokens=8192,
127
+ response_mime_type="application/json",
128
+ )
129
+ )
130
+
131
+ response_text = response.text.strip()
132
+ decision = json.loads(response_text)
133
+
134
+ # Validate required fields
135
+ decision.setdefault("action", "DO_NOTHING")
136
+ decision.setdefault("reasoning", "No reasoning provided")
137
+ decision.setdefault("confidence", 0.5)
138
+ decision.setdefault("sl", None)
139
+ decision.setdefault("tp", None)
140
+ decision.setdefault("volume", float(os.getenv("DEFAULT_VOLUME", "0.01")))
141
+
142
+ # Save to history
143
+ self.decision_history.append({
144
+ "time": datetime.now().isoformat(),
145
+ "action": decision["action"],
146
+ "confidence": decision["confidence"],
147
+ })
148
+ if len(self.decision_history) > 50:
149
+ self.decision_history = self.decision_history[-50:]
150
+
151
+ return decision
152
+
153
+ except json.JSONDecodeError as e:
154
+ return {
155
+ "action": "DO_NOTHING",
156
+ "reasoning": f"Failed to parse agent response: {str(e)}. Raw: {response_text[:200]}",
157
+ "confidence": 0.0,
158
+ "sl": None, "tp": None, "volume": None
159
+ }
160
+ except Exception as e:
161
+ return {
162
+ "action": "DO_NOTHING",
163
+ "reasoning": f"Agent error: {str(e)}",
164
+ "confidence": 0.0,
165
+ "sl": None, "tp": None, "volume": None
166
+ }
167
+
168
+ # Mock decision method removed for production/strict mode safety
169
+
170
+ def _format_candles(self, candles: list) -> str:
171
+ """Format candle data as a readable table."""
172
+ lines = ["Time | Open | High | Low | Close | Volume"]
173
+ for c in candles:
174
+ t = datetime.fromtimestamp(c["time"]).strftime("%H:%M")
175
+ lines.append(f"{t} | {c['open']:.2f} | {c['high']:.2f} | {c['low']:.2f} | {c['close']:.2f} | {c['volume']}")
176
+ return "\n".join(lines)
177
+
178
+ def _format_positions(self, positions: list) -> str:
179
+ if not positions:
180
+ return "No open positions."
181
+ lines = []
182
+ for p in positions:
183
+ lines.append(
184
+ f"Ticket #{p['ticket']}: {p['type'].upper()} {p['volume']} lots @ {p['price_open']:.2f} "
185
+ f"→ Current: {p['price_current']:.2f} | P&L: ${p['profit']:.2f} | SL: {p['sl']} | TP: {p['tp']}"
186
+ )
187
+ return "\n".join(lines)
188
+
189
+ def _format_history(self) -> str:
190
+ if not self.decision_history:
191
+ return "No previous decisions."
192
+ recent = self.decision_history[-5:]
193
+ lines = []
194
+ for d in recent:
195
+ lines.append(f"{d['time']}: {d['action']} (conf: {d['confidence']:.1%})")
196
+ return "\n".join(lines)
backend/main.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI Server for Gemini 3 Flash AI Trading Platform.
3
+ Connects the MCP MT5 bridge, Gemini Agent, and Next.js frontend via WebSockets.
4
+ """
5
+
6
+ import os
7
+ import asyncio
8
+ import json
9
+ from contextlib import asynccontextmanager
10
+ from typing import List, Optional
11
+ from datetime import datetime
12
+
13
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, BackgroundTasks
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from dotenv import load_dotenv
16
+
17
+ from models import (
18
+ CandleData, TickData, PositionInfo, AccountInfo, AgentDecision,
19
+ TradeRequest, WSMessage
20
+ )
21
+ from mt5_mcp import MT5Bridge
22
+ from agent import TradingAgent
23
+ from ws_manager import ConnectionManager
24
+
25
+ load_dotenv()
26
+
27
+ # Initialize components
28
+ mt5_bridge = MT5Bridge()
29
+ agent = TradingAgent()
30
+ ws_manager = ConnectionManager()
31
+
32
+ # Global state
33
+ agent_running = False
34
+ background_task_handle = None
35
+
36
+
37
+ @asynccontextmanager
38
+ async def lifespan(app: FastAPI):
39
+ """Lifecycle events: connect details on startup, cleanup on shutdown."""
40
+ print("[Main] Starting up...")
41
+
42
+ # 1. Connect to MT5
43
+ result = mt5_bridge.initialize()
44
+ if not result["success"]:
45
+ print(f"[Main] SEVERE: {result['message']}")
46
+ # In strict mode, we should ideally stop here
47
+ if os.getenv("FORCE_MT5_DATA", "false").lower() == "true":
48
+ raise RuntimeError(f"STRICT MODE: Failed to connect to MT5. {result['message']}")
49
+ else:
50
+ print(f"[Main] {result['message']}")
51
+
52
+ # 2. Start background data loop (non-blocking)
53
+ asyncio.create_task(market_data_loop())
54
+
55
+ yield
56
+
57
+ print("[Main] Shutting down...")
58
+ mt5_bridge.shutdown()
59
+
60
+
61
+ app = FastAPI(title="Gemini 3 Flash Trader", lifespan=lifespan)
62
+
63
+ # CORS middleware
64
+ app.add_middleware(
65
+ CORSMiddleware,
66
+ allow_origins=["*"], # In production, specify frontend URL
67
+ allow_credentials=True,
68
+ allow_methods=["*"],
69
+ allow_headers=["*"],
70
+ )
71
+
72
+
73
+ # ── REST API ────────────────────────────────────────────────
74
+
75
+ @app.get("/api/health")
76
+ async def health_check():
77
+ """Check system health and connection status."""
78
+ return {
79
+ "status": "online",
80
+ "mt5_connected": mt5_bridge.connected,
81
+ "agent_running": agent_running,
82
+ "mode": "simulation" if mt5_bridge.simulation_mode else "live"
83
+ }
84
+
85
+
86
+ @app.get("/api/account", response_model=AccountInfo)
87
+ async def get_account():
88
+ """Get current account information."""
89
+ info = mt5_bridge.get_account_info()
90
+ if "error" in info:
91
+ raise HTTPException(status_code=500, detail=info["error"])
92
+ return info
93
+
94
+
95
+ @app.get("/api/positions", response_model=List[PositionInfo])
96
+ async def get_positions():
97
+ """Get all open positions."""
98
+ return mt5_bridge.get_positions()
99
+
100
+
101
+ @app.get("/api/candles", response_model=List[CandleData])
102
+ async def get_candles(timeframe: str = "M5", count: int = 200):
103
+ """Get historical candle data."""
104
+ return mt5_bridge.get_rates(timeframe=timeframe, count=count)
105
+
106
+
107
+ @app.post("/api/trade")
108
+ async def execute_trade(trade: TradeRequest):
109
+ """Execute a manual trade."""
110
+ if trade.action == "close":
111
+ if not trade.ticket:
112
+ raise HTTPException(status_code=400, detail="Ticket required for close")
113
+ result = mt5_bridge.close_position(trade.ticket)
114
+ else:
115
+ result = mt5_bridge.place_order(
116
+ action=trade.action,
117
+ symbol=trade.symbol,
118
+ volume=trade.volume,
119
+ sl=trade.sl or 0.0,
120
+ tp=trade.tp or 0.0
121
+ )
122
+
123
+ if not result.get("success"):
124
+ raise HTTPException(status_code=400, detail=result.get("message"))
125
+
126
+ # Broadcast trade event
127
+ await ws_manager.broadcast("trade_event", result)
128
+ return result
129
+
130
+
131
+ @app.post("/api/agent/toggle")
132
+ async def toggle_agent(enable: bool):
133
+ """Start or stop the autonomous trading agent."""
134
+ global agent_running
135
+ agent_running = enable
136
+ status = "running" if agent_running else "stopped"
137
+ print(f"[Main] Agent {status}")
138
+ await ws_manager.broadcast("agent_status", {"status": status})
139
+ return {"status": status}
140
+
141
+
142
+ # ── WebSocket ───────────────────────────────────────────────
143
+
144
+ @app.websocket("/ws")
145
+ async def websocket_endpoint(websocket: WebSocket):
146
+ await ws_manager.connect(websocket)
147
+ try:
148
+ # Send initial state
149
+ await ws_manager.send_personal(websocket, "status", {
150
+ "mt5": mt5_bridge.connected,
151
+ "agent": agent_running
152
+ })
153
+
154
+ while True:
155
+ # Keep connection alive + handle incoming messages (e.g. ping)
156
+ data = await websocket.receive_text()
157
+ # Parse if needed, currently we just listen
158
+ except WebSocketDisconnect:
159
+ ws_manager.disconnect(websocket)
160
+
161
+
162
+ # ── Background Loops ────────────────────────────────────────
163
+
164
+ async def market_data_loop():
165
+ """
166
+ Main loop:
167
+ 1. Stream prices (candles/ticks) to frontend
168
+ 2. Invoke Agent if enabled
169
+ """
170
+ print("[Main] Market data loop started")
171
+ last_candle_time = 0
172
+
173
+ while True:
174
+ try:
175
+ # 1. Fetch & Broadcast Market Data
176
+ current_tick = mt5_bridge.get_tick()
177
+ candles = mt5_bridge.get_rates(count=2) # Just need latest for updates
178
+
179
+ if candles:
180
+ latest_candle = candles[-1]
181
+ await ws_manager.broadcast("candle_update", latest_candle)
182
+
183
+ if current_tick:
184
+ await ws_manager.broadcast("tick_update", current_tick)
185
+
186
+ # 2. Agent Logic (if running)
187
+ if agent_running:
188
+ # Run agent roughly every new candle or every N seconds
189
+ # For this demo, we'll run it every 10s to see activity
190
+ if int(datetime.now().timestamp()) % 10 == 0:
191
+ await run_agent_cycle(current_tick)
192
+
193
+ # 3. Broadcast Account/Positions periodically
194
+ if int(datetime.now().timestamp()) % 2 == 0:
195
+ positions = mt5_bridge.get_positions()
196
+ account = mt5_bridge.get_account_info()
197
+ await ws_manager.broadcast("positions", positions)
198
+ await ws_manager.broadcast("account", account)
199
+
200
+ await asyncio.sleep(1) # 1Hz update rate
201
+
202
+ except Exception as e:
203
+ print(f"[Loop Error] {e}")
204
+ await asyncio.sleep(5)
205
+
206
+
207
+ async def run_agent_cycle(tick: dict):
208
+ """One cycle of the AI agent: Analyze -> Reason -> Act."""
209
+ print("[Agent] Analyzing market...")
210
+
211
+ # Gather context
212
+ candles = mt5_bridge.get_rates(count=50) # Give agent more history
213
+ positions = mt5_bridge.get_positions()
214
+ account = mt5_bridge.get_account_info()
215
+
216
+ # 1. Ask Gemini
217
+ decision = await agent.analyze(candles, tick, account, positions)
218
+
219
+ # 2. Broadcast Reasoning
220
+ await ws_manager.broadcast("reasoning", {
221
+ "action": decision["action"],
222
+ "reasoning": decision["reasoning"],
223
+ "confidence": decision["confidence"],
224
+ "timestamp": datetime.now().isoformat()
225
+ })
226
+
227
+ # 3. Execute Decision
228
+ if decision["action"] in ["BUY", "SELL"]:
229
+ # Check confidence threshold
230
+ if decision["confidence"] >= 0.7:
231
+ print(f"[Agent] Executing {decision['action']} ({decision['confidence']})")
232
+ result = mt5_bridge.place_order(
233
+ action=decision["action"],
234
+ volume=decision.get("volume", 0.01),
235
+ sl=decision.get("sl", 0.0),
236
+ tp=decision.get("tp", 0.0)
237
+ )
238
+ await ws_manager.broadcast("trade_event", result)
239
+ elif decision["action"] == "CLOSE":
240
+ # Close all relevant positions
241
+ for p in positions:
242
+ res = mt5_bridge.close_position(p["ticket"])
243
+ await ws_manager.broadcast("trade_event", res)
244
+
245
+
246
+ if __name__ == "__main__":
247
+ import uvicorn
248
+ uvicorn.run(app, host="0.0.0.0", port=8000)
backend/models.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pydantic data models for the Gemini 3 Flash AI Trading Platform.
3
+ """
4
+
5
+ from pydantic import BaseModel, Field
6
+ from typing import Optional, Literal
7
+ from datetime import datetime
8
+ from enum import Enum
9
+
10
+
11
+ class TradeAction(str, Enum):
12
+ BUY = "BUY"
13
+ SELL = "SELL"
14
+ CLOSE = "CLOSE"
15
+ HOLD = "HOLD"
16
+ DO_NOTHING = "DO_NOTHING"
17
+
18
+
19
+ class CandleData(BaseModel):
20
+ time: int
21
+ open: float
22
+ high: float
23
+ low: float
24
+ close: float
25
+ volume: float
26
+
27
+
28
+ class TickData(BaseModel):
29
+ bid: float
30
+ ask: float
31
+ time: int
32
+ symbol: str
33
+
34
+
35
+ class PositionInfo(BaseModel):
36
+ ticket: int
37
+ symbol: str
38
+ type: str # "buy" or "sell"
39
+ volume: float
40
+ price_open: float
41
+ price_current: float
42
+ sl: float
43
+ tp: float
44
+ profit: float
45
+ time: int
46
+
47
+
48
+ class AccountInfo(BaseModel):
49
+ login: int
50
+ balance: float
51
+ equity: float
52
+ margin: float
53
+ free_margin: float
54
+ margin_level: Optional[float] = None
55
+ profit: float
56
+ server: str
57
+ currency: str
58
+ trade_mode: str # "demo" or "live"
59
+
60
+
61
+ class AgentDecision(BaseModel):
62
+ action: TradeAction
63
+ reasoning: str
64
+ confidence: float = Field(ge=0.0, le=1.0)
65
+ sl: Optional[float] = None
66
+ tp: Optional[float] = None
67
+ volume: Optional[float] = None
68
+
69
+
70
+ class TradeRequest(BaseModel):
71
+ action: Literal["buy", "sell", "close"]
72
+ symbol: str = "XAUUSDc"
73
+ volume: float = 0.01
74
+ sl: Optional[float] = None
75
+ tp: Optional[float] = None
76
+ ticket: Optional[int] = None # for closing specific position
77
+
78
+
79
+ class WSMessage(BaseModel):
80
+ type: str # "candles", "tick", "reasoning", "trade_event", "positions", "account", "agent_status"
81
+ data: dict
82
+ timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
backend/mt5_mcp.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MT5 MCP Bridge — wraps the MetaTrader5 Python SDK into clean tool functions
3
+ for the AI agent to call. Supports both demo and live accounts.
4
+ Falls back to simulated data when MT5 is not available.
5
+ """
6
+
7
+ import os
8
+ import time
9
+ import random
10
+ import math
11
+ from datetime import datetime, timedelta
12
+ from typing import Optional
13
+ from dotenv import load_dotenv
14
+
15
+ load_dotenv()
16
+
17
+ # Try to import MetaTrader5; if unavailable, use simulation mode
18
+ try:
19
+ import MetaTrader5 as mt5
20
+ MT5_AVAILABLE = True
21
+ except ImportError:
22
+ MT5_AVAILABLE = False
23
+ if os.getenv("FORCE_MT5_DATA", "false").lower() == "true":
24
+ raise ImportError("CRITICAL: MetaTrader5 package not found and FORCE_MT5_DATA=true")
25
+ print("[MT5] MetaTrader5 package not available — running in SIMULATION mode")
26
+
27
+ try:
28
+ import pandas as pd
29
+ PANDAS_AVAILABLE = True
30
+ except ImportError:
31
+ PANDAS_AVAILABLE = False
32
+
33
+
34
+ class MT5Bridge:
35
+ """Bridge to MetaTrader 5 via the official Python SDK."""
36
+
37
+ TIMEFRAME_MAP = {
38
+ "M1": mt5.TIMEFRAME_M1 if MT5_AVAILABLE else 1,
39
+ "M5": mt5.TIMEFRAME_M5 if MT5_AVAILABLE else 5,
40
+ "M15": mt5.TIMEFRAME_M15 if MT5_AVAILABLE else 15,
41
+ "M30": mt5.TIMEFRAME_M30 if MT5_AVAILABLE else 30,
42
+ "H1": mt5.TIMEFRAME_H1 if MT5_AVAILABLE else 60,
43
+ "H4": mt5.TIMEFRAME_H4 if MT5_AVAILABLE else 240,
44
+ "D1": mt5.TIMEFRAME_D1 if MT5_AVAILABLE else 1440,
45
+ }
46
+
47
+ def __init__(self):
48
+ self.connected = False
49
+ self.simulation_mode = not MT5_AVAILABLE
50
+ self.symbol = os.getenv("TRADING_SYMBOL", "XAUUSDm")
51
+ self._sim_base_price = 2650.0
52
+ self._sim_positions = []
53
+ self._sim_ticket_counter = 1000
54
+
55
+ def initialize(self) -> dict:
56
+ """Connect to the MT5 terminal."""
57
+ if self.simulation_mode:
58
+ if os.getenv("FORCE_MT5_DATA", "false").lower() == "true":
59
+ return {
60
+ "success": False,
61
+ "message": "CRITICAL: MT5 not available but FORCE_MT5_DATA is true. Exiting."
62
+ }
63
+ self.connected = True
64
+ return {
65
+ "success": True,
66
+ "message": "Running in SIMULATION mode (MT5 not available)",
67
+ "mode": "simulation"
68
+ }
69
+
70
+ mt5_path = os.getenv("MT5_PATH")
71
+ login = int(os.getenv("MT5_LOGIN", "0"))
72
+ password = os.getenv("MT5_PASSWORD", "")
73
+ server = os.getenv("MT5_SERVER", "")
74
+
75
+ init_kwargs = {}
76
+ if mt5_path:
77
+ init_kwargs["path"] = mt5_path
78
+
79
+ if not mt5.initialize(**init_kwargs):
80
+ err_code = mt5.last_error()
81
+ if os.getenv("FORCE_MT5_DATA", "false").lower() == "true":
82
+ return {"success": False, "message": f"CRITICAL: MT5 init failed: {err_code}"}
83
+ return {"success": False, "message": f"MT5 init failed: {err_code}"}
84
+
85
+ if login and password and server:
86
+ authorized = mt5.login(login=login, password=password, server=server)
87
+ if not authorized:
88
+ return {"success": False, "message": f"MT5 login failed: {mt5.last_error()}"}
89
+
90
+ self.connected = True
91
+ account = mt5.account_info()
92
+ mode = "demo" if account.trade_mode == 0 else "live"
93
+ return {
94
+ "success": True,
95
+ "message": f"Connected to MT5 ({mode}) — Account #{account.login}",
96
+ "mode": mode
97
+ }
98
+
99
+ def shutdown(self):
100
+ """Disconnect from MT5."""
101
+ if not self.simulation_mode and MT5_AVAILABLE:
102
+ mt5.shutdown()
103
+ self.connected = False
104
+
105
+ def get_account_info(self) -> dict:
106
+ """Get trading account information."""
107
+ if self.simulation_mode:
108
+ return {
109
+ "login": 12345678,
110
+ "balance": 10000.00,
111
+ "equity": 10000.00 + sum(p.get("profit", 0) for p in self._sim_positions),
112
+ "margin": sum(p.get("volume", 0) * 1000 for p in self._sim_positions),
113
+ "free_margin": 10000.00 - sum(p.get("volume", 0) * 1000 for p in self._sim_positions),
114
+ "margin_level": 0.0,
115
+ "profit": sum(p.get("profit", 0) for p in self._sim_positions),
116
+ "server": "SimulationServer",
117
+ "currency": "USD",
118
+ "trade_mode": os.getenv("ACCOUNT_MODE", "demo")
119
+ }
120
+
121
+ account = mt5.account_info()
122
+ if account is None:
123
+ return {"error": "Failed to get account info"}
124
+
125
+ return {
126
+ "login": account.login,
127
+ "balance": account.balance,
128
+ "equity": account.equity,
129
+ "margin": account.margin,
130
+ "free_margin": account.margin_free,
131
+ "margin_level": account.margin_level,
132
+ "profit": account.profit,
133
+ "server": account.server,
134
+ "currency": account.currency,
135
+ "trade_mode": "demo" if account.trade_mode == 0 else "live"
136
+ }
137
+
138
+ def get_rates(self, symbol: Optional[str] = None, timeframe: str = "M5", count: int = 200) -> list:
139
+ """Fetch OHLCV candle data."""
140
+ symbol = symbol or self.symbol
141
+
142
+ if self.simulation_mode:
143
+ return self._simulate_candles(count, timeframe)
144
+
145
+ tf = self.TIMEFRAME_MAP.get(timeframe, mt5.TIMEFRAME_M5)
146
+ rates = mt5.copy_rates_from_pos(symbol, tf, 0, count)
147
+
148
+ if rates is None or len(rates) == 0:
149
+ return []
150
+
151
+ candles = []
152
+ for r in rates:
153
+ candles.append({
154
+ "time": int(r["time"]),
155
+ "open": float(r["open"]),
156
+ "high": float(r["high"]),
157
+ "low": float(r["low"]),
158
+ "close": float(r["close"]),
159
+ "volume": float(r["tick_volume"]),
160
+ })
161
+ return candles
162
+
163
+ def get_tick(self, symbol: Optional[str] = None) -> dict:
164
+ """Get latest tick (bid/ask)."""
165
+ symbol = symbol or self.symbol
166
+
167
+ if self.simulation_mode:
168
+ price = self._sim_base_price + random.uniform(-5, 5)
169
+ spread = random.uniform(0.1, 0.5)
170
+ return {
171
+ "bid": round(price, 2),
172
+ "ask": round(price + spread, 2),
173
+ "time": int(time.time()),
174
+ "symbol": symbol
175
+ }
176
+
177
+ tick = mt5.symbol_info_tick(symbol)
178
+ if tick is None:
179
+ return {"error": f"No tick data for {symbol}"}
180
+
181
+ return {
182
+ "bid": tick.bid,
183
+ "ask": tick.ask,
184
+ "time": tick.time,
185
+ "symbol": symbol
186
+ }
187
+
188
+ def get_positions(self, symbol: Optional[str] = None) -> list:
189
+ """List open positions."""
190
+ symbol = symbol or self.symbol
191
+
192
+ if self.simulation_mode:
193
+ tick = self.get_tick(symbol)
194
+ for p in self._sim_positions:
195
+ if p["type"] == "buy":
196
+ p["price_current"] = tick["bid"]
197
+ p["profit"] = round((tick["bid"] - p["price_open"]) * p["volume"] * 100, 2)
198
+ else:
199
+ p["price_current"] = tick["ask"]
200
+ p["profit"] = round((p["price_open"] - tick["ask"]) * p["volume"] * 100, 2)
201
+ return self._sim_positions
202
+
203
+ positions = mt5.positions_get(symbol=symbol)
204
+ if positions is None:
205
+ return []
206
+
207
+ result = []
208
+ for p in positions:
209
+ result.append({
210
+ "ticket": p.ticket,
211
+ "symbol": p.symbol,
212
+ "type": "buy" if p.type == 0 else "sell",
213
+ "volume": p.volume,
214
+ "price_open": p.price_open,
215
+ "price_current": p.price_current,
216
+ "sl": p.sl,
217
+ "tp": p.tp,
218
+ "profit": p.profit,
219
+ "time": p.time,
220
+ })
221
+ return result
222
+
223
+ def place_order(self, action: str, symbol: Optional[str] = None,
224
+ volume: float = 0.01, sl: float = 0.0, tp: float = 0.0) -> dict:
225
+ """Place a market order (buy or sell)."""
226
+ symbol = symbol or self.symbol
227
+
228
+ if self.simulation_mode:
229
+ if os.getenv("FORCE_MT5_DATA", "false").lower() == "true":
230
+ return {"success": False, "message": "Cannot place mock order in STRICT MT5 mode."}
231
+
232
+ tick = self.get_tick(symbol)
233
+ price = tick["ask"] if action.lower() == "buy" else tick["bid"]
234
+ self._sim_ticket_counter += 1
235
+ pos = {
236
+ "ticket": self._sim_ticket_counter,
237
+ "symbol": symbol,
238
+ "type": action.lower(),
239
+ "volume": volume,
240
+ "price_open": price,
241
+ "price_current": price,
242
+ "sl": sl,
243
+ "tp": tp,
244
+ "profit": 0.0,
245
+ "time": int(time.time()),
246
+ }
247
+ self._sim_positions.append(pos)
248
+ return {"success": True, "ticket": pos["ticket"], "price": price, "action": action}
249
+
250
+ # Real MT5 order
251
+ tick = mt5.symbol_info_tick(symbol)
252
+ if tick is None:
253
+ return {"success": False, "message": f"Cannot get tick for {symbol}"}
254
+
255
+ order_type = mt5.ORDER_TYPE_BUY if action.lower() == "buy" else mt5.ORDER_TYPE_SELL
256
+ price = tick.ask if action.lower() == "buy" else tick.bid
257
+
258
+ request = {
259
+ "action": mt5.TRADE_ACTION_DEAL,
260
+ "symbol": symbol,
261
+ "volume": volume,
262
+ "type": order_type,
263
+ "price": price,
264
+ "deviation": 20,
265
+ "magic": 3000000,
266
+ "comment": "Gemini3Flash-Agent",
267
+ "type_time": mt5.ORDER_TIME_GTC,
268
+ "type_filling": mt5.ORDER_FILLING_IOC,
269
+ }
270
+
271
+ if sl > 0:
272
+ request["sl"] = sl
273
+ if tp > 0:
274
+ request["tp"] = tp
275
+
276
+ result = mt5.order_send(request)
277
+ if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
278
+ error_msg = result.comment if result else "Unknown error"
279
+ return {"success": False, "message": error_msg}
280
+
281
+ return {
282
+ "success": True,
283
+ "ticket": result.order,
284
+ "price": result.price,
285
+ "action": action
286
+ }
287
+
288
+ def close_position(self, ticket: int) -> dict:
289
+ """Close a specific position by ticket."""
290
+ if self.simulation_mode:
291
+ for i, p in enumerate(self._sim_positions):
292
+ if p["ticket"] == ticket:
293
+ closed = self._sim_positions.pop(i)
294
+ return {"success": True, "ticket": ticket, "profit": closed["profit"]}
295
+ return {"success": False, "message": f"Position {ticket} not found"}
296
+
297
+ positions = mt5.positions_get(ticket=ticket)
298
+ if not positions:
299
+ return {"success": False, "message": f"Position {ticket} not found"}
300
+
301
+ pos = positions[0]
302
+ close_type = mt5.ORDER_TYPE_SELL if pos.type == 0 else mt5.ORDER_TYPE_BUY
303
+ tick = mt5.symbol_info_tick(pos.symbol)
304
+ price = tick.bid if pos.type == 0 else tick.ask
305
+
306
+ request = {
307
+ "action": mt5.TRADE_ACTION_DEAL,
308
+ "symbol": pos.symbol,
309
+ "volume": pos.volume,
310
+ "type": close_type,
311
+ "position": ticket,
312
+ "price": price,
313
+ "deviation": 20,
314
+ "magic": 3000000,
315
+ "comment": "Gemini3Flash-Close",
316
+ "type_time": mt5.ORDER_TIME_GTC,
317
+ "type_filling": mt5.ORDER_FILLING_IOC,
318
+ }
319
+
320
+ result = mt5.order_send(request)
321
+ if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
322
+ error_msg = result.comment if result else "Unknown error"
323
+ return {"success": False, "message": error_msg}
324
+
325
+ return {"success": True, "ticket": ticket, "profit": pos.profit}
326
+
327
+ # ── Simulation helpers ──────────────────────────────────────────
328
+
329
+ def _simulate_candles(self, count: int, timeframe: str = "M5") -> list:
330
+ """Generate realistic-looking simulated gold candle data."""
331
+ tf_minutes = {
332
+ "M1": 1, "M5": 5, "M15": 15, "M30": 30,
333
+ "H1": 60, "H4": 240, "D1": 1440
334
+ }.get(timeframe, 5)
335
+
336
+ candles = []
337
+ now = int(time.time())
338
+ price = self._sim_base_price
339
+
340
+ for i in range(count):
341
+ t = now - (count - i) * tf_minutes * 60
342
+ change = random.gauss(0, 2.5)
343
+ o = round(price, 2)
344
+ c = round(price + change, 2)
345
+ h = round(max(o, c) + abs(random.gauss(0, 1.5)), 2)
346
+ low = round(min(o, c) - abs(random.gauss(0, 1.5)), 2)
347
+ vol = random.randint(50, 500)
348
+
349
+ candles.append({
350
+ "time": t,
351
+ "open": o,
352
+ "high": h,
353
+ "low": low,
354
+ "close": c,
355
+ "volume": vol,
356
+ })
357
+ price = c
358
+ self._sim_base_price = c
359
+
360
+ return candles
backend/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ websockets
4
+ MetaTrader5
5
+ google-genai
6
+ python-dotenv
7
+ pydantic
8
+ pandas
backend/ws_manager.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ WebSocket connection manager for broadcasting real-time data
3
+ to all connected frontend clients.
4
+ """
5
+
6
+ import json
7
+ import asyncio
8
+ from fastapi import WebSocket
9
+ from datetime import datetime
10
+
11
+
12
+ class ConnectionManager:
13
+ """Manages WebSocket connections and broadcasts messages."""
14
+
15
+ def __init__(self):
16
+ self.active_connections: list[WebSocket] = []
17
+
18
+ async def connect(self, websocket: WebSocket):
19
+ await websocket.accept()
20
+ self.active_connections.append(websocket)
21
+ print(f"[WS] Client connected. Total: {len(self.active_connections)}")
22
+
23
+ def disconnect(self, websocket: WebSocket):
24
+ if websocket in self.active_connections:
25
+ self.active_connections.remove(websocket)
26
+ print(f"[WS] Client disconnected. Total: {len(self.active_connections)}")
27
+
28
+ async def broadcast(self, message_type: str, data: dict):
29
+ """Broadcast a message to all connected clients."""
30
+ message = json.dumps({
31
+ "type": message_type,
32
+ "data": data,
33
+ "timestamp": datetime.now().isoformat()
34
+ })
35
+
36
+ disconnected = []
37
+ for connection in self.active_connections:
38
+ try:
39
+ await connection.send_text(message)
40
+ except Exception:
41
+ disconnected.append(connection)
42
+
43
+ for conn in disconnected:
44
+ self.disconnect(conn)
45
+
46
+ async def send_personal(self, websocket: WebSocket, message_type: str, data: dict):
47
+ """Send a message to a specific client."""
48
+ message = json.dumps({
49
+ "type": message_type,
50
+ "data": data,
51
+ "timestamp": datetime.now().isoformat()
52
+ })
53
+ try:
54
+ await websocket.send_text(message)
55
+ except Exception:
56
+ self.disconnect(websocket)
57
+
58
+ @property
59
+ def client_count(self) -> int:
60
+ return len(self.active_connections)
frontend_react/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend_react/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
frontend_react/index.html ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Gemini Trader</title>
8
+ <meta name="description" content="AI-powered trading terminal with Gemini 3 Flash" />
9
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ <script type="module" src="/src/main.jsx"></script>
16
+ </body>
17
+ </html>
frontend_react/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend_react/package.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend_react",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "lucide-react": "^0.569.0",
14
+ "react": "^19.2.0",
15
+ "react-dom": "^19.2.0"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^9.39.1",
19
+ "@types/react": "^19.2.7",
20
+ "@types/react-dom": "^19.2.3",
21
+ "@vitejs/plugin-react": "^5.1.4",
22
+ "eslint": "^9.39.1",
23
+ "eslint-plugin-react-hooks": "^7.0.1",
24
+ "eslint-plugin-react-refresh": "^0.4.24",
25
+ "globals": "^16.5.0",
26
+ "vite": "^7.3.1"
27
+ }
28
+ }
frontend_react/public/vite.svg ADDED
frontend_react/src/App.css ADDED
@@ -0,0 +1 @@
 
 
1
+ /* App.css - cleared of Vite defaults */
frontend_react/src/App.jsx ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import CandlestickChart from './components/CandlestickChart';
3
+ import ReasoningSidebar from './components/ReasoningSidebar';
4
+ import TradeControls from './components/TradeControls';
5
+ import PositionPanel from './components/PositionPanel';
6
+ import AccountBar from './components/AccountBar';
7
+ import { useWebSocket } from './lib/useWebSocket';
8
+ import { AlertTriangle } from 'lucide-react';
9
+ import './App.css';
10
+
11
+ function App() {
12
+ const { connected, data, sendMessage } = useWebSocket('ws://localhost:8000/ws');
13
+
14
+ const [candles, setCandles] = useState([]);
15
+ const [tick, setTick] = useState(null);
16
+ const [account, setAccount] = useState(null);
17
+ const [positions, setPositions] = useState([]);
18
+ const [agentStatus, setAgentStatus] = useState('stopped');
19
+ const [logs, setLogs] = useState([]);
20
+
21
+ useEffect(() => {
22
+ if (!data) return;
23
+
24
+ if (data.type === 'status') {
25
+ setAgentStatus(data.data.agent ? 'running' : 'stopped');
26
+ } else if (data.type === 'candle_update') {
27
+ setCandles(prev => {
28
+ const newCandle = data.data;
29
+ const last = prev[prev.length - 1];
30
+ if (last && last.time === newCandle.time) {
31
+ return [...prev.slice(0, -1), newCandle];
32
+ }
33
+ return [...prev, newCandle].slice(-500);
34
+ });
35
+ } else if (data.type === 'tick_update') {
36
+ setTick(data.data);
37
+ } else if (data.type === 'account') {
38
+ setAccount(data.data);
39
+ } else if (data.type === 'positions') {
40
+ setPositions(data.data);
41
+ } else if (data.type === 'agent_status') {
42
+ setAgentStatus(data.data.status);
43
+ } else if (data.type === 'reasoning') {
44
+ setLogs(prev => [...prev, data.data].slice(-50));
45
+ } else if (data.type === 'trade_event') {
46
+ console.log('Trade Event:', data.data);
47
+ }
48
+ }, [data]);
49
+
50
+ useEffect(() => {
51
+ fetch('http://localhost:8000/api/candles').then(r => r.json()).then(setCandles).catch(console.error);
52
+ fetch('http://localhost:8000/api/account').then(r => r.json()).then(setAccount).catch(console.error);
53
+ fetch('http://localhost:8000/api/positions').then(r => r.json()).then(setPositions).catch(console.error);
54
+ }, []);
55
+
56
+ const handleToggleAgent = (running) => {
57
+ fetch(`http://localhost:8000/api/agent/toggle?enable=${running}`, { method: 'POST' });
58
+ };
59
+
60
+ const handleManualTrade = (action, volume, sl, tp) => {
61
+ fetch('http://localhost:8000/api/trade', {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ body: JSON.stringify({ action, symbol: 'XAUUSDc', volume, sl, tp })
65
+ });
66
+ };
67
+
68
+ return (
69
+ <div className="app-layout">
70
+ {/* Top Bar */}
71
+ <div className="top-bar">
72
+ <div className="top-bar-brand">
73
+ <div className="top-bar-logo">G3</div>
74
+ <span className="top-bar-title">GEMINI TRADER</span>
75
+ </div>
76
+ <div className="top-bar-content">
77
+ <AccountBar account={account} connected={connected} />
78
+ </div>
79
+ </div>
80
+
81
+ {/* Main Content */}
82
+ <div className="main-content">
83
+ {/* Left: Chart & Positions */}
84
+ <div className="chart-column">
85
+ {/* Chart */}
86
+ <div className="chart-area">
87
+ <CandlestickChart data={candles} tick={tick} />
88
+
89
+ {/* Trade Controls Overlay */}
90
+ <div className="trade-overlay">
91
+ <TradeControls
92
+ status={agentStatus}
93
+ onToggleAgent={handleToggleAgent}
94
+ onManualTrade={handleManualTrade}
95
+ />
96
+ </div>
97
+ </div>
98
+
99
+ {/* Positions */}
100
+ <div className="positions-panel">
101
+ <div className="positions-panel-header">
102
+ Open Positions
103
+ </div>
104
+ <div className="positions-panel-body">
105
+ <PositionPanel positions={positions} currentTick={tick} />
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ {/* Right: AI Reasoning Sidebar */}
111
+ <div className="sidebar">
112
+ <ReasoningSidebar logs={logs} status={agentStatus} />
113
+ </div>
114
+ </div>
115
+
116
+ {/* Mobile Warning */}
117
+ <div className="mobile-warning">
118
+ <AlertTriangle size={40} color="var(--accent-gold)" />
119
+ <h2>Desktop Only</h2>
120
+ <p>This trading platform is designed for large screens.</p>
121
+ </div>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ export default App;
frontend_react/src/assets/react.svg ADDED
frontend_react/src/components/AccountBar.jsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Signal } from 'lucide-react';
2
+
3
+ export default function AccountBar({ account, connected }) {
4
+ if (!account || !account.balance) return null;
5
+
6
+ return (
7
+ <div className="account-bar">
8
+ <div className="account-metrics">
9
+ <div className="connection-indicator">
10
+ <div className={`connection-dot ${connected ? 'connection-dot-on' : 'connection-dot-off'}`} />
11
+ <span>{connected ? 'CONNECTED' : 'DISCONNECTED'}</span>
12
+ </div>
13
+ <div className="account-metric">
14
+ <span className="account-metric-label">Balance:</span>
15
+ <span className="account-metric-value">${account.balance.toFixed(2)}</span>
16
+ </div>
17
+ <div className="account-metric">
18
+ <span className="account-metric-label">Equity:</span>
19
+ <span className="account-metric-value">${account.equity.toFixed(2)}</span>
20
+ </div>
21
+ <div className="account-metric">
22
+ <span className="account-metric-label">Margin:</span>
23
+ <span className="color-secondary font-mono">${account.margin.toFixed(2)}</span>
24
+ </div>
25
+ </div>
26
+
27
+ <div className={`badge ${account.trade_mode === 'demo' ? 'badge-blue' : 'badge-red'}`}>
28
+ {account.trade_mode ? account.trade_mode.toUpperCase() : 'DEMO'}
29
+ </div>
30
+ </div>
31
+ );
32
+ }
frontend_react/src/components/CandlestickChart.jsx ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef, useState, useCallback } from 'react';
2
+
3
+ export default function CandlestickChart({ data, tick }) {
4
+ const canvasRef = useRef(null);
5
+ const containerRef = useRef(null);
6
+
7
+ // Viewport state: Offset from the RIGHT (0 means latest candle is at edge)
8
+ // Scale: Pixels per candle (width + gap)
9
+ const [viewport, setViewport] = useState({ offset: 0, scale: 10 });
10
+ const [isDragging, setIsDragging] = useState(false);
11
+ const lastMouseX = useRef(0);
12
+
13
+ const colors = {
14
+ bg: '#080b14',
15
+ grid: '#1e2329',
16
+ text: '#8b949e',
17
+ up: '#26a641',
18
+ down: '#da3633',
19
+ crosshair: '#f0b429',
20
+ };
21
+
22
+ // Handle Resize
23
+ useEffect(() => {
24
+ const handleResize = () => draw();
25
+ window.addEventListener('resize', handleResize);
26
+ return () => window.removeEventListener('resize', handleResize);
27
+ }, []);
28
+
29
+ // Main Draw Function
30
+ const draw = useCallback(() => {
31
+ const canvas = canvasRef.current;
32
+ const container = containerRef.current;
33
+ if (!canvas || !container || !data) return;
34
+
35
+ const rect = container.getBoundingClientRect();
36
+ const width = rect.width;
37
+ const height = rect.height;
38
+
39
+ // Handle Retina displays
40
+ const dpr = window.devicePixelRatio || 1;
41
+ canvas.width = width * dpr;
42
+ canvas.height = height * dpr;
43
+ canvas.style.width = `${width}px`;
44
+ canvas.style.height = `${height}px`;
45
+
46
+ const ctx = canvas.getContext('2d');
47
+ ctx.scale(dpr, dpr);
48
+
49
+ // Clear background
50
+ ctx.fillStyle = colors.bg;
51
+ ctx.fillRect(0, 0, width, height);
52
+
53
+ if (data.length === 0) return;
54
+
55
+ const { offset, scale } = viewport;
56
+ const candleWidth = scale * 0.7; // 70% body, 30% gap
57
+ const rightMargin = 80; // Space for price scale
58
+ const chartWidth = width - rightMargin;
59
+
60
+ // Calculate visible range
61
+ // Visible candles = chartWidth / scale
62
+ const visibleCount = Math.ceil(chartWidth / scale);
63
+
64
+ // Index of the rightmost candle to show
65
+ // If offset is 0, we show data[len-1] at the right edge
66
+ const rightIndex = data.length - 1 - Math.floor(offset / scale);
67
+ const leftIndex = Math.max(0, rightIndex - visibleCount - 1);
68
+
69
+ // Subset for rendering
70
+ const visibleData = data.slice(leftIndex, rightIndex + 1);
71
+
72
+ if (visibleData.length === 0) return;
73
+
74
+ // Calculate Y-axis range (Min/Max Price)
75
+ let minPrice = Infinity;
76
+ let maxPrice = -Infinity;
77
+ visibleData.forEach(c => {
78
+ if (c.low < minPrice) minPrice = c.low;
79
+ if (c.high > maxPrice) maxPrice = c.high;
80
+ });
81
+
82
+ // Add padding to price range
83
+ const padding = (maxPrice - minPrice) * 0.1 || 1.0;
84
+ minPrice -= padding;
85
+ maxPrice += padding;
86
+ const priceRange = maxPrice - minPrice;
87
+
88
+ // Helper: Price to Y coordinate
89
+ const getY = (price) => height - ((price - minPrice) / priceRange) * height;
90
+
91
+ // Helper: Index to X coordinate
92
+ // We render from right to left conceptually
93
+ // X = chartWidth - ( (total_data_index - right_index_offset) * scale )
94
+ // Simply: x position relative to the right edge of the chart area
95
+ const getX = (index) => {
96
+ const posFromRight = (data.length - 1 - index) * scale + (offset % scale);
97
+ return chartWidth - posFromRight - (scale / 2);
98
+ };
99
+
100
+ // Draw Grid & Price Labels
101
+ ctx.strokeStyle = colors.grid;
102
+ ctx.lineWidth = 0.5;
103
+ ctx.fillStyle = colors.text;
104
+ ctx.font = '11px monospace';
105
+ ctx.textAlign = 'left';
106
+
107
+ // Vertical Price Grid
108
+ const gridLines = 8;
109
+ for (let i = 0; i <= gridLines; i++) {
110
+ const y = (height / gridLines) * i;
111
+ const price = maxPrice - (i / gridLines) * priceRange;
112
+
113
+ ctx.beginPath();
114
+ ctx.moveTo(0, y);
115
+ ctx.lineTo(chartWidth, y);
116
+ ctx.stroke();
117
+
118
+ ctx.fillText(price.toFixed(2), chartWidth + 5, y + 4);
119
+ }
120
+
121
+ // Horizontal Time Grid (simplified)
122
+ // ... (could add time labels here)
123
+
124
+ // Draw Candles
125
+ visibleData.forEach((candle, i) => {
126
+ const originalIndex = leftIndex + i;
127
+ const x = getX(originalIndex);
128
+
129
+ const yOpen = getY(candle.open);
130
+ const yClose = getY(candle.close);
131
+ const yHigh = getY(candle.high);
132
+ const yLow = getY(candle.low);
133
+
134
+ const isUp = candle.close >= candle.open;
135
+ const color = isUp ? colors.up : colors.down;
136
+
137
+ ctx.fillStyle = color;
138
+ ctx.strokeStyle = color;
139
+ ctx.lineWidth = 1;
140
+
141
+ // Wick
142
+ ctx.beginPath();
143
+ ctx.moveTo(x, yHigh);
144
+ ctx.lineTo(x, yLow);
145
+ ctx.stroke();
146
+
147
+ // Body
148
+ const bodyH = Math.max(Math.abs(yClose - yOpen), 1);
149
+ ctx.fillRect(x - candleWidth / 2, Math.min(yOpen, yClose), candleWidth, bodyH);
150
+ });
151
+
152
+ // Draw Current Price Line (Bid)
153
+ if (tick && tick.bid) {
154
+ const yBid = getY(tick.bid);
155
+ if (yBid >= 0 && yBid <= height) {
156
+ ctx.strokeStyle = colors.crosshair;
157
+ ctx.setLineDash([4, 4]);
158
+ ctx.beginPath();
159
+ ctx.moveTo(0, yBid);
160
+ ctx.lineTo(chartWidth, yBid);
161
+ ctx.stroke();
162
+ ctx.setLineDash([]);
163
+
164
+ // Label
165
+ ctx.fillStyle = colors.crosshair;
166
+ ctx.fillRect(chartWidth, yBid - 10, 60, 20);
167
+ ctx.fillStyle = '#000';
168
+ ctx.fillText(tick.bid.toFixed(2), chartWidth + 5, yBid + 4);
169
+ }
170
+ }
171
+ }, [data, tick, viewport, colors]);
172
+
173
+ // Redraw when dependencies change
174
+ useEffect(() => {
175
+ draw();
176
+ }, [draw]);
177
+
178
+ // Interaction Handlers
179
+ const handleMouseDown = (e) => {
180
+ setIsDragging(true);
181
+ lastMouseX.current = e.clientX;
182
+ };
183
+
184
+ const handleMouseMove = (e) => {
185
+ if (!isDragging) return;
186
+ const deltaX = e.clientX - lastMouseX.current;
187
+ lastMouseX.current = e.clientX;
188
+
189
+ setViewport(prev => ({
190
+ ...prev,
191
+ offset: prev.offset - deltaX // Dragging right moves view left (history)
192
+ }));
193
+ };
194
+
195
+ const handleMouseUp = () => {
196
+ setIsDragging(false);
197
+ };
198
+
199
+ const handleWheel = (e) => {
200
+ e.preventDefault();
201
+ const zoomSensitivity = 0.001;
202
+ setViewport(prev => {
203
+ const newScale = Math.max(2, Math.min(50, prev.scale * (1 - e.deltaY * zoomSensitivity)));
204
+ return { ...prev, scale: newScale };
205
+ });
206
+ };
207
+
208
+ return (
209
+ <div
210
+ ref={containerRef}
211
+ className="chart-container"
212
+ onMouseDown={handleMouseDown}
213
+ onMouseMove={handleMouseMove}
214
+ onMouseUp={handleMouseUp}
215
+ onMouseLeave={handleMouseUp}
216
+ onWheel={handleWheel}
217
+ style={{
218
+ cursor: isDragging ? 'grabbing' : 'grab',
219
+ touchAction: 'none',
220
+ width: '100%',
221
+ height: '100%',
222
+ position: 'relative'
223
+ }}
224
+ >
225
+ <canvas ref={canvasRef} style={{ display: 'block' }} />
226
+
227
+ {/* Simple OHLC overlay */}
228
+ {data && data.length > 0 && (
229
+ <div style={{
230
+ position: 'absolute',
231
+ top: 10,
232
+ left: 10,
233
+ color: '#8b949e',
234
+ fontFamily: 'monospace',
235
+ fontSize: '12px',
236
+ pointerEvents: 'none'
237
+ }}>
238
+ Last: O:{data[data.length - 1].open.toFixed(2)} H:{data[data.length - 1].high.toFixed(2)} L:{data[data.length - 1].low.toFixed(2)} C:{data[data.length - 1].close.toFixed(2)}
239
+ </div>
240
+ )}
241
+ </div>
242
+ );
243
+ }
frontend_react/src/components/PositionPanel.jsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { XCircle } from 'lucide-react';
2
+
3
+ export default function PositionPanel({ positions }) {
4
+ if (!positions || positions.length === 0) {
5
+ return (
6
+ <div className="pos-table-empty">
7
+ No open positions
8
+ </div>
9
+ );
10
+ }
11
+
12
+ return (
13
+ <div className="pos-table">
14
+ <div className="pos-header">
15
+ <div>Ticket</div>
16
+ <div>Type</div>
17
+ <div>Vol</div>
18
+ <div>Open</div>
19
+ <div>Current</div>
20
+ <div style={{ textAlign: 'right' }}>P&L</div>
21
+ <div style={{ textAlign: 'right' }}>Action</div>
22
+ </div>
23
+
24
+ {positions.map(pos => {
25
+ const isProfit = pos.profit >= 0;
26
+ return (
27
+ <div key={pos.ticket} className="pos-row">
28
+ <div className="pos-cell color-muted">#{pos.ticket}</div>
29
+ <div className={`pos-cell font-bold ${pos.type === 'buy' ? 'color-green' : 'color-red'}`}>
30
+ {pos.type.toUpperCase()}
31
+ </div>
32
+ <div className="pos-cell color-secondary">{pos.volume}</div>
33
+ <div className="pos-cell">{pos.price_open.toFixed(2)}</div>
34
+ <div className="pos-cell color-secondary">{pos.price_current.toFixed(2)}</div>
35
+ <div className={`pos-cell-right font-bold ${isProfit ? 'color-green' : 'color-red'}`}>
36
+ {pos.profit > 0 ? '+' : ''}{pos.profit.toFixed(2)}
37
+ </div>
38
+ <div className="pos-close-btn">
39
+ <button
40
+ title="Close Position"
41
+ onClick={() => {
42
+ fetch('http://localhost:8000/api/trade', {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify({ action: 'close', ticket: pos.ticket })
46
+ });
47
+ }}
48
+ >
49
+ <XCircle size={14} />
50
+ </button>
51
+ </div>
52
+ </div>
53
+ );
54
+ })}
55
+ </div>
56
+ );
57
+ }
frontend_react/src/components/ReasoningSidebar.jsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useEffect } from 'react';
2
+ import { Cpu, Activity } from 'lucide-react';
3
+
4
+ export default function ReasoningSidebar({ logs, status }) {
5
+ const bottomRef = useRef(null);
6
+
7
+ useEffect(() => {
8
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
9
+ }, [logs]);
10
+
11
+ return (
12
+ <>
13
+ {/* Header */}
14
+ <div className="sidebar-header">
15
+ <div className="sidebar-header-left">
16
+ <Cpu size={16} className={status === 'running' ? 'color-green anim-pulse' : 'color-muted'} />
17
+ <span className="sidebar-header-title">GEMINI 3 FLASH</span>
18
+ </div>
19
+ <div className={`badge ${status === 'running' ? 'badge-green' : 'badge-red'}`}>
20
+ {status.toUpperCase()}
21
+ </div>
22
+ </div>
23
+
24
+ {/* Log Stream */}
25
+ <div className="sidebar-body">
26
+ {(!logs || logs.length === 0) && (
27
+ <div className="sidebar-empty">
28
+ <Activity size={28} strokeWidth={1} />
29
+ <span>Waiting for agent thoughts...</span>
30
+ </div>
31
+ )}
32
+
33
+ {logs && [...logs].reverse().map((log, i) => (
34
+ <div key={i} className="log-entry fade-in">
35
+ <div className="log-entry-header">
36
+ <span className={`badge ${log.action === 'BUY' ? 'badge-green' :
37
+ log.action === 'SELL' ? 'badge-red' :
38
+ log.action === 'CLOSE' ? 'badge-blue' :
39
+ 'badge-neutral'
40
+ }`}>
41
+ {log.action}
42
+ </span>
43
+ <div className="confidence-info">
44
+ <div className="confidence-bar-track">
45
+ <div
46
+ className={`confidence-bar-fill ${log.confidence > 0.7 ? 'confidence-bar-high' : 'confidence-bar-low'}`}
47
+ style={{ width: `${log.confidence * 100}%` }}
48
+ />
49
+ </div>
50
+ <span>{(log.confidence * 100).toFixed(0)}%</span>
51
+ </div>
52
+ </div>
53
+
54
+ <p className="log-entry-body">{log.reasoning}</p>
55
+
56
+ <div className="log-entry-footer">
57
+ <span>{new Date(log.timestamp).toLocaleTimeString()}</span>
58
+ <span>GEMINI-2.0-FLASH</span>
59
+ </div>
60
+ </div>
61
+ ))}
62
+ <div ref={bottomRef} />
63
+ </div>
64
+
65
+ {/* Footer */}
66
+ <div className="sidebar-footer">
67
+ <span className="live-dot" />
68
+ <span>Live connection established</span>
69
+ </div>
70
+ </>
71
+ );
72
+ }
frontend_react/src/components/TradeControls.jsx ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { Play, Pause, Activity } from 'lucide-react';
3
+
4
+ export default function TradeControls({ status, onToggleAgent, onManualTrade }) {
5
+ const [vol, setVol] = useState(0.01);
6
+ const [sl, setSl] = useState(0);
7
+ const [tp, setTp] = useState(0);
8
+
9
+ const isRunning = status === 'running';
10
+
11
+ return (
12
+ <div className="trade-controls-gap">
13
+ {/* Agent Toggle */}
14
+ <div className="agent-toggle">
15
+ <div className="agent-toggle-left">
16
+ <div className={`agent-icon ${isRunning ? 'agent-icon-on anim-pulse' : 'agent-icon-off'}`}>
17
+ <Activity size={16} />
18
+ </div>
19
+ <div>
20
+ <div className="agent-info-title">Gemini Agent</div>
21
+ <div className="agent-info-status">{isRunning ? 'Analyzing Market...' : 'Stopped'}</div>
22
+ </div>
23
+ </div>
24
+ <button
25
+ className={`btn ${isRunning ? 'btn-stop' : 'btn-start'}`}
26
+ onClick={() => onToggleAgent(!isRunning)}
27
+ >
28
+ {isRunning ? <><Pause size={12} /> STOP</> : <><Play size={12} /> START</>}
29
+ </button>
30
+ </div>
31
+
32
+ {/* Inputs */}
33
+ <div className="inputs-row">
34
+ <div className="input-group">
35
+ <label className="input-label">Volume</label>
36
+ <input
37
+ type="number" step="0.01"
38
+ value={vol} onChange={e => setVol(parseFloat(e.target.value))}
39
+ className="input-field"
40
+ />
41
+ </div>
42
+ <div className="input-group">
43
+ <label className="input-label">SL</label>
44
+ <input
45
+ type="number" step="0.1"
46
+ value={sl} onChange={e => setSl(parseFloat(e.target.value))}
47
+ className="input-field"
48
+ placeholder="Opt"
49
+ />
50
+ </div>
51
+ <div className="input-group">
52
+ <label className="input-label">TP</label>
53
+ <input
54
+ type="number" step="0.1"
55
+ value={tp} onChange={e => setTp(parseFloat(e.target.value))}
56
+ className="input-field"
57
+ placeholder="Opt"
58
+ />
59
+ </div>
60
+ </div>
61
+
62
+ {/* Buy / Sell */}
63
+ <div className="trade-buttons-row">
64
+ <button className="btn-buy" onClick={() => onManualTrade('BUY', vol, sl, tp)}>
65
+ BUY
66
+ </button>
67
+ <button className="btn-sell" onClick={() => onManualTrade('SELL', vol, sl, tp)}>
68
+ SELL
69
+ </button>
70
+ </div>
71
+ </div>
72
+ );
73
+ }
frontend_react/src/index.css ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ GEMINI TRADER - Trading Terminal Design System
3
+ ============================================================ */
4
+
5
+ /* --- Color Tokens --- */
6
+ :root {
7
+ --bg-primary: #080b14;
8
+ --bg-secondary: #0d1117;
9
+ --bg-panel: #161b22;
10
+ --bg-panel-hover: #1c2128;
11
+ --bg-elevated: #1e242c;
12
+ --bg-input: #0d1117;
13
+
14
+ --border-primary: rgba(255, 255, 255, 0.08);
15
+ --border-secondary: rgba(255, 255, 255, 0.04);
16
+ --border-hover: rgba(255, 255, 255, 0.15);
17
+
18
+ --text-primary: #e6edf3;
19
+ --text-secondary: #8b949e;
20
+ --text-muted: #484f58;
21
+ --text-heading: #f0f6fc;
22
+
23
+ --accent-gold: #f0b429;
24
+ --accent-gold-dim: rgba(240, 180, 41, 0.15);
25
+ --accent-green: #26a641;
26
+ --accent-green-bright: #3fb950;
27
+ --accent-green-dim: rgba(46, 160, 67, 0.15);
28
+ --accent-red: #da3633;
29
+ --accent-red-bright: #f85149;
30
+ --accent-red-dim: rgba(218, 54, 33, 0.15);
31
+ --accent-blue: #388bfd;
32
+ --accent-blue-dim: rgba(56, 139, 253, 0.15);
33
+ --accent-yellow: #d29922;
34
+ --accent-yellow-dim: rgba(210, 153, 34, 0.15);
35
+
36
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
37
+ --font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
38
+
39
+ --radius-sm: 4px;
40
+ --radius-md: 6px;
41
+ --radius-lg: 8px;
42
+
43
+ --transition-fast: 120ms ease;
44
+ --transition-normal: 200ms ease;
45
+ }
46
+
47
+ /* --- Global Reset --- */
48
+ *,
49
+ *::before,
50
+ *::after {
51
+ box-sizing: border-box;
52
+ margin: 0;
53
+ padding: 0;
54
+ }
55
+
56
+ html,
57
+ body {
58
+ width: 100%;
59
+ height: 100%;
60
+ overflow: hidden;
61
+ background-color: var(--bg-primary);
62
+ color: var(--text-primary);
63
+ font-family: var(--font-sans);
64
+ font-size: 14px;
65
+ line-height: 1.5;
66
+ -webkit-font-smoothing: antialiased;
67
+ -moz-osx-font-smoothing: grayscale;
68
+ }
69
+
70
+ #root {
71
+ width: 100%;
72
+ height: 100%;
73
+ overflow: hidden;
74
+ }
75
+
76
+ /* --- App Layout --- */
77
+ .app-layout {
78
+ display: flex;
79
+ flex-direction: column;
80
+ width: 100vw;
81
+ height: 100vh;
82
+ overflow: hidden;
83
+ background-color: var(--bg-primary);
84
+ color: var(--text-primary);
85
+ }
86
+
87
+ /* --- Top Bar --- */
88
+ .top-bar {
89
+ display: flex;
90
+ align-items: center;
91
+ height: 44px;
92
+ min-height: 44px;
93
+ max-height: 44px;
94
+ padding: 0 16px;
95
+ background-color: var(--bg-panel);
96
+ border-bottom: 1px solid var(--border-primary);
97
+ gap: 16px;
98
+ flex-shrink: 0;
99
+ }
100
+
101
+ .top-bar-brand {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 8px;
105
+ flex-shrink: 0;
106
+ }
107
+
108
+ .top-bar-logo {
109
+ width: 24px;
110
+ height: 24px;
111
+ background: linear-gradient(135deg, #f0b429, #e67e22);
112
+ border-radius: var(--radius-sm);
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: center;
116
+ font-weight: 700;
117
+ font-size: 9px;
118
+ color: #000;
119
+ letter-spacing: -0.5px;
120
+ }
121
+
122
+ .top-bar-title {
123
+ font-weight: 700;
124
+ font-size: 14px;
125
+ letter-spacing: 0.5px;
126
+ color: var(--text-heading);
127
+ }
128
+
129
+ .top-bar-content {
130
+ flex: 1;
131
+ min-width: 0;
132
+ }
133
+
134
+ /* --- Main Content Area --- */
135
+ .main-content {
136
+ display: flex;
137
+ flex: 1;
138
+ min-height: 0;
139
+ overflow: hidden;
140
+ }
141
+
142
+ /* --- Chart Column (left side) --- */
143
+ .chart-column {
144
+ display: flex;
145
+ flex-direction: column;
146
+ flex: 1;
147
+ min-width: 0;
148
+ min-height: 0;
149
+ overflow: hidden;
150
+ }
151
+
152
+ /* --- Chart Area --- */
153
+ .chart-area {
154
+ flex: 1;
155
+ min-height: 0;
156
+ position: relative;
157
+ background-color: var(--bg-secondary);
158
+ overflow: hidden;
159
+ }
160
+
161
+ .chart-container {
162
+ position: absolute;
163
+ inset: 0;
164
+ cursor: crosshair;
165
+ }
166
+
167
+ .chart-container canvas {
168
+ display: block;
169
+ width: 100%;
170
+ height: 100%;
171
+ }
172
+
173
+ .chart-ohlc-overlay {
174
+ position: absolute;
175
+ top: 12px;
176
+ left: 12px;
177
+ display: flex;
178
+ gap: 12px;
179
+ padding: 6px 10px;
180
+ background: rgba(8, 11, 20, 0.75);
181
+ backdrop-filter: blur(8px);
182
+ border: 1px solid var(--border-primary);
183
+ border-radius: var(--radius-md);
184
+ font-family: var(--font-mono);
185
+ font-size: 11px;
186
+ color: var(--text-secondary);
187
+ pointer-events: none;
188
+ z-index: 5;
189
+ }
190
+
191
+ .chart-ohlc-overlay span {
192
+ white-space: nowrap;
193
+ }
194
+
195
+ .chart-ohlc-value {
196
+ color: var(--text-primary);
197
+ font-weight: 500;
198
+ }
199
+
200
+ /* --- Trade Controls Overlay --- */
201
+ .trade-overlay {
202
+ position: absolute;
203
+ top: 12px;
204
+ right: 12px;
205
+ width: 260px;
206
+ z-index: 10;
207
+ background: rgba(22, 27, 34, 0.92);
208
+ backdrop-filter: blur(16px);
209
+ border: 1px solid var(--border-primary);
210
+ border-radius: var(--radius-lg);
211
+ padding: 14px;
212
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
213
+ }
214
+
215
+ /* --- Positions Panel (bottom of chart column) --- */
216
+ .positions-panel {
217
+ height: 180px;
218
+ min-height: 180px;
219
+ max-height: 180px;
220
+ flex-shrink: 0;
221
+ border-top: 1px solid var(--border-primary);
222
+ background-color: var(--bg-panel);
223
+ display: flex;
224
+ flex-direction: column;
225
+ overflow: hidden;
226
+ }
227
+
228
+ .positions-panel-header {
229
+ padding: 8px 16px;
230
+ border-bottom: 1px solid var(--border-secondary);
231
+ font-size: 10px;
232
+ font-weight: 700;
233
+ letter-spacing: 1px;
234
+ text-transform: uppercase;
235
+ color: var(--text-muted);
236
+ flex-shrink: 0;
237
+ }
238
+
239
+ .positions-panel-body {
240
+ flex: 1;
241
+ overflow-y: auto;
242
+ padding: 6px;
243
+ min-height: 0;
244
+ }
245
+
246
+ /* --- Sidebar --- */
247
+ .sidebar {
248
+ width: 360px;
249
+ min-width: 360px;
250
+ max-width: 360px;
251
+ flex-shrink: 0;
252
+ border-left: 1px solid var(--border-primary);
253
+ background-color: var(--bg-secondary);
254
+ display: flex;
255
+ flex-direction: column;
256
+ overflow: hidden;
257
+ }
258
+
259
+ .sidebar-header {
260
+ display: flex;
261
+ align-items: center;
262
+ justify-content: space-between;
263
+ padding: 0 16px;
264
+ height: 44px;
265
+ min-height: 44px;
266
+ flex-shrink: 0;
267
+ border-bottom: 1px solid var(--border-primary);
268
+ background-color: var(--bg-panel);
269
+ }
270
+
271
+ .sidebar-header-left {
272
+ display: flex;
273
+ align-items: center;
274
+ gap: 8px;
275
+ }
276
+
277
+ .sidebar-header-title {
278
+ font-size: 12px;
279
+ font-weight: 600;
280
+ letter-spacing: 0.5px;
281
+ color: var(--text-heading);
282
+ }
283
+
284
+ .sidebar-body {
285
+ flex: 1;
286
+ overflow-y: auto;
287
+ padding: 12px;
288
+ min-height: 0;
289
+ display: flex;
290
+ flex-direction: column;
291
+ gap: 10px;
292
+ }
293
+
294
+ .sidebar-footer {
295
+ height: 32px;
296
+ min-height: 32px;
297
+ flex-shrink: 0;
298
+ display: flex;
299
+ align-items: center;
300
+ padding: 0 16px;
301
+ border-top: 1px solid var(--border-primary);
302
+ background-color: var(--bg-primary);
303
+ font-size: 10px;
304
+ color: var(--text-muted);
305
+ gap: 6px;
306
+ }
307
+
308
+ .sidebar-empty {
309
+ display: flex;
310
+ flex-direction: column;
311
+ align-items: center;
312
+ justify-content: center;
313
+ flex: 1;
314
+ color: var(--text-muted);
315
+ gap: 8px;
316
+ font-size: 12px;
317
+ }
318
+
319
+ /* --- Status Badge --- */
320
+ .badge {
321
+ display: inline-flex;
322
+ align-items: center;
323
+ padding: 2px 8px;
324
+ border-radius: var(--radius-sm);
325
+ font-family: var(--font-mono);
326
+ font-size: 10px;
327
+ font-weight: 600;
328
+ letter-spacing: 0.5px;
329
+ border: 1px solid transparent;
330
+ }
331
+
332
+ .badge-green {
333
+ background: var(--accent-green-dim);
334
+ border-color: rgba(46, 160, 67, 0.2);
335
+ color: var(--accent-green-bright);
336
+ }
337
+
338
+ .badge-red {
339
+ background: var(--accent-red-dim);
340
+ border-color: rgba(218, 54, 33, 0.2);
341
+ color: var(--accent-red-bright);
342
+ }
343
+
344
+ .badge-blue {
345
+ background: var(--accent-blue-dim);
346
+ border-color: rgba(56, 139, 253, 0.2);
347
+ color: var(--accent-blue);
348
+ }
349
+
350
+ .badge-yellow {
351
+ background: var(--accent-yellow-dim);
352
+ border-color: rgba(210, 153, 34, 0.2);
353
+ color: var(--accent-yellow);
354
+ }
355
+
356
+ .badge-neutral {
357
+ background: rgba(110, 118, 129, 0.1);
358
+ border-color: rgba(110, 118, 129, 0.2);
359
+ color: var(--text-secondary);
360
+ }
361
+
362
+ /* --- Account Bar --- */
363
+ .account-bar {
364
+ display: flex;
365
+ align-items: center;
366
+ justify-content: space-between;
367
+ width: 100%;
368
+ font-family: var(--font-mono);
369
+ font-size: 11px;
370
+ }
371
+
372
+ .account-metrics {
373
+ display: flex;
374
+ align-items: center;
375
+ gap: 20px;
376
+ }
377
+
378
+ .account-metric {
379
+ display: flex;
380
+ align-items: center;
381
+ gap: 6px;
382
+ }
383
+
384
+ .account-metric-label {
385
+ color: var(--text-muted);
386
+ }
387
+
388
+ .account-metric-value {
389
+ color: var(--text-primary);
390
+ font-weight: 600;
391
+ }
392
+
393
+ .connection-indicator {
394
+ display: flex;
395
+ align-items: center;
396
+ gap: 6px;
397
+ color: var(--text-secondary);
398
+ }
399
+
400
+ .connection-dot {
401
+ width: 6px;
402
+ height: 6px;
403
+ border-radius: 50%;
404
+ }
405
+
406
+ .connection-dot-on {
407
+ background-color: var(--accent-green-bright);
408
+ box-shadow: 0 0 6px var(--accent-green);
409
+ }
410
+
411
+ .connection-dot-off {
412
+ background-color: var(--accent-red);
413
+ }
414
+
415
+ /* --- Log Entry Card --- */
416
+ .log-entry {
417
+ background: var(--bg-panel);
418
+ border: 1px solid var(--border-secondary);
419
+ border-radius: var(--radius-md);
420
+ padding: 10px 12px;
421
+ transition: border-color var(--transition-normal);
422
+ }
423
+
424
+ .log-entry:hover {
425
+ border-color: var(--border-hover);
426
+ }
427
+
428
+ .log-entry-header {
429
+ display: flex;
430
+ align-items: center;
431
+ justify-content: space-between;
432
+ margin-bottom: 8px;
433
+ }
434
+
435
+ .log-entry-body {
436
+ font-size: 12px;
437
+ color: var(--text-secondary);
438
+ line-height: 1.6;
439
+ font-family: var(--font-sans);
440
+ }
441
+
442
+ .log-entry-footer {
443
+ display: flex;
444
+ justify-content: space-between;
445
+ margin-top: 8px;
446
+ padding-top: 6px;
447
+ border-top: 1px solid var(--border-secondary);
448
+ font-size: 10px;
449
+ font-family: var(--font-mono);
450
+ color: var(--text-muted);
451
+ }
452
+
453
+ .confidence-bar-track {
454
+ width: 48px;
455
+ height: 4px;
456
+ background: rgba(110, 118, 129, 0.2);
457
+ border-radius: 2px;
458
+ overflow: hidden;
459
+ }
460
+
461
+ .confidence-bar-fill {
462
+ height: 100%;
463
+ border-radius: 2px;
464
+ transition: width var(--transition-normal);
465
+ }
466
+
467
+ .confidence-bar-high {
468
+ background: var(--accent-green-bright);
469
+ }
470
+
471
+ .confidence-bar-low {
472
+ background: var(--accent-yellow);
473
+ }
474
+
475
+ .confidence-info {
476
+ display: flex;
477
+ align-items: center;
478
+ gap: 6px;
479
+ font-size: 10px;
480
+ font-family: var(--font-mono);
481
+ color: var(--text-muted);
482
+ }
483
+
484
+ /* --- Trade Controls --- */
485
+ .agent-toggle {
486
+ display: flex;
487
+ align-items: center;
488
+ justify-content: space-between;
489
+ padding: 10px 12px;
490
+ background: var(--bg-panel);
491
+ border: 1px solid var(--border-primary);
492
+ border-radius: var(--radius-md);
493
+ }
494
+
495
+ .agent-toggle-left {
496
+ display: flex;
497
+ align-items: center;
498
+ gap: 10px;
499
+ }
500
+
501
+ .agent-icon {
502
+ width: 32px;
503
+ height: 32px;
504
+ border-radius: 50%;
505
+ display: flex;
506
+ align-items: center;
507
+ justify-content: center;
508
+ flex-shrink: 0;
509
+ }
510
+
511
+ .agent-icon-on {
512
+ background: var(--accent-green-dim);
513
+ color: var(--accent-green-bright);
514
+ }
515
+
516
+ .agent-icon-off {
517
+ background: rgba(110, 118, 129, 0.15);
518
+ color: var(--text-muted);
519
+ }
520
+
521
+ .agent-info-title {
522
+ font-size: 12px;
523
+ font-weight: 600;
524
+ color: var(--text-heading);
525
+ }
526
+
527
+ .agent-info-status {
528
+ font-size: 10px;
529
+ color: var(--text-secondary);
530
+ }
531
+
532
+ /* --- Buttons --- */
533
+ .btn {
534
+ display: inline-flex;
535
+ align-items: center;
536
+ justify-content: center;
537
+ gap: 6px;
538
+ padding: 6px 14px;
539
+ border: none;
540
+ border-radius: var(--radius-sm);
541
+ font-family: var(--font-sans);
542
+ font-size: 11px;
543
+ font-weight: 700;
544
+ cursor: pointer;
545
+ transition: all var(--transition-fast);
546
+ outline: none;
547
+ }
548
+
549
+ .btn:active {
550
+ transform: scale(0.97);
551
+ }
552
+
553
+ .btn-start {
554
+ background: var(--accent-green-dim);
555
+ color: var(--accent-green-bright);
556
+ }
557
+
558
+ .btn-start:hover {
559
+ background: rgba(46, 160, 67, 0.25);
560
+ }
561
+
562
+ .btn-stop {
563
+ background: var(--accent-red-dim);
564
+ color: var(--accent-red-bright);
565
+ }
566
+
567
+ .btn-stop:hover {
568
+ background: rgba(218, 54, 33, 0.25);
569
+ }
570
+
571
+ .btn-buy {
572
+ flex: 1;
573
+ padding: 10px 0;
574
+ background: var(--accent-green);
575
+ color: #fff;
576
+ font-size: 12px;
577
+ font-weight: 700;
578
+ border-radius: var(--radius-md);
579
+ border: none;
580
+ cursor: pointer;
581
+ transition: background var(--transition-fast);
582
+ box-shadow: 0 2px 8px rgba(38, 166, 65, 0.2);
583
+ }
584
+
585
+ .btn-buy:hover {
586
+ background: var(--accent-green-bright);
587
+ }
588
+
589
+ .btn-buy:active {
590
+ transform: scale(0.97);
591
+ }
592
+
593
+ .btn-sell {
594
+ flex: 1;
595
+ padding: 10px 0;
596
+ background: var(--accent-red);
597
+ color: #fff;
598
+ font-size: 12px;
599
+ font-weight: 700;
600
+ border-radius: var(--radius-md);
601
+ border: none;
602
+ cursor: pointer;
603
+ transition: background var(--transition-fast);
604
+ box-shadow: 0 2px 8px rgba(218, 54, 33, 0.2);
605
+ }
606
+
607
+ .btn-sell:hover {
608
+ background: var(--accent-red-bright);
609
+ }
610
+
611
+ .btn-sell:active {
612
+ transform: scale(0.97);
613
+ }
614
+
615
+ /* --- Input Fields --- */
616
+ .input-group {
617
+ display: flex;
618
+ flex-direction: column;
619
+ gap: 4px;
620
+ }
621
+
622
+ .input-label {
623
+ font-size: 10px;
624
+ color: var(--text-muted);
625
+ text-transform: uppercase;
626
+ letter-spacing: 0.5px;
627
+ }
628
+
629
+ .input-field {
630
+ width: 100%;
631
+ padding: 6px 8px;
632
+ background: var(--bg-input);
633
+ border: 1px solid var(--border-primary);
634
+ border-radius: var(--radius-sm);
635
+ color: var(--text-primary);
636
+ font-family: var(--font-mono);
637
+ font-size: 12px;
638
+ outline: none;
639
+ transition: border-color var(--transition-fast);
640
+ }
641
+
642
+ .input-field:focus {
643
+ border-color: var(--border-hover);
644
+ }
645
+
646
+ .inputs-row {
647
+ display: grid;
648
+ grid-template-columns: 1fr 1fr 1fr;
649
+ gap: 8px;
650
+ }
651
+
652
+ .trade-buttons-row {
653
+ display: grid;
654
+ grid-template-columns: 1fr 1fr;
655
+ gap: 8px;
656
+ }
657
+
658
+ .trade-controls-gap {
659
+ display: flex;
660
+ flex-direction: column;
661
+ gap: 12px;
662
+ }
663
+
664
+ /* --- Position Table --- */
665
+ .pos-table-empty {
666
+ display: flex;
667
+ align-items: center;
668
+ justify-content: center;
669
+ height: 100%;
670
+ font-size: 11px;
671
+ color: var(--text-muted);
672
+ }
673
+
674
+ .pos-table {
675
+ display: flex;
676
+ flex-direction: column;
677
+ gap: 4px;
678
+ }
679
+
680
+ .pos-header {
681
+ display: grid;
682
+ grid-template-columns: 1fr 0.7fr 0.7fr 1fr 1fr 0.8fr 0.5fr;
683
+ padding: 4px 10px;
684
+ font-size: 10px;
685
+ font-family: var(--font-mono);
686
+ font-weight: 600;
687
+ color: var(--text-muted);
688
+ text-transform: uppercase;
689
+ letter-spacing: 0.5px;
690
+ }
691
+
692
+ .pos-row {
693
+ display: grid;
694
+ grid-template-columns: 1fr 0.7fr 0.7fr 1fr 1fr 0.8fr 0.5fr;
695
+ align-items: center;
696
+ padding: 8px 10px;
697
+ background: var(--bg-panel);
698
+ border: 1px solid var(--border-secondary);
699
+ border-radius: var(--radius-sm);
700
+ font-size: 11px;
701
+ transition: background var(--transition-fast), border-color var(--transition-fast);
702
+ }
703
+
704
+ .pos-row:hover {
705
+ background: var(--bg-panel-hover);
706
+ border-color: var(--border-hover);
707
+ }
708
+
709
+ .pos-cell {
710
+ font-family: var(--font-mono);
711
+ }
712
+
713
+ .pos-cell-right {
714
+ text-align: right;
715
+ font-family: var(--font-mono);
716
+ }
717
+
718
+ .pos-close-btn {
719
+ display: flex;
720
+ align-items: center;
721
+ justify-content: flex-end;
722
+ }
723
+
724
+ .pos-close-btn button {
725
+ background: none;
726
+ border: none;
727
+ color: var(--text-muted);
728
+ cursor: pointer;
729
+ padding: 4px;
730
+ border-radius: var(--radius-sm);
731
+ transition: all var(--transition-fast);
732
+ display: flex;
733
+ align-items: center;
734
+ }
735
+
736
+ .pos-close-btn button:hover {
737
+ color: var(--text-primary);
738
+ background: rgba(255, 255, 255, 0.06);
739
+ }
740
+
741
+ /* --- Color Utility Classes --- */
742
+ .color-green {
743
+ color: var(--accent-green-bright);
744
+ }
745
+
746
+ .color-red {
747
+ color: var(--accent-red-bright);
748
+ }
749
+
750
+ .color-blue {
751
+ color: var(--accent-blue);
752
+ }
753
+
754
+ .color-gold {
755
+ color: var(--accent-gold);
756
+ }
757
+
758
+ .color-muted {
759
+ color: var(--text-muted);
760
+ }
761
+
762
+ .color-secondary {
763
+ color: var(--text-secondary);
764
+ }
765
+
766
+ .color-primary {
767
+ color: var(--text-primary);
768
+ }
769
+
770
+ .color-heading {
771
+ color: var(--text-heading);
772
+ }
773
+
774
+ .font-bold {
775
+ font-weight: 700;
776
+ }
777
+
778
+ .font-mono {
779
+ font-family: var(--font-mono);
780
+ }
781
+
782
+ /* --- Scrollbar --- */
783
+ ::-webkit-scrollbar {
784
+ width: 5px;
785
+ height: 5px;
786
+ }
787
+
788
+ ::-webkit-scrollbar-track {
789
+ background: transparent;
790
+ }
791
+
792
+ ::-webkit-scrollbar-thumb {
793
+ background: rgba(110, 118, 129, 0.3);
794
+ border-radius: 3px;
795
+ }
796
+
797
+ ::-webkit-scrollbar-thumb:hover {
798
+ background: rgba(110, 118, 129, 0.5);
799
+ }
800
+
801
+ /* --- Animations --- */
802
+ @keyframes fadeIn {
803
+ from {
804
+ opacity: 0;
805
+ transform: translateY(4px);
806
+ }
807
+
808
+ to {
809
+ opacity: 1;
810
+ transform: translateY(0);
811
+ }
812
+ }
813
+
814
+ @keyframes pulse {
815
+
816
+ 0%,
817
+ 100% {
818
+ opacity: 1;
819
+ }
820
+
821
+ 50% {
822
+ opacity: 0.5;
823
+ }
824
+ }
825
+
826
+ @keyframes liveDot {
827
+
828
+ 0%,
829
+ 100% {
830
+ opacity: 1;
831
+ box-shadow: 0 0 4px var(--accent-green);
832
+ }
833
+
834
+ 50% {
835
+ opacity: 0.4;
836
+ box-shadow: 0 0 1px var(--accent-green);
837
+ }
838
+ }
839
+
840
+ .fade-in {
841
+ animation: fadeIn 0.25s ease-out forwards;
842
+ }
843
+
844
+ .anim-pulse {
845
+ animation: pulse 2s ease-in-out infinite;
846
+ }
847
+
848
+ .live-dot {
849
+ display: inline-block;
850
+ width: 5px;
851
+ height: 5px;
852
+ border-radius: 50%;
853
+ background-color: var(--accent-green-bright);
854
+ animation: liveDot 2s ease-in-out infinite;
855
+ }
856
+
857
+ /* --- Mobile Warning --- */
858
+ .mobile-warning {
859
+ display: none;
860
+ }
861
+
862
+ @media (max-width: 768px) {
863
+ .app-layout>*:not(.mobile-warning) {
864
+ display: none;
865
+ }
866
+
867
+ .mobile-warning {
868
+ display: flex;
869
+ position: fixed;
870
+ inset: 0;
871
+ z-index: 100;
872
+ background: var(--bg-primary);
873
+ align-items: center;
874
+ justify-content: center;
875
+ padding: 32px;
876
+ text-align: center;
877
+ flex-direction: column;
878
+ gap: 12px;
879
+ }
880
+
881
+ .mobile-warning h2 {
882
+ font-size: 18px;
883
+ color: var(--accent-gold);
884
+ margin: 0;
885
+ }
886
+
887
+ .mobile-warning p {
888
+ color: var(--text-secondary);
889
+ font-size: 13px;
890
+ }
891
+ }
frontend_react/src/lib/useWebSocket.js ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
+
3
+ export const useWebSocket = (url) => {
4
+ const [data, setData] = useState(null);
5
+ const [connected, setConnected] = useState(false);
6
+ const ws = useRef(null);
7
+ const reconnectTimer = useRef(null);
8
+ const mountedRef = useRef(true);
9
+ const reconnectDelay = useRef(1000);
10
+
11
+ const connect = useCallback(() => {
12
+ if (!mountedRef.current) return;
13
+
14
+ // Clean up any existing connection
15
+ if (ws.current) {
16
+ ws.current.onopen = null;
17
+ ws.current.onclose = null;
18
+ ws.current.onmessage = null;
19
+ ws.current.onerror = null;
20
+ if (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING) {
21
+ ws.current.close();
22
+ }
23
+ }
24
+
25
+ console.log('[WS] Connecting to', url);
26
+ ws.current = new WebSocket(url);
27
+
28
+ ws.current.onopen = () => {
29
+ if (!mountedRef.current) return;
30
+ console.log('[WS] Connected');
31
+ setConnected(true);
32
+ reconnectDelay.current = 1000; // Reset backoff on success
33
+ };
34
+
35
+ ws.current.onclose = (event) => {
36
+ if (!mountedRef.current) return;
37
+ console.log('[WS] Disconnected, code:', event.code);
38
+ setConnected(false);
39
+
40
+ // Auto-reconnect with exponential backoff
41
+ const delay = reconnectDelay.current;
42
+ reconnectDelay.current = Math.min(delay * 2, 10000);
43
+ console.log(`[WS] Reconnecting in ${delay}ms...`);
44
+ reconnectTimer.current = setTimeout(() => {
45
+ if (mountedRef.current) connect();
46
+ }, delay);
47
+ };
48
+
49
+ ws.current.onmessage = (event) => {
50
+ try {
51
+ const message = JSON.parse(event.data);
52
+ setData(message);
53
+ } catch (err) {
54
+ console.error('[WS] Parse Error', err);
55
+ }
56
+ };
57
+
58
+ ws.current.onerror = (err) => {
59
+ console.error('[WS] Error', err);
60
+ };
61
+ }, [url]);
62
+
63
+ useEffect(() => {
64
+ mountedRef.current = true;
65
+ connect();
66
+
67
+ return () => {
68
+ mountedRef.current = false;
69
+ clearTimeout(reconnectTimer.current);
70
+ if (ws.current) {
71
+ ws.current.onopen = null;
72
+ ws.current.onclose = null;
73
+ ws.current.onmessage = null;
74
+ ws.current.onerror = null;
75
+ ws.current.close();
76
+ }
77
+ };
78
+ }, [connect]);
79
+
80
+ const sendMessage = useCallback((msg) => {
81
+ if (ws.current && ws.current.readyState === WebSocket.OPEN) {
82
+ ws.current.send(JSON.stringify(msg));
83
+ }
84
+ }, []);
85
+
86
+ return { connected, data, sendMessage };
87
+ };
frontend_react/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
frontend_react/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })
verify_strict_mode.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Ensure we can import from backend
5
+ sys.path.append(os.path.join(os.getcwd(), 'backend'))
6
+
7
+ # Mocking environment for the test
8
+ os.environ["FORCE_MT5_DATA"] = "true"
9
+ os.environ["MT5_PATH"] = r"C:\Program Files\MetaTrader 5\terminal64.exe" # Typical path
10
+
11
+ try:
12
+ # Try importing directly if we are in the folder, or via backend if not
13
+ try:
14
+ from backend.mt5_mcp import MT5Bridge
15
+ except ImportError:
16
+ sys.path.append(os.getcwd())
17
+ from mt5_mcp import MT5Bridge
18
+
19
+ print("[TEST] Importing MT5Bridge successful.")
20
+
21
+ bridge = MT5Bridge()
22
+ print(f"[TEST] Bridge initialized. Simulation mode: {bridge.simulation_mode}")
23
+
24
+ # We expect this to FAIL if MT5 is not installed/running, or SUCCEED if it is.
25
+ # But crucially, it should NOT return success=True with mode="simulation"
26
+ result = bridge.initialize()
27
+ print(f"[TEST] Initialize result: {result}")
28
+
29
+ if result["success"] and result.get("mode") == "simulation":
30
+ print("[FAIL] Strict mode failed! It fell back to simulation.")
31
+ sys.exit(1)
32
+ elif not result["success"] and "CRITICAL" in result["message"]:
33
+ print("[PASS] Strict mode correctly blocked simulation (or MT5 connect failed as expected in test env).")
34
+ elif result["success"] and result.get("mode") != "simulation":
35
+ print("[PASS] Connected to real MT5.")
36
+ else:
37
+ print(f"[WARN] Unexpected state: {result}")
38
+
39
+ except ImportError:
40
+ print("[PASS] Strict mode correctly raised ImportError (if MT5 lib missing).")
41
+ except Exception as e:
42
+ print(f"[TEST] Exception: {e}")
43
+ import traceback
44
+ traceback.print_exc()