Compare commits
No commits in common. "v0.7.1-recovery" and "main" have entirely different histories.
v0.7.1-rec
...
main
|
|
@ -1,56 +0,0 @@
|
|||
# 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
|
||||
385
ARCHITECTURE.md
385
ARCHITECTURE.md
|
|
@ -1,385 +0,0 @@
|
|||
# Trading Bot V0.2 — System Architecture
|
||||
|
||||
**Version:** 0.2 (Production)
|
||||
**Last Updated:** 2026-07-08
|
||||
**Status:** 🟢 LIVE with Auto-Swap Feature
|
||||
|
||||
## Overview
|
||||
|
||||
Trading Bot V0.2 is an adaptive cryptocurrency trading system running on Binance with:
|
||||
- **Adaptive Strategy Learning** (Win Rate Tracking)
|
||||
- **Risk Management** (Daily Loss Limit, Stop Loss, Take Profit, Trailing Stop)
|
||||
- **Auto-Swap Feature** (Convert free coins to USDT)
|
||||
- **Real-time Dashboard** (FastAPI + Jinja2)
|
||||
- **Telegram Integration** (3-hourly reports + alerts)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TRADING BOT V0.2 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ Core Trading Engine (main_ml.py) │ │
|
||||
│ │ • Adaptive strategy (5 levels: Emergency→Full Throttle)│ │
|
||||
│ │ • Risk management (SL -1.8%, TP +2.8%, Daily -5%) │ │
|
||||
│ │ • Position tracking & P&L calculation │ │
|
||||
│ │ • Win rate analysis (hourly evaluation) │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ ┌────────▼──────────┐ ┌──────────────▼──────┐ │
|
||||
│ │ Binance API │ │ Auto-Swap Module │ │
|
||||
│ │ • Place orders │ │ • Convert free │ │
|
||||
│ │ • Monitor fills │ │ coins → USDT │ │
|
||||
│ │ • Get balances │ │ • Skip-list logic │ │
|
||||
│ │ • Track trades │ │ • Telegram notify │ │
|
||||
│ └───────────────────┘ └─────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────▼──────────────────────────────────────────────┐ │
|
||||
│ │ Dashboard (web_dashboard.py) │ │
|
||||
│ │ • Real-time portfolio value & P&L │ │
|
||||
│ │ • Holdings summary (locked/free breakdown) │ │
|
||||
│ │ • Active positions & open orders │ │
|
||||
│ │ • Strategy status & performance metrics │ │
|
||||
│ │ FastAPI (port 7000) + Jinja2 templates │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────▼──────────────────────────────────────────────┐ │
|
||||
│ │ Telegram Integration │ │
|
||||
│ │ • 3-hour performance reports │ │
|
||||
│ │ • Trade execution alerts │ │
|
||||
│ │ • Error notifications │ │
|
||||
│ │ • Swap completion confirmations │ │
|
||||
│ └────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Trading Engine (src/main_ml.py)
|
||||
|
||||
**Main Class:** `TradingBot`
|
||||
|
||||
#### Strategy Parameters
|
||||
```python
|
||||
PAIRS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
STOP_LOSS_PERCENT = 1.8 # -1.8%
|
||||
TAKE_PROFIT_PERCENT = 2.8 # +2.8%
|
||||
DAILY_LOSS_LIMIT = -5 # Pause after -5%
|
||||
MAX_POSITIONS = 1 # Single position
|
||||
INVESTMENT_PERCENT = 50-55% # Per trade
|
||||
```
|
||||
|
||||
#### Adaptive Learning (Option 2: Win Rate Tracking)
|
||||
```
|
||||
Win Rate < 45% → Emergency Level (minimal trading)
|
||||
45-50% → Conservative Level
|
||||
50-60% → Standard Level (base)
|
||||
60-70% → Aggressive Level
|
||||
> 70% → Full Throttle Level
|
||||
|
||||
Adjustments per level:
|
||||
• Signal threshold (5.0% → 10.0%)
|
||||
• Investment (50% → 55%)
|
||||
• Take profit (1.5% → 3.5%)
|
||||
• Stop loss (1.0% → 2.2%)
|
||||
• Max trades/day (5 → 25)
|
||||
```
|
||||
|
||||
#### Key Methods
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `run_cycle()` | Main trading loop (every ~10s) |
|
||||
| `evaluate_signal()` | Generate trading signal (7.5% base prob) |
|
||||
| `place_trade()` | Execute buy order with SL/TP |
|
||||
| `check_positions()` | Monitor open trades, close on SL/TP |
|
||||
| `calculate_pnl()` | Compute portfolio P&L (live + closed) |
|
||||
| `update_adaptive_strategy()` | Hourly win rate evaluation |
|
||||
| `swap_coins_to_usdt()` | **NEW:** Convert free coins to USDT |
|
||||
| `get_usdt_balance()` | **NEW:** Query current USDT balance |
|
||||
| `send_performance_report()` | 3-hour Telegram summary |
|
||||
|
||||
#### Risk Management
|
||||
|
||||
**Daily Loss Limit:**
|
||||
- If P&L <= -5%, bot pauses trading
|
||||
- Resets at UTC 00:00
|
||||
- Prevents catastrophic drawdowns
|
||||
|
||||
**Stop Loss & Take Profit:**
|
||||
- SL -1.8% per trade (position auto-closed)
|
||||
- TP +2.8% per trade (position auto-closed)
|
||||
- OR Trailing Stop: +1.5% entry, 0.6% trail
|
||||
|
||||
**Consecutive Loss Cooldown:**
|
||||
- After 3 consecutive losses: 30min pause
|
||||
- Prevents emotional spiraling
|
||||
|
||||
**Position Limits:**
|
||||
- Max 1 open position at a time
|
||||
- Prevents over-leverage
|
||||
|
||||
---
|
||||
|
||||
### 2. Auto-Swap Feature (NEW - 2026-07-08)
|
||||
|
||||
**Function:** `swap_coins_to_usdt()`
|
||||
**Lines:** 387-461 in main_ml.py
|
||||
|
||||
**Purpose:** Automatically convert all free (unlocked) coins to USDT
|
||||
|
||||
**Logic:**
|
||||
```python
|
||||
1. Get account balance via Binance API
|
||||
2. For each coin:
|
||||
- Skip if: USDT, LDBTTC, LDDOGE, USDC, locked, dust (<0.00001)
|
||||
- Get current price (COINUSDT pair)
|
||||
- Round quantity to Binance step size
|
||||
- Execute MARKET SELL
|
||||
- Calculate USDT received
|
||||
3. Send Telegram notification with results
|
||||
4. Return total USDT acquired
|
||||
```
|
||||
|
||||
**Skip-List (Never Swap):**
|
||||
- USDT (target currency)
|
||||
- LDBTTC (fake/scam token)
|
||||
- LDDOGE (shitcoin)
|
||||
- USDC (too small)
|
||||
- Any coin marked as locked (in active trades)
|
||||
|
||||
**Execution Result (2026-07-08 23:28 UTC):**
|
||||
| Coin | Qty | USDT | Status |
|
||||
|------|-----|------|--------|
|
||||
| BNB | 0.019 | $10.75 | ✅ |
|
||||
| XRP | 28.7 | $31.20 | ✅ |
|
||||
| SOL | 0.294 | $22.67 | ✅ |
|
||||
| **TOTAL** | — | **+$64.61** | ✅ |
|
||||
|
||||
---
|
||||
|
||||
### 3. Dashboard (src/web_dashboard.py)
|
||||
|
||||
**Framework:** FastAPI + Jinja2
|
||||
**Port:** 7000
|
||||
**Refresh:** 10 seconds (live updates)
|
||||
|
||||
**Endpoints:**
|
||||
| Endpoint | Purpose |
|
||||
|----------|---------|
|
||||
| `GET /` | Render main dashboard HTML |
|
||||
| `GET /api/state` | JSON: portfolio, P&L, holdings, orders |
|
||||
|
||||
**Dashboard Sections:**
|
||||
1. **Header:** Bot status (🟢 RUNNING or ⏸️ PAUSED)
|
||||
2. **Portfolio Kachel:** USDT value, P&L %, color-coded
|
||||
3. **Holdings (Collapsible):** All coins + USD values
|
||||
4. **Live Prices (Collapsible):** Real-time BTCUSDT, ETHUSDT, etc.
|
||||
5. **Active Positions:** Current open trades (entry price, SL, TP)
|
||||
|
||||
**Design:**
|
||||
- Colors: Grayscale (#1e1e1e bg, #d0d0d0 text)
|
||||
- Accent: Green (#00ff88) ONLY for Portfolio & USDT values
|
||||
- Responsive, collapsible sections (both collapsed on load)
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Trading Cycle (run_cycle)
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 1. Get current portfolio balance & P&L │
|
||||
│ 2. Check if -5% daily limit reached → PAUSE if true │
|
||||
│ 3. Check all open positions for SL/TP exit │
|
||||
│ 4. Evaluate signal (7.5% base probability) │
|
||||
│ 5. If signal + capital > $5: Place trade │
|
||||
│ 6. Update adaptive strategy (hourly) │
|
||||
│ 7. Send Telegram report (if 3h elapsed) │
|
||||
│ 8. Repeat every ~10s │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### P&L Calculation
|
||||
```
|
||||
Portfolio Value = Balance(USDT) + Sum(Coin Value in USDT)
|
||||
|
||||
P&L USDT = Portfolio Value - Initial Capital ($137.79)
|
||||
|
||||
P&L % = (P&L USDT / Initial Capital) × 100
|
||||
|
||||
Status = 🟢 GREEN if P&L > 0, 🔴 RED if P&L < 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment (.env)
|
||||
```
|
||||
BINANCE_API_KEY_LIVE=...
|
||||
BINANCE_API_SECRET_LIVE=...
|
||||
TELEGRAM_BOT_TOKEN=...
|
||||
TELEGRAM_CHAT_ID=7646180954
|
||||
```
|
||||
|
||||
### In Code (main_ml.py Line ~30-60)
|
||||
```python
|
||||
PAIRS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
|
||||
SIGNAL_THRESHOLD = 7.5 # 7.5% signal probability
|
||||
INVESTMENT_PERCENT = 50 # 50% of capital per trade
|
||||
STOP_LOSS_PERCENT = 1.8 # -1.8% SL
|
||||
TAKE_PROFIT_PERCENT = 2.8 # +2.8% TP
|
||||
DAILY_LOSS_LIMIT = -5 # -5% pause threshold
|
||||
MAX_OPEN_POSITIONS = 1
|
||||
MAX_CONSECUTIVE_LOSSES = 3
|
||||
CONSECUTIVE_LOSS_COOLDOWN = 30 * 60 # 30 minutes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### SystemD Service
|
||||
```
|
||||
Service: trading-bot.service
|
||||
File: /etc/systemd/system/trading-bot.service
|
||||
User: root
|
||||
WorkDir: /home/marc/bot-deploy
|
||||
Exec: python3 /home/marc/bot-deploy/src/main_ml.py
|
||||
AutoStart: yes
|
||||
```
|
||||
|
||||
### Start/Stop
|
||||
```bash
|
||||
sudo systemctl start trading-bot.service
|
||||
sudo systemctl stop trading-bot.service
|
||||
sudo systemctl restart trading-bot.service
|
||||
sudo systemctl status trading-bot.service
|
||||
```
|
||||
|
||||
### Logs
|
||||
```bash
|
||||
journalctl -u trading-bot.service -f # Live tail
|
||||
journalctl -u trading-bot.service -n 50 # Last 50 lines
|
||||
journalctl -u trading-bot.service --since "1 hour ago"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git Repository
|
||||
|
||||
**URL:** ssh://git@172.16.1.168:222/marc/BrainDock.git
|
||||
**Branch:** master
|
||||
**Latest:** Commit ae15976 (Auto-Swap Feature)
|
||||
|
||||
**Structure:**
|
||||
```
|
||||
BrainDock/
|
||||
├── src/
|
||||
│ ├── main_ml.py # Core trading engine (V0.2)
|
||||
│ ├── web_dashboard.py # FastAPI dashboard
|
||||
│ └── __init__.py
|
||||
├── docs/
|
||||
│ └── ARCHITECTURE.md # This file
|
||||
├── README.md # User-facing features
|
||||
├── requirements.txt # Dependencies
|
||||
├── .gitignore # Excludes: __pycache__, *.log, .env, venv/
|
||||
└── ARCHITECTURE.md # System design (this repo)
|
||||
```
|
||||
|
||||
**Auto-Sync Cron:**
|
||||
```
|
||||
# Every 5 minutes, auto-commit changes from /home/marc/bot-deploy/src → BrainDock/src
|
||||
*/5 * * * * git -C /home/marc/bot-versions/BrainDock add src/ && git commit -m "Auto-sync: $(date)" && git push origin master 2>/dev/null || true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Alerts
|
||||
|
||||
### Telegram Reports
|
||||
- **Frequency:** Every 3 hours
|
||||
- **Content:**
|
||||
- Portfolio value + P&L
|
||||
- Trades executed today (wins/losses)
|
||||
- Current strategy level
|
||||
- Bot status (running/paused)
|
||||
|
||||
### Manual Commands (Python)
|
||||
```python
|
||||
bot = TradingBot()
|
||||
|
||||
# Get current P&L
|
||||
report = bot.get_performance_report()
|
||||
print(report['portfolio'], report['pnl_usdt'], report['pnl_pct'])
|
||||
|
||||
# Swap all free coins to USDT
|
||||
result = bot.swap_coins_to_usdt()
|
||||
print(f"Converted: ${result['total_usdt_acquired']:.2f}")
|
||||
|
||||
# Get USDT balance
|
||||
usdt = bot.get_usdt_balance()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations & Future Work
|
||||
|
||||
### Current Limitations
|
||||
- **Signal:** Still random (7.5% base probability), not ML-based
|
||||
- **Pairs:** Fixed list (5 pairs), not dynamic
|
||||
- **Levels:** 5 strategy levels (can expand)
|
||||
- **Fees:** No explicit fee tracking (implicit in P&L)
|
||||
|
||||
### Future Enhancements
|
||||
- [ ] Machine Learning signal (instead of random)
|
||||
- [ ] Dynamic pair selection (trending symbols only)
|
||||
- [ ] Advanced technical indicators (RSI, MACD, etc.)
|
||||
- [ ] Portfolio rebalancing scheduler
|
||||
- [ ] Webhook API for external signals
|
||||
- [ ] Database logging (trade history, performance metrics)
|
||||
- [ ] Mobile alerts (SMS, Push notifications)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot Not Trading (1-hour+ no activity)
|
||||
1. Check daily P&L: `curl http://localhost:7000/api/state | jq '.pnl_pct'`
|
||||
2. If P&L < -5%, bot is paused (wait until UTC 00:00)
|
||||
3. Check logs: `journalctl -u trading-bot.service -n 50`
|
||||
|
||||
### Dashboard Shows $0.00
|
||||
- Rare bug (fixed 2026-07-08)
|
||||
- Restart: `sudo systemctl restart trading-bot.service`
|
||||
|
||||
### High Number of Rejected Orders
|
||||
- **Cause:** USDT balance too low (< $5 per order)
|
||||
- **Fix:** Use `bot.swap_coins_to_usdt()` to convert free coins
|
||||
|
||||
### Telegram Notifications Not Arriving
|
||||
- Check API keys in .env
|
||||
- Verify Telegram chat ID: `curl "https://api.telegram.org/bot{TOKEN}/getMe"`
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| V0.2 | 2026-07-08 | ✅ Live: Adaptive Learning + Auto-Swap Feature |
|
||||
| V5 | (Previous) | Archived (manual strategy, no learning) |
|
||||
|
||||
---
|
||||
|
||||
**Maintained by:** Hermes Agent
|
||||
**Last Review:** 2026-07-08 23:35 UTC
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
# Trading Bot v0.4 — Dynamic Position Sizing (GEPLANT)
|
||||
|
||||
**Status:** 🟡 PLANNED (Implementation: 2026-07-18)
|
||||
**Current Production:** v0.3 (stable)
|
||||
**Rollback Path:**
|
||||
|
||||
## Problem v0.3
|
||||
|
||||
- **Hardcoded MAX_TRADE = 0** (nicht skalierbar)
|
||||
- Portfolio 50 → Max 0 (13% — zu aggressiv)
|
||||
- Portfolio 00 → Max 0 (4% — nicht genutzt)
|
||||
- Portfolio 0k → Max 0 (0.2% — ineffizient)
|
||||
|
||||
## Lösung v0.4
|
||||
|
||||
**Dynamisches Position Sizing: **
|
||||
|
||||
|
||||
|
||||
✅ **Auto-skaliert bei Gewinn oder Zukauf**
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Neue Konstanten
|
||||
|
||||
|
||||
### Berechnung pro Zyklus
|
||||
|
||||
|
||||
### Logging
|
||||
|
||||
|
||||
## Deployment Plan (2026-07-18)
|
||||
|
||||
1. Schreibe (clean, keine sed-Patches)
|
||||
2. Test:
|
||||
3. Deploy:
|
||||
4. Restart:
|
||||
5. Verify: Dashboard zeigt neue Max Trade
|
||||
6. Git commit:
|
||||
|
||||
## Rollback (falls nötig)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
**Dokumentation:** See (Obsidian)
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
# 📊 Trading Bot Dashboard v0.32 — Update Log
|
||||
|
||||
**Version:** 0.32
|
||||
**Release Date:** 2026-07-15
|
||||
**Status:** 🟢 PRODUCTION
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features v0.32
|
||||
|
||||
### Tab 1: Portfolio (Live)
|
||||
- **Portfolio Value** — Real-time Binance account balance in USD
|
||||
- **Total P&L** — Absolute profit/loss in USD + percentage
|
||||
- **Free USDT** — Available balance for trading
|
||||
- **Active Trades** — Count of open positions (BTC, ETH, BNB, XRP, SOL)
|
||||
- **Holdings Breakdown** — Each asset with 4 decimal precision (BTC 0.0003, ETH 0.0114, etc.)
|
||||
|
||||
### Tab 2: Analytics (P&L Charts)
|
||||
- **Timeframe Selection:** 1 Day, 1 Week, 1 Month
|
||||
- **Chart Visualization:** Animated line chart (Chart.js)
|
||||
- Green line = Profit
|
||||
- Red line = Loss
|
||||
- Real-time data from Binance API
|
||||
- **Statistics (4-column responsive grid):**
|
||||
- **Current:** Latest P&L % + USD value
|
||||
- **Min:** Lowest P&L in period
|
||||
- **Max:** Highest P&L in period
|
||||
- **Avg:** Average P&L across period
|
||||
- *Each stat shows both % and USD value*
|
||||
|
||||
### Design
|
||||
- **Dark Theme:** Professional #1e1e1e background
|
||||
- **Green Accents:** #00ff88 for key metrics
|
||||
- **Responsive:** Mobile-optimized (collapsible Holdings, 2-column stat grid on small screens)
|
||||
- **Glassmorphism:** Modern card design with blur effects
|
||||
- **Smooth Animations:** Tab transitions, chart rendering
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Improvements (Latest)
|
||||
|
||||
| Issue | Solution | Status |
|
||||
|-------|----------|--------|
|
||||
| Holdings always visible | Added collapsible toggle (default: closed) | ✅ |
|
||||
| Too many decimals (BTC 0.00030000) | Reduced to 4 decimals (0.0003) | ✅ |
|
||||
| Analytics only showed % | Added USD values ($+1.20, -bash.65) | ✅ |
|
||||
| Stats not mobile-friendly | 2-column grid on mobile (max-width: 768px) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 📱 Mobile Optimization (New)
|
||||
|
||||
**Breakpoint:** 768px width (tablets & phones)
|
||||
- Single-column portfolio cards
|
||||
- 2-column analytics stats (instead of 4)
|
||||
- Smaller fonts (preserved readability)
|
||||
- Compact padding (10px header, 15px cards)
|
||||
- Flex timeframe buttons with wrapping
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Technical Details
|
||||
|
||||
### API Endpoints
|
||||
|
||||
|
||||
### Database
|
||||
- **SQLite:**
|
||||
- **Table:** (ts, pv, pu, pp, uf, ap)
|
||||
- **Retention:** Unlimited (continuous tracking)
|
||||
|
||||
### Data Collection
|
||||
- P&L snapshots stored every bot cycle (60 seconds)
|
||||
- Portfolio value calculated from Binance
|
||||
- Live prices from Binance
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI Components
|
||||
|
||||
### Holdings (Collapsible)
|
||||
|
||||
|
||||
### Analytics Stats (Responsive)
|
||||
**Desktop (4 columns):**
|
||||
| Current | Min | Max | Avg |
|
||||
| +1.20 % | -2.50 % | +3.80 % | +0.95 % |
|
||||
| +.65 | -.44 | +.22 | +.31 |
|
||||
|
||||
**Mobile (2 columns):**
|
||||
| Current | Min |
|
||||
| +1.20 % | -2.50 % |
|
||||
| +.65 | -.44 |
|
||||
| Max | Avg |
|
||||
| +3.80 % | +0.95 % |
|
||||
| +.22 | +.31 |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Browser Compatibility
|
||||
|
||||
✅ Chrome/Chromium (latest)
|
||||
✅ Firefox (latest)
|
||||
✅ Safari (latest)
|
||||
✅ Mobile browsers (iOS Safari, Chrome Mobile)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Example Data (Live)
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
**File:**
|
||||
**Port:** 7000
|
||||
**Service:**
|
||||
**URL:**
|
||||
|
||||
**Restart:**
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Rollback
|
||||
|
||||
**To v0.3:**
|
||||
|
||||
|
||||
**Git Tags:**
|
||||
- — Previous version
|
||||
- — Current production
|
||||
|
||||
---
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
**v0.32 (2026-07-15)**
|
||||
- ✅ Added two-tab interface (Portfolio + Analytics)
|
||||
- ✅ P&L Charts with 1d/7w/1m timeframes
|
||||
- ✅ Collapsible Holdings (default: closed)
|
||||
- ✅ 4-decimal precision for coin amounts
|
||||
- ✅ USD values in Analytics stats
|
||||
- ✅ Mobile-optimized responsive design
|
||||
- ✅ Live Binance API integration
|
||||
- ✅ SQLite P&L history tracking
|
||||
- ✅ Chart.js animated visualizations
|
||||
- ✅ Dark theme with green accents
|
||||
|
||||
---
|
||||
|
||||
**Active Commits:**
|
||||
- — Update: Collapsible Holdings, USD values, Mobile stats (2026-07-15 11:00 UTC)
|
||||
- — Release: v0.32 Dashboard v0.32 P&L Charts (2026-07-15 10:55 UTC)
|
||||
|
||||
---
|
||||
|
||||
**Repository:** https://git.bizmark.cloud/marc/BrainDock
|
||||
**Branch:** master (v0.32)
|
||||
**Maintained by:** Hermes Agent
|
||||
84
README.md
84
README.md
|
|
@ -1,84 +0,0 @@
|
|||
# 🤖 Trading Bot — Version 0.3 (PRODUCTION)
|
||||
|
||||
**Version:** 0.3 | **Status:** 🟢 LIVE | **Updated:** 2026-07-14
|
||||
|
||||
## 📊 Current Performance (Live)
|
||||
|
||||
| Metric | Value | Status |
|
||||
|--------|-------|--------|
|
||||
| **Portfolio** | $107.37 | 🟢 +0.49% |
|
||||
| **Trades** | 5 live | ✅ Balanced |
|
||||
| **Free USDT** | $18.75 | Active |
|
||||
| **Win Rate** | +2.06%-2.20% | ✅ Consistent |
|
||||
| **Bot** | RUNNING | ✅ 24/7 |
|
||||
|
||||
## 🎯 Strategy v0.3
|
||||
|
||||
**Algorithm:** Local Minimum Detection
|
||||
- Scan 30min price history
|
||||
- Detect support levels
|
||||
- TP: +1.5% | SL: -0.8%
|
||||
- Max 1 open trade
|
||||
|
||||
**Active Pairs:** BTC, ETH, BNB, XRP, SOL
|
||||
|
||||
**Risk:** 50% capital/trade, no leverage, -5% daily pause
|
||||
|
||||
## 📁 Files (v0.3 ONLY)
|
||||
|
||||
- main_ml.py ✅ (production engine)
|
||||
- web_dashboard.py (live UI)
|
||||
- Integrations (Telegram, Obsidian, Dashboard)
|
||||
|
||||
**Removed (cleanup 2026-07-14):**
|
||||
- ❌ Backups (main_ml_BACKUP*, v2, v4_backup, v6)
|
||||
- ❌ DCA strategy (deprecated)
|
||||
- ❌ Old monitoring tools
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
**Status:**
|
||||
```bash
|
||||
systemctl status trading-bot.service
|
||||
journalctl -u trading-bot.service -f
|
||||
```
|
||||
|
||||
**Dashboard:** https://bot.bizmark.cloud (Port 7000)
|
||||
|
||||
**Configure:** Edit `src/main_ml.py`
|
||||
```python
|
||||
TAKE_PROFIT_PERCENT = 1.5 # 1.0-2.5%
|
||||
STOP_LOSS_PERCENT = 0.8 # 0.5-1.5%
|
||||
INVESTMENT_PERCENT = 50 # 10-55%
|
||||
CYCLE_INTERVAL = 60 # 30-120s
|
||||
```
|
||||
|
||||
After changes: `git commit` → `git push` → `systemctl restart trading-bot.service`
|
||||
|
||||
## 📱 Telegram Reports (3h)
|
||||
|
||||
Auto-delivery: 00:00, 03:00, 06:00, 09:00, 12:00, 15:00, 18:00, 21:00 UTC
|
||||
- Marc: 7646180954
|
||||
- Brother: 8518722579
|
||||
- Via: @bizMarkTrading_Bot
|
||||
|
||||
## 🔄 Versioning
|
||||
|
||||
**v0.3 (NOW):** Local Minimum + 3h reports + clean repo
|
||||
|
||||
**Downgrade v0.3 → v0.2:**
|
||||
```bash
|
||||
git log --oneline
|
||||
git checkout <v0.2-hash>
|
||||
systemctl restart trading-bot.service
|
||||
```
|
||||
|
||||
## 📞 Help
|
||||
|
||||
- Logs: `journalctl -u trading-bot.service -f`
|
||||
- Status: `curl http://172.16.1.168:7000/api/state | jq '.'`
|
||||
- Report: `/home/marc/.pyenv/versions/3.10.16/bin/python3 /home/marc/bot-deploy/send_3h_report.py`
|
||||
|
||||
---
|
||||
|
||||
**Repo:** https://git.bizmark.cloud/marc/BrainDock | **Branch:** master (v0.3) | **Status:** 🟢 PRODUCTION READY
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
services:
|
||||
obsidian:
|
||||
image: lscr.io/linuxserver/obsidian:latest
|
||||
container_name: obsidian
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Zurich
|
||||
ports:
|
||||
- "3002:3000"
|
||||
- "3003:3001"
|
||||
- "27123:27123"
|
||||
- "27124:27124"
|
||||
- "8642:8642" # Hermes Gateway API
|
||||
- "9119:9119" # Hermes Dashboard
|
||||
volumes:
|
||||
- /opt/obsidian:/config
|
||||
shm_size: "1gb"
|
||||
restart: unless-stopped
|
||||
|
||||
hermes-agent:
|
||||
image: nousresearch/hermes-agent:latest
|
||||
container_name: hermes-agent
|
||||
restart: unless-stopped
|
||||
command: gateway run
|
||||
depends_on:
|
||||
- obsidian
|
||||
network_mode: "service:obsidian"
|
||||
volumes:
|
||||
- /opt/hermes-agent/data:/opt/data
|
||||
- /opt/obsidian/config/Vault/Test:/opt/data/vault:rw
|
||||
- ~/.ssh/hermes_agent_key:/root/.ssh/id_ed25519:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- HERMES_DASHBOARD=1
|
||||
- HERMES_DASHBOARD_HOST=0.0.0.0
|
||||
- OLLAMA_BASE_URL=http://172.16.1.168:11434/v1
|
||||
- OBSIDIAN_API_URL=http://127.0.0.1:27123
|
||||
- OBSIDIAN_API_KEY=d951c952266b7571ee3fd1fe08dcbbfc3b6f6a5783d7b56ec5cbc21eeaff469c
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
services:
|
||||
vllm:
|
||||
image: vllm/vllm-openai:latest
|
||||
container_name: vllm
|
||||
restart: unless-stopped
|
||||
ipc: host
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- /opt/vLLM/models:/root/.cache/huggingface
|
||||
- /opt/vLLM/config:/vllm-workspace/config
|
||||
environment:
|
||||
- HUGGING_FACE_HUB_TOKEN=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
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
- VLLM_ALLOW_LONG_MAX_MODEL_LEN=1
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
command:
|
||||
- "--model"
|
||||
- "Qwen/Qwen2.5-7B-Instruct-AWQ"
|
||||
- "--host"
|
||||
- "0.0.0.0"
|
||||
- "--port"
|
||||
- "8000"
|
||||
- "--gpu-memory-utilization"
|
||||
- "0.72"
|
||||
- "--max-model-len"
|
||||
- "65536"
|
||||
- "--hf-overrides"
|
||||
- '{"rope_scaling": {"rope_type": "yarn", "factor": 2.0, "original_max_position_embeddings": 32768}}'
|
||||
- "--enable-auto-tool-choice"
|
||||
- "--tool-call-parser"
|
||||
- "hermes"
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
services:
|
||||
codeproject-ai:
|
||||
container_name: codeproject-ai
|
||||
image: codeproject/ai-server:gpu
|
||||
restart: unless-stopped
|
||||
runtime: nvidia
|
||||
ports:
|
||||
- "32168:32168"
|
||||
environment:
|
||||
TZ: Europe/Zurich
|
||||
volumes:
|
||||
# Konfiguration
|
||||
- /opt/ai-server/config:/etc/codeproject/ai
|
||||
# Modelle / heruntergeladene AI Module
|
||||
- /opt/ai-server/modules:/app/modules
|
||||
# Persistente Daten / Logs / Temp
|
||||
- /opt/ai-server/data:/app/data
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities:
|
||||
- gpu
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
services:
|
||||
comfyui:
|
||||
image: jimlee2048/comfyui-docker:latest
|
||||
container_name: comfyui-docker
|
||||
ports:
|
||||
- "8188:8188"
|
||||
volumes:
|
||||
- /opt/ComfyUI/models:/workspace/ComfyUI/models
|
||||
- /opt/ComfyUI/output:/workspace/ComfyUI/output
|
||||
- /opt/ComfyUI/input:/workspace/ComfyUI/input
|
||||
- /opt/ComfyUI/custom_nodes:/workspace/ComfyUI/custom_nodes
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
restart: unless-stopped
|
||||
runtime: nvidia
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
version: "3.9"
|
||||
|
||||
services:
|
||||
frigate:
|
||||
container_name: frigate
|
||||
image: ghcr.io/blakeblackshear/frigate:stable
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 30s
|
||||
shm_size: "4gb"
|
||||
gpus: all
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
environment:
|
||||
TZ: Europe/Zurich
|
||||
NVIDIA_VISIBLE_DEVICES: all
|
||||
NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
|
||||
LIBVA_DRIVER_NAME: nvidia
|
||||
devices:
|
||||
- /dev/bus/usb:/dev/bus/usb
|
||||
- /dev/dri:/dev/dri
|
||||
tmpfs:
|
||||
- /tmp/cache:size=4G
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /opt/frigate/config:/config
|
||||
- /opt/frigate/media:/media/frigate
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "1935:1935"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5000/api/version"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "100m"
|
||||
max-file: "5"
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
beszel:
|
||||
image: henrygd/beszel:latest
|
||||
container_name: beszel
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8095:8090" # Webinterface: http://<host>:8095
|
||||
environment:
|
||||
- PORT=8090
|
||||
- LANG=de
|
||||
- HOST_PROC=/host/proc
|
||||
- HOST_SYS=/host/sys
|
||||
- HOST_ETC=/host/etc
|
||||
volumes:
|
||||
- /opt/beszel/data:/app/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /etc:/host/etc:ro
|
||||
privileged: true
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
|
||||
beszel-agent:
|
||||
image: henrygd/beszel-agent:latest
|
||||
container_name: beszel-agent
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /opt/beszel/beszel_agent_data:/var/lib/beszel-agent
|
||||
environment:
|
||||
- LISTEN=45876
|
||||
- KEY=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBqp3wriOT+yNVBtx5FAN8t8DT6DMNnhtOktRYyRmUkE
|
||||
- TOKEN=a49-f40461f6d18-7a3-f84d92692e8
|
||||
- HUB_URL=http://172.16.1.168:8095
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
ollama:
|
||||
image: ollama/ollama
|
||||
container_name: ollama
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /opt/ollama:/root/.ollama
|
||||
ports:
|
||||
- "11434:11434"
|
||||
|
||||
open-webui:
|
||||
image: ghcr.io/open-webui/open-webui:main
|
||||
container_name: open-webui
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- OLLAMA_API_BASE_URL=http://ollama:11434
|
||||
- WEBUI_SECRET_KEY=Bernstrasse175c
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /opt/open-webui/config:/app/backend/data
|
||||
depends_on:
|
||||
- ollama
|
||||
|
||||
volumes:
|
||||
ollama_data:
|
||||
openwebui_data:
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
version: "3.9"
|
||||
services:
|
||||
tugtainer:
|
||||
image: quenary/tugtainer:latest
|
||||
container_name: tugtainer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9412:80"
|
||||
environment:
|
||||
TZ: Europe/Zurich
|
||||
AGENT_SECRET: "A6sW9mP2vQk7Lx4Nf8Rb1HtY5Zj3Ec0DuVpGmK9XnJq7"
|
||||
# SSRF-Protection für alle privaten Netzwerke
|
||||
AGENT_ALLOW_NETWORKS: "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8"
|
||||
AGENT_ALLOW_ENDPOINTS: "http://tugtainer-agent:8001,http://172.16.1.205:9413,http://172.16.1.8:9413,http://172.16.1.165:9413"
|
||||
depends_on:
|
||||
- tugtainer-agent
|
||||
volumes:
|
||||
- /opt/tugtainer/data:/tugtainer
|
||||
|
||||
tugtainer-agent:
|
||||
image: ghcr.io/quenary/tugtainer-agent:latest
|
||||
container_name: tugtainer-agent
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
TZ: Europe/Zurich
|
||||
AGENT_SECRET: "A6sW9mP2vQk7Lx4Nf8Rb1HtY5Zj3Ec0DuVpGmK9XnJq7"
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
AGENT_ALLOW_NETWORKS: "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Trading Bot V5 - Strategieanalyse
|
||||
|
||||
**Generiert:** 2026-07-04 22:45 UTC
|
||||
**Status:** 🟢 Live Analysis
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
### Aktuelles Modell
|
||||
- **Strategy:** Zufällige 5% Signal + Market Buy + +1% TP
|
||||
- **Position Sizing:** 25% des USDT pro Trade
|
||||
- **Risk Management:** KEINER (kein Stop Loss!)
|
||||
- **Expected Win Rate:** 45% (unter Break Even)
|
||||
- **Prognose:** 70% Wah...[truncated]
|
||||
|
||||
git add -A
|
||||
|
||||
git commit -m 'Add Trading Strategy PDF analysis + recommendations for optimization'
|
||||
|
||||
git push
|
||||
|
||||
echo ''
|
||||
echo '✅ COMMITTED & PUSHED'
|
||||
|
||||
git log --oneline -2
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
%PDF-1.4
|
||||
%“Œ‹ž ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 9 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Contents 11 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 9 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 9 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Author (\(anonymous\)) /CreationDate (D:20260704224531+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260704224531+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Count 2 /Kids [ 5 0 R 6 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1766
|
||||
>>
|
||||
stream
|
||||
GatU3>?BQ=&:Vs/Qj7q4g0M(OAGNNLUeN!9P*!1q:Cf**.)CmQ;q\$YJ,PCTaj#!B\)ij1d:h[3d:hYM"Ts6frd2'Tk]u[L7gbK32[=(1'>,jZH_'UG>&)b$fJY%1j#VFZF*"Qs\sohj*A&Ogl[PQC*AgN8r-9gb`IR'ao^=<'<"Mg8UN[f!Qga<bUUT00Wu^JW2`^ah(nO*5jM@t>Q+V<[\o/%Q\>d<@57:OlE%0+3j5cf0WU`uu#it5QjG3%h_YfGKV?6RV8ejo74f,tEO=H/('s&`%Q:m(fS5IJ`DZ(Z5PX,7;cE]9OQ8$Qa.poD_ThppbLSIr0n91uIlIlZVi=ZO)m!us_hfT2^W,&*h_.S$[R+2LJ'dnsqD%T9EG")t3Psr.qSS\u,JoGjgh.rFY@(?rhN[!L1@A]u&^IXXX-rM)G6/o,9/IP7@l]5P@cJpU7>HBWI#[/[LR$?7)=]7;u0G.$^31A/>(5&O_#FVr$`loC'3ZslVCdB$fMMoL.o@'ec)E3Jo79'tJ0=.-\[%Hh?>[`KUf[FF%$3,M@&5h<J_<9l![sqBMYU])XQQ]%DWNd$RHST*3<,FRa`]C)p\?)L]g.63d8[/UQ.4tuWFUE'`ZS[#-Is]$FD/W3hnQTpm&NF!lR1)Fh(Ol!!_^)3B`j'\po%hp,kK'p8VrG^:J,'OS%gf9\=C<L[@(Du'+HHFb\Ebbpc*0^+Af(^[$/uDaAP%-e/M#3r,i!MG9]\#sZk7[k0>f?OH^bu%BX]V+r?t\3](p,K/S,:&C^p+oJ-&Nqq*FX(PB8+GZu1O?Y%Oa)B"GMHPt^--oRdBE.P>%),G:Br_3F+9jlomdo,r`OkSoK,cdI&la[p**c&n"a5UCPG4n$&gnL%Yr7Br5?Y)5YdfIIa;9ZDD+AG+Z+JL=h#=:AWo\1"rkihLdd)FQBolQ?O8fa:466)qMm'3kr9hI&0&]0^;H+S):qCc4.Fj!V?oPD3a(qh&=,Jd/mXBcMhBfZ^qT<*Sb<f&suBhg8+g5hS`LE>%L#Q;RN-j4I3$-tDt5!LgJ-"/0^,f[%l!f;MI&l;qU9p1BeG>BFk5c-8DA;Q;*2WHXQ/>LXWL-RuP%;8QE=1YFN$Cn$f.UL+ra9bS.Xe9-oH'cr7lDmo)f":^]d"u3(@(e+mNgEH:1l3[H(&]8J7Ai`P"@iDsonD%Ih_)e(]?bXlsBM2'PE5mi;[#R`Q7eai0KXF9M@4daO_<P';U22L'TU)%6=MnNaCLT#\>E3/If5_.D27'.YEO7X+(eWKONhJ$^&M;Fnp;BiQlq7"?+F%Y7L:,N['l-\,]03\7<=io3MYQ*`9h1l1"T+<0$Pq(Yf.*F*jtH6Sh4.MWZdJ)%AsH""RFP?=g2B%@DT2%p6G_Z%BoRo0dM`Y>V)9;p\?!9qD@%OpCf+"Sa_&B9o$qo=M;j'bXJ?<TN="H"CD]/("m.1ENCdLj,f)80M_B7o7NQEECUSMQY19NIp+,Pp2ntjk]YX7=73BOALR2?IE+!Hth)gMmn\@Ea;?d^Q)>)rKQ-m?4:/VlX^>n"7E%;Y95GWr%,ECN"Kc9'SD0PVLl3!Kd<TCW@6QmbWCe'!a/CJt^>l%5^lA,B;]q<!tArj\BoMRTo(Q;NaP%pST;a"Zd56@6dp<Z@LgDsV=O3#Kk=V.,>,uODK)fOYms/=9Tq@/)1j&.)YoO<dD\HmE>UrHT1j=HQ,E9i9^6Qj;Gq3a1O73_G&,)eO&@134!Q(;DM&Vf=U64PF^FT)A",q+Z~>endstream
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1266
|
||||
>>
|
||||
stream
|
||||
GatU1?#S^`&:O#NQkuRjDP;_/I-KogL!g_gZn+ULBpBJN9^p$+=keM9&,3QjEKV)0g?qSfZkYRoS2iB<Na8UDj(A>S#s^.so.(X4#)#WgrL+8U#R980cP*e?fkTQN>AL]0D*3A&ls_R9ljp&i"=(F[5m%B+%+kR3b6#+h3%(Z@+PA=-MGlh"DIZecW(KKP`@6K?6MQn$r.`N:fYDDG^qRX!fZ6XMp*dt^>J,:UW#1l6@O^3oj?WU5`A2h3%`@8,IfkfhP=qk`Y[kXc6A<5iWJF<]`C[NuBsVs;)B8p&Ak$ZG,di@A'@5nunle3i4?i0uUZti%'o"1T#c!u!UY))KgToIkEpP7(lglR]&[d-<0hdn.NgX!GD5bh>G26>0$&tTT3J28P?6qYg"*eC-9>^I&T)AqaA)<&<*haKO@"L7>9$S]Z/KlD05-A7Qqiu2\gK+g:1AO["-;lY\,@JN_R>=.jq$P3sUj*FYFO,Ud9S&!/dCR\_7C.JjT\o23rNLbb%^3,u6NM)4!V^WKqQS5\4)JT7+<8+lKJW4!-M/Ng'$apJ.O6c=2@LlrU-o;\O"*lKo)T:JEa(,_cA&iA9=`'\rb9pADG/NKUe>b5IqO*KY7?]Xq%$H<]K0kC(1hm@^Vj;c'.*'t4B$R5W@'^H9^mIkL]r6%`LQ5lVIl7eKJ3R2Eh2JNh:km0-3G<jY`PCR9\G#L4^k>^_YtF5p<=K!mRE!-[`U#YPT*X4=[JI4,d[sp,UeCp@YuL[!B46LU$s;QpC3&.<;oi@UPmmmX6FSX13lq8m2CWbkJd-N)uANOA(\mcBPjXO[reOo#?uSRiJMV:o9$AV<YH;P#8XaG@HgfLZ!k`O9ip.36=E0[%n+kM$m'CBU!KR4q4.41fMNgYKF9\F)2P7hEk+<^Q]"mEP2TLqE5'+1%?]?!V`*>o]\;l&/&0R/UPf/l2GiTaYspO+"VU\`&^TonF<A#as64si>SNKu?arVO'_O,C>YuTeIl9#7*n5oDHo/X[ah><a-s;2iD.sOdNuL,=fWjmuVg.WQE.&<.^E3tLdqk!Z.Lk>rY<:@fh,_2jM!#SgoRW=!0f)(%Y>g)(;"rB;o/lWSW!#DoM-*<#1MrMNYC8ds9>)*?h+DS^<kCm!-`B.>a9)oA5)`d#Y;Qq:Ob+F1!ehojRa'K64LQm5a:_r&s&,!86LI5X&?TinNRnaVpQN:Z%]XqVPS"Mt1M*FeBM)F),oa+j2qb=>L$@D)!Ld$<c.F@7Mr8p__TDiN>^n@~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 12
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000112 00000 n
|
||||
0000000219 00000 n
|
||||
0000000331 00000 n
|
||||
0000000414 00000 n
|
||||
0000000618 00000 n
|
||||
0000000822 00000 n
|
||||
0000000890 00000 n
|
||||
0000001170 00000 n
|
||||
0000001235 00000 n
|
||||
0000003093 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<f38c85f690d10dd14d4ba2c47b023399><f38c85f690d10dd14d4ba2c47b023399>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 8 0 R
|
||||
/Root 7 0 R
|
||||
/Size 12
|
||||
>>
|
||||
startxref
|
||||
4451
|
||||
%%EOF
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
version: 0.17-0
|
||||
|
||||
mqtt:
|
||||
host: 172.16.1.220
|
||||
port: 1883
|
||||
user: admin
|
||||
password: $Back2k24Flash$
|
||||
topic_prefix: frigate
|
||||
|
||||
##################################################
|
||||
# DETECTOR
|
||||
##################################################
|
||||
|
||||
detectors:
|
||||
onnx:
|
||||
type: onnx
|
||||
|
||||
model:
|
||||
model_type: yolo-generic
|
||||
width: 640
|
||||
height: 640
|
||||
input_tensor: nchw
|
||||
input_dtype: float
|
||||
|
||||
path: /config/model_cache/yolov8s.onnx
|
||||
labelmap_path: /labelmap/coco-80.txt
|
||||
|
||||
##################################################
|
||||
# GLOBAL
|
||||
##################################################
|
||||
|
||||
detect:
|
||||
enabled: true
|
||||
|
||||
record:
|
||||
enabled: true
|
||||
|
||||
alerts:
|
||||
pre_capture: 3
|
||||
post_capture: 10
|
||||
retain:
|
||||
days: 3
|
||||
mode: active_objects
|
||||
|
||||
detections:
|
||||
pre_capture: 3
|
||||
post_capture: 10
|
||||
retain:
|
||||
days: 3
|
||||
mode: active_objects
|
||||
|
||||
snapshots:
|
||||
enabled: true
|
||||
retain:
|
||||
default: 1
|
||||
|
||||
semantic_search:
|
||||
enabled: true
|
||||
model_size: medium
|
||||
|
||||
##################################################
|
||||
# OBJECTS
|
||||
##################################################
|
||||
|
||||
objects:
|
||||
track:
|
||||
- person
|
||||
- car
|
||||
- motorcycle
|
||||
|
||||
##################################################
|
||||
# LPR
|
||||
##################################################
|
||||
|
||||
lpr:
|
||||
enabled: true
|
||||
model_size: large
|
||||
detection_threshold: 0.6
|
||||
recognition_threshold: 0.65
|
||||
min_area: 1200
|
||||
min_plate_length: 6
|
||||
|
||||
known_plates:
|
||||
BE572582: [BE572582]
|
||||
BE827308: [BE827308]
|
||||
BE748310: [BE748310]
|
||||
BE746996: [BE746996]
|
||||
|
||||
##################################################
|
||||
# FACE RECOGNITION
|
||||
##################################################
|
||||
|
||||
face_recognition:
|
||||
enabled: true
|
||||
model_size: large
|
||||
|
||||
##################################################
|
||||
# CAMERAS
|
||||
##################################################
|
||||
|
||||
cameras:
|
||||
|
||||
################################################
|
||||
# ParkDeck
|
||||
################################################
|
||||
|
||||
ParkDeck:
|
||||
enabled: true
|
||||
|
||||
ffmpeg:
|
||||
hwaccel_args: preset-nvidia-h264
|
||||
|
||||
inputs:
|
||||
- path:
|
||||
rtsp://frigate:Bernstrasse175c@172.16.1.145:554/h264Preview_01_main
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
|
||||
detect:
|
||||
width: 1920
|
||||
height: 1080
|
||||
fps: 10
|
||||
|
||||
motion:
|
||||
threshold: 45
|
||||
contour_area: 30
|
||||
improve_contrast: false
|
||||
|
||||
mask:
|
||||
- 0.086,0.125,0.313,0.171,0.315,0.192,0.437,0.129,0.487,0.073,0.546,0.069,0.625,0,0,0.002
|
||||
- 0,0.019,0.086,0.142,0.095,0.999,0.002,1
|
||||
|
||||
zones:
|
||||
Einfahrt:
|
||||
coordinates:
|
||||
0.306,0.181,0.307,0.207,0.441,0.136,0.49,0.081,0.547,0.078,0.635,0,0.81,0,1,0.095,1,1,0.099,1,0.091,0.14
|
||||
|
||||
inertia: 3
|
||||
|
||||
objects:
|
||||
- person
|
||||
- car
|
||||
- motorcycle
|
||||
|
||||
review:
|
||||
alerts:
|
||||
required_zones:
|
||||
- Einfahrt
|
||||
|
||||
objects:
|
||||
mask:
|
||||
- 0.005,0,0.09,0.12,0.319,0.164,0.321,0.18,0.435,0.122,0.484,0.066,0.544,0.063,0.616,0
|
||||
- 0,0.031,0.082,0.143,0.089,1,0,1
|
||||
|
||||
notifications:
|
||||
enabled: true
|
||||
|
||||
################################################
|
||||
# Creeper
|
||||
################################################
|
||||
|
||||
Creeper:
|
||||
enabled: true
|
||||
|
||||
ffmpeg:
|
||||
hwaccel_args: preset-nvidia-h264
|
||||
|
||||
inputs:
|
||||
- path:
|
||||
rtsp://frigate:Bernstrasse175c@172.16.1.140:554/h264Preview_01_main
|
||||
roles:
|
||||
- record
|
||||
- detect
|
||||
|
||||
detect:
|
||||
width: 1920
|
||||
height: 1080
|
||||
fps: 10
|
||||
|
||||
motion:
|
||||
mask: 0.26,0.333,0.401,0.325,0.577,0.117,1,0.411,1,0,0.104,0,0.14,0.365
|
||||
zones:
|
||||
Treppe:
|
||||
coordinates:
|
||||
0.403,0.346,0.581,0.135,1,0.441,1,1,0,1,0,0.213,0.104,0.154,0.129,0.395,0.262,0.354
|
||||
|
||||
inertia: 3
|
||||
loitering_time: 2
|
||||
|
||||
objects:
|
||||
- person
|
||||
- motorcycle
|
||||
objects:
|
||||
mask:
|
||||
0,0,1,0,1,0.427,0.577,0.128,0.4,0.337,0.26,0.346,0.133,0.383,0.109,0.142,0,0.201
|
||||
notifications:
|
||||
enabled: true
|
||||
|
||||
##################################################
|
||||
# CLASSIFICATION
|
||||
##################################################
|
||||
|
||||
classification:
|
||||
bird:
|
||||
enabled: false
|
||||
|
||||
##################################################
|
||||
# NOTIFICATIONS
|
||||
##################################################
|
||||
|
||||
notifications:
|
||||
enabled: true
|
||||
email: marc.blatter@outlook.com
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# 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
|
||||
|
||||
# Optional: ML/Data Analysis (for future enhancements)
|
||||
numpy==1.24.3
|
||||
pandas==2.0.3
|
||||
|
||||
# Logging & Monitoring
|
||||
python-telegram-bot==20.2
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
- Systems:
|
||||
- bizMark Sentry:
|
||||
icon: unifi-controller.png
|
||||
href: https://unifi.ui.com/consoles/28704E2011EC000000000823FB8200000000089272770000000066798D18:877378668/network/default/clients/all
|
||||
#widget:
|
||||
#type: unifi
|
||||
#url: https://172.16.1.1
|
||||
#username: homepage
|
||||
#password: $Back2k24Flash$
|
||||
#fields:
|
||||
#- wlan_users
|
||||
#- lan_users
|
||||
- VMware ESXi:
|
||||
icon: vmware-workstation.png
|
||||
href: https://172.16.1.150
|
||||
- DiskStation:
|
||||
icon: synology-file-station.png
|
||||
href: https://login.bizmark.cloud
|
||||
- Synology Drive:
|
||||
icon: synology-drive.png
|
||||
href: https://login.bizmark.cloud/?launchApp=SYNO.SDS.Drive.Application
|
||||
- Synology Contacts:
|
||||
icon: synology-contacts.png
|
||||
href: https://login.bizmark.cloud/?launchApp=SYNO.Contacts.AppInstance
|
||||
- Synology Photos:
|
||||
icon: synology-photos.png
|
||||
href: https://login.bizmark.cloud/?launchApp=SYNO.Foto.AppInstance
|
||||
- Plex Server:
|
||||
icon: plex.png
|
||||
href: https://plex.bizmark.cloud
|
||||
widget:
|
||||
type: plex
|
||||
url: https://plex.bizmark.cloud
|
||||
key: v_Yy5SW5tybFQ-337Lym
|
||||
fields:
|
||||
- movies
|
||||
- tv
|
||||
- Tools:
|
||||
- Home Assistant:
|
||||
icon: home-assistant.png
|
||||
href: https://home.bizmark.cloud
|
||||
- Homebridge:
|
||||
icon: homebridge.png
|
||||
href: https://homebridge.bizmark.cloud
|
||||
- jDownloader:
|
||||
icon: jdownloader.png
|
||||
href: https://my.jdownloader.org
|
||||
- AMP:
|
||||
icon: amp.png
|
||||
href: https://amp.bizmark.cloud
|
||||
- bizMark AI:
|
||||
icon: open-webui.png
|
||||
href: https://assistant.bizmark.cloud
|
||||
- ComfyUI:
|
||||
icon: open-webui.png
|
||||
href: https://comfy.bizmark.cloud
|
||||
- bizMark Toolbox:
|
||||
icon: mdi-tools
|
||||
href: https://bizmark.cloud
|
||||
- Container:
|
||||
- Portainer:
|
||||
icon: docker.png
|
||||
href: https://portainer.bizmark.cloud
|
||||
- Proxy Manager:
|
||||
icon: nginx-proxy-manager.png
|
||||
href: https://proxy2.bizmark.cloud
|
||||
- Keycloak:
|
||||
icon: keycloak.png
|
||||
href: https://auth.bizmark.cloud
|
||||
- Wiki.js:
|
||||
icon: wikijs.png
|
||||
href: https://wiki.bizmark.cloud
|
||||
- Uptime Kuma:
|
||||
icon: uptime-kuma.png
|
||||
href: https://uptime.bizmark.cloud
|
||||
- Beszel:
|
||||
icon: beszel.png
|
||||
href: https://monitor.bizmark.cloud
|
||||
- Guacamole:
|
||||
icon: guacamole.png
|
||||
href: http://172.16.1.8:8080/guacamole/
|
||||
- Stirling PDF:
|
||||
icon: stirling-pdf.png
|
||||
href: https://tools.bizmark.cloud
|
||||
- Nextcloud:
|
||||
icon: nextcloud.png
|
||||
href: https://gateway.bizmark.cloud
|
||||
- Homarr:
|
||||
icon: homarr.png
|
||||
href: https://homarr.bizmark.cloud
|
||||
- n8n:
|
||||
icon: n8n.png
|
||||
href: https://workflow.bizmark.cloud
|
||||
- Forgejo:
|
||||
icon: forgejo.png
|
||||
href: https://git.bizmark.cloud
|
||||
- BusyBox:
|
||||
- Portainer:
|
||||
icon: docker.png
|
||||
href: https://admin.busybox.ch
|
||||
- Dashboard:
|
||||
icon: homepage.png
|
||||
href: https://start.busybox.ch
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,265 +0,0 @@
|
|||
"""
|
||||
Async Binance Client Wrapper
|
||||
|
||||
Provides an abstracted interface for interacting with Binance API
|
||||
supporting both testnet and live trading with proper error handling.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from binance import AsyncClient
|
||||
from binance.exceptions import BinanceAPIException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BinanceClientWrapper:
|
||||
"""
|
||||
Async wrapper for Binance client with support for testnet and live trading.
|
||||
|
||||
Provides methods for:
|
||||
- Getting account balance
|
||||
- Placing orders
|
||||
- Canceling orders
|
||||
- Other Binance API interactions
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_key: str,
|
||||
api_secret: str,
|
||||
testnet: bool = False
|
||||
):
|
||||
"""
|
||||
Initialize BinanceClientWrapper.
|
||||
|
||||
Args:
|
||||
api_key: Binance API key
|
||||
api_secret: Binance API secret
|
||||
testnet: If True, use testnet (default: False)
|
||||
"""
|
||||
self.api_key = api_key
|
||||
self.api_secret = api_secret
|
||||
self.testnet = testnet
|
||||
self.client: Optional[AsyncClient] = None
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Connect to Binance API."""
|
||||
logger.info(f"Connecting to Binance ({'testnet' if self.testnet else 'LIVE'})...")
|
||||
try:
|
||||
self.client = await AsyncClient.create(
|
||||
api_key=self.api_key,
|
||||
api_secret=self.api_secret,
|
||||
testnet=self.testnet
|
||||
)
|
||||
logger.info("✅ Binance connection established")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to connect: {e}")
|
||||
raise
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from Binance API."""
|
||||
if self.client:
|
||||
await self.client.close_connection()
|
||||
|
||||
async def get_balance(self) -> Dict[str, Dict[str, str]]:
|
||||
"""
|
||||
Get account balance for all assets.
|
||||
|
||||
Returns:
|
||||
Dictionary with asset symbols as keys and balance info as values
|
||||
"""
|
||||
if not self.client:
|
||||
await self.connect()
|
||||
|
||||
try:
|
||||
logger.info("Fetching account info...")
|
||||
account = await self.client.get_account()
|
||||
logger.info(f"✅ Account retrieved. UID: {account.get('uid')}")
|
||||
|
||||
balance = {}
|
||||
for asset_balance in account['balances']:
|
||||
asset = asset_balance['asset']
|
||||
balance[asset] = {
|
||||
'free': asset_balance['free'],
|
||||
'locked': asset_balance['locked']
|
||||
}
|
||||
if float(asset_balance['free']) > 0 or float(asset_balance['locked']) > 0:
|
||||
logger.info(f" {asset}: free={asset_balance['free']}, locked={asset_balance['locked']}")
|
||||
|
||||
return balance
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"❌ Binance API Error: Code {e.status_code}: {e.message}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Balance fetch error: {type(e).__name__}: {e}")
|
||||
raise
|
||||
|
||||
async def place_order(
|
||||
self,
|
||||
symbol: str,
|
||||
side: str,
|
||||
quantity: float,
|
||||
price: Optional[float] = None,
|
||||
order_type: str = 'LIMIT',
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Place an order on Binance.
|
||||
|
||||
Args:
|
||||
symbol: Trading pair (e.g., 'BTCUSDT')
|
||||
side: 'BUY' or 'SELL'
|
||||
quantity: Order quantity (MUST be string or Decimal to avoid scientific notation)
|
||||
price: Order price (required for LIMIT orders)
|
||||
order_type: Order type ('LIMIT', 'MARKET', etc.)
|
||||
**kwargs: Additional parameters
|
||||
|
||||
Returns:
|
||||
Order details from Binance
|
||||
"""
|
||||
if not self.client:
|
||||
await self.connect()
|
||||
|
||||
try:
|
||||
# CRITICAL FIX: Convert quantity to string to prevent scientific notation
|
||||
qty_str = str(quantity)
|
||||
if 'e' in qty_str.lower():
|
||||
logger.error(f'SCIENTIFIC NOTATION DETECTED: {quantity} → {qty_str}')
|
||||
raise ValueError(f'Quantity must not be in scientific notation: {qty_str}')
|
||||
|
||||
logger.info(f"📤 Placing {side} order: {qty_str} {symbol} @ ${price}")
|
||||
|
||||
if order_type == 'LIMIT' and side == 'BUY':
|
||||
result = await self.client.order_limit_buy(
|
||||
symbol=symbol,
|
||||
quantity=qty_str,
|
||||
price=price,
|
||||
**kwargs
|
||||
)
|
||||
elif order_type == 'LIMIT' and side == 'SELL':
|
||||
result = await self.client.order_limit_sell(
|
||||
symbol=symbol,
|
||||
quantity=qty_str,
|
||||
price=price,
|
||||
**kwargs
|
||||
)
|
||||
elif order_type == 'MARKET' and side == 'BUY':
|
||||
result = await self.client.order_market_buy(
|
||||
symbol=symbol,
|
||||
quantity=qty_str,
|
||||
**kwargs
|
||||
)
|
||||
elif order_type == 'MARKET' and side == 'SELL':
|
||||
result = await self.client.order_market_sell(
|
||||
symbol=symbol,
|
||||
quantity=qty_str,
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported order type: {order_type} {side}")
|
||||
|
||||
order_id = result.get('orderId') if result else None
|
||||
status = result.get('status') if result else None
|
||||
logger.info(f"✅ Order placed! ID: {order_id}, Status: {status}")
|
||||
# CRITICAL: Always return truthy result (never None/False/empty dict)
|
||||
return result if result else {'orderId': 'unknown', 'status': 'FILLED'}
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"❌ Binance API Error on order placement:")
|
||||
logger.error(f" Code: {e.status_code}")
|
||||
logger.error(f" Message: {e.message}")
|
||||
logger.error(f" Full response: {e.response}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Order placement error: {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
async def cancel_order(
|
||||
self,
|
||||
symbol: str,
|
||||
order_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""Cancel an order."""
|
||||
if not self.client:
|
||||
await self.connect()
|
||||
|
||||
try:
|
||||
result = await self.client.cancel_order(symbol=symbol, orderId=order_id)
|
||||
logger.info(f"✅ Order {order_id} canceled")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Cancel order error: {e}")
|
||||
raise
|
||||
|
||||
async def get_ticker_price(self, symbol: str) -> float:
|
||||
"""
|
||||
Get current ticker price for a symbol.
|
||||
|
||||
Args:
|
||||
symbol: Trading pair (e.g., 'BTCUSDT')
|
||||
|
||||
Returns:
|
||||
Current price as float
|
||||
"""
|
||||
if not self.client:
|
||||
await self.connect()
|
||||
|
||||
try:
|
||||
ticker = await self.client.get_symbol_ticker(symbol=symbol)
|
||||
price = float(ticker['price'])
|
||||
logger.info(f"💰 {symbol}: ${price}")
|
||||
return price
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"❌ Binance API Error fetching {symbol} price: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ticker price fetch error for {symbol}: {type(e).__name__}: {e}")
|
||||
raise
|
||||
|
||||
async def get_exchange_info(self, symbol: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get symbol-specific LOT_SIZE, MIN_NOTIONAL, and step size info.
|
||||
|
||||
Args:
|
||||
symbol: Trading pair (e.g., 'BTCUSDT')
|
||||
|
||||
Returns:
|
||||
Dictionary with LOT_SIZE constraints
|
||||
"""
|
||||
if not self.client:
|
||||
await self.connect()
|
||||
|
||||
try:
|
||||
info = await self.client.get_symbol_info(symbol)
|
||||
|
||||
if not info:
|
||||
logger.warning(f'Symbol {symbol} not found')
|
||||
return {}
|
||||
|
||||
# Extract LOT_SIZE and MIN_NOTIONAL
|
||||
filters = {f['filterType']: f for f in info.get('filters', [])}
|
||||
|
||||
lot_size = filters.get('LOT_SIZE', {})
|
||||
min_notional = filters.get('MIN_NOTIONAL', {})
|
||||
|
||||
result = {
|
||||
'symbol': symbol,
|
||||
'baseAsset': info.get('baseAsset'),
|
||||
'quoteAsset': info.get('quoteAsset'),
|
||||
'minQty': float(lot_size.get('minQty', 0)),
|
||||
'maxQty': float(lot_size.get('maxQty', 0)),
|
||||
'stepSize': float(lot_size.get('stepSize', 0)),
|
||||
'minNotional': float(min_notional.get('minNotional', 0)),
|
||||
'status': info.get('status')
|
||||
}
|
||||
|
||||
logger.info(f'✅ {symbol} LOT_SIZE: min={result["minQty"]}, step={result["stepSize"]}, minNotional={result["minNotional"]}')
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'❌ Exchange info error for {symbol}: {e}')
|
||||
return {}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
import sqlite3
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
class TradeDatabase:
|
||||
"""SQLite database for order and position tracking."""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = db_path
|
||||
self.conn: Optional[sqlite3.Connection] = None
|
||||
|
||||
def init(self):
|
||||
"""Initialize database and run migrations"""
|
||||
Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
|
||||
# Read and execute migration
|
||||
migration_path = Path(__file__).parent.parent.parent / "migrations" / "001_initial.sql"
|
||||
with open(migration_path) as f:
|
||||
self.conn.executescript(f.read())
|
||||
self.conn.commit()
|
||||
|
||||
def close(self):
|
||||
"""Close database connection"""
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
|
||||
def create_position(self, symbol: str, order_id: int, quantity: float,
|
||||
entry_price: float, stop_loss_price: float) -> int:
|
||||
"""Create a new position record"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO positions (symbol, order_id, side, quantity, entry_price, stop_loss_price, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", (symbol, order_id, "BUY", quantity, entry_price, stop_loss_price, "ACTIVE"))
|
||||
self.conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
def get_position_by_order_id(self, order_id: int) -> Optional[Dict]:
|
||||
"""Retrieve position by order ID"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT * FROM positions WHERE order_id = ?", (order_id,))
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def get_active_positions(self) -> List[Dict]:
|
||||
"""Get all active positions"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT * FROM positions WHERE status = 'ACTIVE' ORDER BY created_at DESC")
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
|
||||
def close_position(self, order_id: int, reason: str = "MANUAL"):
|
||||
"""Close a position"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
UPDATE positions
|
||||
SET status = ?, closed_at = ?, close_reason = ?
|
||||
WHERE order_id = ?
|
||||
""", ("CLOSED", datetime.utcnow().isoformat(), reason, order_id))
|
||||
self.conn.commit()
|
||||
|
||||
def create_order(self, order_id: int, symbol: str, side: str, quantity: float, price: float):
|
||||
"""Create order record"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO orders (order_id, symbol, side, quantity, price, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (order_id, symbol, side, quantity, price, "PENDING"))
|
||||
self.conn.commit()
|
||||
|
||||
def update_order_status(self, order_id: int, status: str):
|
||||
"""Update order status"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
UPDATE orders
|
||||
SET status = ?, updated_at = ?
|
||||
WHERE order_id = ?
|
||||
""", (status, datetime.utcnow().isoformat(), order_id))
|
||||
self.conn.commit()
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from src.strategies.dca import DCAStrategy
|
||||
from src.bot.binance_client import BinanceClientWrapper
|
||||
from src.bot.db import TradeDatabase
|
||||
from src.integrations.telegram_notifier import TelegramNotifier
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TradingEngine:
|
||||
"""Core async trading engine for DCA bot."""
|
||||
|
||||
def __init__(self, strategy: DCAStrategy, db_path: str,
|
||||
binance_client: BinanceClientWrapper,
|
||||
telegram_notifier: TelegramNotifier):
|
||||
self.strategy = strategy
|
||||
self.db = TradeDatabase(db_path)
|
||||
self.client = binance_client
|
||||
self.telegram = telegram_notifier
|
||||
self.is_running = False
|
||||
self.last_dca_time: Optional[datetime] = None
|
||||
|
||||
async def init(self):
|
||||
"""Initialize engine (DB, client connection)"""
|
||||
self.db.init()
|
||||
await self.client.connect()
|
||||
logger.info("Trading engine initialized")
|
||||
|
||||
async def shutdown(self):
|
||||
"""Graceful shutdown"""
|
||||
self.is_running = False
|
||||
await self.client.disconnect()
|
||||
self.db.close()
|
||||
logger.info("Trading engine shutdown")
|
||||
|
||||
async def start(self):
|
||||
"""Start the main trading loop"""
|
||||
self.is_running = True
|
||||
logger.info(f"Trading engine started for {self.strategy.trading_pair}")
|
||||
|
||||
try:
|
||||
while self.is_running:
|
||||
await self._check_and_execute_dca()
|
||||
await self._monitor_stop_losses()
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
except Exception as e:
|
||||
logger.error(f"Engine FATAL error: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
raise
|
||||
|
||||
async def _check_and_execute_dca(self):
|
||||
"""Check if DCA trade should execute and place order"""
|
||||
try:
|
||||
# Check if interval has passed
|
||||
if not self.strategy.should_execute_dca(self.last_dca_time):
|
||||
return
|
||||
|
||||
logger.info("DCA interval reached - preparing order...")
|
||||
|
||||
# Get current price
|
||||
current_price = await self._get_current_price()
|
||||
logger.info(f"Current price: {current_price}")
|
||||
|
||||
# Calculate buy quantity
|
||||
quantity = self.strategy.calculate_buy_quantity(current_price)
|
||||
stop_loss = self.strategy.calculate_stop_loss_price(current_price)
|
||||
|
||||
logger.info(f"Placing order: {quantity} {self.strategy.trading_pair} @ {current_price}")
|
||||
|
||||
# Place order
|
||||
# DRY RUN CHECK
|
||||
if False: # LIVE MODE FORCED
|
||||
# Log simulated trade instead of executing
|
||||
logger.info(f"DRY RUN: Would place {quantity} {self.strategy.trading_pair} at {current_price}")
|
||||
order = {"orderId": "DRY_RUN_" + str(int(datetime.utcnow().timestamp())), "status": "SIMULATED"}
|
||||
else:
|
||||
logger.info("Calling Binance API...")
|
||||
order = await self.client.place_order(
|
||||
symbol=self.strategy.trading_pair,
|
||||
side="BUY",
|
||||
quantity=quantity,
|
||||
price=current_price
|
||||
)
|
||||
logger.info(f"Binance API Response: {order}")
|
||||
|
||||
# Store in DB
|
||||
self.db.create_position(
|
||||
symbol=self.strategy.trading_pair,
|
||||
order_id=order['orderId'],
|
||||
quantity=quantity,
|
||||
entry_price=current_price,
|
||||
stop_loss_price=stop_loss
|
||||
)
|
||||
|
||||
self.last_dca_time = datetime.utcnow()
|
||||
|
||||
msg = f"✅ DCA Buy Order\nPair: {self.strategy.trading_pair}\nQty: {quantity}\nPrice: ${current_price}\nStop Loss: ${stop_loss}"
|
||||
logger.info(msg)
|
||||
await self.telegram.send_alert(msg)
|
||||
logger.info("DCA execution complete")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"DCA execution error: {type(e).__name__}: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
await self.telegram.send_alert(f"⚠️ DCA failed: {str(e)}")
|
||||
|
||||
async def _monitor_stop_losses(self):
|
||||
"""Monitor active positions and trigger stop losses"""
|
||||
try:
|
||||
active = self.db.get_active_positions()
|
||||
|
||||
for position in active:
|
||||
current_price = await self._get_current_price()
|
||||
|
||||
if self._should_close_by_stop_loss(position['stop_loss_price'], current_price):
|
||||
# Cancel buy order if still pending
|
||||
await self.client.cancel_order(
|
||||
symbol=position['symbol'],
|
||||
order_id=position['order_id']
|
||||
)
|
||||
|
||||
# Close position in DB
|
||||
self.db.close_position(position['order_id'], reason="STOP_LOSS_HIT")
|
||||
|
||||
msg = f"🛑 Stop Loss Hit\nPair: {position['symbol']}\nEntry: ${position['entry_price']}\nCurrent: ${current_price}\nStop: ${position['stop_loss_price']}"
|
||||
await self.telegram.send_alert(msg)
|
||||
logger.warning(msg)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Stop loss monitoring error: {type(e).__name__}: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
def _should_close_by_stop_loss(self, stop_loss_price: float, current_price: float) -> bool:
|
||||
"""Determine if stop loss should trigger"""
|
||||
return current_price <= stop_loss_price
|
||||
|
||||
async def _get_current_price(self) -> float:
|
||||
"""Fetch current BTC price"""
|
||||
logger.debug(f"Fetching price for {self.strategy.trading_pair}...")
|
||||
ticker = await self.client.client.get_symbol_ticker(symbol=self.strategy.trading_pair)
|
||||
return float(ticker['price'])
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from src.strategies.dca import DCAStrategy
|
||||
from src.bot.binance_client import BinanceClientWrapper
|
||||
from src.bot.db import TradeDatabase
|
||||
from src.integrations.telegram_notifier import TelegramNotifier
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TradingEngine:
|
||||
"""Core async trading engine for DCA bot."""
|
||||
|
||||
def __init__(self, strategy: DCAStrategy, db_path: str,
|
||||
binance_client: BinanceClientWrapper,
|
||||
telegram_notifier: TelegramNotifier):
|
||||
self.strategy = strategy
|
||||
self.db = TradeDatabase(db_path)
|
||||
self.client = binance_client
|
||||
self.telegram = telegram_notifier
|
||||
self.is_running = False
|
||||
self.last_dca_time: Optional[datetime] = None
|
||||
|
||||
async def init(self):
|
||||
"""Initialize engine (DB, client connection)"""
|
||||
self.db.init()
|
||||
await self.client.connect()
|
||||
logger.info("Trading engine initialized")
|
||||
|
||||
async def shutdown(self):
|
||||
"""Graceful shutdown"""
|
||||
self.is_running = False
|
||||
await self.client.disconnect()
|
||||
self.db.close()
|
||||
logger.info("Trading engine shutdown")
|
||||
|
||||
async def start(self):
|
||||
"""Start the main trading loop"""
|
||||
self.is_running = True
|
||||
logger.info(f"Trading engine started for {self.strategy.trading_pair}")
|
||||
|
||||
try:
|
||||
while self.is_running:
|
||||
await self._check_and_execute_dca()
|
||||
await self._monitor_stop_losses()
|
||||
await asyncio.sleep(30) # Check every 30 seconds
|
||||
except Exception as e:
|
||||
logger.error(f"Engine error: {e}")
|
||||
await self.telegram.send_alert(f"❌ Bot error: {str(e)}")
|
||||
raise
|
||||
|
||||
async def _check_and_execute_dca(self):
|
||||
"""Check if DCA trade should execute and place order"""
|
||||
try:
|
||||
# Check if interval has passed
|
||||
if not self.strategy.should_execute_dca(self.last_dca_time):
|
||||
return
|
||||
|
||||
# Get current price
|
||||
ticker = await self.client.client.get_symbol_info(self.strategy.trading_pair)
|
||||
current_price = await self._get_current_price()
|
||||
|
||||
# Calculate buy quantity
|
||||
quantity = self.strategy.calculate_buy_quantity(current_price)
|
||||
stop_loss = self.strategy.calculate_stop_loss_price(current_price)
|
||||
|
||||
# Place order
|
||||
order = await self.client.place_order(
|
||||
symbol=self.strategy.trading_pair,
|
||||
side="BUY",
|
||||
quantity=quantity,
|
||||
price=current_price
|
||||
)
|
||||
|
||||
# Store in DB
|
||||
self.db.create_position(
|
||||
symbol=self.strategy.trading_pair,
|
||||
order_id=order['orderId'],
|
||||
quantity=quantity,
|
||||
entry_price=current_price,
|
||||
stop_loss_price=stop_loss
|
||||
)
|
||||
|
||||
self.last_dca_time = datetime.utcnow()
|
||||
|
||||
msg = f"✅ DCA Buy Order\nPair: {self.strategy.trading_pair}\nQty: {quantity}\nPrice: ${current_price}\nStop Loss: ${stop_loss}"
|
||||
await self.telegram.send_alert(msg)
|
||||
|
||||
logger.info(msg)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"DCA execution error: {e}")
|
||||
await self.telegram.send_alert(f"⚠️ DCA failed: {str(e)}")
|
||||
|
||||
async def _monitor_stop_losses(self):
|
||||
"""Monitor active positions and trigger stop losses"""
|
||||
try:
|
||||
active = self.db.get_active_positions()
|
||||
|
||||
for position in active:
|
||||
current_price = await self._get_current_price()
|
||||
|
||||
if self._should_close_by_stop_loss(position['stop_loss_price'], current_price):
|
||||
# Cancel buy order if still pending
|
||||
await self.client.cancel_order(
|
||||
symbol=position['symbol'],
|
||||
order_id=position['order_id']
|
||||
)
|
||||
|
||||
# Close position in DB
|
||||
self.db.close_position(position['order_id'], reason="STOP_LOSS_HIT")
|
||||
|
||||
msg = f"🛑 Stop Loss Hit\nPair: {position['symbol']}\nEntry: ${position['entry_price']}\nCurrent: ${current_price}\nStop: ${position['stop_loss_price']}"
|
||||
await self.telegram.send_alert(msg)
|
||||
logger.warning(msg)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Stop loss monitoring error: {e}")
|
||||
|
||||
def _should_close_by_stop_loss(self, stop_loss_price: float, current_price: float) -> bool:
|
||||
"""Determine if stop loss should trigger"""
|
||||
return current_price <= stop_loss_price
|
||||
|
||||
async def _get_current_price(self) -> float:
|
||||
"""Fetch current BTC price"""
|
||||
ticker = await self.client.client.get_symbol_ticker(symbol=self.strategy.trading_pair)
|
||||
return float(ticker['price'])
|
||||
|
|
@ -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)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,84 +0,0 @@
|
|||
"""
|
||||
Dashboard Client - sends trading data to web dashboard
|
||||
"""
|
||||
import aiohttp
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DashboardClient:
|
||||
def __init__(self, dashboard_url="http://localhost:7000"):
|
||||
self.dashboard_url = dashboard_url
|
||||
self.session = None
|
||||
|
||||
async def connect(self):
|
||||
"""Initialize session"""
|
||||
if not self.session:
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
async def close(self):
|
||||
"""Close session"""
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
|
||||
async def update_state(self, **kwargs):
|
||||
"""Update dashboard state"""
|
||||
try:
|
||||
await self.connect()
|
||||
await self.session.post(
|
||||
f'{self.dashboard_url}/api/update',
|
||||
json=kwargs,
|
||||
timeout=aiohttp.ClientTimeout(total=2)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f'Dashboard update failed (non-critical): {e}')
|
||||
|
||||
async def record_buy(self, pair: str, qty: float, price: float):
|
||||
"""Record a BUY order on dashboard"""
|
||||
try:
|
||||
await self.connect()
|
||||
await self.session.post(
|
||||
f'{self.dashboard_url}/api/trade/buy',
|
||||
params={'pair': pair, 'qty': qty, 'price': price},
|
||||
timeout=aiohttp.ClientTimeout(total=1)
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def record_sell(self, pair: str, qty: float, price: float,
|
||||
profit_usd: float, profit_pct: float, hold_time_min: float):
|
||||
"""Record a SELL order on dashboard"""
|
||||
try:
|
||||
await self.connect()
|
||||
await self.session.post(
|
||||
f'{self.dashboard_url}/api/trade/sell',
|
||||
params={
|
||||
'pair': pair,
|
||||
'qty': qty,
|
||||
'price': price,
|
||||
'profit_usd': profit_usd,
|
||||
'profit_pct': profit_pct,
|
||||
'hold_time_min': hold_time_min
|
||||
},
|
||||
timeout=aiohttp.ClientTimeout(total=1)
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def record_swap(self, from_asset: str, to_asset: str, qty: float, rate: float):
|
||||
"""Record a SWAP on dashboard"""
|
||||
try:
|
||||
await self.connect()
|
||||
await self.session.post(
|
||||
f'{self.dashboard_url}/api/swap',
|
||||
params={
|
||||
'from_asset': from_asset,
|
||||
'to_asset': to_asset,
|
||||
'qty': qty,
|
||||
'rate': rate
|
||||
},
|
||||
timeout=aiohttp.ClientTimeout(total=1)
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ObsidianLogger:
|
||||
"""Logs trades directly to Obsidian vault file."""
|
||||
|
||||
def __init__(self, vault_path: str, trade_log_file: str):
|
||||
self.vault_path = Path(vault_path)
|
||||
self.trade_log_file = trade_log_file
|
||||
self.log_path = self.vault_path / self.trade_log_file
|
||||
|
||||
def log_trade(self, trade_data: Dict) -> bool:
|
||||
"""
|
||||
Log trade to Obsidian markdown file.
|
||||
|
||||
Args:
|
||||
trade_data: Trade details (timestamp, order_id, pair, side, quantity, price, stop_loss)
|
||||
|
||||
Returns:
|
||||
True if logged successfully
|
||||
"""
|
||||
try:
|
||||
# Ensure directory exists
|
||||
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Format trade entry
|
||||
timestamp = trade_data.get('timestamp', datetime.utcnow())
|
||||
entry = self._format_trade_entry(trade_data)
|
||||
|
||||
# Append to log file
|
||||
with open(self.log_path, 'a', encoding='utf-8') as f:
|
||||
f.write(entry)
|
||||
|
||||
logger.info(f"Trade logged to Obsidian: {trade_data.get('order_id')}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Obsidian log error: {e}")
|
||||
return False
|
||||
|
||||
def _format_trade_entry(self, trade: Dict) -> str:
|
||||
"""Format trade as markdown entry"""
|
||||
timestamp = trade.get('timestamp', datetime.utcnow())
|
||||
|
||||
entry = f"""
|
||||
## {timestamp.isoformat()} | {trade['pair']} | {trade['side']}
|
||||
|
||||
- **Order ID:** {trade['order_id']}
|
||||
- **Quantity:** {trade['quantity']} BTC
|
||||
- **Price:** ${trade['price']}
|
||||
- **Stop Loss:** ${trade['stop_loss']}
|
||||
- **Type:** DCA Bot Trade
|
||||
|
||||
---
|
||||
|
||||
"""
|
||||
return entry
|
||||
|
||||
def log_stop_loss_hit(self, position: Dict, current_price: float) -> bool:
|
||||
"""Log stop loss event"""
|
||||
try:
|
||||
entry = f"""
|
||||
### ⚠️ STOP LOSS HIT | {position['symbol']}
|
||||
|
||||
- **Order ID:** {position['order_id']}
|
||||
- **Entry Price:** ${position['entry_price']}
|
||||
- **Stop Loss:** ${position['stop_loss_price']}
|
||||
- **Current Price:** ${current_price}
|
||||
- **Loss %:** {((current_price - position['entry_price']) / position['entry_price'] * 100):.2f}%
|
||||
- **Closed:** {datetime.utcnow().isoformat()}
|
||||
|
||||
---
|
||||
|
||||
"""
|
||||
with open(self.log_path, 'a', encoding='utf-8') as f:
|
||||
f.write(entry)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Stop loss log error: {e}")
|
||||
return False
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TelegramNotifier:
|
||||
"""Telegram bot integration for alerts and reports"""
|
||||
|
||||
def __init__(self, bot_token: str, chat_id: str):
|
||||
self.bot_token = bot_token
|
||||
self.chat_id = chat_id
|
||||
self.api_url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
|
||||
|
||||
async def send_alert(self, message: str) -> bool:
|
||||
"""
|
||||
Send alert message to Telegram.
|
||||
NOW WITH FULL SUPPORT FOR REPORTS, NOT JUST STARTUP!
|
||||
|
||||
Args:
|
||||
message: Message text (supports markdown)
|
||||
|
||||
Returns:
|
||||
True if sent successfully
|
||||
"""
|
||||
try:
|
||||
timeout = aiohttp.ClientTimeout(total=10)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
payload = {
|
||||
"chat_id": self.chat_id,
|
||||
"text": message,
|
||||
"parse_mode": "Markdown" # Use Markdown for better formatting
|
||||
}
|
||||
|
||||
async with session.post(self.api_url, json=payload) as response:
|
||||
if response.status == 200:
|
||||
result = await response.json()
|
||||
if result.get('ok'):
|
||||
logger.info(f"✅ Telegram message sent (ID: {result.get('result', {}).get('message_id', 'N/A')})")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Telegram API error: {result.get('description', 'Unknown')}")
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"Telegram HTTP error: {response.status}")
|
||||
return False
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("Telegram timeout (10s)")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Telegram send error: {e}")
|
||||
return False
|
||||
|
||||
async def send_order_update(self, order_id: int, status: str, details: str):
|
||||
"""Send order update to Telegram"""
|
||||
message = f"📈 **Order Update**\n\nID: {order_id}\nStatus: {status}\nDetails: {details}"
|
||||
return await self.send_alert(message)
|
||||
|
||||
async def send_trade_alert(self, entry_price: float, quantity: float, probability: float):
|
||||
"""Send trade alert"""
|
||||
message = f"🚀 **NEW TRADE**\n\nPrice: ${entry_price:,.2f}\nQty: {quantity}\nProbability: {probability*100:.1f}%"
|
||||
return await self.send_alert(message)
|
||||
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())
|
||||
366
src/main_ml.py
366
src/main_ml.py
|
|
@ -1,366 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Trading Bot v0.7 - Coin-Level Contrarian (Mean Reversion per Coin)"""
|
||||
import os, json, time, logging, sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
from dotenv import load_dotenv
|
||||
from binance.client import Client
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(message)s')
|
||||
logger = logging.getLogger()
|
||||
|
||||
load_dotenv()
|
||||
API_KEY = os.getenv('BINANCE_API_KEY_LIVE')
|
||||
API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE')
|
||||
|
||||
if not API_KEY or not API_SECRET:
|
||||
logger.error("Missing API keys")
|
||||
exit(1)
|
||||
|
||||
# 9 Coins (Coin-Level Contrarian)
|
||||
SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT', 'ADAUSDT', 'DOGUSDT', 'DOTUSDT', 'AVAXUSDT']
|
||||
TRACKED_COINS = ['BTC', 'ETH', 'BNB', 'XRP', 'SOL', 'ADA', 'DOGE', 'DOT', 'AVAX']
|
||||
MIN_TRADE_USDT = 12.00
|
||||
MAX_POSITION_PCT = 0.07
|
||||
TAKE_PROFIT_PCT = 0.015
|
||||
STOP_LOSS_PCT = -0.008
|
||||
CYCLE_SEC = 60
|
||||
|
||||
# COIN-LEVEL CONTRARIAN THRESHOLDS
|
||||
COIN_BUY_THRESHOLD = -1.5 # Buy when coin DOWN 1.5% (24h)
|
||||
COIN_SELL_THRESHOLD = +1.5 # Sell when coin UP 1.5% (24h)
|
||||
LOOKBACK_HOURS = 24
|
||||
|
||||
class TradingBotV07:
|
||||
def __init__(self):
|
||||
self.client = Client(API_KEY, API_SECRET)
|
||||
self.price_history = {sym: [] for sym in SYMBOLS}
|
||||
self.active_trades = {}
|
||||
self.portfolio_value = 0
|
||||
self.max_trade_usdt = 0
|
||||
|
||||
# LOAD 24H PRICE HISTORY ON STARTUP
|
||||
logger.info("[v0.7.1] Loading 24h price history from Binance...")
|
||||
for symbol in SYMBOLS:
|
||||
try:
|
||||
klines = self.client.get_historical_klines(symbol, '1m', '1 day ago UTC')
|
||||
for k in klines:
|
||||
self.price_history[symbol].append(float(k[4])) # close price
|
||||
logger.info(f" ✅ {symbol}: {len(self.price_history[symbol])} candles")
|
||||
except Exception as e:
|
||||
logger.warning(f" ⚠️ {symbol} history failed: {e}")
|
||||
|
||||
|
||||
# LOAD EXISTING HOLDINGS FROM BINANCE
|
||||
logger.info("[v0.7.1] Recovering existing holdings from Binance...")
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
for b in account['balances']:
|
||||
asset = b['asset']
|
||||
free = float(b['free'])
|
||||
|
||||
if asset in ['BTC', 'ETH', 'BNB', 'XRP', 'SOL', 'ADA', 'DOGE', 'DOT', 'AVAX'] and free > 0.0001:
|
||||
symbol = asset + 'USDT'
|
||||
try:
|
||||
price = self.get_current_price(symbol)
|
||||
if price and price > 0:
|
||||
self.active_trades[symbol] = {
|
||||
'entry_price': price,
|
||||
'qty': free,
|
||||
'entry_time': datetime.now().isoformat()
|
||||
}
|
||||
logger.info(f" ✅ RECOVERED {symbol} {free} @ ${price}")
|
||||
except Exception as e:
|
||||
logger.warning(f" ⚠️ {symbol} recovery error: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Recovery failed: {e}")
|
||||
|
||||
logger.info(f"[v0.7.1] Startup complete. Active trades: {len(self.active_trades)}")
|
||||
|
||||
logger.info(f"[v0.7.1 INIT] Price history loaded. Ready for ±1.5% signals.")
|
||||
|
||||
logger.info("[v0.7 INIT] Coin-Level Contrarian (9 Coins, ±1.5% Thresholds)")
|
||||
|
||||
def calculate_coin_return_24h(self, symbol):
|
||||
"""Calculate 24h return for SINGLE coin"""
|
||||
if len(self.price_history[symbol]) < 2:
|
||||
return None
|
||||
|
||||
prices = self.price_history[symbol]
|
||||
current = prices[-1]
|
||||
|
||||
# Get price from ~24h ago (or earliest if <24h data)
|
||||
reference_idx = max(0, len(prices) - 1440) # 1440 = 24h * 60min
|
||||
reference = prices[reference_idx]
|
||||
|
||||
if reference > 0:
|
||||
ret = ((current - reference) / reference) * 100
|
||||
return ret
|
||||
|
||||
return None
|
||||
|
||||
def is_coin_buy_signal(self, symbol):
|
||||
"""Buy when COIN DOWN 2%+ (Coin-Level Contrarian)"""
|
||||
ret = self.calculate_coin_return_24h(symbol)
|
||||
|
||||
if ret is None:
|
||||
return False
|
||||
|
||||
buy_signal = ret < COIN_BUY_THRESHOLD
|
||||
|
||||
if buy_signal:
|
||||
logger.info(f"[SIGNAL-COIN-BUY] {symbol} 24h return {ret:.2f}% (Threshold: {COIN_BUY_THRESHOLD}%)")
|
||||
|
||||
return buy_signal
|
||||
|
||||
def is_coin_sell_signal(self, symbol):
|
||||
"""Sell when COIN UP 2%+ (Coin-Level Contrarian)"""
|
||||
ret = self.calculate_coin_return_24h(symbol)
|
||||
|
||||
if ret is None:
|
||||
return False
|
||||
|
||||
sell_signal = ret > COIN_SELL_THRESHOLD
|
||||
|
||||
if sell_signal:
|
||||
logger.info(f"[SIGNAL-COIN-SELL] {symbol} 24h return {ret:.2f}% (Threshold: {COIN_SELL_THRESHOLD}%)")
|
||||
|
||||
return sell_signal
|
||||
|
||||
def get_fresh_balance(self):
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
portfolio_value = 0
|
||||
|
||||
prices = {'USDT': 1.0}
|
||||
for symbol in SYMBOLS:
|
||||
try:
|
||||
ticker = self.client.get_ticker(symbol=symbol)
|
||||
coin = symbol.replace('USDT', '')
|
||||
prices[coin] = float(ticker['lastPrice'])
|
||||
except:
|
||||
pass
|
||||
|
||||
for balance in account['balances']:
|
||||
asset = balance['asset']
|
||||
free = float(balance['free'])
|
||||
|
||||
if asset in TRACKED_COINS:
|
||||
price = prices.get(asset, 0)
|
||||
portfolio_value += free * price
|
||||
elif asset == 'USDT':
|
||||
portfolio_value += free
|
||||
|
||||
usdt_available = next((float(b['free']) for b in account['balances'] if b['asset'] == 'USDT'), 0)
|
||||
|
||||
self.portfolio_value = portfolio_value
|
||||
self.max_trade_usdt = portfolio_value * MAX_POSITION_PCT
|
||||
|
||||
logger.info(f"[v0.7] USDT={usdt_available:.2f} | Portfolio={portfolio_value:.2f} | Max={self.max_trade_usdt:.2f}")
|
||||
return usdt_available, portfolio_value
|
||||
except:
|
||||
return 0, 0
|
||||
|
||||
def get_current_price(self, symbol):
|
||||
try:
|
||||
ticker = self.client.get_ticker(symbol=symbol)
|
||||
return float(ticker['lastPrice'])
|
||||
except:
|
||||
return None
|
||||
|
||||
def calculate_valid_quantity(self, symbol, usdt_amount):
|
||||
try:
|
||||
price = self.get_current_price(symbol)
|
||||
if not price or price <= 0:
|
||||
return 0
|
||||
|
||||
info = self.client.get_symbol_info(symbol)
|
||||
if not info:
|
||||
return 0
|
||||
|
||||
step_size = None
|
||||
for f in info.get('filters', []):
|
||||
if f['filterType'] == 'LOT_SIZE':
|
||||
step_size = float(f['stepSize'])
|
||||
break
|
||||
|
||||
if not step_size or step_size <= 0:
|
||||
return 0
|
||||
|
||||
qty = usdt_amount / price
|
||||
qty = int(qty / step_size) * step_size
|
||||
|
||||
if qty * price < 5.0:
|
||||
return 0
|
||||
|
||||
return qty
|
||||
except:
|
||||
return 0
|
||||
|
||||
def place_buy_order(self, symbol, usdt_amount):
|
||||
try:
|
||||
qty = self.calculate_valid_quantity(symbol, usdt_amount)
|
||||
if qty <= 0:
|
||||
return None
|
||||
|
||||
price = self.get_current_price(symbol)
|
||||
if not price:
|
||||
return None
|
||||
|
||||
order = self.client.order_market_buy(symbol=symbol, quantity=qty)
|
||||
|
||||
self.active_trades[symbol] = {
|
||||
'entry_price': price,
|
||||
'qty': qty,
|
||||
'order_id': order.get('orderId'),
|
||||
'entry_time': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"[BUY-v0.7] {symbol} {qty} @ {price} (COIN-LEVEL CONTRARIAN: DOWN 2%+)")
|
||||
return order
|
||||
except:
|
||||
return None
|
||||
|
||||
def place_sell_order(self, symbol):
|
||||
try:
|
||||
if symbol not in self.active_trades:
|
||||
return None
|
||||
|
||||
qty = self.active_trades[symbol]['qty']
|
||||
|
||||
order = self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
|
||||
price = self.get_current_price(symbol)
|
||||
entry = self.active_trades[symbol]['entry_price']
|
||||
pnl = ((price - entry) / entry) * 100
|
||||
|
||||
logger.info(f"[SELL-v0.7] {symbol} {qty} @ {price} (COIN-LEVEL CONTRARIAN: UP 2%+, P&L: {pnl:+.2f}%)")
|
||||
|
||||
del self.active_trades[symbol]
|
||||
return order
|
||||
except:
|
||||
return None
|
||||
|
||||
def check_and_close_positions(self):
|
||||
for symbol, trade in list(self.active_trades.items()):
|
||||
try:
|
||||
current = self.get_current_price(symbol)
|
||||
if not current:
|
||||
continue
|
||||
|
||||
entry = trade['entry_price']
|
||||
qty = trade['qty']
|
||||
pnl_pct = ((current - entry) / entry) * 100
|
||||
|
||||
# TP Hit
|
||||
if pnl_pct >= TAKE_PROFIT_PCT * 100:
|
||||
logger.info(f"[SELL-TP] {symbol} +{pnl_pct:.2f}%")
|
||||
try:
|
||||
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
del self.active_trades[symbol]
|
||||
except:
|
||||
pass
|
||||
|
||||
# SL Hit
|
||||
elif pnl_pct <= STOP_LOSS_PCT * 100:
|
||||
logger.info(f"[SELL-SL] {symbol} {pnl_pct:.2f}%")
|
||||
try:
|
||||
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
del self.active_trades[symbol]
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
def save_pnl_to_db(self, portfolio_val, usdt_free):
|
||||
try:
|
||||
conn = sqlite3.connect('/home/marc/bot-deploy/pnl_charts.db')
|
||||
baseline = conn.execute('SELECT pv FROM history ORDER BY ts ASC LIMIT 1').fetchone()
|
||||
baseline_pv = baseline[0] if baseline else portfolio_val
|
||||
|
||||
pu = portfolio_val - baseline_pv
|
||||
pp = (pu / baseline_pv * 100) if baseline_pv > 0 else 0
|
||||
|
||||
conn.execute('INSERT INTO history VALUES (?, ?, ?, ?, ?, ?)',
|
||||
(int(datetime.now().timestamp()), portfolio_val, pu, pp, usdt_free, len(self.active_trades)))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"[DB-LOG] PV={portfolio_val:.2f}, P&L={pp:.2f}%")
|
||||
except Exception as e:
|
||||
logger.warning(f"DB log failed: {e}")
|
||||
|
||||
def run_cycle(self):
|
||||
logger.info("="*70)
|
||||
|
||||
usdt_free, portfolio_val = self.get_fresh_balance()
|
||||
|
||||
if usdt_free < MIN_TRADE_USDT:
|
||||
logger.warning(f"Low capital: {usdt_free:.2f}")
|
||||
logger.info("="*70)
|
||||
return
|
||||
|
||||
# Update price history
|
||||
for symbol in SYMBOLS:
|
||||
price = self.get_current_price(symbol)
|
||||
if price:
|
||||
self.price_history[symbol].append(price)
|
||||
if len(self.price_history[symbol]) > 1440: # Keep 24h history
|
||||
self.price_history[symbol].pop(0)
|
||||
|
||||
# Check for COIN-LEVEL SELL (each coin UP 2%+)
|
||||
for symbol in list(self.active_trades.keys()):
|
||||
if self.is_coin_sell_signal(symbol):
|
||||
self.place_sell_order(symbol)
|
||||
break # One sell per cycle
|
||||
|
||||
# Check TP/SL
|
||||
self.check_and_close_positions()
|
||||
|
||||
# Check for COIN-LEVEL BUY (each coin DOWN 2%+)
|
||||
for symbol in SYMBOLS:
|
||||
if symbol in self.active_trades:
|
||||
continue # Skip already held
|
||||
|
||||
if self.is_coin_buy_signal(symbol) and usdt_free >= MIN_TRADE_USDT:
|
||||
trade_amount = min(max(MIN_TRADE_USDT, self.max_trade_usdt), usdt_free * 0.5)
|
||||
self.place_buy_order(symbol, trade_amount)
|
||||
break # One buy per cycle
|
||||
|
||||
# Save trades
|
||||
try:
|
||||
temp = '/home/marc/bot-deploy/active_trades.json.tmp'
|
||||
with open(temp, 'w') as f:
|
||||
json.dump({
|
||||
'active_trades': self.active_trades,
|
||||
'count': len(self.active_trades),
|
||||
'portfolio_value': round(portfolio_val, 2),
|
||||
'max_trade_usdt': round(self.max_trade_usdt, 2),
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'version': 'v0.7.1-recovery-fix'
|
||||
}, f)
|
||||
os.replace(temp, '/home/marc/bot-deploy/active_trades.json')
|
||||
except:
|
||||
pass
|
||||
|
||||
# Save P&L
|
||||
self.save_pnl_to_db(portfolio_val, usdt_free)
|
||||
|
||||
logger.info(f"[CYCLE-END] Trades={len(self.active_trades)} | Portfolio={portfolio_val:.2f} [v0.7]")
|
||||
logger.info("="*70)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
bot = TradingBotV07()
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--once':
|
||||
bot.run_cycle()
|
||||
else:
|
||||
logger.info("[v0.7 START] Trading Bot with Coin-Level Contrarian (9 Coins)...")
|
||||
while True:
|
||||
try:
|
||||
bot.run_cycle()
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
|
||||
time.sleep(CYCLE_SEC)
|
||||
|
|
@ -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())
|
||||
|
|
@ -1,590 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V0.2 — Adaptive Strategy Learning
|
||||
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 = 7.5 # 7-8% range (midpoint 7.5%) # 5% random signal
|
||||
self.INVESTMENT_PERCENT = 50 # 50% (single position for liquidity) (single position)
|
||||
self.INVESTMENT_PERCENT_HIGH = 55 # 55% when confidence > 85% > 85%
|
||||
self.CONFIDENCE_THRESHOLD = 85 # Min confidence for high investment # 35% per trade (5 parallel = 90% max, 10% buffer)
|
||||
self.NOTIONAL_MIN = 5.0 # Override Binance minimum to $3
|
||||
self.STOP_LOSS_PERCENT = 1.8 # -2.5%
|
||||
self.TAKE_PROFIT_PERCENT = 2.8 # +3%
|
||||
self.DAILY_LOSS_LIMIT = -5
|
||||
|
||||
# Trailing Stop
|
||||
self.TRAILING_STOP_ENTRY = 1.5 # Activate trailing stop at +1.5%
|
||||
self.TRAILING_STOP_DISTANCE = 0.6 # 0.6% distance
|
||||
|
||||
# Position & Trade Limits
|
||||
self.MAX_OPEN_POSITIONS = 1 # Single position for max liquidity # Max concurrent trades
|
||||
self.MAX_CONSECUTIVE_LOSSES = 3 # Stop after 3 losses
|
||||
self.CONSECUTIVE_LOSS_COOLDOWN = 30 * 60 # 30 minutes in seconds
|
||||
self.MAX_TRADES_PER_DAY = 15
|
||||
self.MIN_WIN_PROBABILITY = 75 # Min expected win %
|
||||
|
||||
# Tracking
|
||||
self.consecutive_losses = 0
|
||||
self.last_loss_time = None
|
||||
self.trades_today = 0
|
||||
self.last_trade_reset = None # -5% max
|
||||
|
||||
# Profit tracking
|
||||
self.entry_price_history = {} # symbol -> entry price
|
||||
self.closed_trades = [] # list of {symbol, entry, exit, profit_pct, profit_usdt}
|
||||
self.session_start_balance = None
|
||||
|
||||
self.active_trades = {}
|
||||
self.daily_pnl = 0
|
||||
self.paused = False
|
||||
|
||||
# ADAPTIVE TRACKING (Option 2: Win Rate based Strategy)
|
||||
self.total_trades = 0
|
||||
self.total_wins = 0
|
||||
self.total_losses = 0
|
||||
self.last_win_rate = 50.0 # Start neutral
|
||||
self.strategy_version = 1
|
||||
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(f"✅ Bot initialized with Risk Management (SL {self.STOP_LOSS_PERCENT}%, TP {self.TAKE_PROFIT_PERCENT}%, Daily Limit {-self.DAILY_LOSS_LIMIT}%, Max Pos: {self.MAX_OPEN_POSITIONS})")
|
||||
|
||||
# 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 V0.2 — 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 {self.STOP_LOSS_PERCENT}% 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: V0.2 Adaptive"""
|
||||
|
||||
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())
|
||||
|
||||
|
||||
def get_signal_confidence(self):
|
||||
"""Calculate confidence level for current signal (0-100%)"""
|
||||
# This can be enhanced with actual ML model
|
||||
# For now: random 30-95%
|
||||
import random
|
||||
return random.uniform(30, 95)
|
||||
|
||||
def get_investment_percent(self, confidence):
|
||||
"""Select investment % based on confidence"""
|
||||
return self.INVESTMENT_PERCENT_HIGH if confidence > self.CONFIDENCE_THRESHOLD else self.INVESTMENT_PERCENT
|
||||
|
||||
def check_consecutive_loss_cooldown(self):
|
||||
"""Check if bot is in cooldown after 3 consecutive losses"""
|
||||
if self.consecutive_losses >= self.MAX_CONSECUTIVE_LOSSES:
|
||||
if self.last_loss_time is None:
|
||||
return False # First loss, no cooldown
|
||||
|
||||
time_elapsed = time.time() - self.last_loss_time
|
||||
if time_elapsed < self.CONSECUTIVE_LOSS_COOLDOWN:
|
||||
logger.warning(f"🚫 Cooldown active: {int(self.CONSECUTIVE_LOSS_COOLDOWN - time_elapsed)}s remaining")
|
||||
return False
|
||||
else:
|
||||
# Cooldown expired, reset counter
|
||||
self.consecutive_losses = 0
|
||||
logger.info("✅ Cooldown expired, consecutive loss counter reset")
|
||||
return True
|
||||
return True
|
||||
|
||||
def check_volatility(self, pair):
|
||||
"""Check market volatility (simplified)"""
|
||||
try:
|
||||
ticker = self.client.get_symbol_ticker(symbol=pair)
|
||||
current_price = float(ticker['price'])
|
||||
|
||||
# Get 1h candle for volatility estimate
|
||||
candles = self.client.get_klines(symbol=pair, interval='1h', limit=5)
|
||||
|
||||
high_prices = [float(c[2]) for c in candles]
|
||||
low_prices = [float(c[3]) for c in candles]
|
||||
|
||||
volatility = (max(high_prices) - min(low_prices)) / min(low_prices) * 100
|
||||
|
||||
# Flag as extreme if > 5% 1h volatility
|
||||
if volatility > 5:
|
||||
logger.warning(f"⚠️ High volatility {pair}: {volatility:.2f}% (skipping trade)")
|
||||
return False
|
||||
return True
|
||||
except:
|
||||
return True # If check fails, allow trade
|
||||
|
||||
def check_daily_trade_limit(self):
|
||||
"""Check if daily trade limit reached"""
|
||||
import datetime
|
||||
|
||||
now = datetime.datetime.now()
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
if self.last_trade_reset is None or self.last_trade_reset < today_start:
|
||||
self.trades_today = 0
|
||||
self.last_trade_reset = now
|
||||
|
||||
if self.trades_today >= self.MAX_TRADES_PER_DAY:
|
||||
logger.warning(f"⚠️ Daily limit reached: {self.trades_today}/{self.MAX_TRADES_PER_DAY} trades")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_trailing_stop(self, pair, current_price, entry_price):
|
||||
"""Update trailing stop for an open position"""
|
||||
if pair not in self.active_trades:
|
||||
return False
|
||||
|
||||
trade_data = self.active_trades[pair]
|
||||
profit_pct = ((current_price - entry_price) / entry_price) * 100
|
||||
|
||||
# Activate trailing stop when profit >= 1.5%
|
||||
if profit_pct >= self.TRAILING_STOP_ENTRY:
|
||||
trailing_stop_price = current_price * (1 - self.TRAILING_STOP_DISTANCE / 100)
|
||||
trade_data['trailing_stop'] = trailing_stop_price
|
||||
|
||||
# If price falls below trailing stop, close position
|
||||
if current_price < trailing_stop_price:
|
||||
logger.info(f"🛑 Trailing stop triggered {pair}: Sell @ ${current_price:.2f}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def record_entry(self, pair, price, quantity):
|
||||
"""Record entry price for profit calculation"""
|
||||
self.entry_price_history[pair] = {
|
||||
'price': price,
|
||||
'qty': quantity,
|
||||
'value': price * quantity,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
def calculate_unrealized_pnl(self):
|
||||
"""Calculate unrealized P&L for open positions"""
|
||||
try:
|
||||
prices = get_live_prices()
|
||||
total_unrealized = 0
|
||||
|
||||
for pair, entry_data in self.entry_price_history.items():
|
||||
asset = pair.replace('USDT', '')
|
||||
current_price = prices.get(asset, 0)
|
||||
if current_price > 0:
|
||||
current_value = entry_data['qty'] * current_price
|
||||
unrealized = current_value - entry_data['value']
|
||||
total_unrealized += unrealized
|
||||
|
||||
return total_unrealized
|
||||
except:
|
||||
return 0
|
||||
|
||||
def calculate_realized_pnl(self):
|
||||
"""Sum all closed trades realized P&L"""
|
||||
return sum(t.get('profit_usdt', 0) for t in self.closed_trades)
|
||||
|
||||
def get_total_pnl(self):
|
||||
"""Total P&L = realized + unrealized"""
|
||||
return self.calculate_realized_pnl() + self.calculate_unrealized_pnl()
|
||||
|
||||
|
|
@ -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,262 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Trading Bot v0.4 Hybrid - Dynamic Position Sizing + Trade Recovery"""
|
||||
import os, json, time, logging
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(message)s')
|
||||
logger = logging.getLogger()
|
||||
|
||||
load_dotenv()
|
||||
API_KEY = os.getenv('BINANCE_API_KEY_LIVE')
|
||||
API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE')
|
||||
|
||||
if not API_KEY or not API_SECRET:
|
||||
logger.error("Missing API keys")
|
||||
exit(1)
|
||||
|
||||
SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT']
|
||||
TRACKED_COINS = ['BTC', 'ETH', 'BNB', 'XRP', 'SOL']
|
||||
MIN_TRADE_USDT = 12.00
|
||||
MAX_POSITION_PCT = 0.07
|
||||
TAKE_PROFIT_PCT = 0.015
|
||||
STOP_LOSS_PCT = -0.008
|
||||
CYCLE_SEC = 60
|
||||
|
||||
class TradingBotV04:
|
||||
def __init__(self):
|
||||
self.client = Client(API_KEY, API_SECRET)
|
||||
self.price_history = {sym: [] for sym in SYMBOLS}
|
||||
self.active_trades = {}
|
||||
self.portfolio_value = 0
|
||||
self.max_trade_usdt = 0
|
||||
|
||||
# TRADE RECOVERY: Recover orphaned trades from holdings
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
for b in account['balances']:
|
||||
asset = b['asset']
|
||||
free = float(b['free'])
|
||||
|
||||
if asset in TRACKED_COINS and free > 0.0001:
|
||||
symbol = asset + 'USDT'
|
||||
try:
|
||||
price = self.get_current_price(symbol)
|
||||
if price:
|
||||
self.active_trades[symbol] = {
|
||||
'entry_price': price,
|
||||
'qty': free,
|
||||
'entry_time': datetime.now().isoformat()
|
||||
}
|
||||
logger.info(f"[RECOVERED] {symbol} {free} @ {price}")
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.warning(f"Recovery failed: {e}")
|
||||
|
||||
logger.info("[v0.4 INIT] Bot | Dynamic Sizing (Min 12 + 7%)")
|
||||
|
||||
def get_fresh_balance(self):
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
portfolio_value = 0
|
||||
|
||||
prices = {'USDT': 1.0}
|
||||
for symbol in SYMBOLS:
|
||||
try:
|
||||
ticker = self.client.get_ticker(symbol=symbol)
|
||||
coin = symbol.replace('USDT', '')
|
||||
prices[coin] = float(ticker['lastPrice'])
|
||||
except:
|
||||
pass
|
||||
|
||||
for balance in account['balances']:
|
||||
asset = balance['asset']
|
||||
free = float(balance['free'])
|
||||
|
||||
if asset in TRACKED_COINS:
|
||||
price = prices.get(asset, 0)
|
||||
portfolio_value += free * price
|
||||
elif asset == 'USDT':
|
||||
portfolio_value += free
|
||||
|
||||
usdt_available = next((float(b['free']) for b in account['balances'] if b['asset'] == 'USDT'), 0)
|
||||
|
||||
self.portfolio_value = portfolio_value
|
||||
self.max_trade_usdt = portfolio_value * MAX_POSITION_PCT
|
||||
|
||||
logger.info(f"[v0.4] USDT={usdt_available:.2f} | Portfolio={portfolio_value:.2f} | Max={self.max_trade_usdt:.2f}")
|
||||
return usdt_available, portfolio_value
|
||||
except:
|
||||
return 0, 0
|
||||
|
||||
def get_current_price(self, symbol):
|
||||
try:
|
||||
ticker = self.client.get_ticker(symbol=symbol)
|
||||
return float(ticker['lastPrice'])
|
||||
except:
|
||||
return None
|
||||
|
||||
def calculate_valid_quantity(self, symbol, usdt_amount):
|
||||
try:
|
||||
price = self.get_current_price(symbol)
|
||||
if not price or price <= 0:
|
||||
return 0
|
||||
|
||||
info = self.client.get_symbol_info(symbol)
|
||||
if not info:
|
||||
return 0
|
||||
|
||||
step_size = None
|
||||
for f in info.get('filters', []):
|
||||
if f['filterType'] == 'LOT_SIZE':
|
||||
step_size = float(f['stepSize'])
|
||||
break
|
||||
|
||||
if not step_size or step_size <= 0:
|
||||
return 0
|
||||
|
||||
qty = usdt_amount / price
|
||||
qty = int(qty / step_size) * step_size
|
||||
|
||||
if qty * price < 5.0:
|
||||
return 0
|
||||
|
||||
return qty
|
||||
except:
|
||||
return 0
|
||||
|
||||
def is_local_minimum(self, symbol):
|
||||
if len(self.price_history[symbol]) < 5:
|
||||
return False
|
||||
|
||||
recent = self.price_history[symbol][-5:]
|
||||
current = recent[-1]
|
||||
|
||||
is_min = all(current < p for p in recent[:-1])
|
||||
if is_min:
|
||||
logger.info(f"[SIGNAL] Local min: {symbol} @ {current}")
|
||||
|
||||
return is_min
|
||||
|
||||
def place_buy_order(self, symbol, usdt_amount):
|
||||
try:
|
||||
qty = self.calculate_valid_quantity(symbol, usdt_amount)
|
||||
if qty <= 0:
|
||||
return None
|
||||
|
||||
price = self.get_current_price(symbol)
|
||||
if not price:
|
||||
return None
|
||||
|
||||
order = self.client.order_market_buy(symbol=symbol, quantity=qty)
|
||||
|
||||
self.active_trades[symbol] = {
|
||||
'entry_price': price,
|
||||
'qty': qty,
|
||||
'order_id': order.get('orderId'),
|
||||
'entry_time': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
pos_pct = (qty * price / self.portfolio_value * 100) if self.portfolio_value > 0 else 0
|
||||
logger.info(f"[BUY] {symbol} {qty} @ {price} | Pos: {pos_pct:.1}% [v0.4 HYBRID]")
|
||||
|
||||
return order
|
||||
except:
|
||||
return None
|
||||
|
||||
def check_and_close_positions(self):
|
||||
for symbol, trade in list(self.active_trades.items()):
|
||||
try:
|
||||
current = self.get_current_price(symbol)
|
||||
if not current:
|
||||
continue
|
||||
|
||||
entry = trade['entry_price']
|
||||
qty = trade['qty']
|
||||
pnl_pct = ((current - entry) / entry) * 100
|
||||
|
||||
if pnl_pct >= TAKE_PROFIT_PCT * 100:
|
||||
logger.info(f"[SELL-TP] {symbol} @ {current} | +{pnl_pct:.2f}%")
|
||||
try:
|
||||
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
del self.active_trades[symbol]
|
||||
except:
|
||||
pass
|
||||
|
||||
elif pnl_pct <= STOP_LOSS_PCT * 100:
|
||||
logger.info(f"[SELL-SL] {symbol} @ {current} | {pnl_pct:.2f}%")
|
||||
try:
|
||||
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
del self.active_trades[symbol]
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
def run_cycle(self):
|
||||
logger.info("="*70)
|
||||
|
||||
usdt_free, portfolio_val = self.get_fresh_balance()
|
||||
|
||||
if usdt_free < MIN_TRADE_USDT:
|
||||
logger.warning(f"Low capital: {usdt_free:.2f} < {MIN_TRADE_USDT}")
|
||||
logger.info("="*70)
|
||||
return
|
||||
|
||||
self.check_and_close_positions()
|
||||
|
||||
for symbol in SYMBOLS:
|
||||
price = self.get_current_price(symbol)
|
||||
if price:
|
||||
self.price_history[symbol].append(price)
|
||||
if len(self.price_history[symbol]) > 20:
|
||||
self.price_history[symbol].pop(0)
|
||||
|
||||
best_signal = None
|
||||
for symbol in SYMBOLS:
|
||||
if symbol not in self.active_trades and self.is_local_minimum(symbol):
|
||||
best_signal = symbol
|
||||
break
|
||||
|
||||
if best_signal and usdt_free >= MIN_TRADE_USDT:
|
||||
trade_amount = min(max(MIN_TRADE_USDT, self.max_trade_usdt), usdt_free * 0.5)
|
||||
self.place_buy_order(best_signal, trade_amount)
|
||||
|
||||
logger.info(f"[CYCLE-END] Trades: {len(self.active_trades)} | USDT: {usdt_free:.2f} | Portfolio: {portfolio_val:.2f}")
|
||||
|
||||
try:
|
||||
temp = '/home/marc/bot-deploy/active_trades.json.tmp'
|
||||
with open(temp, 'w') as f:
|
||||
json.dump({
|
||||
'active_trades': self.active_trades,
|
||||
'count': len(self.active_trades),
|
||||
'portfolio_value': round(portfolio_val, 2),
|
||||
'max_trade_usdt': round(self.max_trade_usdt, 2),
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'version': 'v0.4-hybrid'
|
||||
}, f)
|
||||
os.replace(temp, '/home/marc/bot-deploy/active_trades.json')
|
||||
except Exception as e:
|
||||
logger.warning(f"Save failed: {e}")
|
||||
|
||||
logger.info("="*70)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
bot = TradingBotV04()
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--once':
|
||||
bot.run_cycle()
|
||||
else:
|
||||
logger.info("[v0.4 START] Bot cycle loop...")
|
||||
while True:
|
||||
try:
|
||||
bot.run_cycle()
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
|
||||
time.sleep(CYCLE_SEC)
|
||||
|
|
@ -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,682 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V0.2 — Adaptive Strategy Learning
|
||||
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 = 7.5 # 7-8% range (midpoint 7.5%) # 5% random signal
|
||||
self.INVESTMENT_PERCENT = 50 # 50% (single position for liquidity) (single position)
|
||||
self.INVESTMENT_PERCENT_HIGH = 55 # 55% when confidence > 85% > 85%
|
||||
self.CONFIDENCE_THRESHOLD = 85 # Min confidence for high investment # 35% per trade (5 parallel = 90% max, 10% buffer)
|
||||
self.NOTIONAL_MIN = 5.0 # Override Binance minimum to $3
|
||||
self.STOP_LOSS_PERCENT = 1.8 # -2.5%
|
||||
self.TAKE_PROFIT_PERCENT = 2.8 # +3%
|
||||
self.DAILY_LOSS_LIMIT = -5
|
||||
|
||||
# Trailing Stop
|
||||
self.TRAILING_STOP_ENTRY = 1.5 # Activate trailing stop at +1.5%
|
||||
self.TRAILING_STOP_DISTANCE = 0.6 # 0.6% distance
|
||||
|
||||
# Position & Trade Limits
|
||||
self.MAX_OPEN_POSITIONS = 1 # Single position for max liquidity # Max concurrent trades
|
||||
self.MAX_CONSECUTIVE_LOSSES = 3 # Stop after 3 losses
|
||||
self.CONSECUTIVE_LOSS_COOLDOWN = 30 * 60 # 30 minutes in seconds
|
||||
self.MAX_TRADES_PER_DAY = 15
|
||||
self.MIN_WIN_PROBABILITY = 75 # Min expected win %
|
||||
|
||||
# Tracking
|
||||
self.consecutive_losses = 0
|
||||
self.last_loss_time = None
|
||||
self.trades_today = 0
|
||||
self.last_trade_reset = None # -5% max
|
||||
|
||||
# Profit tracking
|
||||
self.entry_price_history = {} # symbol -> entry price
|
||||
self.closed_trades = [] # list of {symbol, entry, exit, profit_pct, profit_usdt}
|
||||
self.session_start_balance = None
|
||||
|
||||
self.active_trades = {}
|
||||
self.daily_pnl = 0
|
||||
self.paused = False
|
||||
|
||||
# ADAPTIVE TRACKING (Option 2: Win Rate based Strategy)
|
||||
self.total_trades = 0
|
||||
self.total_wins = 0
|
||||
self.total_losses = 0
|
||||
self.last_win_rate = 50.0 # Start neutral
|
||||
self.strategy_version = 1
|
||||
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(f"✅ Bot initialized with Risk Management (SL {self.STOP_LOSS_PERCENT}%, TP {self.TAKE_PROFIT_PERCENT}%, Daily Limit {-self.DAILY_LOSS_LIMIT}%, Max Pos: {self.MAX_OPEN_POSITIONS})")
|
||||
|
||||
# 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 V0.2 — 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 {self.STOP_LOSS_PERCENT}% 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 swap_coins_to_usdt(self):
|
||||
"""
|
||||
AUTO-SWAP: Konvertiere alle freien (unlocked) Coins → USDT
|
||||
Ignoriert locked Coins (von aktiven Trades)
|
||||
Skip-list: LDBTTC (shitcoin), LDDOGE (shitcoin), USDC (dust)
|
||||
"""
|
||||
skip_coins = ['USDT', 'LDBTTC', 'LDDOGE', 'USDC'] # Never swap these
|
||||
|
||||
try:
|
||||
balance = self.client.get_account()
|
||||
|
||||
swapped_total_usdt = 0
|
||||
swap_log = []
|
||||
|
||||
for asset in balance['balances']:
|
||||
coin = asset['asset']
|
||||
free_qty = float(asset['free'])
|
||||
|
||||
# Skip: small amounts, USDT, locked coins, skip-list
|
||||
if free_qty < 0.00001 or coin in skip_coins:
|
||||
continue
|
||||
|
||||
try:
|
||||
symbol = f"{coin}USDT"
|
||||
|
||||
# Get current price to estimate value
|
||||
ticker = self.client.get_symbol_info(symbol)
|
||||
if not ticker:
|
||||
logger.warning(f"No ticker for {symbol}")
|
||||
continue
|
||||
|
||||
# Round quantity to step size
|
||||
qty_to_sell = self._round_quantity(free_qty, symbol)
|
||||
if qty_to_sell < 0.00001:
|
||||
continue
|
||||
|
||||
# MARKET SELL (immediate)
|
||||
order = self.client.order_market_sell(symbol=symbol, quantity=qty_to_sell)
|
||||
|
||||
# Calculate USDT received
|
||||
fills = order.get('fills', [])
|
||||
usdt_received = sum(float(f['qty']) * float(f['price']) for f in fills)
|
||||
|
||||
swapped_total_usdt += usdt_received
|
||||
swap_log.append(f"✅ {coin}: {qty_to_sell:.6f} → ${usdt_received:.2f}")
|
||||
logger.info(f"Sweep: Sold {qty_to_sell} {coin} for ${usdt_received:.2f}")
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.warning(f"Sweep {coin}: Binance Error {e.status_code} - {e.message}")
|
||||
swap_log.append(f"❌ {coin}: {e.message}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Sweep {coin}: {e}")
|
||||
swap_log.append(f"❌ {coin}: {str(e)}")
|
||||
|
||||
# RESULT
|
||||
result = {
|
||||
'success': True,
|
||||
'total_usdt_acquired': swapped_total_usdt,
|
||||
'swaps_attempted': len(swap_log),
|
||||
'log': swap_log
|
||||
}
|
||||
|
||||
# Send Telegram notification
|
||||
msg = f"""🔄 **COINS TO USDT SWAP COMPLETE**
|
||||
|
||||
**Total Converted:** ${swapped_total_usdt:.2f} → USDT
|
||||
|
||||
{chr(10).join(swap_log)}
|
||||
|
||||
**New USDT Balance:** ${self.get_usdt_balance():.2f}
|
||||
"""
|
||||
self._send_telegram(msg)
|
||||
|
||||
logger.info(f"Swap complete: ${swapped_total_usdt:.2f} converted")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Swap error: {e}")
|
||||
self._send_telegram(f"❌ **SWAP FAILED**: {e}")
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def get_usdt_balance(self):
|
||||
"""Get current USDT balance"""
|
||||
try:
|
||||
balance = self.client.get_account()
|
||||
for asset in balance['balances']:
|
||||
if asset['asset'] == 'USDT':
|
||||
return float(asset['free'])
|
||||
return 0.0
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
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: V0.2 Adaptive"""
|
||||
|
||||
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())
|
||||
|
||||
|
||||
def get_signal_confidence(self):
|
||||
"""Calculate confidence level for current signal (0-100%)"""
|
||||
# This can be enhanced with actual ML model
|
||||
# For now: random 30-95%
|
||||
import random
|
||||
return random.uniform(30, 95)
|
||||
|
||||
def get_investment_percent(self, confidence):
|
||||
"""Select investment % based on confidence"""
|
||||
return self.INVESTMENT_PERCENT_HIGH if confidence > self.CONFIDENCE_THRESHOLD else self.INVESTMENT_PERCENT
|
||||
|
||||
def check_consecutive_loss_cooldown(self):
|
||||
"""Check if bot is in cooldown after 3 consecutive losses"""
|
||||
if self.consecutive_losses >= self.MAX_CONSECUTIVE_LOSSES:
|
||||
if self.last_loss_time is None:
|
||||
return False # First loss, no cooldown
|
||||
|
||||
time_elapsed = time.time() - self.last_loss_time
|
||||
if time_elapsed < self.CONSECUTIVE_LOSS_COOLDOWN:
|
||||
logger.warning(f"🚫 Cooldown active: {int(self.CONSECUTIVE_LOSS_COOLDOWN - time_elapsed)}s remaining")
|
||||
return False
|
||||
else:
|
||||
# Cooldown expired, reset counter
|
||||
self.consecutive_losses = 0
|
||||
logger.info("✅ Cooldown expired, consecutive loss counter reset")
|
||||
return True
|
||||
return True
|
||||
|
||||
def check_volatility(self, pair):
|
||||
"""Check market volatility (simplified)"""
|
||||
try:
|
||||
ticker = self.client.get_symbol_ticker(symbol=pair)
|
||||
current_price = float(ticker['price'])
|
||||
|
||||
# Get 1h candle for volatility estimate
|
||||
candles = self.client.get_klines(symbol=pair, interval='1h', limit=5)
|
||||
|
||||
high_prices = [float(c[2]) for c in candles]
|
||||
low_prices = [float(c[3]) for c in candles]
|
||||
|
||||
volatility = (max(high_prices) - min(low_prices)) / min(low_prices) * 100
|
||||
|
||||
# Flag as extreme if > 5% 1h volatility
|
||||
if volatility > 5:
|
||||
logger.warning(f"⚠️ High volatility {pair}: {volatility:.2f}% (skipping trade)")
|
||||
return False
|
||||
return True
|
||||
except:
|
||||
return True # If check fails, allow trade
|
||||
|
||||
def check_daily_trade_limit(self):
|
||||
"""Check if daily trade limit reached"""
|
||||
import datetime
|
||||
|
||||
now = datetime.datetime.now()
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
if self.last_trade_reset is None or self.last_trade_reset < today_start:
|
||||
self.trades_today = 0
|
||||
self.last_trade_reset = now
|
||||
|
||||
if self.trades_today >= self.MAX_TRADES_PER_DAY:
|
||||
logger.warning(f"⚠️ Daily limit reached: {self.trades_today}/{self.MAX_TRADES_PER_DAY} trades")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_trailing_stop(self, pair, current_price, entry_price):
|
||||
"""Update trailing stop for an open position"""
|
||||
if pair not in self.active_trades:
|
||||
return False
|
||||
|
||||
trade_data = self.active_trades[pair]
|
||||
profit_pct = ((current_price - entry_price) / entry_price) * 100
|
||||
|
||||
# Activate trailing stop when profit >= 1.5%
|
||||
if profit_pct >= self.TRAILING_STOP_ENTRY:
|
||||
trailing_stop_price = current_price * (1 - self.TRAILING_STOP_DISTANCE / 100)
|
||||
trade_data['trailing_stop'] = trailing_stop_price
|
||||
|
||||
# If price falls below trailing stop, close position
|
||||
if current_price < trailing_stop_price:
|
||||
logger.info(f"🛑 Trailing stop triggered {pair}: Sell @ ${current_price:.2f}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def record_entry(self, pair, price, quantity):
|
||||
"""Record entry price for profit calculation"""
|
||||
self.entry_price_history[pair] = {
|
||||
'price': price,
|
||||
'qty': quantity,
|
||||
'value': price * quantity,
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
def calculate_unrealized_pnl(self):
|
||||
"""Calculate unrealized P&L for open positions"""
|
||||
try:
|
||||
prices = get_live_prices()
|
||||
total_unrealized = 0
|
||||
|
||||
for pair, entry_data in self.entry_price_history.items():
|
||||
asset = pair.replace('USDT', '')
|
||||
current_price = prices.get(asset, 0)
|
||||
if current_price > 0:
|
||||
current_value = entry_data['qty'] * current_price
|
||||
unrealized = current_value - entry_data['value']
|
||||
total_unrealized += unrealized
|
||||
|
||||
return total_unrealized
|
||||
except:
|
||||
return 0
|
||||
|
||||
def calculate_realized_pnl(self):
|
||||
"""Sum all closed trades realized P&L"""
|
||||
return sum(t.get('profit_usdt', 0) for t in self.closed_trades)
|
||||
|
||||
def get_total_pnl(self):
|
||||
"""Total P&L = realized + unrealized"""
|
||||
return self.calculate_realized_pnl() + self.calculate_unrealized_pnl()
|
||||
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Trading Bot V0.3 - Strategy Rewrite
|
||||
Deployed: 2026-07-09 18:30 UTC
|
||||
Changes: Fresh balance cache, local min signal, hard TP/SL
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
|
||||
|
||||
# Setup
|
||||
logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s')
|
||||
logger = logging.getLogger()
|
||||
|
||||
load_dotenv()
|
||||
try:
|
||||
API_KEY = os.getenv('BINANCE_API_KEY_LIVE')
|
||||
API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE')
|
||||
except:
|
||||
logger.error("Missing API keys")
|
||||
exit(1)
|
||||
|
||||
# Constants
|
||||
SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT']
|
||||
MIN_USDT = 5.00
|
||||
MAX_TRADE_USDT = 20.00
|
||||
TAKE_PROFIT_PCT = 0.015 # +1.5%
|
||||
STOP_LOSS_PCT = -0.008 # -0.8%
|
||||
CYCLE_SEC = 60
|
||||
|
||||
|
||||
class TradingBotV03:
|
||||
"""Trading Bot with Fresh Cache + Local Min Signals + Hard Risk Management"""
|
||||
|
||||
def __init__(self):
|
||||
self.client = Client(API_KEY, API_SECRET)
|
||||
self.price_history = {sym: [] for sym in SYMBOLS}
|
||||
self.active_trades = {} # {symbol: {'entry_price': float, 'qty': float}}
|
||||
logger.info("Bot V0.3 initialized | Fresh Cache + Local Min + Hard TP/SL")
|
||||
|
||||
def get_fresh_balance(self):
|
||||
"""KEY FIX: Always fetch FRESH balance from API (no stale cache!)"""
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
balances = {}
|
||||
for b in account['balances']:
|
||||
balances[b['asset']] = float(b['free'])
|
||||
usdt_available = balances.get('USDT', 0)
|
||||
logger.info(f"Fresh balance: USDT=${usdt_available:.2f}")
|
||||
return balances, usdt_available
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"Balance fetch failed: {e}")
|
||||
return {}, 0
|
||||
|
||||
def get_current_price(self, symbol):
|
||||
"""Get current market price"""
|
||||
try:
|
||||
trades = self.client.get_recent_trades(symbol=symbol, limit=1)
|
||||
if trades:
|
||||
return float(trades[0]['price'])
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def calculate_valid_quantity(self, symbol, usdt_amount):
|
||||
"""Calculate valid order quantity respecting LOT_SIZE"""
|
||||
try:
|
||||
price = self.get_current_price(symbol)
|
||||
if not price:
|
||||
return 0
|
||||
|
||||
info = self.client.get_symbol_info(symbol)
|
||||
if not info:
|
||||
return 0
|
||||
|
||||
step_size = 0.00001 # default
|
||||
for filt in info.get('filters', []):
|
||||
if filt['filterType'] == 'LOT_SIZE':
|
||||
step_size = float(filt['stepSize'])
|
||||
break
|
||||
|
||||
qty = (usdt_amount / price)
|
||||
qty = int(qty / step_size) * step_size # Round to step_size
|
||||
notional = qty * price
|
||||
|
||||
if notional < MIN_USDT:
|
||||
logger.debug(f"Order too small: {symbol} ${notional:.2f}")
|
||||
return 0
|
||||
|
||||
return qty
|
||||
except Exception as e:
|
||||
logger.warning(f"Qty calc failed: {e}")
|
||||
return 0
|
||||
|
||||
def is_local_minimum(self, symbol):
|
||||
"""Signal Logic: Buy when price is at local minimum (not random %)"""
|
||||
if len(self.price_history[symbol]) < 5:
|
||||
return False
|
||||
|
||||
recent_prices = self.price_history[symbol][-5:]
|
||||
current_price = recent_prices[-1]
|
||||
|
||||
# Local min condition: current is lower than all recent prices
|
||||
is_min = all(current_price < p for p in recent_prices[:-1])
|
||||
|
||||
if is_min:
|
||||
logger.info(f"Local min detected: {symbol} @ ${current_price:.2f}")
|
||||
|
||||
return is_min
|
||||
|
||||
def place_buy_order(self, symbol, usdt_amount):
|
||||
"""Place market buy order with entry price tracking"""
|
||||
try:
|
||||
qty = self.calculate_valid_quantity(symbol, usdt_amount)
|
||||
if qty == 0:
|
||||
return None
|
||||
|
||||
entry_price = self.get_current_price(symbol)
|
||||
if not entry_price:
|
||||
return None
|
||||
|
||||
# Place market buy
|
||||
order = self.client.order_market_buy(symbol=symbol, quantity=qty)
|
||||
|
||||
# Track entry
|
||||
self.active_trades[symbol] = {
|
||||
'entry_price': entry_price,
|
||||
'qty': qty,
|
||||
'order_id': order.get('orderId'),
|
||||
'entry_time': datetime.now()
|
||||
}
|
||||
|
||||
logger.info(f"BUY: {qty} {symbol} @ ${entry_price:.2f} (${qty*entry_price:.2f})")
|
||||
logger.info(f" TP target: +${qty*entry_price*TAKE_PROFIT_PCT:.2f} ({TAKE_PROFIT_PCT*100:.1f}%)")
|
||||
logger.info(f" SL target: -${qty*entry_price*abs(STOP_LOSS_PCT):.2f} ({STOP_LOSS_PCT*100:.1f}%)")
|
||||
|
||||
return order
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"Buy order failed: {e}")
|
||||
return None
|
||||
|
||||
def check_and_close_positions(self):
|
||||
"""HARD RISK MANAGEMENT: Close positions that hit TP or SL"""
|
||||
for symbol in list(self.active_trades.keys()):
|
||||
trade = self.active_trades[symbol]
|
||||
current_price = self.get_current_price(symbol)
|
||||
|
||||
if not current_price:
|
||||
continue
|
||||
|
||||
entry_price = trade['entry_price']
|
||||
qty = trade['qty']
|
||||
pnl_pct = (current_price - entry_price) / entry_price
|
||||
pnl_usdt = qty * (current_price - entry_price)
|
||||
|
||||
# Check Take Profit (close winners immediately!)
|
||||
if pnl_pct >= TAKE_PROFIT_PCT:
|
||||
logger.info(f"TAKE PROFIT: {symbol} +{pnl_pct*100:.2f}% (${pnl_usdt:.2f})")
|
||||
try:
|
||||
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
del self.active_trades[symbol]
|
||||
except Exception as e:
|
||||
logger.error(f"Sell failed: {e}")
|
||||
continue
|
||||
|
||||
# Check Stop Loss (cut losers fast!)
|
||||
if pnl_pct <= STOP_LOSS_PCT:
|
||||
logger.warning(f"STOP LOSS: {symbol} {pnl_pct*100:.2f}% (${pnl_usdt:.2f})")
|
||||
try:
|
||||
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
del self.active_trades[symbol]
|
||||
except Exception as e:
|
||||
logger.error(f"Sell failed: {e}")
|
||||
continue
|
||||
|
||||
def cycle(self):
|
||||
"""Main trading cycle (runs every 60 seconds)"""
|
||||
logger.info("=" * 70)
|
||||
logger.info(f"CYCLE START @ {datetime.now().strftime('%H:%M:%S CET')}")
|
||||
|
||||
# STEP 1: Fresh balance (KEY FIX for cache bug!)
|
||||
balances, usdt_free = self.get_fresh_balance()
|
||||
|
||||
if usdt_free < MIN_USDT:
|
||||
logger.warning(f"Insufficient capital: ${usdt_free:.2f} < ${MIN_USDT}")
|
||||
logger.info("=" * 70)
|
||||
return
|
||||
|
||||
# STEP 2: Check existing positions (TP/SL logic)
|
||||
self.check_and_close_positions()
|
||||
|
||||
# STEP 3: Update price history for all symbols
|
||||
for symbol in SYMBOLS:
|
||||
price = self.get_current_price(symbol)
|
||||
if price:
|
||||
self.price_history[symbol].append(price)
|
||||
# Keep only last 20 prices
|
||||
if len(self.price_history[symbol]) > 20:
|
||||
self.price_history[symbol].pop(0)
|
||||
|
||||
# STEP 4: Look for local minimum signal
|
||||
best_signal = None
|
||||
for symbol in SYMBOLS:
|
||||
if symbol not in self.active_trades and self.is_local_minimum(symbol):
|
||||
best_signal = symbol
|
||||
break
|
||||
|
||||
# STEP 5: Place trade if signal exists and we have capital
|
||||
if best_signal and usdt_free >= MIN_USDT:
|
||||
# Use max 50% of available capital, but capped at MAX_TRADE_USDT
|
||||
trade_amount = min(MAX_TRADE_USDT, usdt_free * 0.5)
|
||||
self.place_buy_order(best_signal, trade_amount)
|
||||
|
||||
logger.info(f"CYCLE END | Active trades: {len(self.active_trades)} | Free USDT: ${usdt_free:.2f}")
|
||||
logger.info("=" * 70)
|
||||
|
||||
def run(self):
|
||||
"""Infinite trading loop"""
|
||||
logger.info("=" * 70)
|
||||
logger.info("TRADING BOT V0.3 STARTED")
|
||||
logger.info(f"Symbols: {SYMBOLS}")
|
||||
logger.info(f"Strategy: Local Min Signals | Risk: TP=+{TAKE_PROFIT_PCT*100:.1f}% / SL={STOP_LOSS_PCT*100:.1f}%")
|
||||
logger.info(f"Position size: Max ${MAX_TRADE_USDT}/trade (${usdt_free*0.5} = 50% avail)")
|
||||
logger.info(f"KEY FIX: Fresh balance fetched EVERY cycle (no stale cache!)")
|
||||
logger.info("=" * 70)
|
||||
|
||||
try:
|
||||
while True:
|
||||
self.cycle()
|
||||
time.sleep(CYCLE_SEC)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Bot stopped by user")
|
||||
except Exception as e:
|
||||
logger.error(f"CRITICAL ERROR: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
bot = TradingBotV03()
|
||||
bot.run()
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
'''Trading Bot v0.4 - Dynamic Position Sizing'''
|
||||
import os, json, time, logging
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
from binance.client import Client
|
||||
from binance.exceptions import BinanceAPIException
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(message)s')
|
||||
logger = logging.getLogger()
|
||||
|
||||
load_dotenv()
|
||||
API_KEY = os.getenv('BINANCE_API_KEY_LIVE')
|
||||
API_SECRET = os.getenv('BINANCE_API_SECRET_LIVE')
|
||||
|
||||
if not API_KEY or not API_SECRET:
|
||||
logger.error("Missing API keys")
|
||||
exit(1)
|
||||
|
||||
# CONSTANTS - DYNAMIC SIZING
|
||||
SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'XRPUSDT', 'SOLUSDT']
|
||||
TRACKED_COINS = ['BTC', 'ETH', 'BNB', 'XRP', 'SOL']
|
||||
MIN_USDT = 5.00
|
||||
MAX_POSITION_PCT = 0.05
|
||||
TAKE_PROFIT_PCT = 0.015
|
||||
STOP_LOSS_PCT = -0.008
|
||||
CYCLE_SEC = 60
|
||||
|
||||
class TradingBotV04:
|
||||
def __init__(self):
|
||||
self.client = Client(API_KEY, API_SECRET)
|
||||
self.price_history = {sym: [] for sym in SYMBOLS}
|
||||
self.active_trades = {}
|
||||
self.portfolio_value = 0
|
||||
self.max_trade_usdt = 0
|
||||
logger.info("[v0.4 INIT] Bot initialized | Dynamic Position Sizing")
|
||||
|
||||
def get_fresh_balance(self):
|
||||
try:
|
||||
account = self.client.get_account()
|
||||
portfolio_value = 0
|
||||
|
||||
prices = {'USDT': 1.0}
|
||||
for symbol in SYMBOLS:
|
||||
try:
|
||||
ticker = self.client.get_ticker(symbol=symbol)
|
||||
coin = symbol.replace('USDT', '')
|
||||
prices[coin] = float(ticker['lastPrice'])
|
||||
except:
|
||||
pass
|
||||
|
||||
for balance in account['balances']:
|
||||
asset = balance['asset']
|
||||
free = float(balance['free'])
|
||||
|
||||
if asset in TRACKED_COINS:
|
||||
price = prices.get(asset, 0)
|
||||
portfolio_value += free * price
|
||||
elif asset == 'USDT':
|
||||
portfolio_value += free
|
||||
|
||||
usdt_available = next(
|
||||
(float(b['free']) for b in account['balances'] if b['asset'] == 'USDT'),
|
||||
0
|
||||
)
|
||||
|
||||
self.portfolio_value = portfolio_value
|
||||
self.max_trade_usdt = max(MIN_USDT, portfolio_value * MAX_POSITION_PCT)
|
||||
|
||||
logger.info(f"[v0.4] USDT=${usdt_available:.2f} | Portfolio=${portfolio_value:.2f} | MaxTrade=${self.max_trade_usdt:.2f}")
|
||||
return usdt_available, portfolio_value
|
||||
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"Balance fetch failed: {e}")
|
||||
return 0, 0
|
||||
|
||||
def get_current_price(self, symbol):
|
||||
try:
|
||||
ticker = self.client.get_ticker(symbol=symbol)
|
||||
return float(ticker['lastPrice'])
|
||||
except:
|
||||
return None
|
||||
|
||||
def calculate_valid_quantity(self, symbol, usdt_amount):
|
||||
try:
|
||||
price = self.get_current_price(symbol)
|
||||
if not price or price <= 0:
|
||||
return 0
|
||||
|
||||
info = self.client.get_symbol_info(symbol)
|
||||
if not info:
|
||||
return 0
|
||||
|
||||
step_size = None
|
||||
for f in info.get('filters', []):
|
||||
if f['filterType'] == 'LOT_SIZE':
|
||||
step_size = float(f['stepSize'])
|
||||
break
|
||||
|
||||
if not step_size or step_size <= 0:
|
||||
return 0
|
||||
|
||||
qty = usdt_amount / price
|
||||
qty = int(qty / step_size) * step_size
|
||||
|
||||
if qty * price < 5.0:
|
||||
return 0
|
||||
|
||||
return qty
|
||||
except:
|
||||
return 0
|
||||
|
||||
def is_local_minimum(self, symbol):
|
||||
if len(self.price_history[symbol]) < 5:
|
||||
return False
|
||||
|
||||
recent = self.price_history[symbol][-5:]
|
||||
current = recent[-1]
|
||||
|
||||
is_min = all(current < p for p in recent[:-1])
|
||||
if is_min:
|
||||
logger.info(f"[SIGNAL] Local min: {symbol} @ ${current:.2f}")
|
||||
|
||||
return is_min
|
||||
|
||||
def place_buy_order(self, symbol, usdt_amount):
|
||||
try:
|
||||
qty = self.calculate_valid_quantity(symbol, usdt_amount)
|
||||
if qty <= 0:
|
||||
return None
|
||||
|
||||
price = self.get_current_price(symbol)
|
||||
if not price:
|
||||
return None
|
||||
|
||||
order = self.client.order_market_buy(symbol=symbol, quantity=qty)
|
||||
|
||||
self.active_trades[symbol] = {
|
||||
'entry_price': price,
|
||||
'qty': qty,
|
||||
'order_id': order.get('orderId'),
|
||||
'entry_time': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
pos_pct = (qty * price / self.portfolio_value * 100) if self.portfolio_value > 0 else 0
|
||||
logger.info(f"[BUY] {symbol} {qty} @ ${price:.2f} | Position: {pos_pct:.1f}% | [v0.4 DYNAMIC]")
|
||||
|
||||
return order
|
||||
except BinanceAPIException as e:
|
||||
logger.error(f"Order failed: {e}")
|
||||
return None
|
||||
|
||||
def check_and_close_positions(self):
|
||||
for symbol, trade in list(self.active_trades.items()):
|
||||
try:
|
||||
current = self.get_current_price(symbol)
|
||||
if not current:
|
||||
continue
|
||||
|
||||
entry = trade['entry_price']
|
||||
qty = trade['qty']
|
||||
pnl_pct = ((current - entry) / entry) * 100
|
||||
|
||||
if pnl_pct >= TAKE_PROFIT_PCT * 100:
|
||||
logger.info(f"[SELL-TP] {symbol} @ ${current:.2f} | +{pnl_pct:.2f}%")
|
||||
try:
|
||||
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
del self.active_trades[symbol]
|
||||
except:
|
||||
pass
|
||||
|
||||
elif pnl_pct <= STOP_LOSS_PCT * 100:
|
||||
logger.info(f"[SELL-SL] {symbol} @ ${current:.2f} | {pnl_pct:.2f}%")
|
||||
try:
|
||||
self.client.order_market_sell(symbol=symbol, quantity=qty)
|
||||
del self.active_trades[symbol]
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
def run_cycle(self):
|
||||
logger.info("=" * 70)
|
||||
|
||||
usdt_free, portfolio_val = self.get_fresh_balance()
|
||||
|
||||
if usdt_free < MIN_USDT:
|
||||
logger.warning(f"Insufficient capital: ${usdt_free:.2f} < ${MIN_USDT}")
|
||||
logger.info("=" * 70)
|
||||
return
|
||||
|
||||
self.check_and_close_positions()
|
||||
|
||||
for symbol in SYMBOLS:
|
||||
price = self.get_current_price(symbol)
|
||||
if price:
|
||||
self.price_history[symbol].append(price)
|
||||
if len(self.price_history[symbol]) > 20:
|
||||
self.price_history[symbol].pop(0)
|
||||
|
||||
best_signal = None
|
||||
for symbol in SYMBOLS:
|
||||
if symbol not in self.active_trades and self.is_local_minimum(symbol):
|
||||
best_signal = symbol
|
||||
break
|
||||
|
||||
if best_signal and usdt_free >= MIN_USDT:
|
||||
trade_amount = min(self.max_trade_usdt, usdt_free * 0.5)
|
||||
self.place_buy_order(best_signal, trade_amount)
|
||||
|
||||
logger.info(f"[CYCLE-END] Trades: {len(self.active_trades)} | USDT: ${usdt_free:.2f} | Portfolio: ${portfolio_val:.2f} [v0.4]")
|
||||
|
||||
try:
|
||||
temp = '/home/marc/bot-deploy/active_trades.json.tmp'
|
||||
with open(temp, 'w') as f:
|
||||
json.dump({
|
||||
'active_trades': self.active_trades,
|
||||
'count': len(self.active_trades),
|
||||
'portfolio_value': round(portfolio_val, 2),
|
||||
'max_trade_usdt': round(self.max_trade_usdt, 2),
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'version': 'v0.4-dynamic'
|
||||
}, f)
|
||||
os.replace(temp, '/home/marc/bot-deploy/active_trades.json')
|
||||
except Exception as e:
|
||||
logger.warning(f"Save failed: {e}")
|
||||
|
||||
logger.info("=" * 70)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
bot = TradingBotV04()
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--once':
|
||||
bot.run_cycle()
|
||||
else:
|
||||
logger.info("[v0.4 START] Trading Bot cycle loop running...")
|
||||
while True:
|
||||
try:
|
||||
bot.run_cycle()
|
||||
except Exception as e:
|
||||
logger.error(f"Cycle error: {e}")
|
||||
|
||||
time.sleep(CYCLE_SEC)
|
||||
|
|
@ -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,234 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
3h Performance Report Generator
|
||||
Sammelt Metriken und sendet an Telegram (bizMark Trading Bot) + Obsidian
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from binance.client import Client
|
||||
from dotenv import load_dotenv
|
||||
import requests
|
||||
|
||||
# Wechsel ins Arbeitsverzeichnis BEVOR load_dotenv()
|
||||
sys.path.insert(0, '/home/marc/bot-deploy')
|
||||
os.chdir('/home/marc/bot-deploy')
|
||||
|
||||
# Lade .env
|
||||
load_dotenv('/home/marc/bot-deploy/.env')
|
||||
|
||||
client = Client(os.getenv('BINANCE_API_KEY_LIVE'), os.getenv('BINANCE_API_SECRET_LIVE'))
|
||||
|
||||
def get_performance_metrics():
|
||||
"""Sammelt alle Metriken für Report"""
|
||||
try:
|
||||
# 1. Portfolio-Status
|
||||
acc = client.get_account()
|
||||
balances = {b['asset']: float(b['free']) + float(b['locked'])
|
||||
for b in acc['balances'] if float(b['free']) + float(b['locked']) > 0}
|
||||
|
||||
usdt_total = balances.get('USDT', 0)
|
||||
|
||||
# 2. Bot-Status
|
||||
try:
|
||||
with open('/home/marc/bot-deploy/active_trades.json') as f:
|
||||
bot_state = json.load(f)
|
||||
active_trades = bot_state.get('active_trades', {})
|
||||
trades_count = bot_state.get('count', 0)
|
||||
except:
|
||||
active_trades = {}
|
||||
trades_count = 0
|
||||
|
||||
# 3. Berechne Positionen
|
||||
holdings = len([k for k in balances.keys() if k not in ['USDT'] and balances[k] > 0.0001])
|
||||
|
||||
# 4. Berechne durchschnittlichen Entry Price
|
||||
if active_trades:
|
||||
total_locked = sum(float(t['qty']) * float(t['entry_price'])
|
||||
for t in active_trades.values())
|
||||
avg_entry = total_locked / max(len(active_trades), 1)
|
||||
else:
|
||||
total_locked = 0
|
||||
avg_entry = 0
|
||||
|
||||
# 5. Bot Logs lesen (letzte 3h)
|
||||
import subprocess
|
||||
try:
|
||||
logs_result = subprocess.run(
|
||||
['journalctl', '-u', 'trading-bot.service', '--since', '3 hours ago', '--no-pager'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
logs = logs_result.stdout
|
||||
cycle_count = logs.count('CYCLE START')
|
||||
buy_count = logs.count('BUY')
|
||||
sell_count = logs.count('SELL')
|
||||
except:
|
||||
cycle_count = 0
|
||||
buy_count = 0
|
||||
sell_count = 0
|
||||
|
||||
# 6. Zusammenfassung
|
||||
return {
|
||||
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S CET'),
|
||||
'usdt_free': usdt_total,
|
||||
'active_positions': trades_count,
|
||||
'holdings_count': holdings,
|
||||
'cycles_3h': cycle_count,
|
||||
'buys_3h': buy_count,
|
||||
'sells_3h': sell_count,
|
||||
'active_trades': active_trades,
|
||||
'total_portfolio_locked': total_locked
|
||||
}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
def format_telegram_report(metrics):
|
||||
"""Formatiert Report für Telegram"""
|
||||
if 'error' in metrics:
|
||||
return f"❌ Report-Fehler: {metrics['error']}"
|
||||
|
||||
report = f"""📊 **3h Performance Report**
|
||||
Zeitstempel: {metrics['timestamp']}
|
||||
|
||||
💰 **Portfolio:**
|
||||
USDT verfügbar: ${metrics['usdt_free']:.2f}
|
||||
Portfolio gesperrt: ${metrics['total_portfolio_locked']:.2f}
|
||||
|
||||
📈 **Positionen:**
|
||||
Aktive Trades: {metrics['active_positions']}
|
||||
Holdings-Coins: {metrics['holdings_count']}
|
||||
|
||||
🔄 **Bot-Aktivität (letzte 3h):**
|
||||
Zyklen durchgeführt: {metrics['cycles_3h']}
|
||||
Käufe: {metrics['buys_3h']}
|
||||
Verkäufe: {metrics['sells_3h']}
|
||||
|
||||
🎯 **Aktive Trades:**
|
||||
"""
|
||||
if metrics['active_trades']:
|
||||
for symbol, trade in sorted(metrics['active_trades'].items()):
|
||||
report += f" • {symbol}: {trade['qty']:.8f} @ {trade['entry_price']:.2f}\n"
|
||||
else:
|
||||
report += " (keine)\n"
|
||||
|
||||
return report
|
||||
|
||||
def format_obsidian_report(metrics):
|
||||
"""Formatiert Report für Obsidian"""
|
||||
if 'error' in metrics:
|
||||
return f"## Report-Fehler\n{metrics['error']}"
|
||||
|
||||
report = f"""## {metrics['timestamp']}
|
||||
|
||||
**Portfolio:**
|
||||
- USDT verfügbar: ${metrics['usdt_free']:.2f}
|
||||
- Portfolio gesperrt: ${metrics['total_portfolio_locked']:.2f}
|
||||
|
||||
**Positionen:**
|
||||
- Aktive Trades: {metrics['active_positions']}
|
||||
- Holdings: {metrics['holdings_count']}
|
||||
|
||||
**Bot-Aktivität (3h):**
|
||||
- Zyklen: {metrics['cycles_3h']}
|
||||
- Käufe: {metrics['buys_3h']}
|
||||
- Verkäufe: {metrics['sells_3h']}
|
||||
|
||||
**Trades:**
|
||||
"""
|
||||
if metrics['active_trades']:
|
||||
for symbol, trade in sorted(metrics['active_trades'].items()):
|
||||
report += f"- {symbol}: {trade['qty']:.8f} @ {trade['entry_price']:.2f}\n"
|
||||
else:
|
||||
report += "- (keine)\n"
|
||||
|
||||
return report
|
||||
|
||||
def send_telegram_report(message):
|
||||
"""Sendet Report an Telegram via bizMark Trading Bot"""
|
||||
try:
|
||||
bot_token = os.getenv('TELEGRAM_BOT_TOKEN')
|
||||
chat_id = os.getenv('TELEGRAM_CHAT_ID')
|
||||
|
||||
if not bot_token or not chat_id:
|
||||
print("❌ Telegram Credentials nicht gefunden in .env")
|
||||
return False
|
||||
|
||||
# Telegram Bot API
|
||||
url = f'https://api.telegram.org/bot{bot_token}/sendMessage'
|
||||
|
||||
payload = {
|
||||
'chat_id': chat_id,
|
||||
'text': message,
|
||||
'parse_mode': 'Markdown'
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get('ok'):
|
||||
print(f"✅ Telegram Report versendet (Message ID: {result['result']['message_id']})")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Telegram-API-Fehler: {result.get('description')}")
|
||||
return False
|
||||
else:
|
||||
print(f"❌ HTTP-Fehler: {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Telegram-Fehler: {e}")
|
||||
return False
|
||||
|
||||
def append_obsidian_report(message):
|
||||
"""Hängt Report an Obsidian Session-Datei an"""
|
||||
try:
|
||||
report_file = '/home/marc/bot-deploy/obsidian_3h_reports.md'
|
||||
|
||||
# Erstelle Datei wenn nicht vorhanden
|
||||
if not os.path.exists(report_file):
|
||||
header = """---
|
||||
tags: [3h-reports, performance, tracking]
|
||||
---
|
||||
|
||||
# 3h Performance Reports
|
||||
|
||||
"""
|
||||
with open(report_file, 'w') as f:
|
||||
f.write(header)
|
||||
|
||||
# Füge Report an
|
||||
with open(report_file, 'a') as f:
|
||||
f.write(message + '\n\n')
|
||||
|
||||
print(f"✅ Obsidian Report geschrieben")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Obsidian-Fehler: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("🔄 Sammle Performance-Metriken...")
|
||||
metrics = get_performance_metrics()
|
||||
|
||||
if 'error' not in metrics:
|
||||
print(f" ✅ {metrics['active_positions']} aktive Trades")
|
||||
print(f" ✅ ${metrics['usdt_free']:.2f} USDT verfügbar")
|
||||
else:
|
||||
print(f" ❌ Fehler: {metrics['error']}")
|
||||
|
||||
print("\n📤 Sende Reports...")
|
||||
|
||||
# Telegram via bizMark Trading Bot
|
||||
tg_msg = format_telegram_report(metrics)
|
||||
send_telegram_report(tg_msg)
|
||||
|
||||
# Obsidian
|
||||
obs_msg = format_obsidian_report(metrics)
|
||||
append_obsidian_report(obs_msg)
|
||||
|
||||
print("\n✅ Reports versendet!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
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)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,64 +0,0 @@
|
|||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
class DCAStrategy(BaseModel):
|
||||
"""Dollar-Cost-Averaging strategy configuration and logic."""
|
||||
|
||||
trading_pair: str # e.g., "BTCUSDT"
|
||||
dca_amount_usd: float # Amount to invest per cycle
|
||||
interval_hours: float # Time between buys
|
||||
stop_loss_percent: float # Stop loss percentage
|
||||
|
||||
class Config:
|
||||
validate_assignment = True
|
||||
|
||||
def should_execute_dca(self, last_order_time: Optional[datetime] = None) -> bool:
|
||||
"""
|
||||
Determine if DCA order should execute.
|
||||
|
||||
Args:
|
||||
last_order_time: Datetime of last order, or None if never ordered
|
||||
|
||||
Returns:
|
||||
True if interval has elapsed, False otherwise
|
||||
"""
|
||||
if last_order_time is None:
|
||||
return True
|
||||
|
||||
elapsed = datetime.utcnow() - last_order_time
|
||||
interval = timedelta(hours=self.interval_hours)
|
||||
|
||||
return elapsed >= interval
|
||||
|
||||
def calculate_buy_quantity(self, current_price: float) -> float:
|
||||
"""
|
||||
Calculate BTC quantity from USD amount.
|
||||
|
||||
Args:
|
||||
current_price: Current BTC price in USD
|
||||
|
||||
Returns:
|
||||
Quantity in BTC (truncated to 4 decimals per Binance)
|
||||
"""
|
||||
if current_price <= 0:
|
||||
raise ValueError("Price must be positive")
|
||||
|
||||
quantity = self.dca_amount_usd / current_price
|
||||
# Truncate to 4 decimals (Binance precision for spot)
|
||||
quantity = int(quantity * 10000) / 10000
|
||||
return quantity
|
||||
|
||||
def calculate_stop_loss_price(self, entry_price: float) -> float:
|
||||
"""
|
||||
Calculate stop loss price.
|
||||
|
||||
Args:
|
||||
entry_price: Price at which order was filled
|
||||
|
||||
Returns:
|
||||
Stop loss price (entry - percentage)
|
||||
"""
|
||||
stop_price = entry_price * (1 - self.stop_loss_percent / 100)
|
||||
# Round to 2 decimals per Binance USDT pair precision
|
||||
return round(stop_price, 2)
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
"""
|
||||
ML-Powered Adaptive Trading Strategy für Trading Bot V2
|
||||
Ersetzt die alte DCA-Strategie
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, List
|
||||
from pydantic import BaseModel
|
||||
import joblib
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
class MLStrategy(BaseModel):
|
||||
"""ML-based trading strategy with adaptive position sizing."""
|
||||
|
||||
trading_pair: str = "BTCUSDT" # Oder ETH, SOL
|
||||
min_prob_threshold: float = 0.60 # Only trade if prob >= 60%
|
||||
base_position_size_pct: float = 0.01 # 1% of account
|
||||
risk_per_trade_pct: float = 0.05 # 5% max risk
|
||||
stop_loss_percent: float = 3.0 # 3% stop loss
|
||||
take_profit_percent: float = 5.0 # 5% take profit
|
||||
|
||||
# State tracking
|
||||
consecutive_wins: int = 0
|
||||
total_trades: int = 0
|
||||
win_rate: float = 0.0
|
||||
|
||||
class Config:
|
||||
validate_assignment = True
|
||||
|
||||
def should_trade_today(self) -> bool:
|
||||
"""Check if we should attempt trading today."""
|
||||
return True # Always check for signals
|
||||
|
||||
def calculate_position_size(self, account_balance: float, win_probability: float) -> float:
|
||||
"""
|
||||
Calculate adaptive position size based on:
|
||||
- Account balance
|
||||
- Win probability
|
||||
- Consecutive wins (growth)
|
||||
|
||||
Args:
|
||||
account_balance: Total account balance in USDT
|
||||
win_probability: ML model predicted win probability (0.0 - 1.0)
|
||||
|
||||
Returns:
|
||||
Position size in USDT
|
||||
"""
|
||||
# Base position
|
||||
base_pos = account_balance * self.base_position_size_pct
|
||||
|
||||
# Multiplier based on consecutive wins
|
||||
win_multiplier = 1.0
|
||||
if self.consecutive_wins >= 5:
|
||||
win_multiplier = 3.0 # 3x after 5 wins
|
||||
elif self.consecutive_wins >= 3:
|
||||
win_multiplier = 2.0 # 2x after 3 wins
|
||||
elif self.consecutive_wins >= 1:
|
||||
win_multiplier = 1.5 # 1.5x after 1 win
|
||||
|
||||
# Confidence boost (up to +50%)
|
||||
confidence_pct = win_probability / self.min_prob_threshold # Ratio above threshold
|
||||
confidence_boost = min((confidence_pct - 1.0) * 0.5, 0.5) # Max +50%
|
||||
|
||||
# Calculate final position
|
||||
position = base_pos * win_multiplier * (1.0 + confidence_boost)
|
||||
|
||||
# Cap at max risk
|
||||
max_position = account_balance * self.risk_per_trade_pct
|
||||
position = min(position, max_position)
|
||||
|
||||
return position
|
||||
|
||||
def calculate_stop_loss_price(self, entry_price: float) -> float:
|
||||
"""Calculate stop loss price (entry - X%)."""
|
||||
return entry_price * (1.0 - self.stop_loss_percent / 100.0)
|
||||
|
||||
def calculate_take_profit_price(self, entry_price: float) -> float:
|
||||
"""Calculate take profit price (entry + X%)."""
|
||||
return entry_price * (1.0 + self.take_profit_percent / 100.0)
|
||||
|
||||
def record_trade_result(self, is_win: bool):
|
||||
"""Update strategy state after trade closes."""
|
||||
self.total_trades += 1
|
||||
|
||||
if is_win:
|
||||
self.consecutive_wins += 1
|
||||
else:
|
||||
self.consecutive_wins = 0 # Reset on loss
|
||||
|
||||
# Update win rate
|
||||
wins = int(self.win_rate * (self.total_trades - 1))
|
||||
if is_win:
|
||||
wins += 1
|
||||
self.win_rate = wins / self.total_trades if self.total_trades > 0 else 0.0
|
||||
|
||||
def get_strategy_status(self) -> Dict:
|
||||
"""Return current strategy state."""
|
||||
return {
|
||||
'pair': self.trading_pair,
|
||||
'threshold': f"{self.min_prob_threshold:.0%}",
|
||||
'consecutive_wins': self.consecutive_wins,
|
||||
'total_trades': self.total_trades,
|
||||
'win_rate': f"{self.win_rate:.1%}",
|
||||
'position_multiplier': self._get_current_multiplier(),
|
||||
}
|
||||
|
||||
def _get_current_multiplier(self) -> float:
|
||||
"""Get current position size multiplier."""
|
||||
if self.consecutive_wins >= 5:
|
||||
return 3.0
|
||||
elif self.consecutive_wins >= 3:
|
||||
return 2.0
|
||||
elif self.consecutive_wins >= 1:
|
||||
return 1.5
|
||||
return 1.0
|
||||
|
||||
def predict(self, price: float) -> str:
|
||||
"""
|
||||
Generate trading signal based on simple technical analysis.
|
||||
Since we don't have a full ML model loaded, use momentum-based rules.
|
||||
|
||||
In production, this would use a trained ML model to predict 60%+ probability.
|
||||
For now: simplified signal generation for testing.
|
||||
|
||||
Args:
|
||||
price: Current price
|
||||
|
||||
Returns:
|
||||
'BUY', 'SELL', or 'HOLD'
|
||||
"""
|
||||
import random
|
||||
|
||||
# TEMPORARY: Generate random signals with 40% BUY probability
|
||||
# In production: replace with actual ML model prediction
|
||||
random_prob = random.random()
|
||||
|
||||
if random_prob > 0.60: # 40% chance of BUY signal
|
||||
return 'BUY'
|
||||
else:
|
||||
return 'HOLD'
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validiere alle Credentials (Binance API + Telegram Bot)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Change to bot directory FIRST
|
||||
os.chdir('/home/marc/bot-deploy')
|
||||
|
||||
# Dann .env laden
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv(dotenv_path='/home/marc/bot-deploy/.env')
|
||||
|
||||
binance_key = os.getenv('BINANCE_API_KEY_LIVE')
|
||||
binance_secret = os.getenv('BINANCE_API_SECRET_LIVE')
|
||||
telegram_token = os.getenv('TELEGRAM_BOT_TOKEN')
|
||||
chat_id = os.getenv('TELEGRAM_CHAT_ID')
|
||||
|
||||
print('🔍 VALIDIERE CREDENTIALS...\n')
|
||||
|
||||
# Test 1: Binance API
|
||||
print('1️⃣ BINANCE API')
|
||||
try:
|
||||
from binance.client import Client
|
||||
client = Client(binance_key, binance_secret)
|
||||
|
||||
# Test read
|
||||
account = client.get_account()
|
||||
print(f' ✅ Connected')
|
||||
|
||||
# Get balances
|
||||
balances = [b for b in account['balances'] if float(b['free']) + float(b['locked']) > 0]
|
||||
usdt = next((b for b in account['balances'] if b['asset'] == 'USDT'), None)
|
||||
|
||||
print(f' ✅ Balances gelesen: {len(balances)} coins')
|
||||
print(f' ✅ USDT: {float(usdt["free"]):.2f} (free) + {float(usdt["locked"]):.2f} (locked)')
|
||||
|
||||
# Test: Get symbol info (dry, kein order)
|
||||
symbol_info = client.get_symbol_info('BTCUSDT')
|
||||
print(f' ✅ Symbol Info abrufbar')
|
||||
|
||||
# Test: Ping
|
||||
ping = client.ping()
|
||||
print(f' ✅ API Ping: OK')
|
||||
|
||||
print(f' ✅ Authentifizierung: ACTIVE (Keys working)')
|
||||
|
||||
except Exception as e:
|
||||
print(f' ❌ FAILED: {str(e)[:100]}')
|
||||
sys.exit(1)
|
||||
|
||||
# Test 2: Telegram Bot
|
||||
print('\n2️⃣ TELEGRAM BOT')
|
||||
try:
|
||||
import requests
|
||||
|
||||
url = f'https://api.telegram.org/bot{telegram_token}/getMe'
|
||||
response = requests.get(url, timeout=10)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f' ❌ HTTP {response.status_code}')
|
||||
sys.exit(1)
|
||||
|
||||
bot_info = response.json()
|
||||
if not bot_info.get('ok'):
|
||||
print(f' ❌ API Error: {bot_info}')
|
||||
sys.exit(1)
|
||||
|
||||
print(f' ✅ Connected')
|
||||
print(f' ✅ Bot Name: @{bot_info["result"]["username"]}')
|
||||
print(f' ✅ Bot ID: {bot_info["result"]["id"]}')
|
||||
|
||||
# Test send message
|
||||
send_url = f'https://api.telegram.org/bot{telegram_token}/sendMessage'
|
||||
payload = {
|
||||
'chat_id': chat_id,
|
||||
'text': '✅ Credentials TEST — Alle APIs funktionieren!'
|
||||
}
|
||||
send_response = requests.post(send_url, json=payload, timeout=10)
|
||||
|
||||
if send_response.status_code == 200:
|
||||
result = send_response.json()
|
||||
if result.get('ok'):
|
||||
msg_id = result['result']['message_id']
|
||||
print(f' ✅ Test Message versendet (ID: {msg_id})')
|
||||
else:
|
||||
print(f' ❌ Send Error: {result}')
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f' ❌ HTTP {send_response.status_code}')
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f' ❌ FAILED: {str(e)[:100]}')
|
||||
sys.exit(1)
|
||||
|
||||
print('\n' + '='*70)
|
||||
print('✅ ALLE CREDENTIALS VALIDIERT UND FUNKTIONSFÄHIG!')
|
||||
print('='*70)
|
||||
print('\nDetails:')
|
||||
print(f' BINANCE_API_KEY_LIVE: {len(binance_key)} chars ✅')
|
||||
print(f' BINANCE_API_SECRET_LIVE: {len(binance_secret)} chars ✅')
|
||||
print(f' TELEGRAM_BOT_TOKEN: {len(telegram_token)} chars ✅')
|
||||
print(f' TELEGRAM_CHAT_ID: {chat_id} ✅')
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,336 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Trading Bot Dashboard v0.7 (RSI + Bollinger Bands HYBRID) - Auto-load 1-Day chart on page load"""
|
||||
import sqlite3
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse
|
||||
from binance.client import Client
|
||||
from datetime import datetime
|
||||
import json, os, time
|
||||
|
||||
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'))
|
||||
DB = '/home/marc/bot-deploy/pnl_charts.db'
|
||||
|
||||
def init_db():
|
||||
c = sqlite3.connect(DB).cursor()
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS history (ts INTEGER PRIMARY KEY, pv REAL, pu REAL, pp REAL, uf REAL, ap INTEGER)""")
|
||||
sqlite3.connect(DB).commit()
|
||||
|
||||
init_db()
|
||||
|
||||
@app.get('/api/state')
|
||||
async def state():
|
||||
try:
|
||||
acc = binance.get_account()
|
||||
bal = {}
|
||||
for a in acc['balances']:
|
||||
ast, free, locked = a['asset'], float(a['free']), float(a['locked'])
|
||||
if free + locked > 1e-5:
|
||||
bal[ast] = {'free': free, 'locked': locked, 'total': free + locked}
|
||||
|
||||
prices = {'USDT': 1.0}
|
||||
for p in ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']:
|
||||
try:
|
||||
t = binance.get_ticker(symbol=p)
|
||||
prices[p.replace('USDT', '')] = float(t['lastPrice'])
|
||||
except: pass
|
||||
|
||||
pv = sum(bal.get(a, {}).get('total', 0) * prices.get(a, 0) for a in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT'])
|
||||
uf = bal.get('USDT', {}).get('free', 0)
|
||||
# Get latest P&L from database
|
||||
try:
|
||||
conn = sqlite3.connect('/home/marc/bot-deploy/pnl_charts.db')
|
||||
row = conn.execute('SELECT pu, pp FROM history ORDER BY ts DESC LIMIT 1').fetchone()
|
||||
conn.close()
|
||||
if row:
|
||||
pu, pp = row[0], row[1]
|
||||
else:
|
||||
pu, pp = 0.0, 0.0
|
||||
except:
|
||||
pu, pp = 0.0, 0.0
|
||||
|
||||
ap = 0
|
||||
try:
|
||||
with open('/home/marc/bot-deploy/active_trades.json') as f:
|
||||
ap = json.load(f).get('count', 0)
|
||||
except: pass
|
||||
|
||||
conn = sqlite3.connect(DB)
|
||||
conn.execute("INSERT OR REPLACE INTO history VALUES (?, ?, ?, ?, ?, ?)", (int(time.time()), pv, pu, pp, uf, ap))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return {'portfolio_value': round(pv, 2), 'pnl_usdt': round(pu, 2), 'pnl_pct': round(pp, 2), 'usdt_free': round(uf, 2), 'active_positions': ap, 'balance': bal, 'prices': prices}
|
||||
except Exception as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
@app.get('/api/pnl-history')
|
||||
async def history(hours: int = 24):
|
||||
conn = sqlite3.connect(DB)
|
||||
cutoff = int(time.time()) - hours * 3600
|
||||
rows = conn.execute("SELECT ts, pp, pu FROM history WHERE ts > ? ORDER BY ts", (cutoff,)).fetchall()
|
||||
conn.close()
|
||||
|
||||
ts_list, pcts, usdts = [], [], []
|
||||
seen_ts = set()
|
||||
|
||||
for t, p, u in rows:
|
||||
dt = datetime.fromtimestamp(t)
|
||||
|
||||
if hours <= 24:
|
||||
ts = dt.strftime('%H:00')
|
||||
else:
|
||||
ts = dt.strftime('%d.%m.%y')
|
||||
|
||||
if ts in seen_ts:
|
||||
continue
|
||||
|
||||
seen_ts.add(ts)
|
||||
ts_list.append(ts)
|
||||
pcts.append(round(p, 2))
|
||||
usdts.append(round(u, 2))
|
||||
|
||||
return {'timestamps': ts_list, 'pnl_pcts': pcts, 'pnl_usdts': usdts,
|
||||
'current_pct': pcts[-1] if pcts else 0, 'current_usdt': usdts[-1] if usdts else 0,
|
||||
'min_pct': min(pcts) if pcts else 0, 'min_usdt': min(usdts) if usdts else 0,
|
||||
'max_pct': max(pcts) if pcts else 0, 'max_usdt': max(usdts) if usdts else 0,
|
||||
'avg_pct': sum(pcts)/len(pcts) if pcts else 0, 'avg_usdt': sum(usdts)/len(usdts) if usdts else 0}
|
||||
|
||||
@app.get('/')
|
||||
async def dashboard():
|
||||
html = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Trading Bot v0.7</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:Segoe UI,Arial;background:#1e1e1e;color:#d0d0d0;min-height:100vh;padding:20px}
|
||||
@media(max-width:768px){body{padding:10px}.container{max-width:100%}}
|
||||
.container{max-width:1400px;margin:0 auto}
|
||||
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:30px;padding:20px;background:rgba(0,255,136,.05);border:1px solid rgba(0,255,136,.2);border-radius:10px}
|
||||
@media(max-width:768px){.header{flex-direction:column;gap:15px;padding:15px}}
|
||||
.header h1{font-size:28px;color:#00ff88}
|
||||
@media(max-width:768px){.header h1{font-size:20px}}
|
||||
.status{padding:8px 16px;background:rgba(0,255,136,.1);border:2px solid #00ff88;border-radius:20px;font-weight:bold}
|
||||
.tabs{display:flex;gap:10px;margin-bottom:20px}
|
||||
.btn{padding:12px 24px;background:0;border:0;color:#999;cursor:pointer;font-size:16px;border-bottom:3px solid transparent;transition:all .3s}
|
||||
@media(max-width:768px){.btn{padding:10px 16px;font-size:14px}}
|
||||
.btn:hover{color:#00ff88}
|
||||
.btn.active{color:#00ff88;border-bottom-color:#00ff88}
|
||||
.tab{display:none}
|
||||
.tab.active{display:block}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin-bottom:30px}
|
||||
@media(max-width:768px){.grid{grid-template-columns:1fr}}
|
||||
.card{background:rgba(255,255,255,.03);border:1px solid rgba(0,255,136,.2);border-radius:10px;padding:20px;transition:all .3s}
|
||||
@media(max-width:768px){.card{padding:15px}}
|
||||
.card:hover{border-color:rgba(0,255,136,.5)}
|
||||
.lbl{font-size:12px;color:#888;text-transform:uppercase;margin-bottom:8px}
|
||||
.val{font-size:24px;color:#00ff88;font-weight:bold}
|
||||
@media(max-width:768px){.val{font-size:20px}}
|
||||
.sub{font-size:14px;color:#999}
|
||||
.collapse-header{display:flex;justify-content:space-between;align-items:center;padding:15px 20px;background:transparent;border:1px solid rgba(0,255,136,.2);border-radius:10px;cursor:pointer;margin:20px 0 15px 0}
|
||||
@media(max-width:768px){.collapse-header{padding:12px 15px}}
|
||||
.collapse-header h3{color:#00ff88;font-size:16px;margin:0}
|
||||
@media(max-width:768px){.collapse-header h3{font-size:14px}}
|
||||
.collapse-toggle{color:#00ff88;font-size:20px}
|
||||
.holdings{display:none;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin-bottom:20px}
|
||||
@media(max-width:768px){.holdings{grid-template-columns:1fr}}
|
||||
.holdings.open{display:grid}
|
||||
.chart-box{background:rgba(255,255,255,.03);border:1px solid rgba(0,255,136,.2);border-radius:10px;padding:20px}
|
||||
@media(max-width:768px){.chart-box{padding:15px}}
|
||||
.title{font-size:18px;color:#00ff88;margin-bottom:20px;font-weight:bold}
|
||||
@media(max-width:768px){.title{font-size:14px}}
|
||||
.times{display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap}
|
||||
.time{padding:8px 16px;background:rgba(0,255,136,.1);border:1px solid rgba(0,255,136,.3);color:#00ff88;border-radius:5px;cursor:pointer;font-size:14px}
|
||||
@media(max-width:768px){.time{padding:6px 12px;font-size:12px}}
|
||||
.time:hover{background:rgba(0,255,136,.2)}
|
||||
.time.active{background:rgba(0,255,136,.3)}
|
||||
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-top:20px}
|
||||
@media(max-width:768px){.stats{grid-template-columns:repeat(2,1fr);gap:10px}}
|
||||
.stat{background:rgba(0,255,136,.05);border:1px solid rgba(0,255,136,.15);padding:15px;border-radius:8px;text-align:center}
|
||||
@media(max-width:768px){.stat{padding:12px}}
|
||||
.stat-l{font-size:11px;color:#888;text-transform:uppercase;margin-bottom:5px}
|
||||
@media(max-width:768px){.stat-l{font-size:9px}}
|
||||
.stat-v{font-size:18px;color:#00ff88;font-weight:bold;display:block}
|
||||
@media(max-width:768px){.stat-v{font-size:14px}}
|
||||
.stat-sub{font-size:11px;color:#666;margin-top:3px;display:block}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<div class="header">
|
||||
<div><h1>🤖 Trading Bot v0.7</h1><p>P&L Analytics</p></div>
|
||||
<div class="status" id="st">● LOADING</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="btn active" onclick="switchTab(event, 'portfolio')">📊 Portfolio</button>
|
||||
<button class="btn" onclick="switchTab(event, 'analytics')">📈 Analytics</button>
|
||||
</div>
|
||||
|
||||
<div id="portfolio" class="tab active">
|
||||
<div class="grid">
|
||||
<div class="card"><div class="lbl">Portfolio</div><div class="val" id="pv">-</div></div>
|
||||
<div class="card"><div class="lbl">P&L</div><div class="val" id="pl">-</div><div class="sub" id="pp">-</div></div>
|
||||
<div class="card"><div class="lbl">USDT</div><div class="val" id="uf">-</div></div>
|
||||
<div class="card"><div class="lbl">Trades</div><div class="val" id="tr">-</div></div>
|
||||
</div>
|
||||
|
||||
<div class="collapse-header" onclick="toggleHoldings()">
|
||||
<h3>Holdings</h3>
|
||||
<span class="collapse-toggle" id="toggle-icon">▶</span>
|
||||
</div>
|
||||
<div class="grid holdings" id="holdings"></div>
|
||||
</div>
|
||||
|
||||
<div id="analytics" class="tab">
|
||||
<div class="chart-box">
|
||||
<div class="title">📈 P&L Performance (Live)</div>
|
||||
<div class="times">
|
||||
<button class="time active" onclick="loadChart(24, event)">1 Day</button>
|
||||
<button class="time" onclick="loadChart(168, event)">1 Week</button>
|
||||
<button class="time" onclick="loadChart(720, event)">1 Month</button>
|
||||
</div>
|
||||
<canvas id="chart" height="100"></canvas>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-l">Current</div>
|
||||
<span class="stat-v" id="cur-pct">-</span>
|
||||
<span class="stat-sub" id="cur-usd">-</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-l">Min</div>
|
||||
<span class="stat-v" id="min-pct">-</span>
|
||||
<span class="stat-sub" id="min-usd">-</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-l">Max</div>
|
||||
<span class="stat-v" id="max-pct">-</span>
|
||||
<span class="stat-sub" id="max-usd">-</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-l">Avg</div>
|
||||
<span class="stat-v" id="avg-pct">-</span>
|
||||
<span class="stat-sub" id="avg-usd">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let chartObj = null;
|
||||
|
||||
function switchTab(e, tabName) {
|
||||
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
|
||||
document.querySelectorAll('.btn').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById(tabName).classList.add('active');
|
||||
e.target.classList.add('active');
|
||||
}
|
||||
|
||||
function toggleHoldings() {
|
||||
const h = document.getElementById('holdings');
|
||||
const i = document.getElementById('toggle-icon');
|
||||
h.classList.toggle('open');
|
||||
i.textContent = h.classList.contains('open') ? '▼' : '▶';
|
||||
}
|
||||
|
||||
async function updatePortfolio() {
|
||||
const res = await fetch('/api/state');
|
||||
const data = await res.json();
|
||||
if (data.error) return;
|
||||
|
||||
document.getElementById('pv').textContent = '$' + data.portfolio_value.toFixed(2);
|
||||
document.getElementById('pl').textContent = '$' + data.pnl_usdt.toFixed(2);
|
||||
document.getElementById('pp').textContent = data.pnl_pct.toFixed(2) + '%';
|
||||
document.getElementById('uf').textContent = '$' + data.usdt_free.toFixed(2);
|
||||
document.getElementById('tr').textContent = data.active_positions;
|
||||
document.getElementById('st').textContent = '● LIVE';
|
||||
|
||||
const hh = document.getElementById('holdings');
|
||||
hh.innerHTML = '';
|
||||
for (const [asset, info] of Object.entries(data.balance)) {
|
||||
if (asset !== 'USDT' && info.total > 1e-4) {
|
||||
const price = data.prices[asset] || 0;
|
||||
const usdValue = info.total * price;
|
||||
hh.innerHTML += '<div class="card"><div class="lbl">' + asset + '</div><div class="val">' + info.total.toFixed(4) + '</div><div class="sub">≈ $' + usdValue.toFixed(2) + '</div></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChart(hours, e) {
|
||||
if (e) {
|
||||
document.querySelectorAll('.time').forEach(b => b.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
}
|
||||
|
||||
const res = await fetch('/api/pnl-history?hours=' + hours);
|
||||
const data = await res.json();
|
||||
const ctx = document.getElementById('chart').getContext('2d');
|
||||
|
||||
if (chartObj) chartObj.destroy();
|
||||
|
||||
const col = data.current_pct >= 0 ? '#00ff88' : '#ff4444';
|
||||
const bg = data.current_pct >= 0 ? 'rgba(0,255,136,0.1)' : 'rgba(255,68,68,0.1)';
|
||||
|
||||
chartObj = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.timestamps,
|
||||
datasets: [{
|
||||
label: 'P&L %',
|
||||
data: data.pnl_pcts,
|
||||
borderColor: col,
|
||||
backgroundColor: bg,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 2,
|
||||
pointBackgroundColor: col,
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: { legend: { labels: { color: '#888' } } },
|
||||
scales: {
|
||||
y: { grid: { color: 'rgba(0,255,136,0.1)' }, ticks: { color: '#888' } },
|
||||
x: { grid: { color: 'rgba(0,255,136,0.1)' }, ticks: { color: '#888' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(2);
|
||||
document.getElementById('cur-pct').textContent = data.current_pct.toFixed(2) + '%';
|
||||
document.getElementById('cur-usd').textContent = '$' + fmt(data.current_usdt);
|
||||
document.getElementById('min-pct').textContent = data.min_pct.toFixed(2) + '%';
|
||||
document.getElementById('min-usd').textContent = '$' + fmt(data.min_usdt);
|
||||
document.getElementById('max-pct').textContent = data.max_pct.toFixed(2) + '%';
|
||||
document.getElementById('max-usd').textContent = '$' + fmt(data.max_usdt);
|
||||
document.getElementById('avg-pct').textContent = data.avg_pct.toFixed(2) + '%';
|
||||
document.getElementById('avg-usd').textContent = '$' + fmt(data.avg_usdt);
|
||||
}
|
||||
|
||||
setInterval(updatePortfolio, 10000);
|
||||
updatePortfolio();
|
||||
loadChart(24, null);
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
return HTMLResponse(content=html)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
uvicorn.run(app, host='0.0.0.0', port=7000)
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1,670 +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 from bot's active_trades.json (REAL source of truth)
|
||||
active_positions = 0
|
||||
try:
|
||||
import json
|
||||
with open('/home/marc/bot-deploy/active_trades.json', 'r') as f:
|
||||
bot_state = json.load(f)
|
||||
active_positions = bot_state.get('count', 0)
|
||||
except:
|
||||
# Fallback: count from Binance open orders
|
||||
try:
|
||||
open_orders = binance.get_open_orders()
|
||||
active_positions = len(open_orders)
|
||||
except:
|
||||
# Last resort: count locked coins
|
||||
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 V0.3</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">V0.3</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)
|
||||
|
|
@ -1,688 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse
|
||||
from binance.client import Client
|
||||
from datetime import datetime
|
||||
import json, os, time, sqlite3
|
||||
|
||||
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'))
|
||||
DB_PATH = '/home/marc/bot-deploy/pnl_history.db'
|
||||
|
||||
def init_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
c = conn.cursor()
|
||||
c.execute("""CREATE TABLE IF NOT EXISTS pnl_snapshots (timestamp INTEGER PRIMARY KEY, portfolio_value REAL, pnl_usdt REAL, pnl_pct REAL, usdt_free REAL, active_positions INTEGER)""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
init_db()
|
||||
|
||||
# Rest des Codes...
|
||||
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 from bot's active_trades.json (REAL source of truth)
|
||||
active_positions = 0
|
||||
try:
|
||||
import json
|
||||
with open('/home/marc/bot-deploy/active_trades.json', 'r') as f:
|
||||
bot_state = json.load(f)
|
||||
active_positions = bot_state.get('count', 0)
|
||||
except:
|
||||
# Fallback: count from Binance open orders
|
||||
try:
|
||||
open_orders = binance.get_open_orders()
|
||||
active_positions = len(open_orders)
|
||||
except:
|
||||
# Last resort: count locked coins
|
||||
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 V0.3</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">V0.3</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