Added basic paper-trader. Fix signals_health_client.py to support the multiple personalities.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Health Check Client for Signal Generator
|
||||
Health Check Client for Multi-Personality Signal Generator
|
||||
Query the running signal generator status
|
||||
"""
|
||||
|
||||
@@ -28,19 +28,61 @@ def check_health(socket_path="/tmp/signals_health.sock"):
|
||||
# Parse and display
|
||||
health = json.loads(response.decode('utf-8'))
|
||||
|
||||
print("=" * 60)
|
||||
print("SIGNAL GENERATOR HEALTH STATUS")
|
||||
print("=" * 60)
|
||||
print("=" * 70)
|
||||
print("MULTI-PERSONALITY SIGNAL GENERATOR HEALTH STATUS")
|
||||
print("=" * 70)
|
||||
print(f"Status: {health['status']}")
|
||||
print(f"Personality: {health['personality']}")
|
||||
print(f"Mode: {health.get('mode', 'single-personality')}")
|
||||
|
||||
# Handle both old single-personality and new multi-personality format
|
||||
if 'personalities' in health:
|
||||
print(f"Personalities: {', '.join(health['personalities'])}")
|
||||
elif 'personality' in health:
|
||||
print(f"Personality: {health['personality']}")
|
||||
|
||||
print(f"Timeframes: {', '.join(health['timeframes'])}")
|
||||
print(f"Uptime: {health['uptime_seconds']}s ({health['uptime_seconds']//60}m)")
|
||||
print(f"Total Signals: {health['total_signals']}")
|
||||
print(f" - Buy: {health['buy_signals']}")
|
||||
print(f" - Sell: {health['sell_signals']}")
|
||||
print(f"Last Signal: {health['last_signal'] or 'None'}")
|
||||
print(f"Errors: {health['errors']}")
|
||||
print(f"Connected Clients: {health['connected_clients']}")
|
||||
print(f"Debug Mode: {'ON' if health.get('debug_mode') else 'OFF'}")
|
||||
|
||||
# Total stats (aggregated across all personalities)
|
||||
if 'total_stats' in health:
|
||||
total = health['total_stats']
|
||||
print(f"\nAGGREGATED STATISTICS:")
|
||||
print(f" Total Signals: {total['total_signals']}")
|
||||
print(f" - Buy: {total['buy_signals']}")
|
||||
print(f" - Sell: {total['sell_signals']}")
|
||||
print(f" Errors: {total['errors']}")
|
||||
else:
|
||||
# Fallback for old format
|
||||
print(f"\nSTATISTICS:")
|
||||
print(f" Total Signals: {health.get('total_signals', 0)}")
|
||||
print(f" - Buy: {health.get('buy_signals', 0)}")
|
||||
print(f" - Sell: {health.get('sell_signals', 0)}")
|
||||
print(f" Errors: {health.get('errors', 0)}")
|
||||
|
||||
# Per-personality breakdown (if available)
|
||||
if 'personality_stats' in health:
|
||||
print("\n" + "=" * 70)
|
||||
print("PER-PERSONALITY BREAKDOWN")
|
||||
print("=" * 70)
|
||||
|
||||
for personality, stats in health['personality_stats'].items():
|
||||
print(f"\n{personality.upper()}:")
|
||||
print(f" Total Signals: {stats['total_signals']}")
|
||||
print(f" - Buy: {stats['buy_signals']}")
|
||||
print(f" - Sell: {stats['sell_signals']}")
|
||||
print(f" Last Signal: {stats['last_signal'] or 'None'}")
|
||||
print(f" Errors: {stats['errors']}")
|
||||
|
||||
if stats.get('recent_signals'):
|
||||
print(f" Recent Signals:")
|
||||
for sig in stats['recent_signals'][:3]:
|
||||
print(f" [{sig['timeframe']}] {sig['signal']} @ ${sig['price']:.2f} "
|
||||
f"(conf: {sig['confidence']:.2f})")
|
||||
else:
|
||||
# Old format
|
||||
print(f" Last Signal: {health.get('last_signal') or 'None'}")
|
||||
|
||||
# Database Status
|
||||
print("\n" + "=" * 70)
|
||||
@@ -58,20 +100,22 @@ def check_health(socket_path="/tmp/signals_health.sock"):
|
||||
print(f" Total Rows: {candles.get('row_count', 0)}")
|
||||
for tf, info in candles.get('timeframes', {}).items():
|
||||
age = info.get('age_seconds')
|
||||
age_str = f"{age}s ago" if age else "N/A"
|
||||
print(f" [{tf}]: {info['count']} rows, latest: {age_str}")
|
||||
age_str = f"{age}s ago" if age is not None else "N/A"
|
||||
status = "⚠ STALE" if age and age > 300 else ""
|
||||
print(f" [{tf}]: {info['count']} rows, latest: {age_str} {status}")
|
||||
|
||||
# Analysis DB
|
||||
analysis = db.get('analysis_db', {})
|
||||
print(f"Analysis DB: {'✓ OK' if analysis.get('accessible') else '✗ FAILED'}")
|
||||
print(f"\nAnalysis DB: {'✓ OK' if analysis.get('accessible') else '✗ FAILED'}")
|
||||
if analysis.get('error'):
|
||||
print(f" Error: {analysis['error']}")
|
||||
else:
|
||||
print(f" Total Rows: {analysis.get('row_count', 0)}")
|
||||
for tf, info in analysis.get('timeframes', {}).items():
|
||||
age = info.get('age_seconds')
|
||||
age_str = f"{age}s ago" if age else "N/A"
|
||||
print(f" [{tf}]: {info['count']} rows, latest: {age_str}")
|
||||
age_str = f"{age}s ago" if age is not None else "N/A"
|
||||
status = "⚠ STALE" if age and age > 300 else ""
|
||||
print(f" [{tf}]: {info['count']} rows, latest: {age_str} {status}")
|
||||
|
||||
# Configuration
|
||||
print("\n" + "=" * 70)
|
||||
@@ -81,21 +125,38 @@ def check_health(socket_path="/tmp/signals_health.sock"):
|
||||
print(f"Min Confidence: {cfg.get('min_confidence', 'N/A')}")
|
||||
print(f"Cooldown: {cfg.get('cooldown_seconds', 'N/A')}s")
|
||||
print(f"Lookback: {cfg.get('lookback', 'N/A')} candles")
|
||||
print(f"Config Reloads: {cfg.get('reloads', 0)}")
|
||||
|
||||
if cfg.get('weights'):
|
||||
print(f"Weights:")
|
||||
for indicator, weight in cfg['weights'].items():
|
||||
print(f" {indicator:12s} {weight}")
|
||||
# Multi-personality format
|
||||
if isinstance(cfg['weights'], dict) and any(k in cfg['weights'] for k in ['scalping', 'swing']):
|
||||
for personality, weights in cfg['weights'].items():
|
||||
print(f"\n{personality.upper()} Weights:")
|
||||
for indicator, weight in weights.items():
|
||||
print(f" {indicator:12s} {weight}")
|
||||
else:
|
||||
# Single personality format
|
||||
print(f"Weights:")
|
||||
for indicator, weight in cfg['weights'].items():
|
||||
print(f" {indicator:12s} {weight}")
|
||||
|
||||
if health['recent_signals']:
|
||||
print("\n" + "=" * 60)
|
||||
print("RECENT SIGNALS")
|
||||
print("=" * 60)
|
||||
for sig in health['recent_signals']:
|
||||
print(f" [{sig['timeframe']}] {sig['signal']} @ ${sig['price']:.2f} "
|
||||
# Recent signals (all personalities combined)
|
||||
recent = health.get('recent_signals', [])
|
||||
if recent:
|
||||
print("\n" + "=" * 70)
|
||||
print("RECENT SIGNALS (ALL PERSONALITIES)")
|
||||
print("=" * 70)
|
||||
for sig in recent[:10]:
|
||||
personality_tag = f"[{sig.get('personality', '?').upper()}]"
|
||||
print(f" {personality_tag:12s} [{sig['timeframe']}] {sig['signal']} @ ${sig['price']:.2f} "
|
||||
f"(conf: {sig['confidence']:.2f})")
|
||||
print(f" Reasons: {', '.join(sig['reasons'])}")
|
||||
if sig.get('reasons'):
|
||||
reasons_str = ', '.join(sig['reasons'][:3])
|
||||
if len(sig['reasons']) > 3:
|
||||
reasons_str += f" (+{len(sig['reasons'])-3} more)"
|
||||
print(f" Reasons: {reasons_str}")
|
||||
|
||||
print("=" * 60)
|
||||
print("=" * 70)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -106,8 +167,15 @@ def check_health(socket_path="/tmp/signals_health.sock"):
|
||||
except ConnectionRefusedError:
|
||||
print(f"Error: Connection refused at {socket_path}")
|
||||
return 1
|
||||
except KeyError as e:
|
||||
print(f"Error: Missing expected field in health response: {e}")
|
||||
print("\nRaw response:")
|
||||
print(json.dumps(health, indent=2))
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
12
trader/Pipfile
Normal file
12
trader/Pipfile
Normal file
@@ -0,0 +1,12 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
rich = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.13"
|
||||
139
trader/README.md
Normal file
139
trader/README.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Paper Trading Bot
|
||||
|
||||
A real-time paper trading bot that executes trades based on signals from the multi-personality signal generator.
|
||||
|
||||
## Features
|
||||
|
||||
- **Real-time TUI Interface** - Beautiful terminal UI showing all trading activity
|
||||
- **Multi-personality Support** - Trade signals from both scalping and swing strategies
|
||||
- **Risk Management** - Configurable stop-loss, take-profit, and position sizing
|
||||
- **SQLite Tracking** - All trades stored in database for analysis
|
||||
- **Live Price Monitoring** - Reads current BTC price from candles.db
|
||||
- **Signal Filtering** - Filter by confidence, personality, and timeframe
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd traider
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config.json` to customize:
|
||||
|
||||
```json
|
||||
{
|
||||
"initial_balance": 10000.0, // Starting capital
|
||||
"position_size_percent": 2.0, // % of balance per trade
|
||||
"max_positions": 3, // Max concurrent positions
|
||||
"stop_loss_percent": 2.0, // Stop loss %
|
||||
"take_profit_percent": 4.0, // Take profit %
|
||||
"min_confidence": 0.5, // Minimum signal confidence
|
||||
"enabled_personalities": ["scalping", "swing"],
|
||||
"enabled_timeframes": ["1m", "5m"]
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Make sure the signal generator is running first:
|
||||
|
||||
```bash
|
||||
# In signals/ directory
|
||||
./signals.py
|
||||
```
|
||||
|
||||
Then start the paper trader:
|
||||
|
||||
```bash
|
||||
# In traider/ directory
|
||||
./trader.py
|
||||
```
|
||||
|
||||
## TUI Interface
|
||||
|
||||
The TUI displays:
|
||||
|
||||
- **Header**: Current BTC price, balance, equity, and total PnL
|
||||
- **Statistics**: Win rate, total trades, wins/losses
|
||||
- **Open Positions**: Live view of active trades with unrealized PnL
|
||||
- **Recent Closed Trades**: Last 10 completed trades
|
||||
- **Recent Signals**: Incoming signals from the generator
|
||||
|
||||
## Database Schema
|
||||
|
||||
### trades table
|
||||
- Trade details (entry/exit prices, type, timeframe)
|
||||
- PnL calculations
|
||||
- Signal confidence and personality
|
||||
- Exit reasons (Stop Loss, Take Profit)
|
||||
|
||||
### balance_history table
|
||||
- Historical balance snapshots
|
||||
- Equity tracking over time
|
||||
|
||||
## Trading Logic
|
||||
|
||||
1. **Signal Reception**: Listens to Unix socket from signal generator
|
||||
2. **Filtering**: Applies confidence, personality, and timeframe filters
|
||||
3. **Position Sizing**: Calculates position based on balance %
|
||||
4. **Entry**: Opens LONG (BUY signal) or SHORT (SELL signal) position
|
||||
5. **Exit Monitoring**: Continuously checks stop-loss and take-profit levels
|
||||
6. **Closure**: Calculates PnL and updates balance
|
||||
|
||||
## Risk Management
|
||||
|
||||
- Maximum concurrent positions limit
|
||||
- Stop-loss protection on every trade
|
||||
- Position sizing based on account balance
|
||||
- (Future: Daily loss limits, trailing stops)
|
||||
|
||||
## Keyboard Controls
|
||||
|
||||
- `Ctrl+C` - Graceful shutdown (closes socket connections, saves state)
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
traider/
|
||||
├── trader.py # Main bot with TUI
|
||||
├── config.json # Configuration
|
||||
├── trades.db # Trade history (auto-created)
|
||||
├── requirements.txt # Python dependencies
|
||||
└── logs/ # (Future: logging)
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ PAPER TRADING BOT | BTC: $95,234.50 | Balance: $10,245.32 │
|
||||
│ Equity: $10,387.12 | PnL: +$387.12 (+3.87%) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────┐ ┌──────────────────────────┐
|
||||
│ Statistics │ │ Open Positions (2) │
|
||||
├─────────────┤ ├──────────────────────────┤
|
||||
│ Total: 47 │ │ ID Type Entry PnL │
|
||||
│ Wins: 28 │ │ 48 LONG 95100 +$142 │
|
||||
│ Losses: 19 │ │ 49 LONG 95200 +$34 │
|
||||
│ Win Rate: 60%│ └──────────────────────────┘
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This is **paper trading only** - no real money involved
|
||||
- Requires running signal generator and populated candles.db
|
||||
- Price updates every 1 second from most recent candle
|
||||
- Signals processed in real-time as they arrive
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Trailing stop-loss implementation
|
||||
- Daily loss limit enforcement
|
||||
- Performance analytics dashboard
|
||||
- Export trade history to CSV
|
||||
- Backtesting mode
|
||||
- Web dashboard option
|
||||
20
trader/config.json
Normal file
20
trader/config.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"trades_db": "trades.db",
|
||||
"candles_db": "../onramp/market_data.db",
|
||||
"signal_socket": "/tmp/signals.sock",
|
||||
|
||||
"initial_balance": 100.0,
|
||||
"position_size_percent": 10.0,
|
||||
"max_positions": 3,
|
||||
|
||||
"stop_loss_percent": 2.0,
|
||||
"take_profit_percent": 4.0,
|
||||
|
||||
"min_confidence": 0.5,
|
||||
"enabled_personalities": ["scalping", "swing"],
|
||||
"enabled_timeframes": ["1m", "5m"],
|
||||
|
||||
"max_daily_loss_percent": 5.0,
|
||||
"trailing_stop": false,
|
||||
"trailing_stop_percent": 1.5
|
||||
}
|
||||
701
trader/paper_trader.py
Executable file
701
trader/paper_trader.py
Executable file
@@ -0,0 +1,701 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Paper Trading Bot
|
||||
Executes paper trades based on signals from the signal generator
|
||||
Real-time TUI interface with trade tracking
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List
|
||||
from dataclasses import dataclass, asdict
|
||||
from enum import Enum
|
||||
import threading
|
||||
from collections import deque
|
||||
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from rich.table import Table
|
||||
from rich.layout import Layout
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from rich import box
|
||||
|
||||
|
||||
class TradeStatus(Enum):
|
||||
OPEN = "OPEN"
|
||||
CLOSED = "CLOSED"
|
||||
STOPPED = "STOPPED"
|
||||
|
||||
|
||||
class TradeType(Enum):
|
||||
LONG = "LONG"
|
||||
SHORT = "SHORT"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Trade:
|
||||
id: Optional[int]
|
||||
trade_type: str
|
||||
entry_price: float
|
||||
entry_time: str
|
||||
position_size: float
|
||||
stop_loss: float
|
||||
take_profit: Optional[float]
|
||||
timeframe: str
|
||||
personality: str
|
||||
signal_confidence: float
|
||||
status: str
|
||||
exit_price: Optional[float] = None
|
||||
exit_time: Optional[str] = None
|
||||
pnl: Optional[float] = None
|
||||
pnl_percent: Optional[float] = None
|
||||
exit_reason: Optional[str] = None
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, config_path: str = "config.json"):
|
||||
self.config_path = config_path
|
||||
self.reload()
|
||||
|
||||
def reload(self):
|
||||
with open(self.config_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Database paths
|
||||
self.trades_db = data.get("trades_db", "trades.db")
|
||||
self.candles_db = data.get("candles_db", "../onramp/market_data.db")
|
||||
|
||||
# Signal socket
|
||||
self.signal_socket = data.get("signal_socket", "/tmp/signals.sock")
|
||||
|
||||
# Trading parameters
|
||||
self.initial_balance = data.get("initial_balance", 10000.0)
|
||||
self.position_size_percent = data.get("position_size_percent", 2.0) # % of balance per trade
|
||||
self.max_positions = data.get("max_positions", 3)
|
||||
self.stop_loss_percent = data.get("stop_loss_percent", 2.0)
|
||||
self.take_profit_percent = data.get("take_profit_percent", 4.0)
|
||||
|
||||
# Strategy filters
|
||||
self.min_confidence = data.get("min_confidence", 0.5)
|
||||
self.enabled_personalities = data.get("enabled_personalities", ["scalping", "swing"])
|
||||
self.enabled_timeframes = data.get("enabled_timeframes", ["1m", "5m"])
|
||||
|
||||
# Risk management
|
||||
self.max_daily_loss_percent = data.get("max_daily_loss_percent", 5.0)
|
||||
self.trailing_stop = data.get("trailing_stop", False)
|
||||
self.trailing_stop_percent = data.get("trailing_stop_percent", 1.5)
|
||||
|
||||
|
||||
class TradeDatabase:
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = db_path
|
||||
self.init_database()
|
||||
|
||||
def init_database(self):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS trades (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
trade_type TEXT NOT NULL,
|
||||
entry_price REAL NOT NULL,
|
||||
entry_time TEXT NOT NULL,
|
||||
position_size REAL NOT NULL,
|
||||
stop_loss REAL NOT NULL,
|
||||
take_profit REAL,
|
||||
timeframe TEXT NOT NULL,
|
||||
personality TEXT NOT NULL,
|
||||
signal_confidence REAL NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
exit_price REAL,
|
||||
exit_time TEXT,
|
||||
pnl REAL,
|
||||
pnl_percent REAL,
|
||||
exit_reason TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS balance_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp TEXT NOT NULL,
|
||||
balance REAL NOT NULL,
|
||||
equity REAL NOT NULL,
|
||||
open_positions INTEGER NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def save_trade(self, trade: Trade) -> int:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
if trade.id is None:
|
||||
cursor.execute("""
|
||||
INSERT INTO trades (
|
||||
trade_type, entry_price, entry_time, position_size,
|
||||
stop_loss, take_profit, timeframe, personality,
|
||||
signal_confidence, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
trade.trade_type, trade.entry_price, trade.entry_time,
|
||||
trade.position_size, trade.stop_loss, trade.take_profit,
|
||||
trade.timeframe, trade.personality, trade.signal_confidence,
|
||||
trade.status
|
||||
))
|
||||
trade_id = cursor.lastrowid
|
||||
else:
|
||||
cursor.execute("""
|
||||
UPDATE trades SET
|
||||
exit_price = ?, exit_time = ?, pnl = ?,
|
||||
pnl_percent = ?, status = ?, exit_reason = ?
|
||||
WHERE id = ?
|
||||
""", (
|
||||
trade.exit_price, trade.exit_time, trade.pnl,
|
||||
trade.pnl_percent, trade.status, trade.exit_reason,
|
||||
trade.id
|
||||
))
|
||||
trade_id = trade.id
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return trade_id
|
||||
|
||||
def get_open_trades(self) -> List[Trade]:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT * FROM trades WHERE status = 'OPEN' ORDER BY entry_time DESC")
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
trades = []
|
||||
for row in rows:
|
||||
trades.append(Trade(**dict(row)))
|
||||
return trades
|
||||
|
||||
def get_recent_trades(self, limit: int = 10) -> List[Trade]:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT * FROM trades
|
||||
WHERE status != 'OPEN'
|
||||
ORDER BY exit_time DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
trades = []
|
||||
for row in rows:
|
||||
trades.append(Trade(**dict(row)))
|
||||
return trades
|
||||
|
||||
def save_balance_snapshot(self, balance: float, equity: float, open_positions: int):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO balance_history (timestamp, balance, equity, open_positions)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (datetime.now(timezone.utc).isoformat(), balance, equity, open_positions))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def get_statistics(self) -> Dict:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Total trades
|
||||
cursor.execute("SELECT COUNT(*) FROM trades WHERE status != 'OPEN'")
|
||||
total_trades = cursor.fetchone()[0]
|
||||
|
||||
# Win/Loss
|
||||
cursor.execute("SELECT COUNT(*) FROM trades WHERE status != 'OPEN' AND pnl > 0")
|
||||
wins = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM trades WHERE status != 'OPEN' AND pnl < 0")
|
||||
losses = cursor.fetchone()[0]
|
||||
|
||||
# Total PnL
|
||||
cursor.execute("SELECT SUM(pnl) FROM trades WHERE status != 'OPEN'")
|
||||
total_pnl = cursor.fetchone()[0] or 0.0
|
||||
|
||||
# Average win/loss
|
||||
cursor.execute("SELECT AVG(pnl) FROM trades WHERE status != 'OPEN' AND pnl > 0")
|
||||
avg_win = cursor.fetchone()[0] or 0.0
|
||||
|
||||
cursor.execute("SELECT AVG(pnl) FROM trades WHERE status != 'OPEN' AND pnl < 0")
|
||||
avg_loss = cursor.fetchone()[0] or 0.0
|
||||
|
||||
conn.close()
|
||||
|
||||
win_rate = (wins / total_trades * 100) if total_trades > 0 else 0.0
|
||||
|
||||
return {
|
||||
"total_trades": total_trades,
|
||||
"wins": wins,
|
||||
"losses": losses,
|
||||
"win_rate": win_rate,
|
||||
"total_pnl": total_pnl,
|
||||
"avg_win": avg_win,
|
||||
"avg_loss": avg_loss,
|
||||
}
|
||||
|
||||
|
||||
class PriceMonitor:
|
||||
"""Monitor current price from candles database"""
|
||||
|
||||
def __init__(self, candles_db: str):
|
||||
self.candles_db = candles_db
|
||||
self.current_price = None
|
||||
self.last_update = None
|
||||
|
||||
def get_current_price(self) -> Optional[float]:
|
||||
"""Get most recent close price from 1m timeframe"""
|
||||
try:
|
||||
conn = sqlite3.connect(f"file:{self.candles_db}?mode=ro", uri=True, timeout=5)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT close, timestamp
|
||||
FROM candles
|
||||
WHERE timeframe = '1m'
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if row:
|
||||
self.current_price = float(row[0])
|
||||
self.last_update = row[1]
|
||||
return self.current_price
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
class PaperTrader:
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.db = TradeDatabase(config.trades_db)
|
||||
self.price_monitor = PriceMonitor(config.candles_db)
|
||||
|
||||
self.balance = config.initial_balance
|
||||
self.open_trades: List[Trade] = []
|
||||
self.recent_signals = deque(maxlen=20)
|
||||
|
||||
self.running = False
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# Stats
|
||||
self.stats = {
|
||||
"signals_received": 0,
|
||||
"signals_filtered": 0,
|
||||
"trades_opened": 0,
|
||||
"trades_closed": 0,
|
||||
}
|
||||
|
||||
# Load open trades from DB
|
||||
self.open_trades = self.db.get_open_trades()
|
||||
|
||||
def connect_to_signals(self) -> socket.socket:
|
||||
"""Connect to signal generator socket"""
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.connect(self.config.signal_socket)
|
||||
return sock
|
||||
|
||||
def calculate_position_size(self, price: float) -> float:
|
||||
"""Calculate position size based on balance and config"""
|
||||
position_value = self.balance * (self.config.position_size_percent / 100)
|
||||
return position_value / price
|
||||
|
||||
def should_take_signal(self, signal: Dict) -> bool:
|
||||
"""Filter signals based on config"""
|
||||
# Check confidence
|
||||
if signal.get("confidence", 0) < self.config.min_confidence:
|
||||
return False
|
||||
|
||||
# Check personality
|
||||
if signal.get("personality") not in self.config.enabled_personalities:
|
||||
return False
|
||||
|
||||
# Check timeframe
|
||||
if signal.get("timeframe") not in self.config.enabled_timeframes:
|
||||
return False
|
||||
|
||||
# Check max positions
|
||||
if len(self.open_trades) >= self.config.max_positions:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def open_trade(self, signal: Dict):
|
||||
"""Open a new paper trade"""
|
||||
with self.lock:
|
||||
trade_type = TradeType.LONG.value if signal["signal"] == "BUY" else TradeType.SHORT.value
|
||||
entry_price = signal["price"]
|
||||
position_size = self.calculate_position_size(entry_price)
|
||||
|
||||
# Calculate stop loss and take profit
|
||||
if trade_type == TradeType.LONG.value:
|
||||
stop_loss = entry_price * (1 - self.config.stop_loss_percent / 100)
|
||||
take_profit = entry_price * (1 + self.config.take_profit_percent / 100)
|
||||
else:
|
||||
stop_loss = entry_price * (1 + self.config.stop_loss_percent / 100)
|
||||
take_profit = entry_price * (1 - self.config.take_profit_percent / 100)
|
||||
|
||||
trade = Trade(
|
||||
id=None,
|
||||
trade_type=trade_type,
|
||||
entry_price=entry_price,
|
||||
entry_time=signal.get("generated_at", datetime.now(timezone.utc).isoformat()),
|
||||
position_size=position_size,
|
||||
stop_loss=stop_loss,
|
||||
take_profit=take_profit,
|
||||
timeframe=signal["timeframe"],
|
||||
personality=signal["personality"],
|
||||
signal_confidence=signal["confidence"],
|
||||
status=TradeStatus.OPEN.value,
|
||||
)
|
||||
|
||||
trade_id = self.db.save_trade(trade)
|
||||
trade.id = trade_id
|
||||
self.open_trades.append(trade)
|
||||
|
||||
self.stats["trades_opened"] += 1
|
||||
|
||||
def check_exits(self, current_price: float):
|
||||
"""Check if any open trades should be closed"""
|
||||
with self.lock:
|
||||
for trade in self.open_trades[:]:
|
||||
should_exit = False
|
||||
exit_reason = None
|
||||
|
||||
if trade.trade_type == TradeType.LONG.value:
|
||||
# Long position checks
|
||||
if current_price <= trade.stop_loss:
|
||||
should_exit = True
|
||||
exit_reason = "Stop Loss"
|
||||
elif trade.take_profit and current_price >= trade.take_profit:
|
||||
should_exit = True
|
||||
exit_reason = "Take Profit"
|
||||
else:
|
||||
# Short position checks
|
||||
if current_price >= trade.stop_loss:
|
||||
should_exit = True
|
||||
exit_reason = "Stop Loss"
|
||||
elif trade.take_profit and current_price <= trade.take_profit:
|
||||
should_exit = True
|
||||
exit_reason = "Take Profit"
|
||||
|
||||
if should_exit:
|
||||
self.close_trade(trade, current_price, exit_reason)
|
||||
|
||||
def close_trade(self, trade: Trade, exit_price: float, reason: str):
|
||||
"""Close a trade and calculate PnL"""
|
||||
trade.exit_price = exit_price
|
||||
trade.exit_time = datetime.now(timezone.utc).isoformat()
|
||||
trade.exit_reason = reason
|
||||
|
||||
# Calculate PnL
|
||||
if trade.trade_type == TradeType.LONG.value:
|
||||
pnl = (exit_price - trade.entry_price) * trade.position_size
|
||||
else:
|
||||
pnl = (trade.entry_price - exit_price) * trade.position_size
|
||||
|
||||
trade.pnl = pnl
|
||||
trade.pnl_percent = (pnl / (trade.entry_price * trade.position_size)) * 100
|
||||
trade.status = TradeStatus.CLOSED.value
|
||||
|
||||
self.balance += pnl
|
||||
self.db.save_trade(trade)
|
||||
self.open_trades.remove(trade)
|
||||
|
||||
self.stats["trades_closed"] += 1
|
||||
|
||||
def get_equity(self, current_price: float) -> float:
|
||||
"""Calculate current equity (balance + unrealized PnL)"""
|
||||
unrealized_pnl = 0.0
|
||||
|
||||
for trade in self.open_trades:
|
||||
if trade.trade_type == TradeType.LONG.value:
|
||||
pnl = (current_price - trade.entry_price) * trade.position_size
|
||||
else:
|
||||
pnl = (trade.entry_price - current_price) * trade.position_size
|
||||
unrealized_pnl += pnl
|
||||
|
||||
return self.balance + unrealized_pnl
|
||||
|
||||
def process_signal(self, signal: Dict):
|
||||
"""Process incoming signal"""
|
||||
self.stats["signals_received"] += 1
|
||||
self.recent_signals.append(signal)
|
||||
|
||||
if self.should_take_signal(signal):
|
||||
self.open_trade(signal)
|
||||
else:
|
||||
self.stats["signals_filtered"] += 1
|
||||
|
||||
def signal_listener_thread(self):
|
||||
"""Background thread to listen for signals"""
|
||||
while self.running:
|
||||
try:
|
||||
sock = self.connect_to_signals()
|
||||
sock.settimeout(1.0)
|
||||
|
||||
buffer = ""
|
||||
while self.running:
|
||||
try:
|
||||
data = sock.recv(4096).decode("utf-8")
|
||||
if not data:
|
||||
break
|
||||
|
||||
buffer += data
|
||||
while "\n" in buffer:
|
||||
line, buffer = buffer.split("\n", 1)
|
||||
if line.strip():
|
||||
signal = json.loads(line)
|
||||
self.process_signal(signal)
|
||||
|
||||
except socket.timeout:
|
||||
continue
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
sock.close()
|
||||
|
||||
except Exception as e:
|
||||
if self.running:
|
||||
time.sleep(5) # Wait before reconnecting
|
||||
|
||||
def price_monitor_thread(self):
|
||||
"""Background thread to monitor prices and check exits"""
|
||||
while self.running:
|
||||
current_price = self.price_monitor.get_current_price()
|
||||
|
||||
if current_price and self.open_trades:
|
||||
self.check_exits(current_price)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
def build_tui(self) -> Layout:
|
||||
"""Build the TUI layout"""
|
||||
layout = Layout()
|
||||
layout.split_column(
|
||||
Layout(name="header", size=3),
|
||||
Layout(name="body"),
|
||||
Layout(name="footer", size=3),
|
||||
)
|
||||
|
||||
layout["body"].split_row(
|
||||
Layout(name="left"),
|
||||
Layout(name="right"),
|
||||
)
|
||||
|
||||
layout["left"].split_column(
|
||||
Layout(name="stats", size=10),
|
||||
Layout(name="open_trades"),
|
||||
)
|
||||
|
||||
layout["right"].split_column(
|
||||
Layout(name="recent_closed", size=15),
|
||||
Layout(name="signals"),
|
||||
)
|
||||
|
||||
return layout
|
||||
|
||||
def render_header(self) -> Panel:
|
||||
"""Render header panel"""
|
||||
current_price = self.price_monitor.current_price or 0.0
|
||||
equity = self.get_equity(current_price)
|
||||
pnl = equity - self.config.initial_balance
|
||||
pnl_percent = (pnl / self.config.initial_balance) * 100
|
||||
|
||||
pnl_color = "green" if pnl >= 0 else "red"
|
||||
|
||||
text = Text()
|
||||
text.append("PAPER TRADING BOT", style="bold cyan")
|
||||
text.append(f" | BTC: ${current_price:,.2f}", style="yellow")
|
||||
text.append(f" | Balance: ${self.balance:,.2f}", style="white")
|
||||
text.append(f" | Equity: ${equity:,.2f}", style="white")
|
||||
text.append(f" | PnL: ${pnl:+,.2f} ({pnl_percent:+.2f}%)", style=pnl_color)
|
||||
|
||||
return Panel(text, style="bold")
|
||||
|
||||
def render_stats(self) -> Panel:
|
||||
"""Render statistics panel"""
|
||||
db_stats = self.db.get_statistics()
|
||||
|
||||
table = Table(show_header=False, box=box.SIMPLE)
|
||||
table.add_column("Metric", style="cyan")
|
||||
table.add_column("Value", justify="right")
|
||||
|
||||
table.add_row("Total Trades", str(db_stats["total_trades"]))
|
||||
table.add_row("Wins", f"[green]{db_stats['wins']}[/green]")
|
||||
table.add_row("Losses", f"[red]{db_stats['losses']}[/red]")
|
||||
table.add_row("Win Rate", f"{db_stats['win_rate']:.1f}%")
|
||||
table.add_row("Total PnL", f"${db_stats['total_pnl']:,.2f}")
|
||||
table.add_row("", "")
|
||||
table.add_row("Signals RX", str(self.stats["signals_received"]))
|
||||
table.add_row("Filtered", str(self.stats["signals_filtered"]))
|
||||
|
||||
return Panel(table, title="Statistics", border_style="blue")
|
||||
|
||||
def render_open_trades(self) -> Panel:
|
||||
"""Render open trades table"""
|
||||
table = Table(box=box.SIMPLE)
|
||||
table.add_column("ID", style="cyan")
|
||||
table.add_column("Type")
|
||||
table.add_column("Entry", justify="right")
|
||||
table.add_column("Current", justify="right")
|
||||
table.add_column("PnL", justify="right")
|
||||
table.add_column("TF")
|
||||
table.add_column("Pers")
|
||||
|
||||
current_price = self.price_monitor.current_price or 0.0
|
||||
|
||||
for trade in self.open_trades:
|
||||
if trade.trade_type == TradeType.LONG.value:
|
||||
pnl = (current_price - trade.entry_price) * trade.position_size
|
||||
type_color = "green"
|
||||
else:
|
||||
pnl = (trade.entry_price - current_price) * trade.position_size
|
||||
type_color = "red"
|
||||
|
||||
pnl_color = "green" if pnl >= 0 else "red"
|
||||
|
||||
table.add_row(
|
||||
str(trade.id),
|
||||
f"[{type_color}]{trade.trade_type}[/{type_color}]",
|
||||
f"${trade.entry_price:,.2f}",
|
||||
f"${current_price:,.2f}",
|
||||
f"[{pnl_color}]${pnl:+,.2f}[/{pnl_color}]",
|
||||
trade.timeframe,
|
||||
trade.personality[:4].upper(),
|
||||
)
|
||||
|
||||
return Panel(table, title=f"Open Positions ({len(self.open_trades)})", border_style="green")
|
||||
|
||||
def render_recent_closed(self) -> Panel:
|
||||
"""Render recent closed trades"""
|
||||
table = Table(box=box.SIMPLE)
|
||||
table.add_column("ID", style="cyan")
|
||||
table.add_column("Type")
|
||||
table.add_column("PnL", justify="right")
|
||||
table.add_column("Exit")
|
||||
|
||||
recent = self.db.get_recent_trades(10)
|
||||
|
||||
for trade in recent:
|
||||
type_color = "green" if trade.trade_type == TradeType.LONG.value else "red"
|
||||
pnl_color = "green" if trade.pnl and trade.pnl >= 0 else "red"
|
||||
|
||||
table.add_row(
|
||||
str(trade.id),
|
||||
f"[{type_color}]{trade.trade_type}[/{type_color}]",
|
||||
f"[{pnl_color}]${trade.pnl:+,.2f}[/{pnl_color}]" if trade.pnl else "N/A",
|
||||
trade.exit_reason or "N/A",
|
||||
)
|
||||
|
||||
return Panel(table, title="Recent Closed Trades", border_style="yellow")
|
||||
|
||||
def render_signals(self) -> Panel:
|
||||
"""Render recent signals"""
|
||||
table = Table(box=box.SIMPLE)
|
||||
table.add_column("Signal")
|
||||
table.add_column("TF")
|
||||
table.add_column("Conf", justify="right")
|
||||
table.add_column("Pers")
|
||||
|
||||
for signal in list(self.recent_signals)[-10:]:
|
||||
signal_color = "green" if signal["signal"] == "BUY" else "red"
|
||||
|
||||
table.add_row(
|
||||
f"[{signal_color}]{signal['signal']}[/{signal_color}]",
|
||||
signal["timeframe"],
|
||||
f"{signal['confidence']:.2f}",
|
||||
signal["personality"][:4].upper(),
|
||||
)
|
||||
|
||||
return Panel(table, title="Recent Signals", border_style="magenta")
|
||||
|
||||
def render_footer(self) -> Panel:
|
||||
"""Render footer panel"""
|
||||
text = Text()
|
||||
text.append("Press ", style="dim")
|
||||
text.append("Ctrl+C", style="bold red")
|
||||
text.append(" to exit", style="dim")
|
||||
return Panel(text, style="dim")
|
||||
|
||||
def update_display(self, layout: Layout):
|
||||
"""Update all TUI panels"""
|
||||
layout["header"].update(self.render_header())
|
||||
layout["stats"].update(self.render_stats())
|
||||
layout["open_trades"].update(self.render_open_trades())
|
||||
layout["recent_closed"].update(self.render_recent_closed())
|
||||
layout["signals"].update(self.render_signals())
|
||||
layout["footer"].update(self.render_footer())
|
||||
|
||||
def run(self):
|
||||
"""Main run loop with TUI"""
|
||||
self.running = True
|
||||
|
||||
# Start background threads
|
||||
signal_thread = threading.Thread(target=self.signal_listener_thread, daemon=True)
|
||||
price_thread = threading.Thread(target=self.price_monitor_thread, daemon=True)
|
||||
|
||||
signal_thread.start()
|
||||
price_thread.start()
|
||||
|
||||
# TUI
|
||||
console = Console()
|
||||
layout = self.build_tui()
|
||||
|
||||
try:
|
||||
with Live(layout, console=console, screen=True, refresh_per_second=2):
|
||||
while self.running:
|
||||
self.update_display(layout)
|
||||
time.sleep(0.5)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self.running = False
|
||||
signal_thread.join(timeout=2)
|
||||
price_thread.join(timeout=2)
|
||||
|
||||
|
||||
def main():
|
||||
config = Config()
|
||||
trader = PaperTrader(config)
|
||||
|
||||
def shutdown(sig, frame):
|
||||
trader.running = False
|
||||
|
||||
signal.signal(signal.SIGINT, shutdown)
|
||||
signal.signal(signal.SIGTERM, shutdown)
|
||||
|
||||
trader.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user