Upload 31 files
Browse files- .gitignore +62 -0
- LICENSE +21 -0
- README.md +80 -0
- STRUCTURE.md +48 -0
- TECHSTACK.md +18 -0
- backend/.env.example +16 -0
- backend/agent.py +196 -0
- backend/main.py +248 -0
- backend/models.py +82 -0
- backend/mt5_mcp.py +360 -0
- backend/requirements.txt +8 -0
- backend/ws_manager.py +60 -0
- frontend_react/.gitignore +24 -0
- frontend_react/eslint.config.js +29 -0
- frontend_react/index.html +17 -0
- frontend_react/package-lock.json +0 -0
- frontend_react/package.json +28 -0
- frontend_react/public/vite.svg +1 -0
- frontend_react/src/App.css +1 -0
- frontend_react/src/App.jsx +126 -0
- frontend_react/src/assets/react.svg +1 -0
- frontend_react/src/components/AccountBar.jsx +32 -0
- frontend_react/src/components/CandlestickChart.jsx +243 -0
- frontend_react/src/components/PositionPanel.jsx +57 -0
- frontend_react/src/components/ReasoningSidebar.jsx +72 -0
- frontend_react/src/components/TradeControls.jsx +73 -0
- frontend_react/src/index.css +891 -0
- frontend_react/src/lib/useWebSocket.js +87 -0
- frontend_react/src/main.jsx +10 -0
- frontend_react/vite.config.js +7 -0
- verify_strict_mode.py +44 -0
.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()
|