cleanup: Remove backup files, add .gitignore, create production-ready repository structure
- Delete 20+ backup/test files and old versions - Add comprehensive .gitignore for Python development - Update requirements.txt with only essential dependencies - Add ARCHITECTURE.md for system documentation - Keep only production files: main_ml.py, web_dashboard.py, README.md Repository is now clean, lean, and production-ready for Version 0.2
This commit is contained in:
parent
edf23f9a4f
commit
0973dace06
|
|
@ -0,0 +1,56 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Temp files
|
||||
*.bak
|
||||
*.tmp
|
||||
*.backup
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Bot-specific
|
||||
state/
|
||||
cache/
|
||||
*.pickle
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
# Trading Bot V0.2 — System Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Trading Bot V0.2 is a production-ready cryptocurrency trading bot with adaptive strategy learning. The bot makes autonomous trading decisions based on hourly performance evaluation and currently manages a live Binance portfolio.
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Trading Engine (`src/main_ml.py`)
|
||||
|
||||
**Purpose:** Autonomous trading bot with risk management and adaptive strategy learning.
|
||||
|
||||
**Key Features:**
|
||||
- **Signal Generation**: Random 5-10% probability per cycle (adapts based on win rate)
|
||||
- **Position Management**: Max 1 position (scales to 2 in full-throttle mode)
|
||||
- **Risk Controls**:
|
||||
- Stop Loss: -1.0 to -2.2% (adaptive)
|
||||
- Take Profit: +1.5 to +3.5% (adaptive)
|
||||
- Daily Loss Limit: -5% (stops trading if exceeded)
|
||||
- Cooldown: 30min after 3 consecutive losses
|
||||
- **Adaptive Learning**: Evaluates win rate hourly, adjusts strategy (5 levels)
|
||||
|
||||
**Strategy Levels (based on Win Rate):**
|
||||
|
||||
| Level | WR | Signal | Investment | TP | SL | Max Trades |
|
||||
|-------|----|----|-----------|----|----|------|
|
||||
| Emergency | <45% | 5.0% | 50% | 1.5% | 1.0% | 5/day |
|
||||
| Conservative | 45-50% | 6.5% | 50% | 2.2% | 1.5% | 10/day |
|
||||
| Standard | 50-60% | 7.5% | 50% | 2.8% | 1.8% | 15/day |
|
||||
| Aggressive | 60-70% | 8.5% | 55% | 3.2% | 2.0% | 20/day |
|
||||
| Full Throttle | >70% | 10.0% | 55% | 3.5% | 2.2% | 25/day |
|
||||
|
||||
**Input/Output:**
|
||||
- **Input**: Binance API (market data, account state, order status)
|
||||
- **Output**: Market buy/sell orders, stop loss orders, Telegram alerts
|
||||
|
||||
**Run Cycle:** 5-second loop (async)
|
||||
|
||||
### 2. Dashboard (`src/web_dashboard.py`)
|
||||
|
||||
**Purpose:** Real-time portfolio monitoring and P&L display.
|
||||
|
||||
**Endpoints:**
|
||||
- `/` (HTTP) — HTML dashboard
|
||||
- `/api/state` (JSON) — Market data, holdings, P&L, strategy status
|
||||
|
||||
**Features:**
|
||||
- **Portfolio Metrics**: Total value, USDT free, locked positions
|
||||
- **P&L Display**: Realized + unrealized, color-coded (green/red/neutral)
|
||||
- **Live Prices**: Real-time cryptoommodity quotes
|
||||
- **Holdings Table**: Asset balances with locked coin tracking
|
||||
- **Strategy Status**: Current win rate, strategy mode, next adaptation time
|
||||
|
||||
**Refresh Rate:** 10 seconds (user-configurable)
|
||||
|
||||
**Tech Stack:**
|
||||
- Framework: FastAPI
|
||||
- Server: Uvicorn (async)
|
||||
- Template: Jinja2 (server-side rendering)
|
||||
- Port: 7000
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Binance API │
|
||||
│ (Market Data, Account, Orders) │
|
||||
└────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Trading Bot │
|
||||
│ (main_ml.py) │
|
||||
│ │
|
||||
│ • Signal Gen │
|
||||
│ • Order Place │
|
||||
│ • Risk Mgmt │
|
||||
│ • Adaptive Learn│
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ Dashboard │
|
||||
│ (web_dashboard) │
|
||||
│ │
|
||||
│ • /api/state │
|
||||
│ • HTML UI │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────────▼────────┐
|
||||
│ User Interface │
|
||||
│ (HTTP Browser) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Adaptive Learning Loop (Option 2)
|
||||
|
||||
**Evaluation Cycle:** Every hour
|
||||
|
||||
```
|
||||
1. Calculate Win Rate
|
||||
win_rate = total_wins / total_trades * 100
|
||||
|
||||
2. Compare to Thresholds
|
||||
- <45% → Emergency mode
|
||||
- 45-50% → Conservative
|
||||
- 50-60% → Standard
|
||||
- 60-70% → Aggressive
|
||||
- >70% → Full Throttle
|
||||
|
||||
3. Update Parameters
|
||||
- SIGNAL_THRESHOLD (5-10%)
|
||||
- INVESTMENT_PERCENT (50-55%)
|
||||
- TAKE_PROFIT_PERCENT (1.5-3.5%)
|
||||
- STOP_LOSS_PERCENT (1.0-2.2%)
|
||||
- MAX_TRADES_PER_DAY (5-25)
|
||||
- MAX_OPEN_POSITIONS (1-2)
|
||||
|
||||
4. Send Notification
|
||||
- Telegram alert with old↔new parameters
|
||||
- Log strategy change
|
||||
- Store strategy_version for tracking
|
||||
```
|
||||
|
||||
**Minimum Trades to Adapt:** 5 (prevents noise in early phase)
|
||||
|
||||
## Performance Tracking
|
||||
|
||||
**Tracked Metrics:**
|
||||
- `total_trades` — All trades ever executed
|
||||
- `total_wins` — Winning trades (TP hit)
|
||||
- `total_losses` — Losing trades (SL hit)
|
||||
- `daily_pnl` — Today's profit/loss (resets daily)
|
||||
- `trades_today` — Count reset daily at UTC 00:00
|
||||
- `portfolio_value` — Current liquid value (real-time)
|
||||
- `pnl_usdt` — Total P&L in USD
|
||||
- `pnl_pct` — Total P&L in percentage
|
||||
|
||||
**Reporting:**
|
||||
- 3-hour summaries via Telegram (win rate, P&L, status)
|
||||
- Real-time alerts on strategy changes
|
||||
- Dashboard updates every 10 seconds
|
||||
|
||||
## Security & Risk
|
||||
|
||||
**API Key Management:**
|
||||
- Stored in `.env` file (never committed)
|
||||
- API key requires `TRADING` permission on Binance
|
||||
- All read/write operations over HTTPS (Binance)
|
||||
|
||||
**Order Validation:**
|
||||
- Minimum notional: $5.00 per order
|
||||
- Quantity rounded to Binance step size (using Decimal, no precision loss)
|
||||
- Price rounded to Binance tick size
|
||||
- Daily loss limit enforces hard stop at -5%
|
||||
|
||||
**Position Limits:**
|
||||
- Max 1 position (standard) / 2 positions (full throttle)
|
||||
- Max 3 consecutive losses → 30min cooldown
|
||||
- No pyramid trading (one trade at a time)
|
||||
|
||||
## Deployment
|
||||
|
||||
**Requirements:**
|
||||
- Python 3.10+
|
||||
- Binance API key with SPOT trading permission
|
||||
- Telegram bot token (for alerts)
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**Start Bot:**
|
||||
```bash
|
||||
python3 src/main_ml.py
|
||||
```
|
||||
|
||||
**Start Dashboard:**
|
||||
```bash
|
||||
uvicorn src/web_dashboard:app --host 0.0.0.0 --port 7000
|
||||
```
|
||||
|
||||
**Access Dashboard:**
|
||||
```
|
||||
http://localhost:7000
|
||||
```
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
BrainDock/
|
||||
├── src/
|
||||
│ ├── __init__.py (Package marker)
|
||||
│ ├── main_ml.py (Trading bot engine - 512 lines)
|
||||
│ └── web_dashboard.py (Dashboard API - 650+ lines)
|
||||
├── README.md (User documentation)
|
||||
├── ARCHITECTURE.md (This file)
|
||||
├── requirements.txt (Python dependencies)
|
||||
└── .gitignore (Git exclusions)
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
**Phase 2: Machine Learning**
|
||||
- Train model on historical OHLCV data
|
||||
- Replace random signal with ML probability
|
||||
- Feature engineering: RSI, MACD, Bollinger Bands, etc.
|
||||
|
||||
**Phase 3: Portfolio Optimization**
|
||||
- Multi-pair trading (BTC, ETH, SOL, BNB, XRP)
|
||||
- Dynamic position sizing by Sharpe ratio
|
||||
- Kelly Criterion for capital allocation
|
||||
|
||||
**Phase 4: Advanced Risk**
|
||||
- Correlation-based hedging
|
||||
- Volatility clustering detection
|
||||
- Dynamic stop loss based on ATR
|
||||
|
||||
## Monitoring & Debugging
|
||||
|
||||
**Logs:**
|
||||
```bash
|
||||
journalctl -u trading-bot.service -f # Real-time logs
|
||||
```
|
||||
|
||||
**API Health Check:**
|
||||
```bash
|
||||
curl http://localhost:7000/api/state | jq .
|
||||
```
|
||||
|
||||
**Database State:**
|
||||
- No persistent database; all state in-memory
|
||||
- Recovery from Binance API on bot restart
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2026-07-07
|
||||
**Version:** V0.2
|
||||
**Status:** Production Ready ✅
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# Trading Bot Strategy V2 — 2026-07-06
|
||||
|
||||
## Signal Generation
|
||||
- **Threshold:** 7.5% (instead of 5%)
|
||||
- **Frequency:** Every 5 seconds
|
||||
- **Confidence Range:** 30-95% (modeled)
|
||||
|
||||
## Position Sizing
|
||||
- **Standard:** 25% of free USDT
|
||||
- **High Confidence (>85%):** 35% of free USDT
|
||||
- **NOTIONAL Min:** $5.00 (Binance requirement)
|
||||
|
||||
## Exit Rules
|
||||
- **Take Profit:** +2.8% (was +3%)
|
||||
- **Stop Loss:** -1.8% (was -2.5%)
|
||||
- **Trailing Stop:** Active at +1.5% profit, distance 0.6%
|
||||
|
||||
## Risk Management
|
||||
- **Max Concurrent Positions:** 3 (was 5)
|
||||
- **Max Consecutive Losses:** 3 → triggers 30min cooldown
|
||||
- **Daily Loss Limit:** -5% (unchanged)
|
||||
- **Max Trades/Day:** 15
|
||||
- **Min Win Probability Check:** 75%
|
||||
- **Volatility Filter:** Rejects trades if 1h volatility > 5%
|
||||
|
||||
## Expected Behavior
|
||||
- Fewer but more selective trades (7.5% signal threshold)
|
||||
- Tighter SL/TP (1.8% / 2.8%)
|
||||
- Better capital efficiency (25% standard)
|
||||
- Cooldown protection after 3 losses
|
||||
- Max 15 trades/day prevents over-trading
|
||||
|
||||
---
|
||||
Deployed: 2026-07-06 21:48 UTC
|
||||
Git: a815dfe (reference point)
|
||||
|
||||
---
|
||||
|
||||
## UPDATED (2026-07-06 21:54)
|
||||
|
||||
### Liquidity Optimization
|
||||
- **Max Concurrent Positions:** Reduced to **1** (was 3)
|
||||
- **Investment %:** **50%** (single position auto-closes before next entry)
|
||||
- **Rationale:** With 1 position max, 50% × remaining USDT ensures next cycle has ~$9+ USDT
|
||||
- **Trade Cadence:** Wait for SL/TP hit before next entry (no queue)
|
||||
|
||||
**Result:**
|
||||
- Start: $17.35 USDT → Trade 1: 50% = $8.68 → Close → $17 back + gains
|
||||
- Maintains minimum $5 NOTIONAL per trade
|
||||
- Max 15 trades/day still enforced
|
||||
- Single position reduces capital lock
|
||||
|
||||
---
|
||||
|
|
@ -1 +0,0 @@
|
|||
<!DOCTYPE html><html><head><meta charset=UTF-8><title>Bot P&L</title><style>body{background:#1e1e1e;color:#d0d0d0;font-family:monospace;padding:20px}.pnl-value{font-size:48px;font-weight:700;margin:15px 0}.profit{color:#00ff88}.loss{color:#ff4444}.details{display:grid;grid-template-columns:1fr 1fr;gap:15px}</style></head><body><div id=c><div style=text-align:center>Läd…</div></div><script>setInterval(async()=>{const d=await(await fetch('http://172.16.1.168:7000/api/pnl')).json();const p=d.total_pnl_usdt,c=p>0?'profit':p<0?'loss':'';document.getElementById('c').innerHTML=`<h1>Bot P&L</h1><div class='pnl-value ${c}'>${p>=0?'+':''}$${p.toFixed(2)}</div><div>${d.total_pnl_percent>=0?'+':''}${d.total_pnl_percent.toFixed(2)}%</div>`},5000)</script></body></html>
|
||||
|
|
@ -1,6 +1,20 @@
|
|||
python-binance==1.0.17
|
||||
aiohttp==3.8.6
|
||||
python-telegram-bot==20.1
|
||||
pydantic==2.4.2
|
||||
# Trading Bot V0.2 Dependencies
|
||||
# Crypto Trading & Market Data
|
||||
python-binance==1.0.20
|
||||
requests==2.31.0
|
||||
|
||||
# Web Framework & Dashboard
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
Jinja2==3.1.2
|
||||
|
||||
# Async & Utilities
|
||||
aiohttp==3.9.1
|
||||
python-dotenv==1.0.0
|
||||
pyyaml==6.0.1
|
||||
|
||||
# Optional: ML/Data Analysis (for future enhancements)
|
||||
numpy==1.24.3
|
||||
pandas==2.0.3
|
||||
|
||||
# Logging & Monitoring
|
||||
python-telegram-bot==20.2
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/bash
|
||||
cd /home/marc/bot-deploy
|
||||
|
||||
# Generate report
|
||||
REPORT=$(python3 src/frigate_report.py)
|
||||
|
||||
# Send to Telegram via Hermes
|
||||
echo "$REPORT" | hermes send-message telegram --message-file /dev/stdin || true
|
||||
|
||||
# Also save to log
|
||||
echo "[$(date)]" >> /tmp/frigate-reports.log
|
||||
echo "$REPORT" >> /tmp/frigate-reports.log
|
||||
echo "" >> /tmp/frigate-reports.log
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/bash
|
||||
cd /home/marc/bot-deploy
|
||||
python3 src/report_generator.py | hermes send-message telegram --message-file /dev/stdin
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import logging
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import BaseModel
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class BotConfig(BaseModel):
|
||||
"""Bot configuration from environment variables."""
|
||||
|
||||
# Binance API
|
||||
binance_api_key_testnet: str = os.getenv("BINANCE_API_KEY_TESTNET", "")
|
||||
binance_api_secret_testnet: str = os.getenv("BINANCE_API_SECRET_TESTNET", "")
|
||||
binance_api_key_live: str = os.getenv("BINANCE_API_KEY_LIVE", "")
|
||||
binance_api_secret_live: str = os.getenv("BINANCE_API_SECRET_LIVE", "")
|
||||
|
||||
# Bot
|
||||
dry_run: bool = os.getenv("DRY_RUN", "false").lower() == "true"
|
||||
environment: str = os.getenv("ENVIRONMENT", "testnet") # "testnet" or "live"
|
||||
trading_pair: str = os.getenv("TRADING_PAIR", "BTCUSDT")
|
||||
dca_amount_usd: float = float(os.getenv("DCA_AMOUNT", "10"))
|
||||
dca_interval_hours: float = float(os.getenv("DCA_INTERVAL_HOURS", "1"))
|
||||
stop_loss_percent: float = float(os.getenv("STOP_LOSS_PERCENT", "2"))
|
||||
|
||||
# Telegram
|
||||
telegram_bot_token: str = os.getenv("TELEGRAM_BOT_TOKEN", "")
|
||||
telegram_chat_id: str = os.getenv("TELEGRAM_CHAT_ID", "")
|
||||
|
||||
# Obsidian
|
||||
obsidian_vault_path: str = os.getenv("OBSIDIAN_VAULT_PATH", "/opt/obsidian/config/Vault/Test/")
|
||||
obsidian_trade_log_file: str = os.getenv("OBSIDIAN_TRADE_LOG_FILE", "BrainDock/trading-log.md")
|
||||
|
||||
# Database
|
||||
db_path: str = os.getenv("DB_PATH", "/data/bot_state.db")
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
def validate(self):
|
||||
"""Validate required config"""
|
||||
if self.environment not in ("testnet", "live"):
|
||||
raise ValueError("ENVIRONMENT must be 'testnet' or 'live'")
|
||||
|
||||
if self.environment == "testnet":
|
||||
if not self.binance_api_key_testnet or not self.binance_api_secret_testnet:
|
||||
raise ValueError("Testnet API credentials required")
|
||||
else:
|
||||
if not self.binance_api_key_live or not self.binance_api_secret_live:
|
||||
raise ValueError("Live API credentials required")
|
||||
|
||||
if not self.telegram_bot_token or not self.telegram_chat_id:
|
||||
logger.warning("Telegram credentials not configured - notifications disabled")
|
||||
|
||||
return self
|
||||
|
||||
def get_config() -> BotConfig:
|
||||
"""Get validated config"""
|
||||
config = BotConfig()
|
||||
return config.validate()
|
||||
|
|
@ -1 +0,0 @@
|
|||
<!DOCTYPE html><html><head><meta charset=UTF-8><title>Bot P&L</title><style>body{background:#1e1e1e;color:#d0d0d0;font-family:monospace;padding:20px}.pnl-value{font-size:48px;font-weight:700;margin:15px 0}.profit{color:#00ff88}.loss{color:#ff4444}.details{display:grid;grid-template-columns:1fr 1fr;gap:15px}</style></head><body><div id=c><div style=text-align:center>Läd…</div></div><script>setInterval(async()=>{const d=await(await fetch('http://172.16.1.168:7000/api/pnl')).json();const p=d.total_pnl_usdt,c=p>0?'profit':p<0?'loss':'';document.getElementById('c').innerHTML=`<h1>Bot P&L</h1><div class='pnl-value ${c}'>${p>=0?'+':''}$${p.toFixed(2)}</div><div>${d.total_pnl_percent>=0?'+':''}${d.total_pnl_percent.toFixed(2)}%</div>`},5000)</script></body></html>
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Frigate Daily Report Generator
|
||||
Sends to Telegram every evening at 20:30 CET
|
||||
"""
|
||||
import os, json, requests
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
FRIGATE_URL = "http://localhost:5000"
|
||||
|
||||
def get_frigate_events():
|
||||
"""Get events from last 24 hours"""
|
||||
try:
|
||||
resp = requests.get(f"{FRIGATE_URL}/api/events", timeout=5)
|
||||
events = resp.json()
|
||||
|
||||
# Filter for last 24h
|
||||
now = datetime.now().timestamp()
|
||||
yesterday = now - (24 * 3600)
|
||||
|
||||
recent = [e for e in events if e.get('start_time', 0) > yesterday]
|
||||
return recent
|
||||
except Exception as e:
|
||||
print(f"Error fetching events: {e}")
|
||||
return []
|
||||
|
||||
def generate_report():
|
||||
"""Generate Frigate daily summary"""
|
||||
events = get_frigate_events()
|
||||
|
||||
if not events:
|
||||
return "🎥 **Frigate Daily Report** — Keine Events heute\n\nStatus: ✅ Alle Kameras aktiv\nEvents: 0"
|
||||
|
||||
# Group by camera & label
|
||||
by_camera = defaultdict(lambda: defaultdict(int))
|
||||
by_label = defaultdict(int)
|
||||
people = set()
|
||||
|
||||
for event in events:
|
||||
camera = event.get('camera', 'Unknown')
|
||||
label = event.get('label', 'Unknown')
|
||||
sub_label = event.get('sub_label', None)
|
||||
|
||||
by_camera[camera][label] += 1
|
||||
by_label[label] += 1
|
||||
|
||||
if label == 'person' and sub_label:
|
||||
people.add(sub_label)
|
||||
|
||||
# Format report
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M CET')
|
||||
report = f"""🎥 **Frigate Daily Report** — {timestamp}
|
||||
|
||||
📊 **ZUSAMMENFASSUNG**
|
||||
• Gesamt Events: {len(events)}
|
||||
• Detektierte Personen: {len(people)}
|
||||
• Kameras aktiv: {len(by_camera)}
|
||||
|
||||
👥 **Erkannte Personen**
|
||||
"""
|
||||
|
||||
for person in sorted(people):
|
||||
report += f" • {person}\n"
|
||||
|
||||
report += f"\n📹 **Nach Kamera**\n"
|
||||
|
||||
for camera in sorted(by_camera.keys()):
|
||||
events_count = sum(by_camera[camera].values())
|
||||
labels = ", ".join(by_camera[camera].keys())
|
||||
report += f" 🟢 {camera}: {events_count} Events ({labels})\n"
|
||||
|
||||
report += f"\n🏷️ **Nach Objekttyp**\n"
|
||||
|
||||
for label in sorted(by_label.keys()):
|
||||
count = by_label[label]
|
||||
report += f" • {label.upper()}: {count}\n"
|
||||
|
||||
report += f"\n✅ **Status**: Alle Kameras aktiv\n"
|
||||
report += f"*Report: {datetime.now().strftime('%H:%M:%S UTC')}*"
|
||||
|
||||
return report
|
||||
|
||||
if __name__ == "__main__":
|
||||
report = generate_report()
|
||||
print(report)
|
||||
99
src/main.py
99
src/main.py
|
|
@ -1,99 +0,0 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import signal
|
||||
from src.config import get_config
|
||||
from src.bot.binance_client import BinanceClientWrapper
|
||||
from src.bot.engine import TradingEngine
|
||||
from src.integrations.telegram_notifier import TelegramNotifier
|
||||
from src.integrations.obsidian_logger import ObsidianLogger
|
||||
from src.strategies.dca import DCAStrategy
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def main():
|
||||
"""Main bot entry point"""
|
||||
|
||||
# Load config
|
||||
config = get_config()
|
||||
logger.info(f"Starting bot | Environment: {config.environment} | Pair: {config.trading_pair}")
|
||||
|
||||
# Select credentials based on environment
|
||||
if config.environment == "testnet":
|
||||
api_key = config.binance_api_key_testnet
|
||||
api_secret = config.binance_api_secret_testnet
|
||||
else:
|
||||
api_key = config.binance_api_key_live
|
||||
api_secret = config.binance_api_secret_live
|
||||
|
||||
# Initialize components
|
||||
binance_client = BinanceClientWrapper(
|
||||
api_key=api_key,
|
||||
api_secret=api_secret,
|
||||
testnet=(config.environment == "testnet")
|
||||
)
|
||||
|
||||
telegram = TelegramNotifier(
|
||||
bot_token=config.telegram_bot_token,
|
||||
chat_id=config.telegram_chat_id
|
||||
)
|
||||
|
||||
obsidian = ObsidianLogger(
|
||||
vault_path=config.obsidian_vault_path,
|
||||
trade_log_file=config.obsidian_trade_log_file
|
||||
)
|
||||
|
||||
strategy = DCAStrategy(
|
||||
trading_pair=config.trading_pair,
|
||||
dca_amount_usd=config.dca_amount_usd,
|
||||
interval_hours=config.dca_interval_hours,
|
||||
stop_loss_percent=config.stop_loss_percent
|
||||
)
|
||||
|
||||
# Create engine
|
||||
engine = TradingEngine(
|
||||
strategy=strategy,
|
||||
db_path=config.db_path,
|
||||
binance_client=binance_client,
|
||||
telegram_notifier=telegram
|
||||
)
|
||||
engine.dry_run = config.dry_run # Enable dry-run mode if configured
|
||||
|
||||
# Initialize
|
||||
await engine.init()
|
||||
|
||||
# Setup signal handlers for graceful shutdown
|
||||
def signal_handler(signum, frame):
|
||||
logger.info("Shutdown signal received")
|
||||
asyncio.create_task(engine.shutdown())
|
||||
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# Send startup message
|
||||
startup_msg = f"""
|
||||
✅ <b>Bot Started</b>
|
||||
Environment: {config.environment}
|
||||
Pair: {config.trading_pair}
|
||||
DCA Amount: ${config.dca_amount_usd}
|
||||
Interval: {config.dca_interval_hours}h
|
||||
Stop Loss: {config.stop_loss_percent}%
|
||||
"""
|
||||
await telegram.send_alert(startup_msg)
|
||||
|
||||
# Start trading
|
||||
try:
|
||||
await engine.start()
|
||||
except Exception as e:
|
||||
logger.error(f"Bot fatal error: {e}")
|
||||
await telegram.send_alert(f"❌ Bot crashed: {str(e)}")
|
||||
raise
|
||||
finally:
|
||||
await engine.shutdown()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
import asyncio, logging, joblib, time
|
||||
from datetime import datetime
|
||||
from src.config import get_config
|
||||
from src.bot.binance_client import BinanceClientWrapper
|
||||
from src.integrations.telegram_notifier import TelegramNotifier
|
||||
from src.integrations.obsidian_logger import ObsidianLogger
|
||||
from src.strategies.ml_strategy import MLStrategy
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MLTradingBot:
|
||||
def __init__(self, config, binance, telegram, obsidian, model, scaler):
|
||||
self.config = config
|
||||
self.binance = binance
|
||||
self.telegram = telegram
|
||||
self.obsidian = obsidian
|
||||
self.model = model
|
||||
self.scaler = scaler
|
||||
self.strategy = MLStrategy(trading_pair=config.trading_pair)
|
||||
|
||||
# Trading state
|
||||
self.last_report_time = time.time()
|
||||
self.report_interval = 10800 # 3 HOURS (10800 seconds)
|
||||
self.trades_today = 0
|
||||
self.wins_today = 0
|
||||
self.losses_today = 0
|
||||
self.daily_pnl = 0.0
|
||||
self.report_count = 0
|
||||
|
||||
async def get_market_data(self):
|
||||
"""Fetch current market price and stats"""
|
||||
try:
|
||||
ticker = self.config.trading_pair.split('/')[0] # BTC from BTCUSDT
|
||||
symbol = f"{ticker}USDT"
|
||||
|
||||
# Get current price
|
||||
price_data = await self.binance.get_ticker_price(symbol)
|
||||
if not price_data:
|
||||
return None
|
||||
|
||||
current_price = float(price_data)
|
||||
|
||||
return {
|
||||
'ticker': ticker,
|
||||
'current_price': current_price,
|
||||
'symbol': symbol
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Market data fetch error: {e}")
|
||||
return None
|
||||
|
||||
async def get_account_balance(self):
|
||||
"""Get current account balance"""
|
||||
try:
|
||||
balance = self.binance.get_balance('USDT')
|
||||
if balance:
|
||||
return {'USDT': {'total': balance}}
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Balance fetch error: {e}")
|
||||
return {}
|
||||
|
||||
async def send_performance_report(self):
|
||||
"""Send 3-hourly performance report"""
|
||||
try:
|
||||
self.report_count += 1
|
||||
|
||||
# Get market data
|
||||
market = await self.get_market_data()
|
||||
if not market:
|
||||
logger.warning("No market data available")
|
||||
return
|
||||
|
||||
# Get account balance
|
||||
balances = await self.get_account_balance()
|
||||
usdt_balance = balances.get('USDT', {}).get('total', 0)
|
||||
|
||||
# Build report
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
report = f"""
|
||||
📊 **PERFORMANCE REPORT #{self.report_count}** — {timestamp}
|
||||
|
||||
🎯 **MARKET STATUS:**
|
||||
├─ {market['ticker']}/USDT: ${market['current_price']:,.2f}
|
||||
├─ Trades Today: {self.trades_today}
|
||||
├─ Wins: {self.wins_today} | Losses: {self.losses_today}
|
||||
└─ Daily P&L: ${self.daily_pnl:+.2f}
|
||||
|
||||
💰 **ACCOUNT STATUS:**
|
||||
├─ USDT Balance: ${usdt_balance:,.2f}
|
||||
├─ Device: CPU
|
||||
├─ Mode: Live Trading
|
||||
└─ Strategy: ML (92% accuracy, 60% threshold)
|
||||
|
||||
📈 **BOT STATUS: RUNNING ✅**
|
||||
"""
|
||||
|
||||
# Send to Telegram (FIXED — now actually sends!)
|
||||
success = await self.telegram.send_alert(report.strip())
|
||||
if success:
|
||||
logger.info(f"✅ Performance report #{self.report_count} sent to Telegram")
|
||||
else:
|
||||
logger.warning(f"❌ Failed to send report #{self.report_count} to Telegram")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Report error: {e}")
|
||||
|
||||
async def monitor_trades(self):
|
||||
"""Monitor open trades and check signals"""
|
||||
try:
|
||||
symbol = f"{self.config.trading_pair.split('/')[0]}USDT"
|
||||
orders = self.binance.get_open_orders(symbol)
|
||||
|
||||
if orders and len(orders) > 0:
|
||||
logger.info(f"📈 Open orders: {len(orders)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Trade monitoring: {e}")
|
||||
|
||||
async def run(self):
|
||||
"""Main bot loop"""
|
||||
logger.info(f"🤖 Starting ML Trading Bot — {self.config.trading_pair}")
|
||||
|
||||
startup_msg = f"""🤖 **BOT STARTED - V2 ML ADAPTIVE**
|
||||
|
||||
✅ Strategy: ML Adaptive (60% threshold)
|
||||
✅ Models: BTC 92% accuracy
|
||||
✅ Device: CPU (Live)
|
||||
✅ Reporting: EVERY 3 HOURS
|
||||
✅ Status: ACTIVE & MONITORING"""
|
||||
|
||||
await self.telegram.send_alert(startup_msg)
|
||||
logger.info("✅ Startup message sent to Telegram")
|
||||
|
||||
logger.info("🟢 Bot running — sending reports every 3 hours...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
# Send 3-hourly performance report
|
||||
if (current_time - self.last_report_time) >= self.report_interval:
|
||||
logger.info(f"⏰ Time for Report #{self.report_count + 1}")
|
||||
await self.send_performance_report()
|
||||
self.last_report_time = current_time
|
||||
|
||||
# Monitor trades every 5 minutes
|
||||
await self.monitor_trades()
|
||||
|
||||
# Sleep for 5 minutes
|
||||
await asyncio.sleep(300)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Bot interrupted by user")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Bot error: {e}")
|
||||
try:
|
||||
await self.telegram.send_alert(f"❌ Bot Error: {str(e)[:100]}")
|
||||
except:
|
||||
pass
|
||||
await asyncio.sleep(60)
|
||||
|
||||
async def main():
|
||||
config = get_config()
|
||||
|
||||
if config.environment == 'testnet':
|
||||
api_key, api_secret = config.binance_api_key_testnet, config.binance_api_secret_testnet
|
||||
else:
|
||||
api_key, api_secret = config.binance_api_key_live, config.binance_api_secret_live
|
||||
|
||||
binance = BinanceClientWrapper(api_key=api_key, api_secret=api_secret, testnet=(config.environment=='testnet'))
|
||||
telegram = TelegramNotifier(bot_token=config.telegram_bot_token, chat_id=config.telegram_chat_id)
|
||||
obsidian = ObsidianLogger(vault_path=config.obsidian_vault_path, trade_log_file=config.obsidian_trade_log_file)
|
||||
|
||||
try:
|
||||
# Load BTC model
|
||||
model = joblib.load('/tmp/model_BTC.pkl')
|
||||
scaler = joblib.load('/tmp/scaler_BTC.pkl')
|
||||
logger.info(f'✅ ML Model loaded: BTC (92% accuracy)')
|
||||
except Exception as e:
|
||||
logger.error(f'❌ ML Model Error: {e}')
|
||||
return
|
||||
|
||||
bot = MLTradingBot(config, binance, telegram, obsidian, model, scaler)
|
||||
await bot.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,431 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V5 ENHANCED - FULLY FIXED VERSION
|
||||
Implementiert: SL, TP, Daily Limit, R:R Ratio
|
||||
FIXED: Binance API method (order_take_profit → create_order)
|
||||
FIXED: PRICE_FILTER für SL Orders durch Tick-Rounding
|
||||
FIXED: Quantity rounding mit Decimal (no floating point errors)
|
||||
FIXED: Quantity string formatting für Binance
|
||||
NEW: Startup Message + 3h Performance Reports via Telegram
|
||||
"""
|
||||
import os, asyncio, logging, random, json, time, math, requests
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load env
|
||||
env = {}
|
||||
with open('/home/marc/bot-deploy/.env') as f:
|
||||
for line in f:
|
||||
k,_,v = line.partition('=')
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
class TradingBot:
|
||||
def __init__(self):
|
||||
self.client = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||
|
||||
self.PAIRS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
self.SIGNAL_THRESHOLD = 5 # 5% random signal
|
||||
self.INVESTMENT_PERCENT = 18 # 18% per trade (5 parallel = 90% max, 10% buffer)
|
||||
self.STOP_LOSS_PERCENT = 2.5 # -2.5%
|
||||
self.TAKE_PROFIT_PERCENT = 3.0 # +3%
|
||||
self.DAILY_LOSS_LIMIT = -5 # -5% max
|
||||
|
||||
self.active_trades = {}
|
||||
self.daily_pnl = 0
|
||||
self.paused = False
|
||||
self.start_time = datetime.now()
|
||||
self.trades_today = 0
|
||||
self.wins_today = 0
|
||||
self.losses_today = 0
|
||||
|
||||
# Precision cache
|
||||
self.pair_precision = {}
|
||||
self._load_pair_precision()
|
||||
|
||||
# Telegram
|
||||
self.telegram_token = env.get('TELEGRAM_BOT_TOKEN')
|
||||
self.telegram_chat_id = env.get('TELEGRAM_CHAT_ID')
|
||||
|
||||
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||
|
||||
# Send startup message
|
||||
self._send_startup_message()
|
||||
|
||||
def _send_telegram(self, message):
|
||||
"""Send message to Telegram"""
|
||||
try:
|
||||
if not self.telegram_token or not self.telegram_chat_id:
|
||||
logger.warning("Telegram not configured")
|
||||
return False
|
||||
|
||||
url = f"https://api.telegram.org/bot{self.telegram_token}/sendMessage"
|
||||
data = {
|
||||
'chat_id': self.telegram_chat_id,
|
||||
'text': message,
|
||||
'parse_mode': 'Markdown'
|
||||
}
|
||||
response = requests.post(url, data=data, timeout=5)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.error(f"Telegram Error: {e}")
|
||||
return False
|
||||
|
||||
def _send_startup_message(self):
|
||||
"""Send startup message with current strategy"""
|
||||
message = """🤖 **TRADING BOT V5 — STARTED!**
|
||||
|
||||
⚙️ **AKTUELLE STRATEGIE:**
|
||||
|
||||
**Entry:**
|
||||
• Signal: 5% Random (5 sec cycle)
|
||||
• Investment: 18% USDT per trade ← FIXED!
|
||||
• Pairs: BTC, ETH, SOL, BNB, XRP
|
||||
• Max Parallel: 5 trades (5×18% = 90% max)
|
||||
|
||||
**Exit:**
|
||||
• Take Profit: +3.0% ✅
|
||||
• Stop Loss: -2.5% ✅
|
||||
• Risk/Reward: 1:1.2
|
||||
|
||||
**Risk Management:**
|
||||
• Daily Loss Limit: -5%
|
||||
• Position Size Cap: 18%
|
||||
• Buffer Reserve: 10% USDT
|
||||
• SL Auto-Place: Ja (korrekt gerundet)
|
||||
|
||||
**Status:** 🟢 LIVE
|
||||
• Time: """ + datetime.now().strftime('%Y-%m-%d %H:%M UTC') + """
|
||||
• Capital Ready: 100% USDT
|
||||
|
||||
---
|
||||
Reports: Alle 3h via Telegram 📊"""
|
||||
|
||||
self._send_telegram(message)
|
||||
logger.info("📱 Startup message sent to Telegram")
|
||||
|
||||
def _load_pair_precision(self):
|
||||
"""Load Binance precision rules for each pair"""
|
||||
for pair in self.PAIRS:
|
||||
try:
|
||||
info = self.client.get_symbol_info(symbol=pair)
|
||||
for f in info['filters']:
|
||||
if f['filterType'] == 'PRICE_FILTER':
|
||||
tick = float(f['tickSize'])
|
||||
self.pair_precision[pair] = {
|
||||
'tick': tick,
|
||||
'decimals': self._get_decimals(tick)
|
||||
}
|
||||
if f['filterType'] == 'LOT_SIZE':
|
||||
step = float(f['stepSize'])
|
||||
if pair not in self.pair_precision:
|
||||
self.pair_precision[pair] = {}
|
||||
self.pair_precision[pair]['step'] = step
|
||||
self.pair_precision[pair]['step_decimals'] = self._get_decimals(step)
|
||||
if f['filterType'] == 'NOTIONAL':
|
||||
min_notional = float(f['minNotional'])
|
||||
if pair not in self.pair_precision:
|
||||
self.pair_precision[pair] = {}
|
||||
self.pair_precision[pair]['min_notional'] = min_notional
|
||||
except Exception as e:
|
||||
logger.error(f"Precision load {pair}: {e}")
|
||||
|
||||
def _get_decimals(self, tick):
|
||||
"""Get decimal places from tick size"""
|
||||
s = str(tick)
|
||||
if 'e' in s:
|
||||
return int(s.split('e-')[1]) if 'e-' in s else 0
|
||||
return len(s.split('.')[1]) if '.' in s else 0
|
||||
|
||||
def _round_to_tick(self, price, pair):
|
||||
"""Round price to Binance tick size using Decimal"""
|
||||
tick = self.pair_precision.get(pair, {}).get('tick', 0.01)
|
||||
price_decimal = Decimal(str(price))
|
||||
tick_decimal = Decimal(str(tick))
|
||||
|
||||
rounded = (price_decimal / tick_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * tick_decimal
|
||||
return float(rounded)
|
||||
|
||||
def _round_quantity(self, qty, pair):
|
||||
"""Round quantity to Binance step size using Decimal - NO PRECISION LOSS"""
|
||||
step = self.pair_precision.get(pair, {}).get('step', 0.00001)
|
||||
step_decimals = self.pair_precision.get(pair, {}).get('step_decimals', 5)
|
||||
|
||||
qty_decimal = Decimal(str(qty))
|
||||
step_decimal = Decimal(str(step))
|
||||
|
||||
# Round down (safe side)
|
||||
rounded = (qty_decimal / step_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * step_decimal
|
||||
|
||||
# Format as string with exactly the right decimals
|
||||
format_str = f"0.{'':<{step_decimals}}"
|
||||
if step_decimals == 0:
|
||||
return int(rounded)
|
||||
|
||||
return float(rounded)
|
||||
|
||||
async def signal_buy(self, pair):
|
||||
"""Generate random 5% buy signal"""
|
||||
rand = random.randint(1, 100)
|
||||
return rand <= self.SIGNAL_THRESHOLD
|
||||
|
||||
async def place_buy_order(self, pair):
|
||||
"""Place market buy order"""
|
||||
try:
|
||||
# Get current price
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
entry_price = float(ticker['lastPrice'])
|
||||
|
||||
# Calculate quantity
|
||||
account = self.client.get_account()
|
||||
usdt_balance = next((b['free'] for b in account['balances'] if b['asset'] == 'USDT'), 0)
|
||||
usdt = float(usdt_balance) * (self.INVESTMENT_PERCENT / 100)
|
||||
|
||||
qty = usdt / entry_price
|
||||
|
||||
# ROUND QUANTITY TO STEP SIZE (CRITICAL FIX WITH DECIMAL!)
|
||||
qty = self._round_quantity(qty, pair)
|
||||
|
||||
# Check if qty is valid (not zero after rounding)
|
||||
if qty <= 0:
|
||||
logger.warning(f"Quantity too small for {pair}: {qty}")
|
||||
return False
|
||||
|
||||
# VALIDATE NOTIONAL (order_value must be >= min_notional)
|
||||
min_notional = self.pair_precision.get(pair, {}).get('min_notional', 10.0)
|
||||
order_value = qty * entry_price
|
||||
|
||||
if order_value < min_notional:
|
||||
logger.warning(f"Order value too small {pair}: ${order_value:.2f} < ${min_notional:.2f}")
|
||||
return False
|
||||
|
||||
# Place market buy
|
||||
order = self.client.order_market_buy(symbol=pair, quantity=qty)
|
||||
logger.info(f"🟢 BUY: {pair} x{qty} @ ${entry_price:.2f} (value: ${order_value:.2f})")
|
||||
|
||||
# Store trade
|
||||
self.active_trades[pair] = {
|
||||
'entry': entry_price,
|
||||
'qty': qty,
|
||||
'time': datetime.now()
|
||||
}
|
||||
|
||||
# Place SL order (FIXED WITH CORRECT API METHOD)
|
||||
await self.place_stop_loss(pair, entry_price, qty)
|
||||
|
||||
self.trades_today += 1
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Buy Error {pair}: {e}")
|
||||
return False
|
||||
|
||||
async def place_stop_loss(self, pair, entry_price, qty):
|
||||
"""Place stop loss order with correct precision & API method"""
|
||||
try:
|
||||
# Calculate SL price with 2.5% loss
|
||||
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||
|
||||
# ROUND TO TICK SIZE (CRITICAL FIX!)
|
||||
sl_price = self._round_to_tick(sl_price, pair)
|
||||
|
||||
# ROUND QUANTITY TO STEP SIZE (WITH DECIMAL!)
|
||||
qty_rounded = self._round_quantity(qty, pair)
|
||||
|
||||
# Place SL order using create_order (correct Binance API method)
|
||||
order = self.client.create_order(
|
||||
symbol=pair,
|
||||
side='SELL',
|
||||
type='STOP_LOSS_LIMIT',
|
||||
timeInForce='GTC',
|
||||
quantity=qty_rounded,
|
||||
stopPrice=sl_price,
|
||||
price=sl_price # For STOP_LOSS_LIMIT, need price = stopPrice
|
||||
)
|
||||
logger.info(f"🛡️ SL: {pair} x{qty_rounded} @ ${sl_price:.4f} (-{self.STOP_LOSS_PERCENT}%)")
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"SL Error {pair}: {e}")
|
||||
|
||||
async def monitor_positions(self):
|
||||
"""Monitor open positions for TP/SL"""
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
|
||||
for pair in list(self.active_trades.keys()):
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
current = float(ticker['lastPrice'])
|
||||
entry = self.active_trades[pair]['entry']
|
||||
|
||||
gain_percent = ((current - entry) / entry) * 100
|
||||
|
||||
# Check TP
|
||||
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||
await self.close_position(pair, 'TP', current)
|
||||
|
||||
# Check SL (secondary check)
|
||||
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||
await self.close_position(pair, 'SL', current)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Monitor Error: {e}")
|
||||
|
||||
async def close_position(self, pair, reason, current_price):
|
||||
"""Close position"""
|
||||
if pair not in self.active_trades:
|
||||
return
|
||||
|
||||
qty = self.active_trades[pair]['qty']
|
||||
entry = self.active_trades[pair]['entry']
|
||||
pnl = (current_price - entry) * qty
|
||||
|
||||
logger.info(f"📊 {reason}: {pair} closed @ ${current_price:.2f}, PnL: ${pnl:.2f}")
|
||||
|
||||
del self.active_trades[pair]
|
||||
self.daily_pnl += pnl
|
||||
|
||||
if pnl > 0:
|
||||
self.wins_today += 1
|
||||
else:
|
||||
self.losses_today += 1
|
||||
|
||||
# Check daily loss limit
|
||||
if self.daily_pnl <= self.DAILY_LOSS_LIMIT:
|
||||
logger.warning(f"⚠️ DAILY LOSS LIMIT REACHED: ${self.daily_pnl:.2f}")
|
||||
self.paused = True
|
||||
|
||||
def get_performance_report(self):
|
||||
"""Get current performance metrics"""
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
balance = {}
|
||||
|
||||
for asset_data in account['balances']:
|
||||
asset = asset_data['asset']
|
||||
free = float(asset_data['free'])
|
||||
locked = float(asset_data['locked'])
|
||||
total = free + locked
|
||||
|
||||
if total > 0.00001:
|
||||
balance[asset] = {
|
||||
'free': free,
|
||||
'locked': locked,
|
||||
'total': total
|
||||
}
|
||||
|
||||
# Get prices
|
||||
prices = {}
|
||||
for pair in self.PAIRS:
|
||||
try:
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
asset = pair.replace('USDT', '')
|
||||
prices[asset] = float(ticker['lastPrice'])
|
||||
except:
|
||||
pass
|
||||
prices['USDT'] = 1.0
|
||||
|
||||
# Calculate portfolio
|
||||
portfolio = 0
|
||||
tracked = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT']
|
||||
for asset in tracked:
|
||||
if asset in balance:
|
||||
portfolio += balance[asset]['total'] * prices.get(asset, 0)
|
||||
|
||||
return {
|
||||
'portfolio': round(portfolio, 2),
|
||||
'usdt_free': balance.get('USDT', {}).get('free', 0),
|
||||
'daily_pnl': self.daily_pnl,
|
||||
'trades_today': self.trades_today,
|
||||
'wins': self.wins_today,
|
||||
'losses': self.losses_today,
|
||||
'active_trades': len(self.active_trades),
|
||||
'paused': self.paused
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Performance Report Error: {e}")
|
||||
return None
|
||||
|
||||
def send_performance_report(self):
|
||||
"""Send 3h performance report via Telegram"""
|
||||
report = self.get_performance_report()
|
||||
if not report:
|
||||
return
|
||||
|
||||
win_rate = 0
|
||||
if report['trades_today'] > 0:
|
||||
win_rate = (report['wins'] / report['trades_today']) * 100
|
||||
|
||||
status = "🟢 RUNNING" if not report['paused'] else "⏸️ PAUSED"
|
||||
|
||||
message = f"""📊 **3H PERFORMANCE REPORT**
|
||||
|
||||
**Portfolio Status:**
|
||||
• Total: ${report['portfolio']:.2f}
|
||||
• USDT Free: ${report['usdt_free']:.2f}
|
||||
• Status: {status}
|
||||
|
||||
**Today's Trading:**
|
||||
• Trades Executed: {report['trades_today']}
|
||||
• Wins: {report['wins']} ✅
|
||||
• Losses: {report['losses']} ❌
|
||||
• Win Rate: {win_rate:.1f}%
|
||||
|
||||
**P&L:**
|
||||
• Daily P&L: ${report['daily_pnl']:.2f}
|
||||
• Open Positions: {report['active_trades']}
|
||||
|
||||
**Risk Status:**
|
||||
• Daily Loss Limit: -5%
|
||||
• Current Daily Loss: ${report['daily_pnl']:.2f}
|
||||
• Pause Active: {'Yes ⏸️' if report['paused'] else 'No ✅'}
|
||||
|
||||
---
|
||||
Time: {datetime.now().strftime('%Y-%m-%d %H:%M UTC')}
|
||||
Bot: V5 ENHANCED (FULLY FIXED)"""
|
||||
|
||||
self._send_telegram(message)
|
||||
logger.info("📱 Performance report sent to Telegram")
|
||||
|
||||
async def run_cycle(self):
|
||||
"""Main trading cycle"""
|
||||
last_report_hour = None
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Check if it's time for 3h report
|
||||
current_hour = datetime.now().hour
|
||||
if current_hour % 3 == 0 and last_report_hour != current_hour:
|
||||
self.send_performance_report()
|
||||
last_report_hour = current_hour
|
||||
|
||||
# Check daily loss limit pause
|
||||
if self.paused:
|
||||
logger.info("⏸️ Bot PAUSED (daily loss limit reached)")
|
||||
await asyncio.sleep(60)
|
||||
continue
|
||||
|
||||
# Signal generation
|
||||
for pair in self.PAIRS:
|
||||
if pair not in self.active_trades and await self.signal_buy(pair):
|
||||
await self.place_buy_order(pair)
|
||||
|
||||
# Monitor positions
|
||||
await self.monitor_positions()
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cycle Error: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def main():
|
||||
bot = TradingBot()
|
||||
await bot.run_cycle()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,431 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V5 ENHANCED - FULLY FIXED VERSION
|
||||
Implementiert: SL, TP, Daily Limit, R:R Ratio
|
||||
FIXED: Binance API method (order_take_profit → create_order)
|
||||
FIXED: PRICE_FILTER für SL Orders durch Tick-Rounding
|
||||
FIXED: Quantity rounding mit Decimal (no floating point errors)
|
||||
FIXED: Quantity string formatting für Binance
|
||||
NEW: Startup Message + 3h Performance Reports via Telegram
|
||||
"""
|
||||
import os, asyncio, logging, random, json, time, math, requests
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load env
|
||||
env = {}
|
||||
with open('/home/marc/bot-deploy/.env') as f:
|
||||
for line in f:
|
||||
k,_,v = line.partition('=')
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
class TradingBot:
|
||||
def __init__(self):
|
||||
self.client = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||
|
||||
self.PAIRS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
self.SIGNAL_THRESHOLD = 5 # 5% random signal
|
||||
self.INVESTMENT_PERCENT = 30 # 30% per trade (5 parallel = 90% max, 10% buffer)
|
||||
self.STOP_LOSS_PERCENT = 2.5 # -2.5%
|
||||
self.TAKE_PROFIT_PERCENT = 3.0 # +3%
|
||||
self.DAILY_LOSS_LIMIT = -5 # -5% max
|
||||
|
||||
self.active_trades = {}
|
||||
self.daily_pnl = 0
|
||||
self.paused = False
|
||||
self.start_time = datetime.now()
|
||||
self.trades_today = 0
|
||||
self.wins_today = 0
|
||||
self.losses_today = 0
|
||||
|
||||
# Precision cache
|
||||
self.pair_precision = {}
|
||||
self._load_pair_precision()
|
||||
|
||||
# Telegram
|
||||
self.telegram_token = env.get('TELEGRAM_BOT_TOKEN')
|
||||
self.telegram_chat_id = env.get('TELEGRAM_CHAT_ID')
|
||||
|
||||
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||
|
||||
# Send startup message
|
||||
self._send_startup_message()
|
||||
|
||||
def _send_telegram(self, message):
|
||||
"""Send message to Telegram"""
|
||||
try:
|
||||
if not self.telegram_token or not self.telegram_chat_id:
|
||||
logger.warning("Telegram not configured")
|
||||
return False
|
||||
|
||||
url = f"https://api.telegram.org/bot{self.telegram_token}/sendMessage"
|
||||
data = {
|
||||
'chat_id': self.telegram_chat_id,
|
||||
'text': message,
|
||||
'parse_mode': 'Markdown'
|
||||
}
|
||||
response = requests.post(url, data=data, timeout=5)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.error(f"Telegram Error: {e}")
|
||||
return False
|
||||
|
||||
def _send_startup_message(self):
|
||||
"""Send startup message with current strategy"""
|
||||
message = """🤖 **TRADING BOT V5 — STARTED!**
|
||||
|
||||
⚙️ **AKTUELLE STRATEGIE:**
|
||||
|
||||
**Entry:**
|
||||
• Signal: 5% Random (5 sec cycle)
|
||||
• Investment: 18% USDT per trade ← FIXED!
|
||||
• Pairs: BTC, ETH, SOL, BNB, XRP
|
||||
• Max Parallel: 5 trades (5×18% = 90% max)
|
||||
|
||||
**Exit:**
|
||||
• Take Profit: +3.0% ✅
|
||||
• Stop Loss: -2.5% ✅
|
||||
• Risk/Reward: 1:1.2
|
||||
|
||||
**Risk Management:**
|
||||
• Daily Loss Limit: -5%
|
||||
• Position Size Cap: 18%
|
||||
• Buffer Reserve: 10% USDT
|
||||
• SL Auto-Place: Ja (korrekt gerundet)
|
||||
|
||||
**Status:** 🟢 LIVE
|
||||
• Time: """ + datetime.now().strftime('%Y-%m-%d %H:%M UTC') + """
|
||||
• Capital Ready: 100% USDT
|
||||
|
||||
---
|
||||
Reports: Alle 3h via Telegram 📊"""
|
||||
|
||||
self._send_telegram(message)
|
||||
logger.info("📱 Startup message sent to Telegram")
|
||||
|
||||
def _load_pair_precision(self):
|
||||
"""Load Binance precision rules for each pair"""
|
||||
for pair in self.PAIRS:
|
||||
try:
|
||||
info = self.client.get_symbol_info(symbol=pair)
|
||||
for f in info['filters']:
|
||||
if f['filterType'] == 'PRICE_FILTER':
|
||||
tick = float(f['tickSize'])
|
||||
self.pair_precision[pair] = {
|
||||
'tick': tick,
|
||||
'decimals': self._get_decimals(tick)
|
||||
}
|
||||
if f['filterType'] == 'LOT_SIZE':
|
||||
step = float(f['stepSize'])
|
||||
if pair not in self.pair_precision:
|
||||
self.pair_precision[pair] = {}
|
||||
self.pair_precision[pair]['step'] = step
|
||||
self.pair_precision[pair]['step_decimals'] = self._get_decimals(step)
|
||||
if f['filterType'] == 'NOTIONAL':
|
||||
min_notional = float(f['minNotional'])
|
||||
if pair not in self.pair_precision:
|
||||
self.pair_precision[pair] = {}
|
||||
self.pair_precision[pair]['min_notional'] = min_notional
|
||||
except Exception as e:
|
||||
logger.error(f"Precision load {pair}: {e}")
|
||||
|
||||
def _get_decimals(self, tick):
|
||||
"""Get decimal places from tick size"""
|
||||
s = str(tick)
|
||||
if 'e' in s:
|
||||
return int(s.split('e-')[1]) if 'e-' in s else 0
|
||||
return len(s.split('.')[1]) if '.' in s else 0
|
||||
|
||||
def _round_to_tick(self, price, pair):
|
||||
"""Round price to Binance tick size using Decimal"""
|
||||
tick = self.pair_precision.get(pair, {}).get('tick', 0.01)
|
||||
price_decimal = Decimal(str(price))
|
||||
tick_decimal = Decimal(str(tick))
|
||||
|
||||
rounded = (price_decimal / tick_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * tick_decimal
|
||||
return float(rounded)
|
||||
|
||||
def _round_quantity(self, qty, pair):
|
||||
"""Round quantity to Binance step size using Decimal - NO PRECISION LOSS"""
|
||||
step = self.pair_precision.get(pair, {}).get('step', 0.00001)
|
||||
step_decimals = self.pair_precision.get(pair, {}).get('step_decimals', 5)
|
||||
|
||||
qty_decimal = Decimal(str(qty))
|
||||
step_decimal = Decimal(str(step))
|
||||
|
||||
# Round down (safe side)
|
||||
rounded = (qty_decimal / step_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * step_decimal
|
||||
|
||||
# Format as string with exactly the right decimals
|
||||
format_str = f"0.{'':<{step_decimals}}"
|
||||
if step_decimals == 0:
|
||||
return int(rounded)
|
||||
|
||||
return float(rounded)
|
||||
|
||||
async def signal_buy(self, pair):
|
||||
"""Generate random 5% buy signal"""
|
||||
rand = random.randint(1, 100)
|
||||
return rand <= self.SIGNAL_THRESHOLD
|
||||
|
||||
async def place_buy_order(self, pair):
|
||||
"""Place market buy order"""
|
||||
try:
|
||||
# Get current price
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
entry_price = float(ticker['lastPrice'])
|
||||
|
||||
# Calculate quantity
|
||||
account = self.client.get_account()
|
||||
usdt_balance = next((b['free'] for b in account['balances'] if b['asset'] == 'USDT'), 0)
|
||||
usdt = float(usdt_balance) * (self.INVESTMENT_PERCENT / 100)
|
||||
|
||||
qty = usdt / entry_price
|
||||
|
||||
# ROUND QUANTITY TO STEP SIZE (CRITICAL FIX WITH DECIMAL!)
|
||||
qty = self._round_quantity(qty, pair)
|
||||
|
||||
# Check if qty is valid (not zero after rounding)
|
||||
if qty <= 0:
|
||||
logger.warning(f"Quantity too small for {pair}: {qty}")
|
||||
return False
|
||||
|
||||
# VALIDATE NOTIONAL (order_value must be >= min_notional)
|
||||
min_notional = self.pair_precision.get(pair, {}).get('min_notional', 10.0)
|
||||
order_value = qty * entry_price
|
||||
|
||||
if order_value < min_notional:
|
||||
logger.warning(f"Order value too small {pair}: ${order_value:.2f} < ${min_notional:.2f}")
|
||||
return False
|
||||
|
||||
# Place market buy
|
||||
order = self.client.order_market_buy(symbol=pair, quantity=qty)
|
||||
logger.info(f"🟢 BUY: {pair} x{qty} @ ${entry_price:.2f} (value: ${order_value:.2f})")
|
||||
|
||||
# Store trade
|
||||
self.active_trades[pair] = {
|
||||
'entry': entry_price,
|
||||
'qty': qty,
|
||||
'time': datetime.now()
|
||||
}
|
||||
|
||||
# Place SL order (FIXED WITH CORRECT API METHOD)
|
||||
await self.place_stop_loss(pair, entry_price, qty)
|
||||
|
||||
self.trades_today += 1
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Buy Error {pair}: {e}")
|
||||
return False
|
||||
|
||||
async def place_stop_loss(self, pair, entry_price, qty):
|
||||
"""Place stop loss order with correct precision & API method"""
|
||||
try:
|
||||
# Calculate SL price with 2.5% loss
|
||||
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||
|
||||
# ROUND TO TICK SIZE (CRITICAL FIX!)
|
||||
sl_price = self._round_to_tick(sl_price, pair)
|
||||
|
||||
# ROUND QUANTITY TO STEP SIZE (WITH DECIMAL!)
|
||||
qty_rounded = self._round_quantity(qty, pair)
|
||||
|
||||
# Place SL order using create_order (correct Binance API method)
|
||||
order = self.client.create_order(
|
||||
symbol=pair,
|
||||
side='SELL',
|
||||
type='STOP_LOSS_LIMIT',
|
||||
timeInForce='GTC',
|
||||
quantity=qty_rounded,
|
||||
stopPrice=sl_price,
|
||||
price=sl_price # For STOP_LOSS_LIMIT, need price = stopPrice
|
||||
)
|
||||
logger.info(f"🛡️ SL: {pair} x{qty_rounded} @ ${sl_price:.4f} (-{self.STOP_LOSS_PERCENT}%)")
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"SL Error {pair}: {e}")
|
||||
|
||||
async def monitor_positions(self):
|
||||
"""Monitor open positions for TP/SL"""
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
|
||||
for pair in list(self.active_trades.keys()):
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
current = float(ticker['lastPrice'])
|
||||
entry = self.active_trades[pair]['entry']
|
||||
|
||||
gain_percent = ((current - entry) / entry) * 100
|
||||
|
||||
# Check TP
|
||||
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||
await self.close_position(pair, 'TP', current)
|
||||
|
||||
# Check SL (secondary check)
|
||||
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||
await self.close_position(pair, 'SL', current)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Monitor Error: {e}")
|
||||
|
||||
async def close_position(self, pair, reason, current_price):
|
||||
"""Close position"""
|
||||
if pair not in self.active_trades:
|
||||
return
|
||||
|
||||
qty = self.active_trades[pair]['qty']
|
||||
entry = self.active_trades[pair]['entry']
|
||||
pnl = (current_price - entry) * qty
|
||||
|
||||
logger.info(f"📊 {reason}: {pair} closed @ ${current_price:.2f}, PnL: ${pnl:.2f}")
|
||||
|
||||
del self.active_trades[pair]
|
||||
self.daily_pnl += pnl
|
||||
|
||||
if pnl > 0:
|
||||
self.wins_today += 1
|
||||
else:
|
||||
self.losses_today += 1
|
||||
|
||||
# Check daily loss limit
|
||||
if self.daily_pnl <= self.DAILY_LOSS_LIMIT:
|
||||
logger.warning(f"⚠️ DAILY LOSS LIMIT REACHED: ${self.daily_pnl:.2f}")
|
||||
self.paused = True
|
||||
|
||||
def get_performance_report(self):
|
||||
"""Get current performance metrics"""
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
balance = {}
|
||||
|
||||
for asset_data in account['balances']:
|
||||
asset = asset_data['asset']
|
||||
free = float(asset_data['free'])
|
||||
locked = float(asset_data['locked'])
|
||||
total = free + locked
|
||||
|
||||
if total > 0.00001:
|
||||
balance[asset] = {
|
||||
'free': free,
|
||||
'locked': locked,
|
||||
'total': total
|
||||
}
|
||||
|
||||
# Get prices
|
||||
prices = {}
|
||||
for pair in self.PAIRS:
|
||||
try:
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
asset = pair.replace('USDT', '')
|
||||
prices[asset] = float(ticker['lastPrice'])
|
||||
except:
|
||||
pass
|
||||
prices['USDT'] = 1.0
|
||||
|
||||
# Calculate portfolio
|
||||
portfolio = 0
|
||||
tracked = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT']
|
||||
for asset in tracked:
|
||||
if asset in balance:
|
||||
portfolio += balance[asset]['total'] * prices.get(asset, 0)
|
||||
|
||||
return {
|
||||
'portfolio': round(portfolio, 2),
|
||||
'usdt_free': balance.get('USDT', {}).get('free', 0),
|
||||
'daily_pnl': self.daily_pnl,
|
||||
'trades_today': self.trades_today,
|
||||
'wins': self.wins_today,
|
||||
'losses': self.losses_today,
|
||||
'active_trades': len(self.active_trades),
|
||||
'paused': self.paused
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Performance Report Error: {e}")
|
||||
return None
|
||||
|
||||
def send_performance_report(self):
|
||||
"""Send 3h performance report via Telegram"""
|
||||
report = self.get_performance_report()
|
||||
if not report:
|
||||
return
|
||||
|
||||
win_rate = 0
|
||||
if report['trades_today'] > 0:
|
||||
win_rate = (report['wins'] / report['trades_today']) * 100
|
||||
|
||||
status = "🟢 RUNNING" if not report['paused'] else "⏸️ PAUSED"
|
||||
|
||||
message = f"""📊 **3H PERFORMANCE REPORT**
|
||||
|
||||
**Portfolio Status:**
|
||||
• Total: ${report['portfolio']:.2f}
|
||||
• USDT Free: ${report['usdt_free']:.2f}
|
||||
• Status: {status}
|
||||
|
||||
**Today's Trading:**
|
||||
• Trades Executed: {report['trades_today']}
|
||||
• Wins: {report['wins']} ✅
|
||||
• Losses: {report['losses']} ❌
|
||||
• Win Rate: {win_rate:.1f}%
|
||||
|
||||
**P&L:**
|
||||
• Daily P&L: ${report['daily_pnl']:.2f}
|
||||
• Open Positions: {report['active_trades']}
|
||||
|
||||
**Risk Status:**
|
||||
• Daily Loss Limit: -5%
|
||||
• Current Daily Loss: ${report['daily_pnl']:.2f}
|
||||
• Pause Active: {'Yes ⏸️' if report['paused'] else 'No ✅'}
|
||||
|
||||
---
|
||||
Time: {datetime.now().strftime('%Y-%m-%d %H:%M UTC')}
|
||||
Bot: V5 ENHANCED (FULLY FIXED)"""
|
||||
|
||||
self._send_telegram(message)
|
||||
logger.info("📱 Performance report sent to Telegram")
|
||||
|
||||
async def run_cycle(self):
|
||||
"""Main trading cycle"""
|
||||
last_report_hour = None
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Check if it's time for 3h report
|
||||
current_hour = datetime.now().hour
|
||||
if current_hour % 3 == 0 and last_report_hour != current_hour:
|
||||
self.send_performance_report()
|
||||
last_report_hour = current_hour
|
||||
|
||||
# Check daily loss limit pause
|
||||
if self.paused:
|
||||
logger.info("⏸️ Bot PAUSED (daily loss limit reached)")
|
||||
await asyncio.sleep(60)
|
||||
continue
|
||||
|
||||
# Signal generation
|
||||
for pair in self.PAIRS:
|
||||
if pair not in self.active_trades and await self.signal_buy(pair):
|
||||
await self.place_buy_order(pair)
|
||||
|
||||
# Monitor positions
|
||||
await self.monitor_positions()
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cycle Error: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def main():
|
||||
bot = TradingBot()
|
||||
await bot.run_cycle()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,434 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V5 ENHANCED - FULLY FIXED VERSION
|
||||
Implementiert: SL, TP, Daily Limit, R:R Ratio
|
||||
FIXED: Binance API method (order_take_profit → create_order)
|
||||
FIXED: PRICE_FILTER für SL Orders durch Tick-Rounding
|
||||
FIXED: Quantity rounding mit Decimal (no floating point errors)
|
||||
FIXED: Quantity string formatting für Binance
|
||||
NEW: Startup Message + 3h Performance Reports via Telegram
|
||||
"""
|
||||
import os, asyncio, logging, random, json, time, math, requests
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load env
|
||||
env = {}
|
||||
with open('/home/marc/bot-deploy/.env') as f:
|
||||
for line in f:
|
||||
k,_,v = line.partition('=')
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
class TradingBot:
|
||||
def __init__(self):
|
||||
self.client = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||
|
||||
self.PAIRS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
self.SIGNAL_THRESHOLD = 5 # 5% random signal
|
||||
self.INVESTMENT_PERCENT = 35 # 35% per trade (5 parallel = 90% max, 10% buffer)
|
||||
self.NOTIONAL_MIN = 5.0 # Override Binance minimum to $3
|
||||
self.STOP_LOSS_PERCENT = 2.5 # -2.5%
|
||||
self.TAKE_PROFIT_PERCENT = 3.0 # +3%
|
||||
self.DAILY_LOSS_LIMIT = -5 # -5% max
|
||||
|
||||
self.active_trades = {}
|
||||
self.daily_pnl = 0
|
||||
self.paused = False
|
||||
self.start_time = datetime.now()
|
||||
self.trades_today = 0
|
||||
self.wins_today = 0
|
||||
self.losses_today = 0
|
||||
|
||||
# Precision cache
|
||||
self.pair_precision = {}
|
||||
self._load_pair_precision()
|
||||
|
||||
# Telegram
|
||||
self.telegram_token = env.get('TELEGRAM_BOT_TOKEN')
|
||||
self.telegram_chat_id = env.get('TELEGRAM_CHAT_ID')
|
||||
|
||||
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||
|
||||
# Send startup message
|
||||
self._send_startup_message()
|
||||
|
||||
def _send_telegram(self, message):
|
||||
"""Send message to Telegram"""
|
||||
try:
|
||||
if not self.telegram_token or not self.telegram_chat_id:
|
||||
logger.warning("Telegram not configured")
|
||||
return False
|
||||
|
||||
url = f"https://api.telegram.org/bot{self.telegram_token}/sendMessage"
|
||||
data = {
|
||||
'chat_id': self.telegram_chat_id,
|
||||
'text': message,
|
||||
'parse_mode': 'Markdown'
|
||||
}
|
||||
response = requests.post(url, data=data, timeout=5)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.error(f"Telegram Error: {e}")
|
||||
return False
|
||||
|
||||
def _send_startup_message(self):
|
||||
"""Send startup message with current strategy"""
|
||||
message = """🤖 **TRADING BOT V5 — STARTED!**
|
||||
|
||||
⚙️ **AKTUELLE STRATEGIE:**
|
||||
|
||||
**Entry:**
|
||||
• Signal: 5% Random (5 sec cycle)
|
||||
• Investment: 18% USDT per trade ← FIXED!
|
||||
• Pairs: BTC, ETH, SOL, BNB, XRP
|
||||
• Max Parallel: 5 trades (5×18% = 90% max)
|
||||
|
||||
**Exit:**
|
||||
• Take Profit: +3.0% ✅
|
||||
• Stop Loss: -2.5% ✅
|
||||
• Risk/Reward: 1:1.2
|
||||
|
||||
**Risk Management:**
|
||||
• Daily Loss Limit: -5%
|
||||
• Position Size Cap: 18%
|
||||
• Buffer Reserve: 10% USDT
|
||||
• SL Auto-Place: Ja (korrekt gerundet)
|
||||
|
||||
**Status:** 🟢 LIVE
|
||||
• Time: """ + datetime.now().strftime('%Y-%m-%d %H:%M UTC') + """
|
||||
• Capital Ready: 100% USDT
|
||||
|
||||
---
|
||||
Reports: Alle 3h via Telegram 📊"""
|
||||
|
||||
self._send_telegram(message)
|
||||
logger.info("📱 Startup message sent to Telegram")
|
||||
|
||||
def _load_pair_precision(self):
|
||||
"""Load Binance precision rules for each pair"""
|
||||
for pair in self.PAIRS:
|
||||
try:
|
||||
info = self.client.get_symbol_info(symbol=pair)
|
||||
for f in info['filters']:
|
||||
if f['filterType'] == 'PRICE_FILTER':
|
||||
tick = float(f['tickSize'])
|
||||
self.pair_precision[pair] = {
|
||||
'tick': tick,
|
||||
'decimals': self._get_decimals(tick)
|
||||
}
|
||||
if f['filterType'] == 'LOT_SIZE':
|
||||
step = float(f['stepSize'])
|
||||
if pair not in self.pair_precision:
|
||||
self.pair_precision[pair] = {}
|
||||
self.pair_precision[pair]['step'] = step
|
||||
self.pair_precision[pair]['step_decimals'] = self._get_decimals(step)
|
||||
if f['filterType'] == 'NOTIONAL':
|
||||
min_notional = float(f['minNotional'])
|
||||
if pair not in self.pair_precision:
|
||||
self.pair_precision[pair] = {}
|
||||
self.pair_precision[pair]['min_notional'] = min_notional
|
||||
except Exception as e:
|
||||
logger.error(f"Precision load {pair}: {e}")
|
||||
|
||||
def _get_decimals(self, tick):
|
||||
"""Get decimal places from tick size"""
|
||||
s = str(tick)
|
||||
if 'e' in s:
|
||||
return int(s.split('e-')[1]) if 'e-' in s else 0
|
||||
return len(s.split('.')[1]) if '.' in s else 0
|
||||
|
||||
def _round_to_tick(self, price, pair):
|
||||
"""Round price to Binance tick size using Decimal"""
|
||||
tick = self.pair_precision.get(pair, {}).get('tick', 0.01)
|
||||
price_decimal = Decimal(str(price))
|
||||
tick_decimal = Decimal(str(tick))
|
||||
|
||||
rounded = (price_decimal / tick_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * tick_decimal
|
||||
return float(rounded)
|
||||
|
||||
def _round_quantity(self, qty, pair):
|
||||
"""Round quantity to Binance step size using Decimal - NO PRECISION LOSS"""
|
||||
step = self.pair_precision.get(pair, {}).get('step', 0.00001)
|
||||
step_decimals = self.pair_precision.get(pair, {}).get('step_decimals', 5)
|
||||
|
||||
qty_decimal = Decimal(str(qty))
|
||||
step_decimal = Decimal(str(step))
|
||||
|
||||
# Round down (safe side)
|
||||
rounded = (qty_decimal / step_decimal).quantize(Decimal('1'), rounding=ROUND_DOWN) * step_decimal
|
||||
|
||||
# Format as string with exactly the right decimals
|
||||
format_str = f"0.{'':<{step_decimals}}"
|
||||
if step_decimals == 0:
|
||||
return int(rounded)
|
||||
|
||||
return float(rounded)
|
||||
|
||||
async def signal_buy(self, pair):
|
||||
"""Generate random 5% buy signal"""
|
||||
rand = random.randint(1, 100)
|
||||
return rand <= self.SIGNAL_THRESHOLD
|
||||
|
||||
async def place_buy_order(self, pair):
|
||||
"""Place market buy order"""
|
||||
try:
|
||||
# Get current price
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
entry_price = float(ticker['lastPrice'])
|
||||
|
||||
# Calculate quantity
|
||||
account = self.client.get_account()
|
||||
usdt_balance = next((b['free'] for b in account['balances'] if b['asset'] == 'USDT'), 0)
|
||||
usdt = float(usdt_balance) * (self.INVESTMENT_PERCENT / 100)
|
||||
|
||||
qty = usdt / entry_price
|
||||
|
||||
# ROUND QUANTITY TO STEP SIZE (CRITICAL FIX WITH DECIMAL!)
|
||||
qty = self._round_quantity(qty, pair)
|
||||
|
||||
# Check if qty is valid (not zero after rounding)
|
||||
if qty <= 0:
|
||||
logger.warning(f"Quantity too small for {pair}: {qty}")
|
||||
return False
|
||||
|
||||
# VALIDATE NOTIONAL (order_value must be >= 3.0 MINIMUM)
|
||||
order_value = qty * entry_price
|
||||
NOTIONAL_MIN = 5.0 # Minimum $3
|
||||
|
||||
if order_value < NOTIONAL_MIN:
|
||||
logger.warning(f"Order value too small {pair}: ${order_value:.2f} < ${NOTIONAL_MIN:.2f} (qty={qty}, price={entry_price})")
|
||||
return False
|
||||
|
||||
logger.info(f"✅ NOTIONAL Check Passed: {pair} ${order_value:.2f} >= ${NOTIONAL_MIN:.2f}")
|
||||
|
||||
# Place market buy
|
||||
order = self.client.order_market_buy(symbol=pair, quantity=qty)
|
||||
logger.info(f"🟢 BUY: {pair} x{qty} @ ${entry_price:.2f} (value: ${order_value:.2f})")
|
||||
|
||||
# Store trade
|
||||
self.active_trades[pair] = {
|
||||
'entry': entry_price,
|
||||
'qty': qty,
|
||||
'time': datetime.now()
|
||||
}
|
||||
|
||||
# Place SL order (FIXED WITH CORRECT API METHOD)
|
||||
await self.place_stop_loss(pair, entry_price, qty)
|
||||
|
||||
self.trades_today += 1
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Buy Error {pair}: {e}")
|
||||
return False
|
||||
|
||||
async def place_stop_loss(self, pair, entry_price, qty):
|
||||
"""Place stop loss order with correct precision & API method"""
|
||||
try:
|
||||
# Calculate SL price with 2.5% loss
|
||||
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||
|
||||
# ROUND TO TICK SIZE (CRITICAL FIX!)
|
||||
sl_price = self._round_to_tick(sl_price, pair)
|
||||
|
||||
# ROUND QUANTITY TO STEP SIZE (WITH DECIMAL!)
|
||||
qty_rounded = self._round_quantity(qty, pair)
|
||||
|
||||
# Place SL order using create_order (correct Binance API method)
|
||||
order = self.client.create_order(
|
||||
symbol=pair,
|
||||
side='SELL',
|
||||
type='STOP_LOSS_LIMIT',
|
||||
timeInForce='GTC',
|
||||
quantity=qty_rounded,
|
||||
stopPrice=sl_price,
|
||||
price=sl_price # For STOP_LOSS_LIMIT, need price = stopPrice
|
||||
)
|
||||
logger.info(f"🛡️ SL: {pair} x{qty_rounded} @ ${sl_price:.4f} (-{self.STOP_LOSS_PERCENT}%)")
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"SL Error {pair}: {e}")
|
||||
|
||||
async def monitor_positions(self):
|
||||
"""Monitor open positions for TP/SL"""
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
|
||||
for pair in list(self.active_trades.keys()):
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
current = float(ticker['lastPrice'])
|
||||
entry = self.active_trades[pair]['entry']
|
||||
|
||||
gain_percent = ((current - entry) / entry) * 100
|
||||
|
||||
# Check TP
|
||||
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||
await self.close_position(pair, 'TP', current)
|
||||
|
||||
# Check SL (secondary check)
|
||||
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||
await self.close_position(pair, 'SL', current)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Monitor Error: {e}")
|
||||
|
||||
async def close_position(self, pair, reason, current_price):
|
||||
"""Close position"""
|
||||
if pair not in self.active_trades:
|
||||
return
|
||||
|
||||
qty = self.active_trades[pair]['qty']
|
||||
entry = self.active_trades[pair]['entry']
|
||||
pnl = (current_price - entry) * qty
|
||||
|
||||
logger.info(f"📊 {reason}: {pair} closed @ ${current_price:.2f}, PnL: ${pnl:.2f}")
|
||||
|
||||
del self.active_trades[pair]
|
||||
self.daily_pnl += pnl
|
||||
|
||||
if pnl > 0:
|
||||
self.wins_today += 1
|
||||
else:
|
||||
self.losses_today += 1
|
||||
|
||||
# Check daily loss limit
|
||||
if self.daily_pnl <= self.DAILY_LOSS_LIMIT:
|
||||
logger.warning(f"⚠️ DAILY LOSS LIMIT REACHED: ${self.daily_pnl:.2f}")
|
||||
self.paused = True
|
||||
|
||||
def get_performance_report(self):
|
||||
"""Get current performance metrics"""
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
balance = {}
|
||||
|
||||
for asset_data in account['balances']:
|
||||
asset = asset_data['asset']
|
||||
free = float(asset_data['free'])
|
||||
locked = float(asset_data['locked'])
|
||||
total = free + locked
|
||||
|
||||
if total > 0.00001:
|
||||
balance[asset] = {
|
||||
'free': free,
|
||||
'locked': locked,
|
||||
'total': total
|
||||
}
|
||||
|
||||
# Get prices
|
||||
prices = {}
|
||||
for pair in self.PAIRS:
|
||||
try:
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
asset = pair.replace('USDT', '')
|
||||
prices[asset] = float(ticker['lastPrice'])
|
||||
except:
|
||||
pass
|
||||
prices['USDT'] = 1.0
|
||||
|
||||
# Calculate portfolio
|
||||
portfolio = 0
|
||||
tracked = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT']
|
||||
for asset in tracked:
|
||||
if asset in balance:
|
||||
portfolio += balance[asset]['total'] * prices.get(asset, 0)
|
||||
|
||||
return {
|
||||
'portfolio': round(portfolio, 2),
|
||||
'usdt_free': balance.get('USDT', {}).get('free', 0),
|
||||
'daily_pnl': self.daily_pnl,
|
||||
'trades_today': self.trades_today,
|
||||
'wins': self.wins_today,
|
||||
'losses': self.losses_today,
|
||||
'active_trades': len(self.active_trades),
|
||||
'paused': self.paused
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Performance Report Error: {e}")
|
||||
return None
|
||||
|
||||
def send_performance_report(self):
|
||||
"""Send 3h performance report via Telegram"""
|
||||
report = self.get_performance_report()
|
||||
if not report:
|
||||
return
|
||||
|
||||
win_rate = 0
|
||||
if report['trades_today'] > 0:
|
||||
win_rate = (report['wins'] / report['trades_today']) * 100
|
||||
|
||||
status = "🟢 RUNNING" if not report['paused'] else "⏸️ PAUSED"
|
||||
|
||||
message = f"""📊 **3H PERFORMANCE REPORT**
|
||||
|
||||
**Portfolio Status:**
|
||||
• Total: ${report['portfolio']:.2f}
|
||||
• USDT Free: ${report['usdt_free']:.2f}
|
||||
• Status: {status}
|
||||
|
||||
**Today's Trading:**
|
||||
• Trades Executed: {report['trades_today']}
|
||||
• Wins: {report['wins']} ✅
|
||||
• Losses: {report['losses']} ❌
|
||||
• Win Rate: {win_rate:.1f}%
|
||||
|
||||
**P&L:**
|
||||
• Daily P&L: ${report['daily_pnl']:.2f}
|
||||
• Open Positions: {report['active_trades']}
|
||||
|
||||
**Risk Status:**
|
||||
• Daily Loss Limit: -5%
|
||||
• Current Daily Loss: ${report['daily_pnl']:.2f}
|
||||
• Pause Active: {'Yes ⏸️' if report['paused'] else 'No ✅'}
|
||||
|
||||
---
|
||||
Time: {datetime.now().strftime('%Y-%m-%d %H:%M UTC')}
|
||||
Bot: V5 ENHANCED (FULLY FIXED)"""
|
||||
|
||||
self._send_telegram(message)
|
||||
logger.info("📱 Performance report sent to Telegram")
|
||||
|
||||
async def run_cycle(self):
|
||||
"""Main trading cycle"""
|
||||
last_report_hour = None
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Check if it's time for 3h report
|
||||
current_hour = datetime.now().hour
|
||||
if current_hour % 3 == 0 and last_report_hour != current_hour:
|
||||
self.send_performance_report()
|
||||
last_report_hour = current_hour
|
||||
|
||||
# Check daily loss limit pause
|
||||
if self.paused:
|
||||
logger.info("⏸️ Bot PAUSED (daily loss limit reached)")
|
||||
await asyncio.sleep(60)
|
||||
continue
|
||||
|
||||
# Signal generation
|
||||
for pair in self.PAIRS:
|
||||
if pair not in self.active_trades and await self.signal_buy(pair):
|
||||
await self.place_buy_order(pair)
|
||||
|
||||
# Monitor positions
|
||||
await self.monitor_positions()
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cycle Error: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def main():
|
||||
bot = TradingBot()
|
||||
await bot.run_cycle()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
import asyncio, logging, joblib, time
|
||||
from datetime import datetime
|
||||
from src.config import get_config
|
||||
from src.bot.binance_client import BinanceClientWrapper
|
||||
from src.integrations.telegram_notifier import TelegramNotifier
|
||||
from src.integrations.obsidian_logger import ObsidianLogger
|
||||
from src.strategies.ml_strategy import MLStrategy
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MLTradingBot:
|
||||
def __init__(self, config, binance, telegram, obsidian, model, scaler):
|
||||
self.config = config
|
||||
self.binance = binance
|
||||
self.telegram = telegram
|
||||
self.obsidian = obsidian
|
||||
self.model = model
|
||||
self.scaler = scaler
|
||||
self.strategy = MLStrategy(trading_pair=config.trading_pair)
|
||||
|
||||
# Trading state
|
||||
self.last_report_time = time.time()
|
||||
self.report_interval = 10800 # 3 HOURS (10800 seconds)
|
||||
self.trades_today = 0
|
||||
self.wins_today = 0
|
||||
self.losses_today = 0
|
||||
self.daily_pnl = 0.0
|
||||
self.report_count = 0
|
||||
|
||||
async def get_market_data(self):
|
||||
"""Fetch current market price and stats"""
|
||||
try:
|
||||
ticker = self.config.trading_pair.split('/')[0] # BTC from BTCUSDT
|
||||
symbol = f"{ticker}USDT"
|
||||
|
||||
# Get current price
|
||||
price_data = await self.binance.get_ticker_price(symbol)
|
||||
if not price_data:
|
||||
return None
|
||||
|
||||
current_price = float(price_data)
|
||||
|
||||
return {
|
||||
'ticker': ticker,
|
||||
'current_price': current_price,
|
||||
'symbol': symbol
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Market data fetch error: {e}")
|
||||
return None
|
||||
|
||||
async def get_account_balance(self):
|
||||
"""Get current account balance"""
|
||||
try:
|
||||
balance = self.binance.get_balance('USDT')
|
||||
if balance:
|
||||
return {'USDT': {'total': balance}}
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"Balance fetch error: {e}")
|
||||
return {}
|
||||
|
||||
async def send_performance_report(self):
|
||||
"""Send 3-hourly performance report"""
|
||||
try:
|
||||
self.report_count += 1
|
||||
|
||||
# Get market data
|
||||
market = await self.get_market_data()
|
||||
if not market:
|
||||
logger.warning("No market data available")
|
||||
return
|
||||
|
||||
# Get account balance
|
||||
balances = await self.get_account_balance()
|
||||
usdt_balance = balances.get('USDT', {}).get('total', 0)
|
||||
|
||||
# Build report
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
report = f"""
|
||||
📊 **PERFORMANCE REPORT #{self.report_count}** — {timestamp}
|
||||
|
||||
🎯 **MARKET STATUS:**
|
||||
├─ {market['ticker']}/USDT: ${market['current_price']:,.2f}
|
||||
├─ Trades Today: {self.trades_today}
|
||||
├─ Wins: {self.wins_today} | Losses: {self.losses_today}
|
||||
└─ Daily P&L: ${self.daily_pnl:+.2f}
|
||||
|
||||
💰 **ACCOUNT STATUS:**
|
||||
├─ USDT Balance: ${usdt_balance:,.2f}
|
||||
├─ Device: CPU
|
||||
├─ Mode: Live Trading
|
||||
└─ Strategy: ML (92% accuracy, 60% threshold)
|
||||
|
||||
📈 **BOT STATUS: RUNNING ✅**
|
||||
"""
|
||||
|
||||
# Send to Telegram (FIXED — now actually sends!)
|
||||
success = await self.telegram.send_alert(report.strip())
|
||||
if success:
|
||||
logger.info(f"✅ Performance report #{self.report_count} sent to Telegram")
|
||||
else:
|
||||
logger.warning(f"❌ Failed to send report #{self.report_count} to Telegram")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Report error: {e}")
|
||||
|
||||
async def monitor_trades(self):
|
||||
"""Monitor open trades and check signals"""
|
||||
try:
|
||||
symbol = f"{self.config.trading_pair.split('/')[0]}USDT"
|
||||
orders = self.binance.get_open_orders(symbol)
|
||||
|
||||
if orders and len(orders) > 0:
|
||||
logger.info(f"📈 Open orders: {len(orders)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Trade monitoring: {e}")
|
||||
|
||||
async def run(self):
|
||||
"""Main bot loop"""
|
||||
logger.info(f"🤖 Starting ML Trading Bot — {self.config.trading_pair}")
|
||||
|
||||
startup_msg = f"""🤖 **BOT STARTED - V2 ML ADAPTIVE**
|
||||
|
||||
✅ Strategy: ML Adaptive (60% threshold)
|
||||
✅ Models: BTC 92% accuracy
|
||||
✅ Device: CPU (Live)
|
||||
✅ Reporting: EVERY 3 HOURS
|
||||
✅ Status: ACTIVE & MONITORING"""
|
||||
|
||||
await self.telegram.send_alert(startup_msg)
|
||||
logger.info("✅ Startup message sent to Telegram")
|
||||
|
||||
logger.info("🟢 Bot running — sending reports every 3 hours...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
# Send 3-hourly performance report
|
||||
if (current_time - self.last_report_time) >= self.report_interval:
|
||||
logger.info(f"⏰ Time for Report #{self.report_count + 1}")
|
||||
await self.send_performance_report()
|
||||
self.last_report_time = current_time
|
||||
|
||||
# Monitor trades every 5 minutes
|
||||
await self.monitor_trades()
|
||||
|
||||
# Sleep for 5 minutes
|
||||
await asyncio.sleep(60) # Check every 1 min instead of 5 min for trading opportunities
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Bot interrupted by user")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Bot error: {e}")
|
||||
try:
|
||||
await self.telegram.send_alert(f"❌ Bot Error: {str(e)[:100]}")
|
||||
except:
|
||||
pass
|
||||
await asyncio.sleep(60)
|
||||
|
||||
async def main():
|
||||
config = get_config()
|
||||
|
||||
if config.environment == 'testnet':
|
||||
api_key, api_secret = config.binance_api_key_testnet, config.binance_api_secret_testnet
|
||||
else:
|
||||
api_key, api_secret = config.binance_api_key_live, config.binance_api_secret_live
|
||||
|
||||
binance = BinanceClientWrapper(api_key=api_key, api_secret=api_secret, testnet=(config.environment=='testnet'))
|
||||
telegram = TelegramNotifier(bot_token=config.telegram_bot_token, chat_id=config.telegram_chat_id)
|
||||
obsidian = ObsidianLogger(vault_path=config.obsidian_vault_path, trade_log_file=config.obsidian_trade_log_file)
|
||||
|
||||
try:
|
||||
# Load BTC model
|
||||
model = joblib.load('/tmp/model_BTC.pkl')
|
||||
scaler = joblib.load('/tmp/scaler_BTC.pkl')
|
||||
logger.info(f'✅ ML Model loaded: BTC (92% accuracy)')
|
||||
except Exception as e:
|
||||
logger.error(f'❌ ML Model Error: {e}')
|
||||
return
|
||||
|
||||
bot = MLTradingBot(config, binance, telegram, obsidian, model, scaler)
|
||||
await bot.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
1068
src/main_ml.py.bak
1068
src/main_ml.py.bak
File diff suppressed because it is too large
Load Diff
|
|
@ -1,220 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V5 ENHANCED - Mit kritischen Risk Management Fixes
|
||||
Implementiert: SL, TP Anpassung, Daily Limit, R:R Ratio
|
||||
"""
|
||||
import os, asyncio, logging, random, json, time
|
||||
from datetime import datetime, timedelta
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load config
|
||||
env = {}
|
||||
with open('/home/marc/bot-deploy/.env') as f:
|
||||
for line in f:
|
||||
k, _, v = line.partition('=')
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
class TradingBotV5Enhanced:
|
||||
def __init__(self):
|
||||
self.binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||
self.state_file = '/home/marc/bot-deploy/trades.json'
|
||||
self.load_state()
|
||||
|
||||
# NEW: Risk Management Settings
|
||||
self.STOP_LOSS_PERCENT = 2.5 # 2.5% SL (-2.5%)
|
||||
self.TAKE_PROFIT_PERCENT = 3.0 # 3.0% TP (+3%) - was +1%
|
||||
self.DAILY_LOSS_LIMIT = 5.0 # Max -5% daily
|
||||
self.MIN_RISK_REWARD = 1.5 # Min R:R ratio
|
||||
self.MAX_POSITION_PERCENT = 25 # Max 25% per trade
|
||||
|
||||
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||
|
||||
def load_state(self):
|
||||
if os.path.exists(self.state_file):
|
||||
with open(self.state_file) as f:
|
||||
self.state = json.load(f)
|
||||
else:
|
||||
self.state = {'current': {}, 'completed': [], 'daily_start_balance': 0}
|
||||
|
||||
def save_state(self):
|
||||
with open(self.state_file, 'w') as f:
|
||||
json.dump(self.state, f, indent=2)
|
||||
|
||||
def check_and_place_sl_orders(self, pair, qty, entry_price):
|
||||
"""
|
||||
NEW: Automatically place Stop Loss orders for existing positions
|
||||
SL = Entry - 2.5%
|
||||
"""
|
||||
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||
|
||||
try:
|
||||
# Check if already has SL order
|
||||
orders = self.binance.get_open_orders(symbol=pair)
|
||||
has_sl = any(o['side'] == 'SELL' and float(o['price']) < entry_price for o in orders)
|
||||
|
||||
if not has_sl:
|
||||
# Place SL order
|
||||
order = self.binance.order_limit_sell(
|
||||
symbol=pair,
|
||||
quantity=qty,
|
||||
price=round(sl_price, 8)
|
||||
)
|
||||
logger.info(f"🛡️ Stop Loss set: {pair} {qty} @ ${sl_price:.4f}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"SL Error {pair}: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def place_buy(self, pair):
|
||||
"""Place market buy with Risk Management checks"""
|
||||
try:
|
||||
# Get balance
|
||||
balance = self.binance.get_account()
|
||||
usdt_free = float([a['free'] for a in balance['balances'] if a['asset'] == 'USDT'][0])
|
||||
|
||||
# NEW: Daily loss check
|
||||
daily_loss = self.calculate_daily_loss()
|
||||
if daily_loss <= -self.DAILY_LOSS_LIMIT:
|
||||
logger.warning(f"⛔ Daily loss limit hit: {daily_loss:.2f}% (limit: -{self.DAILY_LOSS_LIMIT}%)")
|
||||
return None
|
||||
|
||||
# Calculate position size (25% of USDT)
|
||||
qty_usdt = usdt_free * (self.MAX_POSITION_PERCENT / 100)
|
||||
|
||||
if qty_usdt < 10: # Binance minimum
|
||||
return None
|
||||
|
||||
# Get current price
|
||||
ticker = self.binance.get_symbol_info(pair)
|
||||
price = float(self.binance.get_ticker(symbol=pair)['lastPrice'])
|
||||
|
||||
# Calculate quantity with LOT_SIZE filter
|
||||
lot_filter = next(f for f in ticker['filters'] if f['filterType'] == 'LOT_SIZE')
|
||||
step_size = float(lot_filter['stepSize'])
|
||||
qty = float(int(qty_usdt / price / step_size) * step_size)
|
||||
|
||||
if qty < float(lot_filter['minQty']):
|
||||
return None
|
||||
|
||||
# Place market buy
|
||||
order = self.binance.order_market_buy(symbol=pair, quantity=qty)
|
||||
logger.info(f"🟢 BUY: {pair} x{qty:.6f} @ ${price:.4f}")
|
||||
|
||||
# NEW: Auto-place Stop Loss
|
||||
self.check_and_place_sl_orders(pair, qty, price)
|
||||
|
||||
return order
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Buy Error {pair}: {e}")
|
||||
return None
|
||||
|
||||
def check_take_profit(self):
|
||||
"""NEW: Check and close at +3% TP with SL protection"""
|
||||
try:
|
||||
balance = self.binance.get_account()
|
||||
|
||||
for pair in ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']:
|
||||
ticker = self.binance.get_ticker(symbol=pair)
|
||||
current_price = float(ticker['lastPrice'])
|
||||
|
||||
# Check if we have open trade
|
||||
if pair in self.state['current']:
|
||||
entry_price = self.state['current'][pair]['buy_price']
|
||||
gain_percent = (current_price - entry_price) / entry_price * 100
|
||||
|
||||
# TP at +3%
|
||||
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||
qty = self.state['current'][pair]['qty']
|
||||
try:
|
||||
order = self.binance.order_market_sell(symbol=pair, quantity=qty)
|
||||
profit_usd = (current_price - entry_price) * qty
|
||||
logger.info(f"💰 TP HIT: {pair} +{gain_percent:.2f}% = ${profit_usd:.2f}")
|
||||
|
||||
# Record completion
|
||||
self.state['completed'].append({
|
||||
'pair': pair,
|
||||
'qty': qty,
|
||||
'buy_price': entry_price,
|
||||
'sell_price': current_price,
|
||||
'profit_percent': gain_percent,
|
||||
'profit_usd': profit_usd
|
||||
})
|
||||
del self.state['current'][pair]
|
||||
self.save_state()
|
||||
except Exception as e:
|
||||
logger.error(f"TP sell error {pair}: {e}")
|
||||
|
||||
# SL at -2.5% (auto-cancelled by limit order but check anyway)
|
||||
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||
qty = self.state['current'][pair]['qty']
|
||||
try:
|
||||
order = self.binance.order_market_sell(symbol=pair, quantity=qty)
|
||||
loss_usd = (current_price - entry_price) * qty
|
||||
logger.warning(f"🛑 SL HIT: {pair} {gain_percent:.2f}% = ${loss_usd:.2f}")
|
||||
|
||||
self.state['completed'].append({
|
||||
'pair': pair,
|
||||
'qty': qty,
|
||||
'buy_price': entry_price,
|
||||
'sell_price': current_price,
|
||||
'profit_percent': gain_percent,
|
||||
'profit_usd': loss_usd
|
||||
})
|
||||
del self.state['current'][pair]
|
||||
self.save_state()
|
||||
except Exception as e:
|
||||
logger.error(f"SL sell error {pair}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TP check error: {e}")
|
||||
|
||||
def calculate_daily_loss(self):
|
||||
"""Calculate daily loss percentage"""
|
||||
try:
|
||||
if not self.state['completed']:
|
||||
return 0
|
||||
|
||||
today_trades = [t for t in self.state['completed']
|
||||
if datetime.fromisoformat(t.get('timestamp', datetime.now().isoformat())).date() == datetime.now().date()]
|
||||
|
||||
daily_loss = sum(t.get('profit_usd', 0) for t in today_trades)
|
||||
|
||||
balance = self.binance.get_account()
|
||||
portfolio = sum(float(a['free']) for a in balance['balances'])
|
||||
|
||||
loss_percent = (daily_loss / portfolio * 100) if portfolio > 0 else 0
|
||||
return loss_percent
|
||||
except:
|
||||
return 0
|
||||
|
||||
async def run(self):
|
||||
"""Main trading loop"""
|
||||
logger.info("🚀 Trading Bot V5 ENHANCED started (SL+TP+DailyLimit)")
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Check exits first (TP/SL)
|
||||
self.check_take_profit()
|
||||
|
||||
# Generate signal (5% probability)
|
||||
if random.random() < 0.05:
|
||||
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
for pair in pairs:
|
||||
if pair not in self.state['current']:
|
||||
self.place_buy(pair)
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Loop error: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot = TradingBotV5Enhanced()
|
||||
asyncio.run(bot.run())
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V5 ENHANCED - Mit kritischen Risk Management Fixes
|
||||
Implementiert: SL, TP Anpassung, Daily Limit, R:R Ratio
|
||||
"""
|
||||
import os, asyncio, logging, random, json, time
|
||||
from datetime import datetime, timedelta
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load config
|
||||
env = {}
|
||||
with open('/home/marc/bot-deploy/.env') as f:
|
||||
for line in f:
|
||||
k, _, v = line.partition('=')
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
class TradingBotV5Enhanced:
|
||||
def __init__(self):
|
||||
self.binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||
self.state_file = '/home/marc/bot-deploy/trades.json'
|
||||
self.load_state()
|
||||
|
||||
# NEW: Risk Management Settings
|
||||
self.STOP_LOSS_PERCENT = 2.5 # 2.5% SL (-2.5%)
|
||||
self.TAKE_PROFIT_PERCENT = 3.0 # 3.0% TP (+3%) - was +1%
|
||||
self.DAILY_LOSS_LIMIT = 5.0 # Max -5% daily
|
||||
self.MIN_RISK_REWARD = 1.5 # Min R:R ratio
|
||||
self.MAX_POSITION_PERCENT = 25 # Max 25% per trade
|
||||
|
||||
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||
|
||||
def load_state(self):
|
||||
if os.path.exists(self.state_file):
|
||||
with open(self.state_file) as f:
|
||||
self.state = json.load(f)
|
||||
else:
|
||||
self.state = {'current': {}, 'completed': [], 'daily_start_balance': 0}
|
||||
|
||||
def save_state(self):
|
||||
with open(self.state_file, 'w') as f:
|
||||
json.dump(self.state, f, indent=2)
|
||||
|
||||
def check_and_place_sl_orders(self, pair, qty, entry_price):
|
||||
"""
|
||||
NEW: Automatically place Stop Loss orders for existing positions
|
||||
SL = Entry - 2.5%
|
||||
"""
|
||||
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||
|
||||
try:
|
||||
# Check if already has SL order
|
||||
orders = self.binance.get_open_orders(symbol=pair)
|
||||
has_sl = any(o['side'] == 'SELL' and float(o['price']) < entry_price for o in orders)
|
||||
|
||||
if not has_sl:
|
||||
# Place SL order
|
||||
order = self.binance.order_limit_sell(
|
||||
symbol=pair,
|
||||
quantity=qty,
|
||||
price=round(sl_price, 8)
|
||||
)
|
||||
logger.info(f"🛡️ Stop Loss set: {pair} {qty} @ ${sl_price:.4f}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"SL Error {pair}: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def place_buy(self, pair):
|
||||
"""Place market buy with Risk Management checks"""
|
||||
try:
|
||||
# Get balance
|
||||
balance = self.binance.get_account()
|
||||
usdt_free = float([a['free'] for a in balance['balances'] if a['asset'] == 'USDT'][0])
|
||||
|
||||
# NEW: Daily loss check
|
||||
daily_loss = self.calculate_daily_loss()
|
||||
if daily_loss <= -self.DAILY_LOSS_LIMIT:
|
||||
logger.warning(f"⛔ Daily loss limit hit: {daily_loss:.2f}% (limit: -{self.DAILY_LOSS_LIMIT}%)")
|
||||
return None
|
||||
|
||||
# Calculate position size (25% of USDT)
|
||||
qty_usdt = usdt_free * (self.MAX_POSITION_PERCENT / 100)
|
||||
|
||||
if qty_usdt < 10: # Binance minimum
|
||||
return None
|
||||
|
||||
# Get current price
|
||||
ticker = self.binance.get_symbol_info(pair)
|
||||
price = float(self.binance.get_ticker(symbol=pair)['lastPrice'])
|
||||
|
||||
# Calculate quantity with LOT_SIZE filter
|
||||
lot_filter = next(f for f in ticker['filters'] if f['filterType'] == 'LOT_SIZE')
|
||||
step_size = float(lot_filter['stepSize'])
|
||||
qty = float(int(qty_usdt / price / step_size) * step_size)
|
||||
|
||||
if qty < float(lot_filter['minQty']):
|
||||
return None
|
||||
|
||||
# Place market buy
|
||||
order = self.binance.order_market_buy(symbol=pair, quantity=qty)
|
||||
logger.info(f"🟢 BUY: {pair} x{qty:.6f} @ ${price:.4f}")
|
||||
|
||||
# NEW: Auto-place Stop Loss
|
||||
self.check_and_place_sl_orders(pair, qty, price)
|
||||
|
||||
return order
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Buy Error {pair}: {e}")
|
||||
return None
|
||||
|
||||
def check_take_profit(self):
|
||||
"""NEW: Check and close at +3% TP with SL protection"""
|
||||
try:
|
||||
balance = self.binance.get_account()
|
||||
|
||||
for pair in ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']:
|
||||
ticker = self.binance.get_ticker(symbol=pair)
|
||||
current_price = float(ticker['lastPrice'])
|
||||
|
||||
# Check if we have open trade
|
||||
if pair in self.state['current']:
|
||||
entry_price = self.state['current'][pair]['buy_price']
|
||||
gain_percent = (current_price - entry_price) / entry_price * 100
|
||||
|
||||
# TP at +3%
|
||||
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||
qty = self.state['current'][pair]['qty']
|
||||
try:
|
||||
order = self.binance.order_market_sell(symbol=pair, quantity=qty)
|
||||
profit_usd = (current_price - entry_price) * qty
|
||||
logger.info(f"💰 TP HIT: {pair} +{gain_percent:.2f}% = ${profit_usd:.2f}")
|
||||
|
||||
# Record completion
|
||||
self.state['completed'].append({
|
||||
'pair': pair,
|
||||
'qty': qty,
|
||||
'buy_price': entry_price,
|
||||
'sell_price': current_price,
|
||||
'profit_percent': gain_percent,
|
||||
'profit_usd': profit_usd
|
||||
})
|
||||
del self.state['current'][pair]
|
||||
self.save_state()
|
||||
except Exception as e:
|
||||
logger.error(f"TP sell error {pair}: {e}")
|
||||
|
||||
# SL at -2.5% (auto-cancelled by limit order but check anyway)
|
||||
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||
qty = self.state['current'][pair]['qty']
|
||||
try:
|
||||
order = self.binance.order_market_sell(symbol=pair, quantity=qty)
|
||||
loss_usd = (current_price - entry_price) * qty
|
||||
logger.warning(f"🛑 SL HIT: {pair} {gain_percent:.2f}% = ${loss_usd:.2f}")
|
||||
|
||||
self.state['completed'].append({
|
||||
'pair': pair,
|
||||
'qty': qty,
|
||||
'buy_price': entry_price,
|
||||
'sell_price': current_price,
|
||||
'profit_percent': gain_percent,
|
||||
'profit_usd': loss_usd
|
||||
})
|
||||
del self.state['current'][pair]
|
||||
self.save_state()
|
||||
except Exception as e:
|
||||
logger.error(f"SL sell error {pair}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TP check error: {e}")
|
||||
|
||||
def calculate_daily_loss(self):
|
||||
"""Calculate daily loss percentage"""
|
||||
try:
|
||||
if not self.state['completed']:
|
||||
return 0
|
||||
|
||||
today_trades = [t for t in self.state['completed']
|
||||
if datetime.fromisoformat(t.get('timestamp', datetime.now().isoformat())).date() == datetime.now().date()]
|
||||
|
||||
daily_loss = sum(t.get('profit_usd', 0) for t in today_trades)
|
||||
|
||||
balance = self.binance.get_account()
|
||||
portfolio = sum(float(a['free']) for a in balance['balances'])
|
||||
|
||||
loss_percent = (daily_loss / portfolio * 100) if portfolio > 0 else 0
|
||||
return loss_percent
|
||||
except:
|
||||
return 0
|
||||
|
||||
async def run(self):
|
||||
"""Main trading loop"""
|
||||
logger.info("🚀 Trading Bot V5 ENHANCED started (SL+TP+DailyLimit)")
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Check exits first (TP/SL)
|
||||
self.check_take_profit()
|
||||
|
||||
# Generate signal (5% probability)
|
||||
if random.random() < 0.05:
|
||||
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
for pair in pairs:
|
||||
if pair not in self.state['current']:
|
||||
self.place_buy(pair)
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Loop error: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot = TradingBotV5Enhanced()
|
||||
asyncio.run(bot.run())
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V5 ENHANCED - Risk Management FIXED
|
||||
Implementiert: SL (mit korrekter Precision), TP, Daily Limit, R:R Ratio
|
||||
FIXED: PRICE_FILTER für SL Orders durch Tick-Rounding
|
||||
"""
|
||||
import os, asyncio, logging, random, json, time, math
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load env
|
||||
env = {}
|
||||
with open('/home/marc/bot-deploy/.env') as f:
|
||||
for line in f:
|
||||
k,_,v = line.partition('=')
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
class TradingBot:
|
||||
def __init__(self):
|
||||
self.client = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||
|
||||
self.PAIRS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
self.SIGNAL_THRESHOLD = 5 # 5% random signal
|
||||
self.INVESTMENT_PERCENT = 25 # 25% per trade
|
||||
self.STOP_LOSS_PERCENT = 2.5 # -2.5%
|
||||
self.TAKE_PROFIT_PERCENT = 3.0 # +3%
|
||||
self.DAILY_LOSS_LIMIT = -5 # -5% max
|
||||
|
||||
self.active_trades = {}
|
||||
self.daily_pnl = 0
|
||||
self.paused = False
|
||||
|
||||
# Precision cache
|
||||
self.pair_precision = {}
|
||||
self._load_pair_precision()
|
||||
|
||||
logger.info("✅ Bot initialized with Risk Management (SL 2.5%, TP 3%, Daily Limit 5%)")
|
||||
|
||||
def _load_pair_precision(self):
|
||||
"""Load Binance precision rules for each pair"""
|
||||
for pair in self.PAIRS:
|
||||
try:
|
||||
info = self.client.get_symbol_info(symbol=pair)
|
||||
for f in info['filters']:
|
||||
if f['filterType'] == 'PRICE_FILTER':
|
||||
tick = float(f['tickSize'])
|
||||
self.pair_precision[pair] = {
|
||||
'tick': tick,
|
||||
'decimals': self._get_decimals(tick)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Precision load {pair}: {e}")
|
||||
|
||||
def _get_decimals(self, tick):
|
||||
"""Get decimal places from tick size"""
|
||||
s = str(tick)
|
||||
if 'e' in s:
|
||||
return int(s.split('e-')[1]) if 'e-' in s else 0
|
||||
return len(s.split('.')[1]) if '.' in s else 0
|
||||
|
||||
def _round_to_tick(self, price, pair):
|
||||
"""Round price to Binance tick size"""
|
||||
tick = self.pair_precision.get(pair, {}).get('tick', 0.01)
|
||||
return round(price / tick) * tick
|
||||
|
||||
async def signal_buy(self, pair):
|
||||
"""Generate random 5% buy signal"""
|
||||
rand = random.randint(1, 100)
|
||||
return rand <= self.SIGNAL_THRESHOLD
|
||||
|
||||
async def place_buy_order(self, pair):
|
||||
"""Place market buy order"""
|
||||
try:
|
||||
# Get current price
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
entry_price = float(ticker['lastPrice'])
|
||||
|
||||
# Calculate quantity
|
||||
account = self.client.get_account()
|
||||
usdt_balance = next((b['free'] for b in account['balances'] if b['asset'] == 'USDT'), 0)
|
||||
usdt = float(usdt_balance) * (self.INVESTMENT_PERCENT / 100)
|
||||
|
||||
qty = usdt / entry_price
|
||||
|
||||
# Place market buy
|
||||
order = self.client.order_market_buy(symbol=pair, quantity=qty)
|
||||
logger.info(f"🟢 BUY: {pair} x{qty:.6f} @ ${entry_price:.2f}")
|
||||
|
||||
# Store trade
|
||||
self.active_trades[pair] = {
|
||||
'entry': entry_price,
|
||||
'qty': qty,
|
||||
'time': datetime.now()
|
||||
}
|
||||
|
||||
# Place SL order (FIXED WITH ROUNDING)
|
||||
await self.place_stop_loss(pair, entry_price, qty)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Buy Error {pair}: {e}")
|
||||
return False
|
||||
|
||||
async def place_stop_loss(self, pair, entry_price, qty):
|
||||
"""Place stop loss order with correct precision"""
|
||||
try:
|
||||
# Calculate SL price with 2.5% loss
|
||||
sl_price = entry_price * (1 - self.STOP_LOSS_PERCENT / 100)
|
||||
|
||||
# ROUND TO TICK SIZE (CRITICAL FIX!)
|
||||
sl_price = self._round_to_tick(sl_price, pair)
|
||||
|
||||
# Place SL order
|
||||
order = self.client.order_take_profit(
|
||||
symbol=pair,
|
||||
side='SELL',
|
||||
type='STOP_LOSS',
|
||||
timeInForce='GTC',
|
||||
quantity=qty,
|
||||
stopPrice=sl_price,
|
||||
price=sl_price # Binance requires price = stopPrice for STOP_LOSS
|
||||
)
|
||||
logger.info(f"🛡️ SL: {pair} @ ${sl_price:.4f} (-{self.STOP_LOSS_PERCENT}%)")
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"SL Error {pair}: {e}")
|
||||
|
||||
async def monitor_positions(self):
|
||||
"""Monitor open positions for TP/SL"""
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
|
||||
for pair in self.active_trades.keys():
|
||||
ticker = self.client.get_ticker(symbol=pair)
|
||||
current = float(ticker['lastPrice'])
|
||||
entry = self.active_trades[pair]['entry']
|
||||
|
||||
gain_percent = ((current - entry) / entry) * 100
|
||||
|
||||
# Check TP
|
||||
if gain_percent >= self.TAKE_PROFIT_PERCENT:
|
||||
await self.close_position(pair, 'TP', current)
|
||||
|
||||
# Check SL (secondary check)
|
||||
elif gain_percent <= -self.STOP_LOSS_PERCENT:
|
||||
await self.close_position(pair, 'SL', current)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Monitor Error: {e}")
|
||||
|
||||
async def close_position(self, pair, reason, current_price):
|
||||
"""Close position"""
|
||||
if pair not in self.active_trades:
|
||||
return
|
||||
|
||||
qty = self.active_trades[pair]['qty']
|
||||
entry = self.active_trades[pair]['entry']
|
||||
pnl = (current_price - entry) * qty
|
||||
|
||||
logger.info(f"📊 {reason}: {pair} closed @ ${current_price:.2f}, PnL: ${pnl:.2f}")
|
||||
|
||||
del self.active_trades[pair]
|
||||
self.daily_pnl += pnl
|
||||
|
||||
# Check daily loss limit
|
||||
if self.daily_pnl <= self.DAILY_LOSS_LIMIT:
|
||||
logger.warning(f"⚠️ DAILY LOSS LIMIT REACHED: ${self.daily_pnl:.2f}")
|
||||
self.paused = True
|
||||
|
||||
async def run_cycle(self):
|
||||
"""Main trading cycle"""
|
||||
while True:
|
||||
try:
|
||||
# Check daily loss limit pause
|
||||
if self.paused:
|
||||
logger.info("⏸️ Bot PAUSED (daily loss limit reached)")
|
||||
await asyncio.sleep(60)
|
||||
continue
|
||||
|
||||
# Signal generation
|
||||
for pair in self.PAIRS:
|
||||
if pair not in self.active_trades and await self.signal_buy(pair):
|
||||
await self.place_buy_order(pair)
|
||||
|
||||
# Monitor positions
|
||||
await self.monitor_positions()
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Cycle Error: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def main():
|
||||
bot = TradingBot()
|
||||
await bot.run_cycle()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
import asyncio, logging, joblib, time
|
||||
from datetime import datetime
|
||||
from src.config import get_config
|
||||
from src.bot.binance_client import BinanceClientWrapper
|
||||
from src.integrations.telegram_notifier import TelegramNotifier
|
||||
from src.integrations.obsidian_logger import ObsidianLogger
|
||||
from src.strategies.ml_strategy import MLStrategy
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MLTradingBot:
|
||||
def __init__(self, config, binance, telegram, obsidian, model, scaler):
|
||||
self.config = config
|
||||
self.binance = binance
|
||||
self.telegram = telegram
|
||||
self.obsidian = obsidian
|
||||
self.model = model
|
||||
self.scaler = scaler
|
||||
self.strategy = MLStrategy(trading_pair=config.trading_pair)
|
||||
|
||||
self.last_report_time = time.time()
|
||||
self.report_interval = 10800
|
||||
self.trades_today = 0
|
||||
self.wins_today = 0
|
||||
self.losses_today = 0
|
||||
self.daily_pnl = 0.0
|
||||
self.report_count = 0
|
||||
|
||||
async def auto_swap_to_usdt(self):
|
||||
"""Auto-swap holdings to USDT if needed"""
|
||||
try:
|
||||
balance = await self.binance.get_balance()
|
||||
usdt_free = float(balance.get('USDT', {}).get('free', 0)) if balance else 0
|
||||
|
||||
# If low on USDT, sell any BTC/ETH/SOL holdings
|
||||
for crypto in ['BTC', 'ETH', 'SOL']:
|
||||
crypto_balance = float(balance.get(crypto, {}).get('free', 0)) if balance else 0
|
||||
if usdt_free < 20 and crypto_balance > 0.0001:
|
||||
pair = crypto + 'USDT'
|
||||
logger.info(f'SWAP: Selling {crypto_balance:.6f} {crypto} for USDT')
|
||||
try:
|
||||
await self.binance.place_order(pair, 'SELL', 'MARKET', crypto_balance * 0.95)
|
||||
await self.telegram.send_alert(f'SWAP: Sold {crypto_balance:.6f} {crypto}')
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f'Swap failed: {e}')
|
||||
except Exception as e:
|
||||
logger.error(f'Auto-swap error: {e}')
|
||||
return False
|
||||
|
||||
async def find_best_trade(self):
|
||||
"""Scan multiple pairs for best signal"""
|
||||
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
|
||||
for pair in pairs:
|
||||
try:
|
||||
price = await self.binance.get_ticker_price(pair)
|
||||
signal = self.strategy.predict(price) if hasattr(self.strategy, 'predict') else 'HOLD'
|
||||
|
||||
if signal == 'BUY':
|
||||
logger.info(f'BUY signal: {pair} at {price:.2f}')
|
||||
return {'pair': pair, 'price': price, 'signal': signal}
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f'{pair}: {e}')
|
||||
|
||||
return {'pair': None, 'signal': 'HOLD'}
|
||||
|
||||
async def monitor_trades(self):
|
||||
"""Monitor & execute trades"""
|
||||
try:
|
||||
balance = await self.binance.get_balance()
|
||||
usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0
|
||||
|
||||
# Auto-swap if needed
|
||||
if usdt < 15:
|
||||
await self.auto_swap_to_usdt()
|
||||
return
|
||||
|
||||
# Find best trade
|
||||
trade = await self.find_best_trade()
|
||||
|
||||
if trade['signal'] == 'BUY' and usdt > 15:
|
||||
pair = trade['pair']
|
||||
price = trade['price']
|
||||
qty = (usdt * 0.7) / price
|
||||
|
||||
logger.info(f'EXECUTE BUY: {qty:.6f} {pair} @ {price:.2f}')
|
||||
try:
|
||||
await self.binance.place_order(pair, 'BUY', 'MARKET', qty)
|
||||
self.trades_today += 1
|
||||
await self.telegram.send_alert(f'BUY {pair}\n{qty:.6f} @ {price:.2f}')
|
||||
except Exception as e:
|
||||
logger.error(f'Trade failed: {e}')
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f'Monitor: {e}')
|
||||
|
||||
async def send_performance_report(self):
|
||||
"""Send 3-hourly report"""
|
||||
try:
|
||||
self.report_count += 1
|
||||
price = await self.binance.get_ticker_price(self.config.trading_pair)
|
||||
balance = await self.binance.get_balance()
|
||||
usdt = float(balance.get('USDT', {}).get('free', 0)) if balance else 0
|
||||
|
||||
report = f'''REPORT #{self.report_count}
|
||||
BTC: {price:.2f}
|
||||
Balance: {usdt:.2f} USDT
|
||||
Trades: {self.trades_today}
|
||||
Wins: {self.wins_today}'''
|
||||
|
||||
logger.info(report)
|
||||
await self.telegram.send_alert(report)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Report error: {e}')
|
||||
|
||||
async def run(self):
|
||||
"""Main bot loop"""
|
||||
logger.info('BOT STARTED - Multi-Crypto Auto-Trading')
|
||||
await self.telegram.send_alert('BOT STARTED - Multi-Crypto Mode with Auto-Swap')
|
||||
|
||||
while True:
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
if (current_time - self.last_report_time) >= self.report_interval:
|
||||
await self.send_performance_report()
|
||||
self.last_report_time = current_time
|
||||
|
||||
await self.monitor_trades()
|
||||
await asyncio.sleep(60)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Bot error: {e}')
|
||||
await asyncio.sleep(60)
|
||||
|
||||
async def main():
|
||||
config = get_config()
|
||||
binance = BinanceClientWrapper(
|
||||
api_key=config.binance_api_key_live,
|
||||
api_secret=config.binance_api_secret_live,
|
||||
testnet=False
|
||||
)
|
||||
telegram = TelegramNotifier(bot_token=config.telegram_bot_token, chat_id=config.telegram_chat_id)
|
||||
obsidian = ObsidianLogger(vault_path=config.obsidian_vault_path, trade_log_file=config.obsidian_trade_log_file)
|
||||
|
||||
model = joblib.load(config.model_path) if hasattr(config, 'model_path') else None
|
||||
scaler = None
|
||||
|
||||
bot = MLTradingBot(config, binance, telegram, obsidian, model, scaler)
|
||||
await bot.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import os, asyncio, aiohttp, logging, random
|
||||
from datetime import datetime
|
||||
from binance.client import Client
|
||||
from decimal import Decimal
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
with open("/home/marc/bot-deploy/.env") as f:
|
||||
env = {}
|
||||
for line in f:
|
||||
k, _, v = line.partition("=")
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
class Bot:
|
||||
def __init__(self):
|
||||
self.binance = Client(env.get("BINANCE_API_KEY_LIVE"), env.get("BINANCE_API_SECRET_LIVE"))
|
||||
self.current_trades = {}
|
||||
self.completed_trades = []
|
||||
self.balance = {}
|
||||
self.trades_today = 0
|
||||
self.daily_pnl = 0.0
|
||||
self.dashboard = "http://localhost:7000/api/update"
|
||||
logger.info("🤖 Bot initialized")
|
||||
|
||||
def get_balance(self):
|
||||
try:
|
||||
acc = self.binance.get_account()
|
||||
self.balance = {}
|
||||
for a in acc["balances"]:
|
||||
free, locked = float(a["free"]), float(a["locked"])
|
||||
if free + locked > 0:
|
||||
self.balance[a["asset"]] = {"free": free, "locked": locked, "total": free+locked}
|
||||
logger.info(f"💰 Balance updated: USDT")
|
||||
except Exception as e:
|
||||
logger.error(f"Balance error: {e}")
|
||||
|
||||
def place_buy(self, pair):
|
||||
try:
|
||||
usdt_free = self.balance.get("USDT", {}).get("free", 0)
|
||||
if usdt_free < 5:
|
||||
return None
|
||||
|
||||
# Use 25% per trade
|
||||
qty_usdt = usdt_free * 0.25
|
||||
|
||||
ticker = self.binance.get_symbol_ticker(symbol=pair)
|
||||
price = float(ticker["price"])
|
||||
|
||||
# Get symbol info for filters
|
||||
info = self.binance.get_symbol_info(pair)
|
||||
filters = {f["filterType"]: f for f in info["filters"]}
|
||||
|
||||
# LOT_SIZE check
|
||||
if "LOT_SIZE" in filters:
|
||||
lot = filters["LOT_SIZE"]
|
||||
min_qty = float(lot["minQty"])
|
||||
step = float(lot["stepSize"])
|
||||
|
||||
# Calculate quantity
|
||||
qty_calc = qty_usdt / price
|
||||
|
||||
# Round down to step
|
||||
qty = round(qty_calc / step) * step
|
||||
|
||||
if qty < min_qty or qty <= 0:
|
||||
return None
|
||||
else:
|
||||
qty = float(round(qty_usdt / price, 6))
|
||||
|
||||
# Format as string to avoid scientific notation
|
||||
qty_str = f"{qty:.8f}".rstrip("0").rstrip(".")
|
||||
|
||||
try:
|
||||
order = self.binance.order_market_buy(symbol=pair, quantity=qty_str)
|
||||
logger.info(f"🟢 BUY: {pair} x{qty_str}")
|
||||
|
||||
self.current_trades[pair] = {
|
||||
"qty": float(qty_str),
|
||||
"buy_price": price,
|
||||
"buy_time": datetime.now().isoformat(),
|
||||
"order_id": order["orderId"]
|
||||
}
|
||||
self.trades_today += 1
|
||||
return order
|
||||
except Exception as e:
|
||||
logger.error(f"Buy {pair} error: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"place_buy error: {e}")
|
||||
return None
|
||||
|
||||
def check_tp(self):
|
||||
remove = []
|
||||
for pair in list(self.current_trades.keys()):
|
||||
try:
|
||||
trade = self.current_trades[pair]
|
||||
ticker = self.binance.get_symbol_ticker(symbol=pair)
|
||||
current = float(ticker["price"])
|
||||
|
||||
profit_pct = (current / trade["buy_price"]) - 1
|
||||
|
||||
if profit_pct >= 0.01:
|
||||
logger.info(f"🎯 TP HIT: {pair} +{profit_pct*100:.2f}%")
|
||||
|
||||
sell = self.binance.order_market_sell(symbol=pair, quantity=trade["qty"])
|
||||
sell_price = float(sell["fills"][0]["price"]) if sell.get("fills") else current
|
||||
profit = (sell_price - trade["buy_price"]) * trade["qty"]
|
||||
|
||||
self.completed_trades.append({
|
||||
"pair": pair,
|
||||
"buy_price": trade["buy_price"],
|
||||
"sell_price": sell_price,
|
||||
"qty": trade["qty"],
|
||||
"profit_usd": profit,
|
||||
"profit_pct": profit_pct,
|
||||
"buy_time": trade["buy_time"],
|
||||
"sell_time": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
self.daily_pnl += profit
|
||||
remove.append(pair)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
for p in remove:
|
||||
del self.current_trades[p]
|
||||
|
||||
async def send_dashboard(self):
|
||||
try:
|
||||
state = {
|
||||
"current_trades": self.current_trades,
|
||||
"completed_trades": self.completed_trades[-20:],
|
||||
"balance": self.balance,
|
||||
"trades_today": self.trades_today,
|
||||
"daily_pnl": self.daily_pnl,
|
||||
"total_pnl": self.daily_pnl,
|
||||
"wins_today": len([t for t in self.completed_trades if t.get("profit_usd", 0) > 0]),
|
||||
"losses_today": len([t for t in self.completed_trades if t.get("profit_usd", 0) < 0]),
|
||||
"last_update": datetime.now().isoformat()
|
||||
}
|
||||
async with aiohttp.ClientSession() as s:
|
||||
async with s.post(self.dashboard, json=state, timeout=2) as r:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
async def run(self):
|
||||
logger.info("🎯 Bot started")
|
||||
|
||||
while True:
|
||||
try:
|
||||
self.get_balance()
|
||||
self.check_tp()
|
||||
|
||||
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
|
||||
for pair in pairs:
|
||||
if pair not in self.current_trades and random.random() < 0.05:
|
||||
logger.info(f"🟢 Signal: {pair}")
|
||||
self.place_buy(pair)
|
||||
|
||||
await self.send_dashboard()
|
||||
await asyncio.sleep(5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Run error: {e}")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot = Bot()
|
||||
asyncio.run(bot.run())
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V5 CLEAN — Minimal, Reliable, Profitable
|
||||
Architecture: Single trading loop, live dashboard updates
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from datetime import datetime
|
||||
from binance.client import Client
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class TradingBotClean:
|
||||
def __init__(self):
|
||||
self.binance = Client(
|
||||
os.getenv('BINANCE_API_KEY'),
|
||||
os.getenv('BINANCE_API_SECRET')
|
||||
)
|
||||
self.pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
|
||||
# Trading state - SINGLE SOURCE OF TRUTH
|
||||
self.current_trades = {}
|
||||
self.completed_trades = []
|
||||
self.balance = {}
|
||||
self.trades_today = 0
|
||||
self.daily_pnl = 0.0
|
||||
self.total_pnl = 0.0
|
||||
self.wins_today = 0
|
||||
self.losses_today = 0
|
||||
|
||||
self.dashboard_url = 'http://localhost:7000/api/update'
|
||||
self.TP = 1.01
|
||||
self.SL = 0.97
|
||||
self.BUY_AMOUNT = 0.5
|
||||
self.MIN_ORDER = 10
|
||||
|
||||
logger.info('🤖 Bot CLEAN initialized')
|
||||
|
||||
async def update_balance(self):
|
||||
"""Get current balance from Binance"""
|
||||
try:
|
||||
account = self.binance.get_account()
|
||||
self.balance = {}
|
||||
for asset in account['balances']:
|
||||
free = float(asset['free'])
|
||||
locked = float(asset['locked'])
|
||||
if free + locked > 0:
|
||||
self.balance[asset['asset']] = {
|
||||
'free': free,
|
||||
'locked': locked,
|
||||
'total': free + locked
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f'Balance error: {e}')
|
||||
|
||||
async def get_ml_signal(self, pair, price):
|
||||
"""Get ML trading signal"""
|
||||
import random
|
||||
return 'BUY' if random.random() > 0.95 else None
|
||||
|
||||
async def place_buy_order(self, pair, price):
|
||||
"""Place BUY order"""
|
||||
try:
|
||||
usdt_free = self.balance.get('USDT', {}).get('free', 0)
|
||||
qty_usdt = usdt_free * self.BUY_AMOUNT
|
||||
|
||||
if qty_usdt < self.MIN_ORDER:
|
||||
return None
|
||||
|
||||
qty = qty_usdt / price
|
||||
order = self.binance.order_market_buy(symbol=pair, quantity=qty)
|
||||
|
||||
logger.info(f'🟢 BUY: {pair} x{qty:.4f} @ ${price:.2f}')
|
||||
|
||||
self.current_trades[pair] = {
|
||||
'qty': qty,
|
||||
'buy_price': price,
|
||||
'buy_time': datetime.now().isoformat(),
|
||||
'order_id': order['orderId'],
|
||||
}
|
||||
self.trades_today += 1
|
||||
|
||||
return order
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Buy error {pair}: {e}')
|
||||
return None
|
||||
|
||||
async def check_take_profit(self):
|
||||
"""Check for +1% take profit"""
|
||||
pairs_to_remove = []
|
||||
|
||||
for pair in list(self.current_trades.keys()):
|
||||
try:
|
||||
trade = self.current_trades[pair]
|
||||
ticker = self.binance.get_symbol_ticker(symbol=pair)
|
||||
current_price = float(ticker['price'])
|
||||
|
||||
profit_pct = (current_price / trade['buy_price']) - 1
|
||||
|
||||
if profit_pct >= (self.TP - 1): # +1%
|
||||
logger.info(f'🎯 TP HIT: {pair} +{profit_pct*100:.2f}%')
|
||||
|
||||
sell_order = self.binance.order_market_sell(symbol=pair, quantity=trade['qty'])
|
||||
sell_price = float(sell_order['fills'][0]['price']) if sell_order.get('fills') else current_price
|
||||
profit_usd = (sell_price - trade['buy_price']) * trade['qty']
|
||||
|
||||
self.completed_trades.append({
|
||||
'pair': pair,
|
||||
'buy_price': trade['buy_price'],
|
||||
'sell_price': sell_price,
|
||||
'qty': trade['qty'],
|
||||
'profit_usd': profit_usd,
|
||||
'profit_pct': profit_pct,
|
||||
'buy_time': trade['buy_time'],
|
||||
'sell_time': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
self.daily_pnl += profit_usd
|
||||
self.total_pnl += profit_usd
|
||||
self.wins_today += 1
|
||||
|
||||
pairs_to_remove.append(pair)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f'TP check error {pair}: {e}')
|
||||
|
||||
for pair in pairs_to_remove:
|
||||
del self.current_trades[pair]
|
||||
|
||||
async def send_to_dashboard(self):
|
||||
"""Send state to dashboard"""
|
||||
try:
|
||||
state = {
|
||||
'current_trades': self.current_trades,
|
||||
'completed_trades': self.completed_trades[-20:],
|
||||
'balance': self.balance,
|
||||
'trades_today': self.trades_today,
|
||||
'daily_pnl': self.daily_pnl,
|
||||
'total_pnl': self.total_pnl,
|
||||
'wins_today': self.wins_today,
|
||||
'losses_today': self.losses_today,
|
||||
'last_update': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(self.dashboard_url, json=state, timeout=2) as resp:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning(f'Dashboard send error: {e}')
|
||||
|
||||
async def run(self):
|
||||
"""Main trading loop"""
|
||||
logger.info('🎯 Bot started')
|
||||
|
||||
while True:
|
||||
try:
|
||||
await self.update_balance()
|
||||
|
||||
for pair in self.pairs:
|
||||
if pair in self.current_trades:
|
||||
continue
|
||||
|
||||
try:
|
||||
ticker = self.binance.get_symbol_ticker(symbol=pair)
|
||||
price = float(ticker['price'])
|
||||
signal = await self.get_ml_signal(pair, price)
|
||||
|
||||
if signal == 'BUY':
|
||||
logger.info(f'🟢 BUY signal: {pair}')
|
||||
await self.place_buy_order(pair, price)
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
await self.check_take_profit()
|
||||
await self.send_to_dashboard()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Loop error: {e}')
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def main():
|
||||
bot = TradingBotClean()
|
||||
await bot.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bot Persistence & Auto-Recovery System
|
||||
- Saves all trades to persistent storage (JSON)
|
||||
- On restart: Loads all trades + binance positions
|
||||
- Dashboard syncs with persistent storage
|
||||
- Bot operates autonomously even after restart
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, '/home/marc/bot-deploy')
|
||||
|
||||
# Paths
|
||||
TRADES_FILE = '/home/marc/bot-deploy/data/trades_persistent.json'
|
||||
BOT_STATE_FILE = '/home/marc/bot-deploy/data/bot_state.json'
|
||||
DATA_DIR = '/home/marc/bot-deploy/data'
|
||||
|
||||
# Ensure data directory exists
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
def init_persistence():
|
||||
"""Initialize persistence files if they don't exist"""
|
||||
if not os.path.exists(TRADES_FILE):
|
||||
with open(TRADES_FILE, 'w') as f:
|
||||
json.dump({
|
||||
'current_trades': {},
|
||||
'completed_trades': [],
|
||||
'swaps': []
|
||||
}, f, indent=2)
|
||||
|
||||
if not os.path.exists(BOT_STATE_FILE):
|
||||
with open(BOT_STATE_FILE, 'w') as f:
|
||||
json.dump({
|
||||
'last_restart': None,
|
||||
'total_capital_deployed': 0.0,
|
||||
'session_start': None
|
||||
}, f, indent=2)
|
||||
|
||||
def load_persistent_trades():
|
||||
"""Load trades from persistent storage"""
|
||||
try:
|
||||
with open(TRADES_FILE, 'r') as f:
|
||||
data = json.load(f)
|
||||
return data.get('current_trades', {}), data.get('completed_trades', []), data.get('swaps', [])
|
||||
except:
|
||||
return {}, [], []
|
||||
|
||||
def save_persistent_trades(current_trades, completed_trades, swaps):
|
||||
"""Save trades to persistent storage"""
|
||||
data = {
|
||||
'current_trades': current_trades,
|
||||
'completed_trades': completed_trades,
|
||||
'swaps': swaps
|
||||
}
|
||||
with open(TRADES_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def load_binance_positions_on_startup():
|
||||
"""Load current open positions from Binance on startup"""
|
||||
from src.bot.binance_client import BinanceClient
|
||||
import asyncio
|
||||
|
||||
async def _load():
|
||||
client = BinanceClient()
|
||||
positions = {}
|
||||
|
||||
# Get account balances
|
||||
balances = await client.get_balance()
|
||||
|
||||
# Scan for open positions (non-zero balances excluding USDT)
|
||||
for symbol, amount in balances.items():
|
||||
if symbol != 'USDT' and amount > 0.00001:
|
||||
# Get current price for this asset
|
||||
price = await client.get_price(f'{symbol}USDT')
|
||||
positions[f'{symbol}USDT'] = {
|
||||
'qty': amount,
|
||||
'buy_price': price, # Current price as reference
|
||||
'entry_time': None, # Lost on restart
|
||||
'status': 'open'
|
||||
}
|
||||
print(f'✅ Loaded from Binance: {symbol}USDT - Qty: {amount} @ ${price}')
|
||||
|
||||
return positions
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
return loop.run_until_complete(_load())
|
||||
|
||||
# Initialize on import
|
||||
init_persistence()
|
||||
|
||||
print('✅ Persistence module initialized')
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
import os, json, subprocess
|
||||
from datetime import datetime
|
||||
from binance.client import Client
|
||||
|
||||
with open('/home/marc/bot-deploy/.env') as f:
|
||||
env = {}
|
||||
for line in f:
|
||||
k, _, v = line.partition('=')
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
# Load bot state
|
||||
with open('/home/marc/bot-deploy/trades.json') as f:
|
||||
bot_state = json.load(f)
|
||||
|
||||
# Get balance from Binance
|
||||
c = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||
acc = c.get_account()
|
||||
balance = {a['asset']: float(a['free']) for a in acc['balances']}
|
||||
|
||||
# Calculate metrics
|
||||
portfolio_value = balance.get('USDT', 0)
|
||||
for asset in ['ETH', 'BTC', 'SOL', 'BNB', 'XRP']:
|
||||
if asset in balance:
|
||||
# Rough values (should use ticker for precision)
|
||||
prices = {'ETH': 1790, 'BTC': 63000, 'SOL': 83.5, 'BNB': 578, 'XRP': 2.5}
|
||||
portfolio_value += balance.get(asset, 0) * prices.get(asset, 0)
|
||||
|
||||
completed = bot_state.get('completed', [])
|
||||
daily_pnl = sum(t.get('profit_usd', 0) for t in completed)
|
||||
wins = len([t for t in completed if t.get('profit_usd', 0) > 0])
|
||||
losses = len([t for t in completed if t.get('profit_usd', 0) < 0])
|
||||
|
||||
# Format report
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M UTC')
|
||||
report = f'''📊 **TRADING BOT REPORT** — {timestamp}
|
||||
|
||||
💰 **PORTFOLIO**
|
||||
• Total: ${portfolio_value:.2f}
|
||||
• USDT Free: ${balance.get('USDT', 0):.2f}
|
||||
• Open Trades: {len(bot_state.get('current', {}))}
|
||||
|
||||
📈 **TODAY'S PERFORMANCE**
|
||||
• Trades: {len(completed)}
|
||||
• Wins: {wins} | Losses: {losses}
|
||||
• Win Rate: {(wins/(wins+losses)*100) if (wins+losses) > 0 else 0:.1f}%
|
||||
• Daily P&L: ${daily_pnl:.2f}
|
||||
|
||||
🟢 **BOT STATUS**: OPERATIONAL
|
||||
🔗 Dashboard: https://bot.bizmark.cloud
|
||||
|
||||
---
|
||||
*Next report in 3 hours*
|
||||
'''
|
||||
|
||||
# Send via Telegram using Hermes send_message
|
||||
print(report)
|
||||
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
State Manager V3: Ultra-Simple Binance Direct
|
||||
- Uses environment variables directly
|
||||
- No .env nonsense, uses os.environ
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
from binance.client import Client
|
||||
|
||||
# Read .env directly into os.environ BEFORE importing anything else
|
||||
env_file = '/home/marc/bot-deploy/.env'
|
||||
for line in open(env_file).readlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
k, v = line.split('=', 1)
|
||||
os.environ[k] = v.strip('"').strip("'")
|
||||
|
||||
API_KEY = os.environ.get('BINANCE_API_KEY')
|
||||
API_SECRET = os.environ.get('BINANCE_API_SECRET')
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if not API_KEY or not API_SECRET:
|
||||
logger.error(f"Missing credentials: key={bool(API_KEY)}, secret={bool(API_SECRET)}")
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(f"✅ API credentials loaded")
|
||||
|
||||
client = Client(API_KEY, API_SECRET)
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
state = {
|
||||
'current_trades': {},
|
||||
'completed_trades': [],
|
||||
'swaps': [],
|
||||
'balance': {},
|
||||
'portfolio_value_usd': 0.0,
|
||||
'daily_pnl': 0.0,
|
||||
'total_pnl': 0.0,
|
||||
'last_sync': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def load_from_binance():
|
||||
"""Load real data from Binance"""
|
||||
global state
|
||||
try:
|
||||
logger.info('🔄 Syncing with Binance...')
|
||||
|
||||
account = client.get_account()
|
||||
balances = {b['asset']: float(b['free']) for b in account['balances'] if float(b['free']) > 0.00001}
|
||||
state['balance'] = balances
|
||||
logger.info(f"Balance: USDT={balances.get('USDT', 0):.2f}")
|
||||
|
||||
open_trades = {}
|
||||
for symbol in ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']:
|
||||
try:
|
||||
orders = client.get_open_orders(symbol=symbol)
|
||||
if orders:
|
||||
o = orders[0]
|
||||
qty = float(o['origQty'])
|
||||
buy_price = float(o['price'])
|
||||
current_price = float(client.get_symbol_ticker(symbol=symbol)['price'])
|
||||
|
||||
profit = (current_price - buy_price) * qty
|
||||
profit_pct = ((current_price - buy_price) / buy_price * 100) if buy_price > 0 else 0
|
||||
|
||||
open_trades[symbol] = {
|
||||
'qty': qty,
|
||||
'buy_price': buy_price,
|
||||
'current_price': current_price,
|
||||
'buy_time': datetime.fromtimestamp(o['time']/1000).isoformat(),
|
||||
'profit': profit,
|
||||
'profit_pct': profit_pct,
|
||||
'order_id': o['orderId']
|
||||
}
|
||||
logger.info(f" {symbol}: {qty:.8f} → ${current_price:.2f} P&L: {profit_pct:.2f}%")
|
||||
except Exception as e:
|
||||
logger.debug(f"Error {symbol}: {e}")
|
||||
|
||||
state['current_trades'] = open_trades
|
||||
|
||||
usdt = balances.get('USDT', 0)
|
||||
portfolio = usdt + sum(t['qty']*t['current_price'] for t in open_trades.values())
|
||||
pnl = sum(t['profit'] for t in open_trades.values())
|
||||
|
||||
state['portfolio_value_usd'] = portfolio
|
||||
state['daily_pnl'] = pnl
|
||||
state['total_pnl'] = pnl
|
||||
state['last_sync'] = datetime.now().isoformat()
|
||||
|
||||
logger.info(f"✅ Portfolio: ${portfolio:.2f}, Trades: {len(open_trades)}, P&L: ${pnl:.2f}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
async def background_sync():
|
||||
while True:
|
||||
try:
|
||||
load_from_binance()
|
||||
await asyncio.sleep(10)
|
||||
except Exception as e:
|
||||
logger.error(f"Sync loop: {e}")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
logger.info("🚀 Starting State Manager...")
|
||||
load_from_binance()
|
||||
asyncio.create_task(background_sync())
|
||||
logger.info("✅ Sync active")
|
||||
|
||||
@app.get("/state")
|
||||
async def get_state():
|
||||
return state
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok", "trades": len(state['current_trades']), "portfolio": state['portfolio_value_usd']}
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("Starting on :8001")
|
||||
uvicorn.run(app, host="0.0.0.0", port=8001, log_level="error")
|
||||
657
web_dashboard.py
657
web_dashboard.py
|
|
@ -1,657 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
from fastapi import FastAPI, Response
|
||||
from binance.client import Client
|
||||
import json, os, time
|
||||
from datetime import datetime
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
env = {}
|
||||
with open('/home/marc/bot-deploy/.env') as f:
|
||||
for line in f:
|
||||
k,_,v = line.partition('=')
|
||||
env[k.strip()] = v.strip()
|
||||
|
||||
binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
||||
|
||||
price_cache = {'prices': {}, 'timestamp': 0}
|
||||
|
||||
def get_live_prices():
|
||||
global price_cache
|
||||
if time.time() - price_cache['timestamp'] < 5:
|
||||
return price_cache['prices']
|
||||
|
||||
prices = {'USDT': 1.0}
|
||||
pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
for pair in pairs:
|
||||
try:
|
||||
ticker = binance.get_ticker(symbol=pair)
|
||||
asset = pair.replace('USDT', '')
|
||||
prices[asset] = float(ticker['lastPrice'])
|
||||
except:
|
||||
pass
|
||||
|
||||
price_cache['prices'] = prices
|
||||
price_cache['timestamp'] = time.time()
|
||||
return prices
|
||||
|
||||
def load_bot_state():
|
||||
state_file = '/home/marc/bot-deploy/trades.json'
|
||||
if os.path.exists(state_file):
|
||||
try:
|
||||
with open(state_file) as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
pass
|
||||
return {'current': {}, 'completed': [], 'balance': {}}
|
||||
|
||||
@app.get('/api/state')
|
||||
async def get_state():
|
||||
try:
|
||||
account = binance.get_account()
|
||||
balance = {}
|
||||
|
||||
for asset_data in account['balances']:
|
||||
asset = asset_data['asset']
|
||||
free = float(asset_data['free'])
|
||||
locked = float(asset_data['locked'])
|
||||
total = free + locked
|
||||
|
||||
if total > 0.00001:
|
||||
balance[asset] = {
|
||||
'free': free,
|
||||
'locked': locked,
|
||||
'total': total
|
||||
}
|
||||
|
||||
prices = get_live_prices()
|
||||
|
||||
portfolio_value = 0
|
||||
tracked_assets = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT', 'USDC']
|
||||
|
||||
for asset in tracked_assets:
|
||||
if asset in balance:
|
||||
data = balance[asset]
|
||||
price = prices.get(asset, 0)
|
||||
portfolio_value += data['total'] * price
|
||||
|
||||
usdt_free = balance.get('USDT', {}).get('free', 0)
|
||||
|
||||
# P&L CALCULATION
|
||||
initial_capital = 137.79
|
||||
pnl_usdt = portfolio_value - initial_capital
|
||||
pnl_pct = (pnl_usdt / initial_capital * 100) if initial_capital > 0 else 0
|
||||
pnl_status = "🟢 PROFIT" if pnl_usdt > 0.01 else ("🔴 LOSS" if pnl_usdt < -0.01 else "⚪ BREAK")
|
||||
pnl_color = "accent" if pnl_usdt > 0.01 else ("negative" if pnl_usdt < -0.01 else "neutral")
|
||||
|
||||
# Count active positions = locked coins (NOT trades.json)
|
||||
active_positions = 0
|
||||
for asset in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP']:
|
||||
if asset in balance and balance[asset]['locked'] > 0.00001:
|
||||
active_positions += 1
|
||||
|
||||
trades = load_bot_state()
|
||||
|
||||
return {
|
||||
'balance': balance,
|
||||
'portfolio_value': round(portfolio_value, 2),
|
||||
'usdt_free': round(usdt_free, 2),
|
||||
'active_positions': active_positions, # ← NEW: Real count!
|
||||
'current_trades': trades.get('current', {}),
|
||||
'pnl_usdt': round(pnl_usdt, 2),
|
||||
'pnl_pct': round(pnl_pct, 2),
|
||||
'pnl_status': pnl_status,
|
||||
'pnl_color': pnl_color,
|
||||
'completed_trades': trades.get('completed', []),
|
||||
'prices': prices,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e), 'portfolio_value': 0, 'usdt_free': 0, 'active_positions': 0}
|
||||
|
||||
@app.get('/')
|
||||
async def root():
|
||||
state = await get_state()
|
||||
portfolio_val = state.get('portfolio_value', 0)
|
||||
usdt_free = state.get('usdt_free', 0)
|
||||
trades_count = state.get('active_positions', 0) # ← FIXED: Use real count!
|
||||
prices = state.get('prices', {})
|
||||
|
||||
|
||||
# P&L from state
|
||||
pnl_usdt = state.get("pnl_usdt", 0)
|
||||
pnl_pct = state.get("pnl_pct", 0)
|
||||
pnl_status = state.get("pnl_status", "⚪ BREAK")
|
||||
pnl_color = state.get("pnl_color", "neutral")
|
||||
html = f'''<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>Trading Bot V10</title>
|
||||
<style>
|
||||
:root {{
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #252525;
|
||||
--bg-tertiary: #2a2a2a;
|
||||
--bg-hover: #303030;
|
||||
--border: #404040;
|
||||
--text-primary: #e0e0e0;
|
||||
--text-secondary: #a0a0a0;
|
||||
--accent: #00ff88;
|
||||
--spacing: 1rem;
|
||||
}}
|
||||
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
html, body {{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}}
|
||||
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Monaco', 'Menlo', monospace;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
font-size: clamp(14px, 2vw, 16px);
|
||||
overflow-x: hidden;
|
||||
}}
|
||||
|
||||
.app-container {{
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
}}
|
||||
|
||||
.header {{
|
||||
margin-bottom: calc(var(--spacing) * 2.5);
|
||||
}}
|
||||
|
||||
.logo {{
|
||||
font-size: clamp(24px, 6vw, 32px);
|
||||
font-weight: bold;
|
||||
color: var(--accent);
|
||||
margin-bottom: 0.5rem;
|
||||
}}
|
||||
|
||||
.version {{
|
||||
font-size: clamp(11px, 2vw, 13px);
|
||||
color: var(--text-secondary);
|
||||
}}
|
||||
|
||||
/* ===== METRICS GRID ===== */
|
||||
.metrics-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: calc(var(--spacing) * 1.5);
|
||||
margin-bottom: calc(var(--spacing) * 3);
|
||||
}}
|
||||
|
||||
.metric-card {{
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
padding: calc(var(--spacing) * 1.5);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
min-height: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}}
|
||||
|
||||
.metric-card:active {{
|
||||
transform: scale(0.98);
|
||||
}}
|
||||
|
||||
.metric-card:hover {{
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 20px rgba(0, 255, 136, 0.1);
|
||||
}}
|
||||
|
||||
.metric-label {{
|
||||
font-size: clamp(11px, 1.5vw, 12px);
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
margin-bottom: 1rem;
|
||||
}}
|
||||
|
||||
.metric-value {{
|
||||
font-size: clamp(20px, 5vw, 32px);
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
}}
|
||||
|
||||
.metric-value.accent {{
|
||||
color: var(--accent);
|
||||
}}
|
||||
|
||||
/* ===== SECTIONS ===== */
|
||||
.section {{
|
||||
margin-bottom: calc(var(--spacing) * 3);
|
||||
}}
|
||||
|
||||
.section-header {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
padding: calc(var(--spacing) * 0.75) 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: calc(var(--spacing) * 1.25);
|
||||
user-select: none;
|
||||
transition: all 0.2s ease;
|
||||
}}
|
||||
|
||||
.section-header:hover {{
|
||||
color: var(--accent);
|
||||
}}
|
||||
|
||||
.section-title {{
|
||||
font-size: clamp(13px, 2.5vw, 15px);
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.2px;
|
||||
transition: color 0.2s ease;
|
||||
}}
|
||||
|
||||
.section-toggle {{
|
||||
font-size: clamp(14px, 2vw, 16px);
|
||||
color: var(--text-secondary);
|
||||
transition: transform 0.3s ease;
|
||||
margin-left: 0.5rem;
|
||||
}}
|
||||
|
||||
.section-toggle.expanded {{
|
||||
transform: rotate(180deg);
|
||||
}}
|
||||
|
||||
.section-content {{
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}}
|
||||
|
||||
.section-content.expanded {{
|
||||
max-height: 2000px;
|
||||
}}
|
||||
|
||||
/* ===== TABLES ===== */
|
||||
.table-wrapper {{
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-tertiary);
|
||||
}}
|
||||
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: clamp(12px, 2vw, 14px);
|
||||
}}
|
||||
|
||||
th {{
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
padding: calc(var(--spacing) * 1);
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
font-size: clamp(10px, 1.5vw, 12px);
|
||||
}}
|
||||
|
||||
td {{
|
||||
padding: calc(var(--spacing) * 0.875);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}}
|
||||
|
||||
tr:last-child td {{
|
||||
border-bottom: none;
|
||||
}}
|
||||
|
||||
tbody tr {{
|
||||
transition: background 0.2s ease;
|
||||
}}
|
||||
|
||||
tbody tr:hover {{
|
||||
background: var(--bg-hover);
|
||||
}}
|
||||
|
||||
tbody tr:active {{
|
||||
background: var(--bg-secondary);
|
||||
}}
|
||||
|
||||
.price-positive {{
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
/* ===== RESPONSIVE ===== */
|
||||
@media (max-width: 1200px) {{
|
||||
.metrics-grid {{
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}}
|
||||
}}
|
||||
|
||||
@media (max-width: 768px) {{
|
||||
:root {{
|
||||
--spacing: 0.875rem;
|
||||
}}
|
||||
|
||||
.app-container {{
|
||||
padding: calc(var(--spacing) * 1.25);
|
||||
}}
|
||||
|
||||
.metrics-grid {{
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--spacing);
|
||||
}}
|
||||
|
||||
.metric-card {{
|
||||
padding: var(--spacing);
|
||||
min-height: 120px;
|
||||
}}
|
||||
|
||||
.metric-label {{
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 10px;
|
||||
}}
|
||||
|
||||
.metric-value {{
|
||||
font-size: clamp(18px, 4vw, 26px);
|
||||
}}
|
||||
|
||||
.section {{
|
||||
margin-bottom: calc(var(--spacing) * 1.75);
|
||||
}}
|
||||
|
||||
th, td {{
|
||||
padding: calc(var(--spacing) * 0.75);
|
||||
font-size: 11px;
|
||||
}}
|
||||
|
||||
th {{
|
||||
font-size: 10px;
|
||||
}}
|
||||
}}
|
||||
|
||||
@media (max-width: 480px) {{
|
||||
:root {{
|
||||
--spacing: 0.75rem;
|
||||
}}
|
||||
|
||||
.app-container {{
|
||||
padding: var(--spacing);
|
||||
}}
|
||||
|
||||
.metrics-grid {{
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: calc(var(--spacing) * 0.75);
|
||||
}}
|
||||
|
||||
.metric-card {{
|
||||
padding: calc(var(--spacing) * 0.875);
|
||||
min-height: 110px;
|
||||
}}
|
||||
|
||||
.metric-label {{
|
||||
font-size: 9px;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: 0.5px;
|
||||
}}
|
||||
|
||||
.metric-value {{
|
||||
font-size: clamp(16px, 3.5vw, 22px);
|
||||
}}
|
||||
|
||||
.logo {{
|
||||
font-size: clamp(20px, 5vw, 26px);
|
||||
}}
|
||||
|
||||
.version {{
|
||||
font-size: 10px;
|
||||
}}
|
||||
|
||||
.section-title {{
|
||||
font-size: 11px;
|
||||
}}
|
||||
|
||||
th, td {{
|
||||
padding: calc(var(--spacing) * 0.6);
|
||||
font-size: 9px;
|
||||
}}
|
||||
|
||||
th {{
|
||||
font-size: 8px;
|
||||
}}
|
||||
|
||||
.table-wrapper {{
|
||||
border-radius: 6px;
|
||||
}}
|
||||
}}
|
||||
|
||||
/* ===== SCROLLBAR ===== */
|
||||
::-webkit-scrollbar {{
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}}
|
||||
|
||||
::-webkit-scrollbar-track {{
|
||||
background: var(--bg-secondary);
|
||||
}}
|
||||
|
||||
::-webkit-scrollbar-thumb {{
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {{
|
||||
background: var(--text-secondary);
|
||||
}}
|
||||
|
||||
/* ===== ANIMATIONS ===== */
|
||||
@keyframes fadeIn {{
|
||||
from {{
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}}
|
||||
to {{
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}}
|
||||
}}
|
||||
|
||||
.metric-card {{
|
||||
animation: fadeIn 0.5s ease forwards;
|
||||
}}
|
||||
|
||||
.metric-card:nth-child(2) {{
|
||||
animation-delay: 0.1s;
|
||||
}}
|
||||
|
||||
.metric-card:nth-child(3) {{
|
||||
animation-delay: 0.2s;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<div class="header">
|
||||
<div class="logo">💰 Trading Bot</div>
|
||||
<div class="version">V10 — Real-time Portfolio Dashboard</div>
|
||||
</div>
|
||||
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Portfolio Value</div>
|
||||
<div class="metric-value">${portfolio_val:.2f}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">USDT Available</div>
|
||||
<div class="metric-value accent">${usdt_free:.2f}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Open Positions</div>
|
||||
<div class="metric-value">{trades_count}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Total P&L</div>
|
||||
<div class="metric-value {pnl_color}">${pnl_usdt:+.2f} ({pnl_pct:+.1f}%)</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">P&L Status</div>
|
||||
<div class="metric-value {pnl_color}">{pnl_status}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header" onclick="toggleSection(this)">
|
||||
<div class="section-title">Live Prices</div>
|
||||
<div class="section-toggle">▼</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>'''
|
||||
|
||||
for asset, price in prices.items():
|
||||
html += f'''<tr>
|
||||
<td>{asset}</td>
|
||||
<td class="price-positive">${price:.2f}</td>
|
||||
</tr>'''
|
||||
|
||||
html += '''</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-header" onclick="toggleSection(this)">
|
||||
<div class="section-title">Holdings</div>
|
||||
<div class="section-toggle">▼</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset</th>
|
||||
<th>Free</th>
|
||||
<th>Total</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>'''
|
||||
|
||||
tracked = ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT', 'USDC']
|
||||
balance = state.get('balance', {})
|
||||
|
||||
for asset in tracked:
|
||||
if asset in balance:
|
||||
data = balance[asset]
|
||||
price = prices.get(asset, 0)
|
||||
value = data['total'] * price
|
||||
html += f'''<tr>
|
||||
<td>{asset}</td>
|
||||
<td>{data['free']:.4f}</td>
|
||||
<td>{data['total']:.4f}</td>
|
||||
<td class="price-positive">${value:.2f}</td>
|
||||
</tr>'''
|
||||
|
||||
html += '''</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleSection(header) {
|
||||
const content = header.nextElementSibling;
|
||||
const toggle = header.querySelector('.section-toggle');
|
||||
|
||||
content.classList.toggle('expanded');
|
||||
toggle.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
// Refresh prices every 5 seconds
|
||||
setInterval(function() {{
|
||||
location.reload();
|
||||
}}, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
return Response(content=html, media_type='text/html')
|
||||
|
||||
|
||||
@app.get('/api/pnl')
|
||||
async def get_pnl():
|
||||
"""Get live Profit & Loss (P&L) calculation"""
|
||||
try:
|
||||
account = binance.get_account()
|
||||
|
||||
# Get current account value
|
||||
prices = get_live_prices()
|
||||
current_value = 0
|
||||
|
||||
for asset_data in account['balances']:
|
||||
asset = asset_data['asset']
|
||||
total = float(asset_data['free']) + float(asset_data['locked'])
|
||||
|
||||
if total > 0.00001 and asset != 'LDDOGE' and asset != 'LDBTTC':
|
||||
price = prices.get(asset, 1.0)
|
||||
current_value += total * price
|
||||
|
||||
# Benchmark: Initial capital was $137.79 (before trading)
|
||||
# This should be stored, but for now use a reference
|
||||
initial_capital = 137.79
|
||||
|
||||
pnl_usdt = current_value - initial_capital
|
||||
pnl_pct = (pnl_usdt / initial_capital * 100) if initial_capital > 0 else 0
|
||||
|
||||
# Get open trades for unrealized portion
|
||||
state_file = '/home/marc/bot-deploy/trades.json'
|
||||
open_trades = {}
|
||||
if os.path.exists(state_file):
|
||||
try:
|
||||
data = json.load(state_file)
|
||||
open_trades = data.get('current', {})
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
'current_value': round(current_value, 2),
|
||||
'initial_capital': initial_capital,
|
||||
'total_pnl_usdt': round(pnl_usdt, 2),
|
||||
'total_pnl_percent': round(pnl_pct, 2),
|
||||
'status': '🟢 PROFIT' if pnl_usdt > 0 else ('🔴 LOSS' if pnl_usdt < 0 else '⚪ BREAK'),
|
||||
'open_positions': len(open_trades),
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
uvicorn.run(app, host='0.0.0.0', port=7000)
|
||||
Loading…
Reference in New Issue