Spaces:
Sleeping
Sleeping
saadrizvi09 commited on
Commit Β·
2b05b19
1
Parent(s): 6c0f223
Sync with backend: 5-level dynamic leverage system, enhanced signal analysis
Browse files- .gitignore +6 -3
- README_HF.md +85 -0
- live_trading.py +0 -900
- main.py +153 -100
- model_manager.py +119 -41
- requirements.txt +0 -0
- simulated_trading.py +10 -7
- strategy_handlers.py +8 -8
.gitignore
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
node_modules
|
| 2 |
-
venv
|
| 3 |
-
__pycache__
|
| 4 |
*.pyc
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
.env
|
| 6 |
-
*.db
|
|
|
|
| 1 |
node_modules
|
| 2 |
+
venv/
|
| 3 |
+
__pycache__/
|
| 4 |
*.pyc
|
| 5 |
+
*.pkl
|
| 6 |
+
models/
|
| 7 |
+
data/
|
| 8 |
+
# Keep environment variables out of version control
|
| 9 |
.env
|
|
|
README_HF.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AlgoQuant Backend API
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# AlgoQuant Backend API π
|
| 12 |
+
|
| 13 |
+
Production-grade FastAPI backend for algorithmic cryptocurrency trading with AI-powered strategies.
|
| 14 |
+
|
| 15 |
+
## Features
|
| 16 |
+
|
| 17 |
+
- π€ **HMM-SVR Walk-Forward Strategy** - Zero lookahead bias backtesting
|
| 18 |
+
- π **Pairs Trading** - Statistical arbitrage (ETH/BTC)
|
| 19 |
+
- πΌ **Paper Trading** - Simulated trading with $10,000 starting capital
|
| 20 |
+
- π **Secure Auth** - JWT authentication with bcrypt
|
| 21 |
+
- β‘ **Real-Time Data** - Binance Testnet + Yahoo Finance
|
| 22 |
+
|
| 23 |
+
## API Documentation
|
| 24 |
+
|
| 25 |
+
Once deployed, access the interactive API docs at:
|
| 26 |
+
- **Swagger UI:** `https://your-space-name.hf.space/docs`
|
| 27 |
+
- **ReDoc:** `https://your-space-name.hf.space/redoc`
|
| 28 |
+
|
| 29 |
+
## Endpoints
|
| 30 |
+
|
| 31 |
+
### Authentication
|
| 32 |
+
- `POST /signup` - Create new user account
|
| 33 |
+
- `POST /login` - Get JWT access token
|
| 34 |
+
|
| 35 |
+
### Trading
|
| 36 |
+
- `POST /backtest` - Run strategy backtesting
|
| 37 |
+
- `POST /start-live-trading` - Start simulated trading session
|
| 38 |
+
- `POST /stop-live-trading/{session_id}` - Stop trading session
|
| 39 |
+
- `GET /portfolio` - Get user portfolio balance
|
| 40 |
+
- `GET /trading-sessions` - List all trading sessions
|
| 41 |
+
- `GET /trades` - Get trade history
|
| 42 |
+
|
| 43 |
+
### Data
|
| 44 |
+
- `GET /price/{ticker}` - Get current price for ticker
|
| 45 |
+
- `GET /dashboard` - Get dashboard metrics
|
| 46 |
+
|
| 47 |
+
## Environment Variables
|
| 48 |
+
|
| 49 |
+
Required for production:
|
| 50 |
+
```bash
|
| 51 |
+
DATABASE_URL=postgresql://user:password@host:5432/dbname
|
| 52 |
+
SECRET_KEY=your-secret-key-here
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
## Tech Stack
|
| 56 |
+
|
| 57 |
+
- **FastAPI** - Modern async web framework
|
| 58 |
+
- **PostgreSQL** - Production database
|
| 59 |
+
- **SQLModel** - SQL ORM with type safety
|
| 60 |
+
- **scikit-learn** - Machine learning
|
| 61 |
+
- **hmmlearn** - Hidden Markov Models
|
| 62 |
+
- **yfinance** - Free market data
|
| 63 |
+
|
| 64 |
+
## Local Development
|
| 65 |
+
|
| 66 |
+
```bash
|
| 67 |
+
# Install dependencies
|
| 68 |
+
pip install -r requirements.txt
|
| 69 |
+
|
| 70 |
+
# Train HMM model
|
| 71 |
+
python train_hmm_model.py
|
| 72 |
+
|
| 73 |
+
# Run server
|
| 74 |
+
uvicorn main:app --reload --port 8000
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## License
|
| 78 |
+
|
| 79 |
+
MIT License - See LICENSE for details
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
**Built with π§ for quantitative traders**
|
| 84 |
+
|
| 85 |
+
*Part of the AlgoQuant AI-Powered Trading Platform*
|
live_trading.py
DELETED
|
@@ -1,900 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Live Trading Module for Binance Testnet
|
| 3 |
-
Handles real trading operations using paper money on Binance Testnet Vision
|
| 4 |
-
"""
|
| 5 |
-
import os
|
| 6 |
-
import time
|
| 7 |
-
import threading
|
| 8 |
-
from datetime import datetime, timedelta
|
| 9 |
-
from typing import Optional, Dict, List
|
| 10 |
-
from binance.client import Client
|
| 11 |
-
from binance.enums import *
|
| 12 |
-
import pandas as pd
|
| 13 |
-
import numpy as np
|
| 14 |
-
import yfinance as yf
|
| 15 |
-
from hmmlearn.hmm import GaussianHMM
|
| 16 |
-
from dotenv import load_dotenv
|
| 17 |
-
from sqlmodel import Session, select
|
| 18 |
-
from database import engine
|
| 19 |
-
from models import Trade, TradingSession
|
| 20 |
-
|
| 21 |
-
load_dotenv()
|
| 22 |
-
|
| 23 |
-
# Binance Testnet Configuration
|
| 24 |
-
TESTNET_API_KEY = os.getenv("BINANCE_API_KEY", "")
|
| 25 |
-
TESTNET_API_SECRET = os.getenv("BINANCE_SECRET_KEY", "")
|
| 26 |
-
|
| 27 |
-
# Storage for active trading sessions (in-memory for real-time tracking)
|
| 28 |
-
active_sessions: Dict[str, "BaseTradingSessionRunner"] = {}
|
| 29 |
-
|
| 30 |
-
# Portfolio cache to prevent API rate limits
|
| 31 |
-
_portfolio_cache = {'data': None, 'timestamp': None}
|
| 32 |
-
_CACHE_DURATION = 30 # seconds
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def get_testnet_client():
|
| 36 |
-
"""Create Binance testnet client with timestamp sync"""
|
| 37 |
-
try:
|
| 38 |
-
# Create client without timestamp initially to sync time
|
| 39 |
-
client = Client(TESTNET_API_KEY, TESTNET_API_SECRET, testnet=True)
|
| 40 |
-
|
| 41 |
-
# Sync time with Binance server to avoid timestamp issues
|
| 42 |
-
try:
|
| 43 |
-
server_time = client.get_server_time()
|
| 44 |
-
time_offset = server_time['serverTime'] - int(time.time() * 1000)
|
| 45 |
-
# Adjust for any offset (subtract 1 second for safety)
|
| 46 |
-
client.timestamp_offset = time_offset - 1000
|
| 47 |
-
except Exception as e:
|
| 48 |
-
print(f"[Warning] Could not sync time with Binance: {e}")
|
| 49 |
-
# Set a small negative offset as fallback
|
| 50 |
-
client.timestamp_offset = -1000
|
| 51 |
-
|
| 52 |
-
return client
|
| 53 |
-
except Exception as e:
|
| 54 |
-
print(f"[Error] Failed to create Binance client: {e}")
|
| 55 |
-
# Return basic client as fallback
|
| 56 |
-
client = Client(TESTNET_API_KEY, TESTNET_API_SECRET, testnet=True)
|
| 57 |
-
client.timestamp_offset = -1000
|
| 58 |
-
return client
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def get_account_balance():
|
| 62 |
-
"""Get current testnet account balance"""
|
| 63 |
-
try:
|
| 64 |
-
client = get_testnet_client()
|
| 65 |
-
account = client.get_account()
|
| 66 |
-
balances = []
|
| 67 |
-
for balance in account['balances']:
|
| 68 |
-
free = float(balance['free'])
|
| 69 |
-
locked = float(balance['locked'])
|
| 70 |
-
if free > 0 or locked > 0:
|
| 71 |
-
balances.append({
|
| 72 |
-
'asset': balance['asset'],
|
| 73 |
-
'free': free,
|
| 74 |
-
'locked': locked,
|
| 75 |
-
'total': free + locked
|
| 76 |
-
})
|
| 77 |
-
return balances
|
| 78 |
-
except Exception as e:
|
| 79 |
-
return {"error": str(e)}
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def get_portfolio_value():
|
| 83 |
-
"""Get total portfolio value in USDT (with caching and SAFE batch price fetch)"""
|
| 84 |
-
global _portfolio_cache
|
| 85 |
-
|
| 86 |
-
# Check cache first
|
| 87 |
-
if _portfolio_cache['data'] is not None and _portfolio_cache['timestamp'] is not None:
|
| 88 |
-
elapsed = (datetime.now() - _portfolio_cache['timestamp']).total_seconds()
|
| 89 |
-
if elapsed < _CACHE_DURATION:
|
| 90 |
-
return _portfolio_cache['data']
|
| 91 |
-
|
| 92 |
-
try:
|
| 93 |
-
client = get_testnet_client()
|
| 94 |
-
account = client.get_account()
|
| 95 |
-
|
| 96 |
-
# Assets that are stablecoins or don't have USDT pairs on testnet
|
| 97 |
-
skip_assets = {'USDT', 'BUSD', 'USDC', 'DAI', 'TUSD', 'PAX', 'TRY', 'ZAR', 'UAH', 'BRL', 'EUR', 'GBP', 'AUD', 'NGN', 'RUB', 'UAH', 'BIDR', 'IDRT', 'VAI'}
|
| 98 |
-
|
| 99 |
-
# First pass: identify symbols to fetch (ONLY what user actually holds)
|
| 100 |
-
symbols_to_fetch = []
|
| 101 |
-
for balance in account['balances']:
|
| 102 |
-
free = float(balance.get('free', 0))
|
| 103 |
-
locked = float(balance.get('locked', 0))
|
| 104 |
-
total = free + locked
|
| 105 |
-
if total > 0.001:
|
| 106 |
-
asset = balance.get('asset', 'UNKNOWN')
|
| 107 |
-
if asset not in skip_assets and asset != 'USDT':
|
| 108 |
-
symbols_to_fetch.append(f"{asset}USDT")
|
| 109 |
-
|
| 110 |
-
# Batch fetch ONLY needed prices (API weight: 2 per symbol - very safe!)
|
| 111 |
-
price_map = {}
|
| 112 |
-
for symbol in symbols_to_fetch:
|
| 113 |
-
try:
|
| 114 |
-
ticker = client.get_symbol_ticker(symbol=symbol)
|
| 115 |
-
price_map[symbol] = float(ticker['price'])
|
| 116 |
-
except Exception as e:
|
| 117 |
-
# Silently skip invalid symbols
|
| 118 |
-
pass
|
| 119 |
-
|
| 120 |
-
total_usdt = 0.0
|
| 121 |
-
holdings = []
|
| 122 |
-
|
| 123 |
-
# Second pass: calculate values using pre-fetched prices
|
| 124 |
-
for balance in account['balances']:
|
| 125 |
-
try:
|
| 126 |
-
free = float(balance.get('free', 0))
|
| 127 |
-
locked = float(balance.get('locked', 0))
|
| 128 |
-
total = free + locked
|
| 129 |
-
|
| 130 |
-
if total > 0.001: # Ignore dust
|
| 131 |
-
asset = balance.get('asset', 'UNKNOWN')
|
| 132 |
-
|
| 133 |
-
if asset == 'USDT':
|
| 134 |
-
value_usdt = total
|
| 135 |
-
if value_usdt > 0.01:
|
| 136 |
-
holdings.append({
|
| 137 |
-
'asset': asset,
|
| 138 |
-
'quantity': total,
|
| 139 |
-
'value_usdt': value_usdt
|
| 140 |
-
})
|
| 141 |
-
total_usdt += value_usdt
|
| 142 |
-
elif asset not in skip_assets:
|
| 143 |
-
# Use pre-fetched price from batch call
|
| 144 |
-
trading_pair = f"{asset}USDT"
|
| 145 |
-
price = price_map.get(trading_pair, 0.0)
|
| 146 |
-
|
| 147 |
-
if price > 0:
|
| 148 |
-
value_usdt = total * price
|
| 149 |
-
if value_usdt > 0.01:
|
| 150 |
-
holdings.append({
|
| 151 |
-
'asset': asset,
|
| 152 |
-
'quantity': total,
|
| 153 |
-
'value_usdt': value_usdt
|
| 154 |
-
})
|
| 155 |
-
total_usdt += value_usdt
|
| 156 |
-
except Exception as balance_error:
|
| 157 |
-
print(f"[Portfolio] Error processing balance: {balance_error}")
|
| 158 |
-
continue
|
| 159 |
-
|
| 160 |
-
result = {
|
| 161 |
-
'total_value_usdt': round(total_usdt, 2),
|
| 162 |
-
'holdings': holdings
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
# Update cache
|
| 166 |
-
_portfolio_cache['data'] = result
|
| 167 |
-
_portfolio_cache['timestamp'] = datetime.now()
|
| 168 |
-
|
| 169 |
-
return result
|
| 170 |
-
except Exception as e:
|
| 171 |
-
print(f"[Portfolio] Error getting portfolio value: {e}")
|
| 172 |
-
import traceback
|
| 173 |
-
traceback.print_exc()
|
| 174 |
-
default = {"error": str(e)}
|
| 175 |
-
# Cache the error result too
|
| 176 |
-
_portfolio_cache['data'] = default
|
| 177 |
-
_portfolio_cache['timestamp'] = datetime.now()
|
| 178 |
-
return default
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
def get_recent_trades_from_db(user_email: str, limit: int = 20) -> List[dict]:
|
| 182 |
-
"""Get recent trades from database - only bot trades (excludes manual trades)"""
|
| 183 |
-
try:
|
| 184 |
-
with Session(engine) as session:
|
| 185 |
-
# Filter out manual trades by excluding session_ids starting with "manual_"
|
| 186 |
-
statement = select(Trade).where(
|
| 187 |
-
Trade.user_email == user_email,
|
| 188 |
-
~Trade.session_id.startswith("manual_")
|
| 189 |
-
).order_by(Trade.executed_at.desc()).limit(limit)
|
| 190 |
-
trades = session.exec(statement).all()
|
| 191 |
-
|
| 192 |
-
result = []
|
| 193 |
-
for t in trades:
|
| 194 |
-
trade_dict = {
|
| 195 |
-
'symbol': t.symbol,
|
| 196 |
-
'side': t.side,
|
| 197 |
-
'price': t.price,
|
| 198 |
-
'quantity': t.quantity,
|
| 199 |
-
'total': t.total,
|
| 200 |
-
'pnl': t.pnl,
|
| 201 |
-
'time': t.executed_at.isoformat()
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
# Calculate pnl_percent for SELL trades
|
| 205 |
-
if t.side == "SELL" and t.pnl is not None and t.total > 0:
|
| 206 |
-
# PnL percent = (pnl / cost_basis) * 100
|
| 207 |
-
cost_basis = t.total - t.pnl
|
| 208 |
-
if cost_basis > 0:
|
| 209 |
-
trade_dict['pnl_percent'] = (t.pnl / cost_basis) * 100
|
| 210 |
-
|
| 211 |
-
result.append(trade_dict)
|
| 212 |
-
|
| 213 |
-
return result
|
| 214 |
-
except Exception as e:
|
| 215 |
-
print(f"Error getting bot trades: {e}")
|
| 216 |
-
return []
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
def get_recent_trades(symbol: Optional[str] = None, limit: int = 20):
|
| 220 |
-
"""Get recent trades from the testnet account"""
|
| 221 |
-
try:
|
| 222 |
-
client = get_testnet_client()
|
| 223 |
-
if symbol:
|
| 224 |
-
trades = client.get_my_trades(symbol=symbol, limit=limit)
|
| 225 |
-
else:
|
| 226 |
-
all_trades = []
|
| 227 |
-
symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT']
|
| 228 |
-
for sym in symbols:
|
| 229 |
-
try:
|
| 230 |
-
trades = client.get_my_trades(symbol=sym, limit=5)
|
| 231 |
-
all_trades.extend(trades)
|
| 232 |
-
except:
|
| 233 |
-
pass
|
| 234 |
-
trades = sorted(all_trades, key=lambda x: x['time'], reverse=True)[:limit]
|
| 235 |
-
|
| 236 |
-
formatted_trades = []
|
| 237 |
-
for trade in trades:
|
| 238 |
-
formatted_trades.append({
|
| 239 |
-
'symbol': trade['symbol'],
|
| 240 |
-
'side': 'BUY' if trade['isBuyer'] else 'SELL',
|
| 241 |
-
'price': float(trade['price']),
|
| 242 |
-
'quantity': float(trade['qty']),
|
| 243 |
-
'total': float(trade['quoteQty']),
|
| 244 |
-
'commission': float(trade['commission']),
|
| 245 |
-
'time': datetime.fromtimestamp(trade['time'] / 1000).isoformat()
|
| 246 |
-
})
|
| 247 |
-
|
| 248 |
-
return formatted_trades
|
| 249 |
-
except Exception as e:
|
| 250 |
-
return {"error": str(e)}
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
def get_current_price(symbol: str):
|
| 254 |
-
"""Get current price for a symbol"""
|
| 255 |
-
try:
|
| 256 |
-
client = get_testnet_client()
|
| 257 |
-
ticker = client.get_symbol_ticker(symbol=symbol)
|
| 258 |
-
return float(ticker['price'])
|
| 259 |
-
except Exception as e:
|
| 260 |
-
return None
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
def get_symbol_filters(symbol: str):
|
| 264 |
-
"""Get trading filters for a symbol (LOT_SIZE, MIN_NOTIONAL, etc.)"""
|
| 265 |
-
try:
|
| 266 |
-
client = get_testnet_client()
|
| 267 |
-
info = client.get_symbol_info(symbol)
|
| 268 |
-
|
| 269 |
-
filters = {}
|
| 270 |
-
for f in info.get('filters', []):
|
| 271 |
-
if f['filterType'] == 'LOT_SIZE':
|
| 272 |
-
filters['min_qty'] = float(f['minQty'])
|
| 273 |
-
filters['max_qty'] = float(f['maxQty'])
|
| 274 |
-
filters['step_size'] = float(f['stepSize'])
|
| 275 |
-
elif f['filterType'] == 'MIN_NOTIONAL':
|
| 276 |
-
filters['min_notional'] = float(f.get('minNotional', f.get('notional', 0)))
|
| 277 |
-
|
| 278 |
-
return filters
|
| 279 |
-
except Exception as e:
|
| 280 |
-
print(f"[Filters] Could not get filters for {symbol}: {e}")
|
| 281 |
-
return None
|
| 282 |
-
|
| 283 |
-
def adjust_quantity(symbol: str, quantity: float):
|
| 284 |
-
"""Adjust quantity to meet exchange requirements"""
|
| 285 |
-
filters = get_symbol_filters(symbol)
|
| 286 |
-
|
| 287 |
-
if not filters:
|
| 288 |
-
# Fallback to default precision
|
| 289 |
-
if 'BTC' in symbol and symbol.endswith('BTC'):
|
| 290 |
-
return round(quantity, 3)
|
| 291 |
-
elif 'USDT' in symbol:
|
| 292 |
-
return round(quantity, 3)
|
| 293 |
-
return quantity
|
| 294 |
-
|
| 295 |
-
min_qty = filters.get('min_qty', 0)
|
| 296 |
-
step_size = filters.get('step_size', 0.00001)
|
| 297 |
-
|
| 298 |
-
# Adjust to step size
|
| 299 |
-
adjusted = round(quantity / step_size) * step_size
|
| 300 |
-
|
| 301 |
-
# Ensure minimum quantity
|
| 302 |
-
if adjusted < min_qty:
|
| 303 |
-
adjusted = min_qty
|
| 304 |
-
|
| 305 |
-
# Round to appropriate precision
|
| 306 |
-
precision = len(str(step_size).rstrip('0').split('.')[-1]) if '.' in str(step_size) else 0
|
| 307 |
-
adjusted = round(adjusted, precision)
|
| 308 |
-
|
| 309 |
-
print(f"[Quantity] Original: {quantity}, Adjusted: {adjusted} (min: {min_qty}, step: {step_size})")
|
| 310 |
-
return adjusted
|
| 311 |
-
|
| 312 |
-
def place_market_order(symbol: str, side: str, quantity: float):
|
| 313 |
-
"""Place a market order on testnet"""
|
| 314 |
-
try:
|
| 315 |
-
client = get_testnet_client()
|
| 316 |
-
|
| 317 |
-
# Adjust quantity to meet exchange requirements
|
| 318 |
-
quantity = adjust_quantity(symbol, quantity)
|
| 319 |
-
|
| 320 |
-
print(f"[Order] Placing {side} order: {symbol} qty={quantity}")
|
| 321 |
-
|
| 322 |
-
order = client.create_order(
|
| 323 |
-
symbol=symbol,
|
| 324 |
-
side=SIDE_BUY if side.upper() == 'BUY' else SIDE_SELL,
|
| 325 |
-
type=ORDER_TYPE_MARKET,
|
| 326 |
-
quantity=quantity
|
| 327 |
-
)
|
| 328 |
-
print(f"[Order] Success: Order ID {order.get('orderId', 'N/A')}")
|
| 329 |
-
return order
|
| 330 |
-
except Exception as e:
|
| 331 |
-
error_msg = str(e)
|
| 332 |
-
print(f"[Order] Failed: {error_msg}")
|
| 333 |
-
return {"error": error_msg}
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
def save_trade_to_db(session_id: str, user_email: str, symbol: str,
|
| 337 |
-
side: str, price: float, quantity: float,
|
| 338 |
-
pnl: Optional[float] = None, order_id: Optional[str] = None):
|
| 339 |
-
"""Save a trade to the database"""
|
| 340 |
-
try:
|
| 341 |
-
with Session(engine) as db_session:
|
| 342 |
-
trade = Trade(
|
| 343 |
-
session_id=session_id,
|
| 344 |
-
user_email=user_email,
|
| 345 |
-
symbol=symbol,
|
| 346 |
-
side=side,
|
| 347 |
-
price=price,
|
| 348 |
-
quantity=quantity,
|
| 349 |
-
total=price * quantity,
|
| 350 |
-
pnl=pnl,
|
| 351 |
-
order_id=order_id,
|
| 352 |
-
executed_at=datetime.now()
|
| 353 |
-
)
|
| 354 |
-
db_session.add(trade)
|
| 355 |
-
db_session.commit()
|
| 356 |
-
except Exception as e:
|
| 357 |
-
print(f"Error saving trade: {e}")
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
def save_session_to_db(session_id: str, user_email: str, strategy: str,
|
| 361 |
-
symbol: str, trade_amount: float, duration_minutes: int):
|
| 362 |
-
"""Save a trading session to the database"""
|
| 363 |
-
try:
|
| 364 |
-
with Session(engine) as db_session:
|
| 365 |
-
trading_session = TradingSession(
|
| 366 |
-
session_id=session_id,
|
| 367 |
-
user_email=user_email,
|
| 368 |
-
strategy=strategy,
|
| 369 |
-
symbol=symbol,
|
| 370 |
-
trade_amount=trade_amount,
|
| 371 |
-
duration_minutes=duration_minutes,
|
| 372 |
-
start_time=datetime.now(),
|
| 373 |
-
is_running=True
|
| 374 |
-
)
|
| 375 |
-
db_session.add(trading_session)
|
| 376 |
-
db_session.commit()
|
| 377 |
-
except Exception as e:
|
| 378 |
-
print(f"Error saving session: {e}")
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
def update_session_in_db(session_id: str, is_running: bool,
|
| 382 |
-
total_pnl: float, trades_count: int):
|
| 383 |
-
"""Update a trading session in the database"""
|
| 384 |
-
try:
|
| 385 |
-
with Session(engine) as db_session:
|
| 386 |
-
statement = select(TradingSession).where(
|
| 387 |
-
TradingSession.session_id == session_id
|
| 388 |
-
)
|
| 389 |
-
trading_session = db_session.exec(statement).first()
|
| 390 |
-
if trading_session:
|
| 391 |
-
trading_session.is_running = is_running
|
| 392 |
-
trading_session.total_pnl = total_pnl
|
| 393 |
-
trading_session.trades_count = trades_count
|
| 394 |
-
if not is_running:
|
| 395 |
-
trading_session.end_time = datetime.now()
|
| 396 |
-
db_session.add(trading_session)
|
| 397 |
-
db_session.commit()
|
| 398 |
-
except Exception as e:
|
| 399 |
-
print(f"Error updating session: {e}")
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
def get_user_sessions_from_db(user_email: str) -> List[dict]:
|
| 403 |
-
"""Get all sessions for a user from database"""
|
| 404 |
-
try:
|
| 405 |
-
with Session(engine) as db_session:
|
| 406 |
-
statement = select(TradingSession).where(
|
| 407 |
-
TradingSession.user_email == user_email
|
| 408 |
-
).order_by(TradingSession.start_time.desc()).limit(20)
|
| 409 |
-
sessions = db_session.exec(statement).all()
|
| 410 |
-
|
| 411 |
-
result = []
|
| 412 |
-
for s in sessions:
|
| 413 |
-
# Check if session is in active_sessions for real-time data
|
| 414 |
-
if s.session_id in active_sessions:
|
| 415 |
-
runner = active_sessions[s.session_id]
|
| 416 |
-
result.append(runner.get_status())
|
| 417 |
-
else:
|
| 418 |
-
# Calculate elapsed time from database
|
| 419 |
-
elapsed = (datetime.now() - s.start_time).total_seconds() / 60
|
| 420 |
-
remaining = max(0, s.duration_minutes - elapsed)
|
| 421 |
-
|
| 422 |
-
# Auto-mark as stopped if time is up and not in active runners
|
| 423 |
-
if s.is_running and remaining <= 0:
|
| 424 |
-
s.is_running = False
|
| 425 |
-
s.end_time = datetime.now()
|
| 426 |
-
db_session.add(s)
|
| 427 |
-
db_session.commit()
|
| 428 |
-
|
| 429 |
-
result.append({
|
| 430 |
-
'session_id': s.session_id,
|
| 431 |
-
'strategy': s.strategy,
|
| 432 |
-
'symbol': s.symbol,
|
| 433 |
-
'trade_amount': s.trade_amount,
|
| 434 |
-
'is_running': s.is_running,
|
| 435 |
-
'position': 'FLAT',
|
| 436 |
-
'trades_count': s.trades_count,
|
| 437 |
-
'pnl': s.total_pnl,
|
| 438 |
-
'elapsed_minutes': round(elapsed, 1),
|
| 439 |
-
'remaining_minutes': round(remaining, 1),
|
| 440 |
-
'trades': []
|
| 441 |
-
})
|
| 442 |
-
return result
|
| 443 |
-
except Exception as e:
|
| 444 |
-
print(f"Error getting sessions: {e}")
|
| 445 |
-
return []
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
class BaseTradingSessionRunner:
|
| 449 |
-
"""Base class for managing a live trading session."""
|
| 450 |
-
|
| 451 |
-
def __init__(self, session_id: str, user_email: str, strategy: str,
|
| 452 |
-
symbols: List[str], trade_amount: float, duration_minutes: int):
|
| 453 |
-
self.session_id = session_id
|
| 454 |
-
self.user_email = user_email
|
| 455 |
-
self.strategy = strategy
|
| 456 |
-
self.symbols = symbols
|
| 457 |
-
self.trade_amount = trade_amount
|
| 458 |
-
self.duration_minutes = duration_minutes
|
| 459 |
-
self.start_time = datetime.now()
|
| 460 |
-
self.is_running = False
|
| 461 |
-
self.trades: List[dict] = []
|
| 462 |
-
self.pnl = 0.0
|
| 463 |
-
self.client = get_testnet_client()
|
| 464 |
-
self._thread: Optional[threading.Thread] = None
|
| 465 |
-
|
| 466 |
-
def start(self):
|
| 467 |
-
self.is_running = True
|
| 468 |
-
save_session_to_db(
|
| 469 |
-
self.session_id, self.user_email, self.strategy,
|
| 470 |
-
",".join(self.symbols), self.trade_amount, self.duration_minutes
|
| 471 |
-
)
|
| 472 |
-
self._thread = threading.Thread(target=self._run_strategy, daemon=True)
|
| 473 |
-
self._thread.start()
|
| 474 |
-
|
| 475 |
-
def stop(self):
|
| 476 |
-
self.is_running = False
|
| 477 |
-
self._on_stop()
|
| 478 |
-
update_session_in_db(
|
| 479 |
-
self.session_id, False, self.pnl, len(self.trades)
|
| 480 |
-
)
|
| 481 |
-
|
| 482 |
-
def _run_strategy(self):
|
| 483 |
-
raise NotImplementedError
|
| 484 |
-
|
| 485 |
-
def _on_stop(self):
|
| 486 |
-
pass
|
| 487 |
-
|
| 488 |
-
def get_status(self) -> dict:
|
| 489 |
-
raise NotImplementedError
|
| 490 |
-
|
| 491 |
-
class MediumFrequencySessionRunner(BaseTradingSessionRunner):
|
| 492 |
-
"""
|
| 493 |
-
Medium-Frequency HMM-SVR Strategy Runner.
|
| 494 |
-
|
| 495 |
-
Uses pre-trained HMM-SVR models (loaded from disk) to make trading decisions
|
| 496 |
-
every 3 hours based on:
|
| 497 |
-
- Regime detection (Safe/Normal/Crash)
|
| 498 |
-
- Volatility prediction
|
| 499 |
-
- EMA crossover signals
|
| 500 |
-
- Dynamic position sizing (0x, 1x, or 3x)
|
| 501 |
-
"""
|
| 502 |
-
|
| 503 |
-
# Trading interval: 3 hours in seconds
|
| 504 |
-
TRADE_INTERVAL_SECONDS = 3 * 60 * 60 # 3 hours
|
| 505 |
-
|
| 506 |
-
def __init__(self, session_id: str, user_email: str, symbol: str,
|
| 507 |
-
trade_amount: float, duration_minutes: int,
|
| 508 |
-
short_window: int = 12, long_window: int = 26):
|
| 509 |
-
super().__init__(session_id, user_email, "hmm_svr", [symbol], trade_amount, duration_minutes)
|
| 510 |
-
self.symbol = symbol
|
| 511 |
-
self.short_window = short_window
|
| 512 |
-
self.long_window = long_window
|
| 513 |
-
|
| 514 |
-
# Position tracking
|
| 515 |
-
self.current_position_qty = 0.0 # Actual quantity held
|
| 516 |
-
self.target_position_size = 0.0 # Target multiplier (0, 1, or 3)
|
| 517 |
-
self.entry_price = 0.0
|
| 518 |
-
self.last_signal_info = {}
|
| 519 |
-
|
| 520 |
-
# Ensure model is loaded
|
| 521 |
-
from model_manager import load_model, is_model_trained
|
| 522 |
-
if not is_model_trained(symbol):
|
| 523 |
-
raise Exception(f"No trained model found for {symbol}. Train it first using /api/models/train/{symbol}")
|
| 524 |
-
|
| 525 |
-
model_data = load_model(symbol)
|
| 526 |
-
if model_data is None:
|
| 527 |
-
raise Exception(f"Failed to load model for {symbol}")
|
| 528 |
-
|
| 529 |
-
print(f"[MediumFreq] Initialized for {symbol} with model trained on {model_data.get('train_days', 0)} days")
|
| 530 |
-
|
| 531 |
-
def _on_stop(self):
|
| 532 |
-
"""Close any open position when session stops."""
|
| 533 |
-
if self.current_position_qty > 0:
|
| 534 |
-
print(f"[MediumFreq] Closing position on stop: {self.current_position_qty} {self.symbol}")
|
| 535 |
-
self._adjust_position(0)
|
| 536 |
-
|
| 537 |
-
def _run_strategy(self):
|
| 538 |
-
"""
|
| 539 |
-
Main strategy loop - runs every 3 hours.
|
| 540 |
-
"""
|
| 541 |
-
end_time = self.start_time.timestamp() + (self.duration_minutes * 60)
|
| 542 |
-
|
| 543 |
-
# Run immediately on start
|
| 544 |
-
self._execute_trading_cycle()
|
| 545 |
-
|
| 546 |
-
while self.is_running and time.time() < end_time:
|
| 547 |
-
try:
|
| 548 |
-
# Sleep in smaller intervals to allow for graceful shutdown
|
| 549 |
-
sleep_remaining = self.TRADE_INTERVAL_SECONDS
|
| 550 |
-
while sleep_remaining > 0 and self.is_running and time.time() < end_time:
|
| 551 |
-
sleep_chunk = min(60, sleep_remaining) # Sleep 1 minute at a time
|
| 552 |
-
time.sleep(sleep_chunk)
|
| 553 |
-
sleep_remaining -= sleep_chunk
|
| 554 |
-
|
| 555 |
-
if self.is_running and time.time() < end_time:
|
| 556 |
-
self._execute_trading_cycle()
|
| 557 |
-
|
| 558 |
-
except Exception as e:
|
| 559 |
-
print(f"[MediumFreq] Strategy error: {e}")
|
| 560 |
-
import traceback
|
| 561 |
-
traceback.print_exc()
|
| 562 |
-
time.sleep(60) # Wait before retrying
|
| 563 |
-
|
| 564 |
-
self.is_running = False
|
| 565 |
-
self._on_stop()
|
| 566 |
-
update_session_in_db(self.session_id, False, self.pnl, len(self.trades))
|
| 567 |
-
print(f"[MediumFreq] Session ended. Total PnL: ${self.pnl:.2f}")
|
| 568 |
-
|
| 569 |
-
def _execute_trading_cycle(self):
|
| 570 |
-
"""
|
| 571 |
-
Execute one trading cycle:
|
| 572 |
-
1. Fetch recent data
|
| 573 |
-
2. Generate signal using pre-trained model
|
| 574 |
-
3. Calculate target position
|
| 575 |
-
4. Adjust position if needed
|
| 576 |
-
"""
|
| 577 |
-
print(f"\n{'='*60}")
|
| 578 |
-
print(f"[MediumFreq] Trading cycle at {datetime.now().isoformat()}")
|
| 579 |
-
print(f"{'='*60}")
|
| 580 |
-
|
| 581 |
-
try:
|
| 582 |
-
# 1. Fetch recent daily data (400 days for proper feature calculation)
|
| 583 |
-
recent_data = self._fetch_recent_data(days=400)
|
| 584 |
-
if recent_data is None or len(recent_data) < 100:
|
| 585 |
-
print("[MediumFreq] Insufficient data, skipping cycle")
|
| 586 |
-
return
|
| 587 |
-
|
| 588 |
-
# 2. Generate signal using model_manager
|
| 589 |
-
from model_manager import calculate_signal_and_position
|
| 590 |
-
signal_result = calculate_signal_and_position(
|
| 591 |
-
symbol=self.symbol,
|
| 592 |
-
recent_data=recent_data,
|
| 593 |
-
short_window=self.short_window,
|
| 594 |
-
long_window=self.long_window
|
| 595 |
-
)
|
| 596 |
-
|
| 597 |
-
if signal_result is None or 'error' in signal_result:
|
| 598 |
-
print(f"[MediumFreq] Signal error: {signal_result}")
|
| 599 |
-
return
|
| 600 |
-
|
| 601 |
-
self.last_signal_info = signal_result
|
| 602 |
-
|
| 603 |
-
# 3. Calculate target position in USDT terms
|
| 604 |
-
target_multiplier = signal_result['target_position'] # 0, 1, or 3
|
| 605 |
-
ema_signal = signal_result['ema_signal']
|
| 606 |
-
current_price = signal_result['close_price']
|
| 607 |
-
|
| 608 |
-
print(f"[MediumFreq] Signal Analysis:")
|
| 609 |
-
print(f" - Regime: {signal_result['regime_label']} (state {signal_result['regime']})")
|
| 610 |
-
print(f" - Predicted Vol: {signal_result['predicted_vol']:.6f}")
|
| 611 |
-
print(f" - Risk Ratio: {signal_result['risk_ratio']:.2f}")
|
| 612 |
-
print(f" - EMA Signal: {'BULLISH' if ema_signal == 1 else 'BEARISH'}")
|
| 613 |
-
print(f" - Position Multiplier: {target_multiplier}x")
|
| 614 |
-
print(f" - Reasoning: {signal_result['reasoning']}")
|
| 615 |
-
|
| 616 |
-
# 4. Calculate target quantity in asset terms
|
| 617 |
-
target_usdt_value = self.trade_amount * target_multiplier
|
| 618 |
-
target_quantity = target_usdt_value / current_price if current_price > 0 else 0
|
| 619 |
-
|
| 620 |
-
# 5. Get current position from exchange
|
| 621 |
-
current_qty = self._get_current_position()
|
| 622 |
-
|
| 623 |
-
print(f"\n[MediumFreq] Position Status:")
|
| 624 |
-
print(f" - Current Position: {current_qty:.8f} {self.symbol.replace('USDT', '')}")
|
| 625 |
-
print(f" - Target Position: {target_quantity:.8f} ({target_multiplier}x = ${target_usdt_value:.2f})")
|
| 626 |
-
|
| 627 |
-
# 6. Calculate and execute trade delta
|
| 628 |
-
trade_delta = target_quantity - current_qty
|
| 629 |
-
|
| 630 |
-
if abs(trade_delta * current_price) > 10: # Minimum $10 trade
|
| 631 |
-
self._adjust_position(target_quantity)
|
| 632 |
-
else:
|
| 633 |
-
print(f"[MediumFreq] Trade delta too small (${abs(trade_delta * current_price):.2f}), no action")
|
| 634 |
-
|
| 635 |
-
self.target_position_size = target_multiplier
|
| 636 |
-
|
| 637 |
-
except Exception as e:
|
| 638 |
-
print(f"[MediumFreq] Error in trading cycle: {e}")
|
| 639 |
-
import traceback
|
| 640 |
-
traceback.print_exc()
|
| 641 |
-
|
| 642 |
-
def _fetch_recent_data(self, days: int = 400) -> Optional[pd.DataFrame]:
|
| 643 |
-
"""
|
| 644 |
-
Fetch recent daily data from Binance for signal generation.
|
| 645 |
-
"""
|
| 646 |
-
try:
|
| 647 |
-
# Use public client (no API keys needed for klines)
|
| 648 |
-
client = Client()
|
| 649 |
-
|
| 650 |
-
klines = client.get_historical_klines(
|
| 651 |
-
symbol=self.symbol,
|
| 652 |
-
interval=Client.KLINE_INTERVAL_1DAY,
|
| 653 |
-
limit=days
|
| 654 |
-
)
|
| 655 |
-
|
| 656 |
-
if not klines or len(klines) < 100:
|
| 657 |
-
print(f"[MediumFreq] Insufficient klines returned: {len(klines) if klines else 0}")
|
| 658 |
-
return None
|
| 659 |
-
|
| 660 |
-
df = pd.DataFrame(klines, columns=[
|
| 661 |
-
'timestamp', 'Open', 'High', 'Low', 'Close', 'Volume',
|
| 662 |
-
'close_time', 'quote_volume', 'trades', 'taker_buy_base',
|
| 663 |
-
'taker_buy_quote', 'ignore'
|
| 664 |
-
])
|
| 665 |
-
|
| 666 |
-
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
|
| 667 |
-
df.set_index('timestamp', inplace=True)
|
| 668 |
-
df['Close'] = df['Close'].astype(float)
|
| 669 |
-
df['Open'] = df['Open'].astype(float)
|
| 670 |
-
df['High'] = df['High'].astype(float)
|
| 671 |
-
df['Low'] = df['Low'].astype(float)
|
| 672 |
-
|
| 673 |
-
print(f"[MediumFreq] Fetched {len(df)} days of data, latest close: ${df['Close'].iloc[-1]:.2f}")
|
| 674 |
-
return df[['Open', 'High', 'Low', 'Close', 'Volume']]
|
| 675 |
-
|
| 676 |
-
except Exception as e:
|
| 677 |
-
print(f"[MediumFreq] Error fetching data: {e}")
|
| 678 |
-
return None
|
| 679 |
-
|
| 680 |
-
def _get_current_position(self) -> float:
|
| 681 |
-
"""Get current quantity of the asset held."""
|
| 682 |
-
try:
|
| 683 |
-
account = self.client.get_account()
|
| 684 |
-
asset = self.symbol.replace('USDT', '')
|
| 685 |
-
|
| 686 |
-
for balance in account['balances']:
|
| 687 |
-
if balance['asset'] == asset:
|
| 688 |
-
qty = float(balance['free']) + float(balance['locked'])
|
| 689 |
-
return qty
|
| 690 |
-
return 0.0
|
| 691 |
-
except Exception as e:
|
| 692 |
-
print(f"[MediumFreq] Error getting position: {e}")
|
| 693 |
-
return self.current_position_qty # Fallback to tracked quantity
|
| 694 |
-
|
| 695 |
-
def _adjust_position(self, target_qty: float):
|
| 696 |
-
"""
|
| 697 |
-
Adjust position to match target quantity.
|
| 698 |
-
Handles both increasing and decreasing positions.
|
| 699 |
-
"""
|
| 700 |
-
current_qty = self._get_current_position()
|
| 701 |
-
delta = target_qty - current_qty
|
| 702 |
-
price = get_current_price(self.symbol)
|
| 703 |
-
|
| 704 |
-
if price is None:
|
| 705 |
-
print(f"[MediumFreq] Cannot get price for {self.symbol}")
|
| 706 |
-
return
|
| 707 |
-
|
| 708 |
-
if abs(delta) < 0.00001: # Negligible difference
|
| 709 |
-
print(f"[MediumFreq] Position already at target")
|
| 710 |
-
return
|
| 711 |
-
|
| 712 |
-
if delta > 0:
|
| 713 |
-
# Need to buy
|
| 714 |
-
side = 'BUY'
|
| 715 |
-
quantity = delta
|
| 716 |
-
else:
|
| 717 |
-
# Need to sell
|
| 718 |
-
side = 'SELL'
|
| 719 |
-
quantity = abs(delta)
|
| 720 |
-
|
| 721 |
-
print(f"[MediumFreq] Placing {side} order: {quantity:.8f} @ ${price:.2f} (${quantity * price:.2f})")
|
| 722 |
-
|
| 723 |
-
order = place_market_order(self.symbol, side, quantity)
|
| 724 |
-
|
| 725 |
-
if 'error' not in order:
|
| 726 |
-
order_id = str(order.get('orderId', ''))
|
| 727 |
-
pnl = None
|
| 728 |
-
|
| 729 |
-
# Calculate PnL for sells
|
| 730 |
-
if side == 'SELL' and self.entry_price > 0:
|
| 731 |
-
pnl = (price - self.entry_price) * quantity
|
| 732 |
-
self.pnl += pnl
|
| 733 |
-
print(f"[MediumFreq] Trade PnL: ${pnl:.2f}")
|
| 734 |
-
|
| 735 |
-
# Update entry price tracking
|
| 736 |
-
if side == 'BUY':
|
| 737 |
-
if self.current_position_qty == 0:
|
| 738 |
-
self.entry_price = price
|
| 739 |
-
else:
|
| 740 |
-
# Average entry price
|
| 741 |
-
total_cost = (self.entry_price * self.current_position_qty) + (price * quantity)
|
| 742 |
-
self.entry_price = total_cost / (self.current_position_qty + quantity)
|
| 743 |
-
|
| 744 |
-
self.current_position_qty = target_qty
|
| 745 |
-
|
| 746 |
-
self.trades.append({
|
| 747 |
-
'type': side,
|
| 748 |
-
'price': price,
|
| 749 |
-
'quantity': quantity,
|
| 750 |
-
'time': datetime.now().isoformat(),
|
| 751 |
-
'pnl': pnl,
|
| 752 |
-
'order_id': order_id,
|
| 753 |
-
'target_multiplier': self.target_position_size,
|
| 754 |
-
'regime': self.last_signal_info.get('regime_label', 'Unknown')
|
| 755 |
-
})
|
| 756 |
-
|
| 757 |
-
save_trade_to_db(
|
| 758 |
-
self.session_id, self.user_email, self.symbol,
|
| 759 |
-
side, price, quantity, pnl, order_id
|
| 760 |
-
)
|
| 761 |
-
|
| 762 |
-
update_session_in_db(self.session_id, True, self.pnl, len(self.trades))
|
| 763 |
-
print(f"[MediumFreq] β
Order executed successfully")
|
| 764 |
-
else:
|
| 765 |
-
print(f"[MediumFreq] β Order failed: {order['error']}")
|
| 766 |
-
|
| 767 |
-
def get_status(self) -> dict:
|
| 768 |
-
elapsed = (datetime.now() - self.start_time).total_seconds() / 60
|
| 769 |
-
remaining = max(0, self.duration_minutes - elapsed)
|
| 770 |
-
|
| 771 |
-
# Calculate next trade time
|
| 772 |
-
time_since_start = (datetime.now() - self.start_time).total_seconds()
|
| 773 |
-
cycles_completed = int(time_since_start / self.TRADE_INTERVAL_SECONDS)
|
| 774 |
-
next_trade_in = self.TRADE_INTERVAL_SECONDS - (time_since_start % self.TRADE_INTERVAL_SECONDS)
|
| 775 |
-
|
| 776 |
-
pos_str = "FLAT"
|
| 777 |
-
if self.current_position_qty > 0:
|
| 778 |
-
pos_str = f"LONG {self.target_position_size}x"
|
| 779 |
-
|
| 780 |
-
return {
|
| 781 |
-
'session_id': self.session_id,
|
| 782 |
-
'strategy': 'hmm_svr',
|
| 783 |
-
'strategy_name': 'HMM-SVR Medium Frequency',
|
| 784 |
-
'symbol': self.symbol,
|
| 785 |
-
'trade_amount': self.trade_amount,
|
| 786 |
-
'is_running': self.is_running,
|
| 787 |
-
'position': pos_str,
|
| 788 |
-
'position_qty': self.current_position_qty,
|
| 789 |
-
'target_multiplier': self.target_position_size,
|
| 790 |
-
'trades_count': len(self.trades),
|
| 791 |
-
'pnl': round(self.pnl, 2),
|
| 792 |
-
'elapsed_minutes': round(elapsed, 1),
|
| 793 |
-
'remaining_minutes': round(remaining, 1),
|
| 794 |
-
'next_trade_in_minutes': round(next_trade_in / 60, 1),
|
| 795 |
-
'trade_interval_hours': 3,
|
| 796 |
-
'last_signal': self.last_signal_info,
|
| 797 |
-
'trades': self.trades[-10:]
|
| 798 |
-
}
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
def start_live_trading(user_email: str, strategy: str, symbol: str,
|
| 802 |
-
trade_amount: float, duration_minutes: int,
|
| 803 |
-
duration_unit: str = "minutes",
|
| 804 |
-
short_window: int = 12, long_window: int = 26,
|
| 805 |
-
interval: str = '5m') -> dict:
|
| 806 |
-
"""Factory function to start a new live trading session."""
|
| 807 |
-
session_id = f"{user_email}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
| 808 |
-
|
| 809 |
-
for sid, session in active_sessions.items():
|
| 810 |
-
if session.user_email == user_email and session.is_running:
|
| 811 |
-
return {"error": "You already have an active trading session"}
|
| 812 |
-
|
| 813 |
-
if not symbol:
|
| 814 |
-
return {"error": "Symbol is required"}
|
| 815 |
-
|
| 816 |
-
if strategy != "hmm_svr":
|
| 817 |
-
return {"error": f"Unsupported strategy '{strategy}'. Only 'hmm_svr' is available."}
|
| 818 |
-
|
| 819 |
-
try:
|
| 820 |
-
# Medium-frequency strategy using pre-trained models
|
| 821 |
-
session = MediumFrequencySessionRunner(
|
| 822 |
-
session_id=session_id, user_email=user_email,
|
| 823 |
-
symbol=symbol, trade_amount=trade_amount, duration_minutes=duration_minutes,
|
| 824 |
-
short_window=short_window, long_window=long_window
|
| 825 |
-
)
|
| 826 |
-
except Exception as e:
|
| 827 |
-
return {"error": f"Failed to start session: {e}"}
|
| 828 |
-
|
| 829 |
-
active_sessions[session_id] = session
|
| 830 |
-
session.start()
|
| 831 |
-
|
| 832 |
-
return {
|
| 833 |
-
"success": True, "session_id": session_id,
|
| 834 |
-
"message": f"Trading session started with {strategy} on {symbol}"
|
| 835 |
-
}
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
def stop_live_trading(session_id: str) -> dict:
|
| 839 |
-
"""Stop an active trading session."""
|
| 840 |
-
if session_id in active_sessions:
|
| 841 |
-
session = active_sessions[session_id]
|
| 842 |
-
session.stop()
|
| 843 |
-
del active_sessions[session_id]
|
| 844 |
-
return {"success": True, "message": "Trading session stopped.", "final_pnl": session.pnl}
|
| 845 |
-
|
| 846 |
-
# Fallback for sessions not in memory
|
| 847 |
-
try:
|
| 848 |
-
with Session(engine) as db_session:
|
| 849 |
-
statement = select(TradingSession).where(TradingSession.session_id == session_id, TradingSession.is_running == True)
|
| 850 |
-
trading_session = db_session.exec(statement).first()
|
| 851 |
-
if trading_session:
|
| 852 |
-
trading_session.is_running = False
|
| 853 |
-
trading_session.end_time = datetime.now()
|
| 854 |
-
db_session.add(trading_session)
|
| 855 |
-
db_session.commit()
|
| 856 |
-
return {"success": True, "message": "Trading session stopped (was not active in memory)."}
|
| 857 |
-
except Exception as e:
|
| 858 |
-
print(f"Error stopping session in DB: {e}")
|
| 859 |
-
|
| 860 |
-
return {"error": "Session not found or already stopped"}
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
def get_session_status(session_id: str) -> dict:
|
| 864 |
-
"""Get status of a trading session."""
|
| 865 |
-
if session_id in active_sessions:
|
| 866 |
-
return active_sessions[session_id].get_status()
|
| 867 |
-
|
| 868 |
-
# If not active, pull final from DB
|
| 869 |
-
try:
|
| 870 |
-
with Session(engine) as db_session:
|
| 871 |
-
statement = select(TradingSession).where(TradingSession.session_id == session_id)
|
| 872 |
-
s = db_session.exec(statement).first()
|
| 873 |
-
if s:
|
| 874 |
-
elapsed = ((s.end_time or datetime.now()) - s.start_time).total_seconds() / 60
|
| 875 |
-
return {'session_id': s.session_id, 'strategy': s.strategy, 'symbol': s.symbol, 'trade_amount': s.trade_amount,
|
| 876 |
-
'is_running': s.is_running, 'position': 'FLAT', 'trades_count': s.trades_count, 'pnl': s.total_pnl,
|
| 877 |
-
'elapsed_minutes': round(elapsed, 1), 'remaining_minutes': 0, 'trades': []}
|
| 878 |
-
except Exception as e:
|
| 879 |
-
print(f"Error getting session status from DB: {e}")
|
| 880 |
-
|
| 881 |
-
return {"error": "Session not found"}
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
def get_user_sessions(user_email: str) -> List[dict]:
|
| 885 |
-
"""Get all sessions for a user."""
|
| 886 |
-
return get_user_sessions_from_db(user_email)
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
# Available strategies for frontend
|
| 890 |
-
AVAILABLE_STRATEGIES = [
|
| 891 |
-
{
|
| 892 |
-
"id": "hmm_svr",
|
| 893 |
-
"name": "HMM-SVR Medium Frequency",
|
| 894 |
-
"description": "Uses pre-trained HMM-SVR models for regime detection and volatility prediction. Trades every 3 hours with dynamic position sizing (0x-3x leverage based on market regime).",
|
| 895 |
-
"risk_level": "Medium",
|
| 896 |
-
"requires_symbol": True,
|
| 897 |
-
"requires_model": True,
|
| 898 |
-
"trade_interval": "3 hours"
|
| 899 |
-
}
|
| 900 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main.py
CHANGED
|
@@ -16,18 +16,6 @@ from jose import JWTError, jwt
|
|
| 16 |
|
| 17 |
# Import your strategy logic
|
| 18 |
from strategy import train_models_and_backtest
|
| 19 |
-
from live_trading import (
|
| 20 |
-
get_account_balance,
|
| 21 |
-
get_portfolio_value,
|
| 22 |
-
get_recent_trades,
|
| 23 |
-
get_recent_trades_from_db,
|
| 24 |
-
get_current_price,
|
| 25 |
-
start_live_trading,
|
| 26 |
-
stop_live_trading,
|
| 27 |
-
get_session_status,
|
| 28 |
-
get_user_sessions,
|
| 29 |
-
AVAILABLE_STRATEGIES
|
| 30 |
-
)
|
| 31 |
|
| 32 |
# Import model manager for HMM-SVR models
|
| 33 |
from model_manager import (
|
|
@@ -107,7 +95,7 @@ class Token(BaseModel):
|
|
| 107 |
access_token: str
|
| 108 |
token_type: str
|
| 109 |
|
| 110 |
-
class
|
| 111 |
symbol: str
|
| 112 |
trade_amount: float
|
| 113 |
duration: int
|
|
@@ -333,89 +321,160 @@ def reload_models(current_user: str = Depends(get_current_user)):
|
|
| 333 |
}
|
| 334 |
|
| 335 |
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
@app.get("/api/live/portfolio")
|
| 345 |
-
def get_portfolio(current_user: str = Depends(get_current_user)):
|
| 346 |
-
"""Get current portfolio value and holdings"""
|
| 347 |
-
portfolio = get_portfolio_value()
|
| 348 |
-
if "error" in portfolio:
|
| 349 |
-
raise HTTPException(status_code=500, detail=portfolio["error"])
|
| 350 |
-
return portfolio
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
@app.get("/api/live/balance")
|
| 354 |
-
def get_balance(current_user: str = Depends(get_current_user)):
|
| 355 |
-
"""Get account balances"""
|
| 356 |
-
balances = get_account_balance()
|
| 357 |
-
if isinstance(balances, dict) and "error" in balances:
|
| 358 |
-
raise HTTPException(status_code=500, detail=balances["error"])
|
| 359 |
-
return {"balances": balances}
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
@app.get("/api/live/trades")
|
| 363 |
-
def get_trades(symbol: Optional[str] = None, limit: int = 20,
|
| 364 |
-
current_user: str = Depends(get_current_user)):
|
| 365 |
-
"""Get recent trades from database for current user"""
|
| 366 |
-
trades = get_recent_trades_from_db(current_user, limit)
|
| 367 |
-
return {"trades": trades}
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
@app.get("/api/live/price/{symbol}")
|
| 371 |
-
def get_price(symbol: str, current_user: str = Depends(get_current_user)):
|
| 372 |
-
"""Get current price for a symbol"""
|
| 373 |
-
price = get_current_price(symbol)
|
| 374 |
-
if price is None:
|
| 375 |
-
raise HTTPException(status_code=404, detail="Symbol not found")
|
| 376 |
-
return {"symbol": symbol, "price": price}
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
@app.post("/api/live/start")
|
| 380 |
-
def start_trading(req: LiveTradingRequest, current_user: str = Depends(get_current_user)):
|
| 381 |
-
"""Start a live trading session"""
|
| 382 |
-
# Convert days to minutes if needed
|
| 383 |
-
duration_minutes = req.duration
|
| 384 |
-
if req.duration_unit == "days":
|
| 385 |
-
duration_minutes = req.duration * 24 * 60
|
| 386 |
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
if
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
|
| 412 |
-
|
| 413 |
-
def get_session(session_id: str, current_user: str = Depends(get_current_user)):
|
| 414 |
-
"""Get status of a trading session"""
|
| 415 |
-
status = get_session_status(session_id)
|
| 416 |
-
if "error" in status:
|
| 417 |
-
raise HTTPException(status_code=404, detail=status["error"])
|
| 418 |
-
return status
|
| 419 |
|
| 420 |
@app.get("/api/simulated/trades")
|
| 421 |
def get_simulated_trades(
|
|
@@ -433,12 +492,6 @@ def get_simulated_sessions(current_user: str = Depends(get_current_user)):
|
|
| 433 |
from simulated_endpoints import get_simulated_sessions_endpoint
|
| 434 |
return get_simulated_sessions_endpoint(current_user)
|
| 435 |
|
| 436 |
-
@app.get("/api/live/sessions")
|
| 437 |
-
def get_sessions(current_user: str = Depends(get_current_user)):
|
| 438 |
-
"""Get all trading sessions for the current user"""
|
| 439 |
-
sessions = get_user_sessions(current_user)
|
| 440 |
-
return {"sessions": sessions}
|
| 441 |
-
|
| 442 |
|
| 443 |
@app.get("/api/simulated/portfolio")
|
| 444 |
def get_simulated_portfolio(current_user: str = Depends(get_current_user)):
|
|
@@ -454,7 +507,7 @@ def get_simulated_portfolio(current_user: str = Depends(get_current_user)):
|
|
| 454 |
|
| 455 |
|
| 456 |
@app.post("/api/simulated/start")
|
| 457 |
-
def start_simulated_session(req:
|
| 458 |
"""Start HMM-SVR trading bot session"""
|
| 459 |
from simulated_trading import start_simulated_trading
|
| 460 |
from database import initialize_portfolio_if_empty
|
|
|
|
| 16 |
|
| 17 |
# Import your strategy logic
|
| 18 |
from strategy import train_models_and_backtest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
# Import model manager for HMM-SVR models
|
| 21 |
from model_manager import (
|
|
|
|
| 95 |
access_token: str
|
| 96 |
token_type: str
|
| 97 |
|
| 98 |
+
class SimulatedTradingRequest(BaseModel):
|
| 99 |
symbol: str
|
| 100 |
trade_amount: float
|
| 101 |
duration: int
|
|
|
|
| 321 |
}
|
| 322 |
|
| 323 |
|
| 324 |
+
@app.get("/api/models/signal/{symbol}")
|
| 325 |
+
def get_instant_signal(symbol: str, current_user: str = Depends(get_current_user)):
|
| 326 |
+
"""
|
| 327 |
+
Get instant trading signal for a symbol using trained HMM-SVR model.
|
| 328 |
+
Auto-trains model if it doesn't exist.
|
| 329 |
+
Returns current regime, recommended position size, and trading signal.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
+
Example: GET /api/models/signal/BTCUSDT
|
| 332 |
+
"""
|
| 333 |
+
from model_manager import is_model_trained, load_model, calculate_signal_and_position, train_and_save_model
|
| 334 |
+
import yfinance as yf
|
| 335 |
+
from datetime import datetime, timedelta
|
| 336 |
+
import pandas as pd
|
| 337 |
+
|
| 338 |
+
symbol = symbol.upper()
|
| 339 |
+
base_symbol = symbol.replace('USDT', '')
|
| 340 |
+
yahoo_symbol = f"{base_symbol}-USD" # Convert to Yahoo Finance format
|
| 341 |
+
|
| 342 |
+
# Check if model exists, train if not (same as bot auto-training)
|
| 343 |
+
if not is_model_trained(base_symbol) and not is_model_trained(symbol):
|
| 344 |
+
print(f"[SignalAPI] No model found for {base_symbol}, training now...")
|
| 345 |
+
|
| 346 |
+
try:
|
| 347 |
+
# Train model with both Yahoo symbol and Binance symbol for fallback
|
| 348 |
+
# Save model with base symbol name (BNB) not Yahoo format (BNB-USD)
|
| 349 |
+
train_result = train_and_save_model(
|
| 350 |
+
symbol=yahoo_symbol,
|
| 351 |
+
n_states=3,
|
| 352 |
+
binance_symbol=symbol,
|
| 353 |
+
save_as=base_symbol
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
if train_result and 'error' not in train_result:
|
| 357 |
+
print(f"[SignalAPI] β
Model trained for {base_symbol} with {train_result.get('train_days', 0)} days")
|
| 358 |
+
else:
|
| 359 |
+
return {
|
| 360 |
+
"success": False,
|
| 361 |
+
"error": f"Failed to train model: {train_result.get('error', 'Unknown error')}",
|
| 362 |
+
"action_required": "Insufficient data to train model"
|
| 363 |
+
}
|
| 364 |
+
except Exception as e:
|
| 365 |
+
return {
|
| 366 |
+
"success": False,
|
| 367 |
+
"error": f"Model training failed: {str(e)}"
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
# Fetch recent price data (450 days for proper feature calculation)
|
| 371 |
+
try:
|
| 372 |
+
end_date = datetime.now()
|
| 373 |
+
start_date = end_date - timedelta(days=450)
|
| 374 |
+
|
| 375 |
+
df = yf.download(yahoo_symbol, start=start_date, end=end_date, progress=False, auto_adjust=True)
|
| 376 |
+
|
| 377 |
+
if df.empty:
|
| 378 |
+
return {
|
| 379 |
+
"success": False,
|
| 380 |
+
"error": f"Could not fetch price data for {yahoo_symbol}"
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
# Handle MultiIndex columns
|
| 384 |
+
if isinstance(df.columns, pd.MultiIndex):
|
| 385 |
+
if 'Close' in df.columns.get_level_values(0):
|
| 386 |
+
df.columns = df.columns.get_level_values(0)
|
| 387 |
+
else:
|
| 388 |
+
df.columns = df.columns.get_level_values(1)
|
| 389 |
+
|
| 390 |
+
# Get signal from model (use base_symbol for model lookup, yahoo_symbol for data)
|
| 391 |
+
result = calculate_signal_and_position(
|
| 392 |
+
symbol=base_symbol,
|
| 393 |
+
recent_data=df,
|
| 394 |
+
short_window=12,
|
| 395 |
+
long_window=26
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
if result is None or 'error' in result:
|
| 399 |
+
return {
|
| 400 |
+
"success": False,
|
| 401 |
+
"error": result.get('error', 'Unknown error') if result else "Failed to calculate signal"
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
# Determine human-readable signal
|
| 405 |
+
ema_signal = result.get('ema_signal', 0)
|
| 406 |
+
target_position = result.get('target_position', 0)
|
| 407 |
+
position_multiplier = result.get('position_size_multiplier', 1.0)
|
| 408 |
+
regime = result.get('regime', 1)
|
| 409 |
+
regime_label = result.get('regime_label', 'Normal')
|
| 410 |
+
|
| 411 |
+
# Generate action recommendation (5-level system: 0x, 0.5x, 1x, 2x, 3x)
|
| 412 |
+
if target_position == 0:
|
| 413 |
+
if regime_label == 'Crash':
|
| 414 |
+
action = "STAY OUT"
|
| 415 |
+
action_color = "red"
|
| 416 |
+
action_description = "π¨ Crash Protocol: Safety override activated"
|
| 417 |
+
else:
|
| 418 |
+
action = "WAIT"
|
| 419 |
+
action_color = "yellow"
|
| 420 |
+
action_description = "Bearish trend - waiting for reversal"
|
| 421 |
+
elif target_position == 3:
|
| 422 |
+
action = "STRONG BUY (3x)"
|
| 423 |
+
action_color = "green"
|
| 424 |
+
action_description = "π Max Leverage: Safe regime + very low risk!"
|
| 425 |
+
elif target_position == 2:
|
| 426 |
+
action = "BUY (2x)"
|
| 427 |
+
action_color = "cyan"
|
| 428 |
+
action_description = "π Medium Leverage: Favorable conditions"
|
| 429 |
+
elif target_position == 0.5:
|
| 430 |
+
action = "CAUTIOUS BUY (0.5x)"
|
| 431 |
+
action_color = "orange"
|
| 432 |
+
action_description = "β οΈ Defensive: High risk detected"
|
| 433 |
+
else:
|
| 434 |
+
action = "BUY (1x)"
|
| 435 |
+
action_color = "blue"
|
| 436 |
+
action_description = "β
Standard bullish position"
|
| 437 |
+
|
| 438 |
+
return {
|
| 439 |
+
"success": True,
|
| 440 |
+
"symbol": symbol,
|
| 441 |
+
"current_price": result.get('close_price', 0),
|
| 442 |
+
"signal": {
|
| 443 |
+
"action": action,
|
| 444 |
+
"action_color": action_color,
|
| 445 |
+
"action_description": action_description,
|
| 446 |
+
"ema_trend": "Bullish" if ema_signal == 1 else "Bearish",
|
| 447 |
+
"position_multiplier": position_multiplier,
|
| 448 |
+
"target_position": target_position,
|
| 449 |
+
"signal_stability": result.get('signal_stability', 0.5), # NEW
|
| 450 |
+
"ema_gap_percent": result.get('ema_gap_percent', 0) # NEW: Trend strength
|
| 451 |
+
},
|
| 452 |
+
"regime": {
|
| 453 |
+
"state": regime,
|
| 454 |
+
"label": regime_label,
|
| 455 |
+
"description": "Low volatility" if regime == 0 else ("High volatility - danger" if regime_label == 'Crash' else "Normal volatility")
|
| 456 |
+
},
|
| 457 |
+
"risk": {
|
| 458 |
+
"ratio": result.get('risk_ratio', 1.0),
|
| 459 |
+
"level": "Low" if result.get('risk_ratio', 1.0) < 0.5 else ("High" if result.get('risk_ratio', 1.0) > 1.5 else "Moderate"),
|
| 460 |
+
"predicted_volatility": result.get('predicted_vol', 0)
|
| 461 |
+
},
|
| 462 |
+
"technicals": {
|
| 463 |
+
"ema_short": result.get('ema_short', 0),
|
| 464 |
+
"ema_long": result.get('ema_long', 0)
|
| 465 |
+
},
|
| 466 |
+
"reasoning": result.get('reasoning', ''),
|
| 467 |
+
"timestamp": datetime.now().isoformat()
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
except Exception as e:
|
| 471 |
+
return {
|
| 472 |
+
"success": False,
|
| 473 |
+
"error": f"Error calculating signal: {str(e)}"
|
| 474 |
+
}
|
| 475 |
|
| 476 |
|
| 477 |
+
# --- SIMULATED TRADING ROUTES ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
@app.get("/api/simulated/trades")
|
| 480 |
def get_simulated_trades(
|
|
|
|
| 492 |
from simulated_endpoints import get_simulated_sessions_endpoint
|
| 493 |
return get_simulated_sessions_endpoint(current_user)
|
| 494 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
|
| 496 |
@app.get("/api/simulated/portfolio")
|
| 497 |
def get_simulated_portfolio(current_user: str = Depends(get_current_user)):
|
|
|
|
| 507 |
|
| 508 |
|
| 509 |
@app.post("/api/simulated/start")
|
| 510 |
+
def start_simulated_session(req: SimulatedTradingRequest, current_user: str = Depends(get_current_user)):
|
| 511 |
"""Start HMM-SVR trading bot session"""
|
| 512 |
from simulated_trading import start_simulated_trading
|
| 513 |
from database import initialize_portfolio_if_empty
|
model_manager.py
CHANGED
|
@@ -207,29 +207,43 @@ def train_svr_model(train_df: pd.DataFrame) -> Tuple[SVR, StandardScaler]:
|
|
| 207 |
return model, scaler
|
| 208 |
|
| 209 |
|
| 210 |
-
def train_and_save_model(symbol: str, n_states: int = 3) -> Dict[str, Any]:
|
| 211 |
"""
|
| 212 |
Train HMM-SVR models for a symbol and save to disk.
|
| 213 |
Returns training results and metadata.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
print(f"\n{'='*60}")
|
| 216 |
-
print(f"[ModelManager] Training HMM-SVR model for {
|
| 217 |
print(f"{'='*60}")
|
| 218 |
|
| 219 |
# Try Yahoo Finance first, then Binance
|
| 220 |
df = fetch_training_data_yfinance(symbol)
|
| 221 |
if df is None or len(df) < 250:
|
| 222 |
print("[ModelManager] Falling back to Binance data...")
|
| 223 |
-
|
|
|
|
|
|
|
| 224 |
|
| 225 |
if df is None or len(df) < 250:
|
| 226 |
-
return {"error": f"Insufficient data for {
|
| 227 |
|
| 228 |
# Engineer features
|
| 229 |
df = engineer_features(df)
|
| 230 |
|
| 231 |
if len(df) < 200:
|
| 232 |
-
return {"error": f"Insufficient data after feature engineering for {
|
| 233 |
|
| 234 |
print(f"[ModelManager] Training on {len(df)} days of data...")
|
| 235 |
|
|
@@ -249,7 +263,7 @@ def train_and_save_model(symbol: str, n_states: int = 3) -> Dict[str, Any]:
|
|
| 249 |
print("[ModelManager] Training SVR model...")
|
| 250 |
svr_model, svr_scaler = train_svr_model(df)
|
| 251 |
|
| 252 |
-
# Prepare model data for saving
|
| 253 |
model_data = {
|
| 254 |
'hmm_model': hmm_model,
|
| 255 |
'svr_model': svr_model,
|
|
@@ -259,15 +273,15 @@ def train_and_save_model(symbol: str, n_states: int = 3) -> Dict[str, Any]:
|
|
| 259 |
'n_states': n_states,
|
| 260 |
'train_days': len(df),
|
| 261 |
'trained_at': datetime.now().isoformat(),
|
| 262 |
-
'symbol':
|
| 263 |
}
|
| 264 |
|
| 265 |
-
# Save to disk
|
| 266 |
-
model_path = get_model_path(
|
| 267 |
joblib.dump(model_data, model_path)
|
| 268 |
|
| 269 |
-
# Update cache
|
| 270 |
-
_model_cache[
|
| 271 |
|
| 272 |
print(f"[ModelManager] β
Model saved to {model_path}")
|
| 273 |
print(f"[ModelManager] States: 0=Low Vol (Safe), {n_states-1}=High Vol (Crash)")
|
|
@@ -275,7 +289,7 @@ def train_and_save_model(symbol: str, n_states: int = 3) -> Dict[str, Any]:
|
|
| 275 |
|
| 276 |
return {
|
| 277 |
"success": True,
|
| 278 |
-
"symbol":
|
| 279 |
"trained_at": model_data['trained_at'],
|
| 280 |
"train_days": len(df),
|
| 281 |
"avg_train_vol": avg_train_vol,
|
|
@@ -408,13 +422,15 @@ def calculate_signal_and_position(
|
|
| 408 |
symbol: str,
|
| 409 |
recent_data: pd.DataFrame,
|
| 410 |
short_window: int = 12,
|
| 411 |
-
long_window: int = 26
|
|
|
|
| 412 |
) -> Optional[Dict[str, Any]]:
|
| 413 |
"""
|
| 414 |
-
|
|
|
|
| 415 |
|
| 416 |
Returns:
|
| 417 |
-
Dict with signal, target_position_size, regime info, etc.
|
| 418 |
"""
|
| 419 |
# Get regime and volatility prediction
|
| 420 |
prediction = predict_regime_and_volatility(symbol, recent_data)
|
|
@@ -424,35 +440,84 @@ def calculate_signal_and_position(
|
|
| 424 |
model_data = load_model(symbol)
|
| 425 |
n_states = model_data['n_states']
|
| 426 |
|
| 427 |
-
#
|
| 428 |
df = recent_data.copy()
|
| 429 |
-
|
| 430 |
-
df['EMA_Long'] = df['Close'].ewm(span=long_window).mean()
|
| 431 |
|
| 432 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
|
| 434 |
# EMA Crossover Signal (1 = bullish, 0 = bearish)
|
| 435 |
ema_signal = 1 if latest['EMA_Short'] > latest['EMA_Long'] else 0
|
| 436 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
# Determine position size based on regime and risk
|
| 438 |
regime = prediction['regime']
|
| 439 |
risk_ratio = prediction['risk_ratio']
|
| 440 |
|
| 441 |
-
#
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
|
| 444 |
-
#
|
| 445 |
-
|
| 446 |
-
position_size = 3.0
|
| 447 |
|
| 448 |
-
#
|
| 449 |
if regime == n_states - 1:
|
| 450 |
position_size = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
|
| 452 |
# Target position = Signal * Position Size
|
| 453 |
# Signal of 0 means no position regardless of size
|
| 454 |
target_position = ema_signal * position_size
|
| 455 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
return {
|
| 457 |
'ema_signal': ema_signal,
|
| 458 |
'ema_short': float(latest['EMA_Short']),
|
|
@@ -464,44 +529,57 @@ def calculate_signal_and_position(
|
|
| 464 |
'position_size_multiplier': position_size,
|
| 465 |
'target_position': target_position, # 0, 1, or 3
|
| 466 |
'close_price': float(latest['Close']),
|
| 467 |
-
'
|
|
|
|
|
|
|
| 468 |
}
|
| 469 |
|
| 470 |
|
| 471 |
def _get_signal_reasoning(ema_signal: int, regime: int, risk_ratio: float,
|
| 472 |
-
position_size: float, n_states: int) -> str:
|
| 473 |
-
"""Generate human-readable reasoning for the signal."""
|
| 474 |
reasons = []
|
| 475 |
|
| 476 |
-
# EMA reasoning
|
| 477 |
if ema_signal == 1:
|
| 478 |
-
|
|
|
|
| 479 |
else:
|
| 480 |
-
|
|
|
|
| 481 |
|
| 482 |
# Regime reasoning
|
| 483 |
if regime == 0:
|
| 484 |
-
reasons.append("Safe
|
| 485 |
elif regime == n_states - 1:
|
| 486 |
-
reasons.append("
|
| 487 |
else:
|
| 488 |
-
reasons.append(f"Normal
|
| 489 |
|
| 490 |
# Risk reasoning
|
| 491 |
if risk_ratio < 0.5:
|
| 492 |
-
reasons.append(f"
|
| 493 |
elif risk_ratio > 1.5:
|
| 494 |
-
reasons.append(f"High
|
| 495 |
else:
|
| 496 |
-
reasons.append(f"
|
| 497 |
|
| 498 |
-
# Position reasoning
|
| 499 |
if position_size == 3.0:
|
| 500 |
-
reasons.append("β
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
elif position_size == 0.0:
|
| 502 |
-
|
|
|
|
|
|
|
|
|
|
| 503 |
else:
|
| 504 |
-
reasons.append("β
|
| 505 |
|
| 506 |
return " | ".join(reasons)
|
| 507 |
|
|
|
|
| 207 |
return model, scaler
|
| 208 |
|
| 209 |
|
| 210 |
+
def train_and_save_model(symbol: str, n_states: int = 3, binance_symbol: str = None, save_as: str = None) -> Dict[str, Any]:
|
| 211 |
"""
|
| 212 |
Train HMM-SVR models for a symbol and save to disk.
|
| 213 |
Returns training results and metadata.
|
| 214 |
+
|
| 215 |
+
Args:
|
| 216 |
+
symbol: Base symbol (e.g., 'BTC') or Yahoo Finance format (e.g., 'BTC-USD')
|
| 217 |
+
n_states: Number of HMM states (default 3)
|
| 218 |
+
binance_symbol: Full Binance symbol (e.g., 'BTCUSDT') for fallback, optional
|
| 219 |
+
save_as: Base symbol to use for saving model (e.g., 'BTC'). If not provided,
|
| 220 |
+
will auto-detect from symbol by stripping USDT/-USD suffixes
|
| 221 |
"""
|
| 222 |
+
# Auto-detect base symbol for saving if not provided
|
| 223 |
+
# Handles: BTCUSDT -> BTC, BTC-USD -> BTC, BTC -> BTC
|
| 224 |
+
if save_as is None:
|
| 225 |
+
save_as = symbol.replace('USDT', '').replace('-USD', '')
|
| 226 |
+
|
| 227 |
print(f"\n{'='*60}")
|
| 228 |
+
print(f"[ModelManager] Training HMM-SVR model for {save_as}")
|
| 229 |
print(f"{'='*60}")
|
| 230 |
|
| 231 |
# Try Yahoo Finance first, then Binance
|
| 232 |
df = fetch_training_data_yfinance(symbol)
|
| 233 |
if df is None or len(df) < 250:
|
| 234 |
print("[ModelManager] Falling back to Binance data...")
|
| 235 |
+
# Use full Binance symbol if provided, otherwise reconstruct it
|
| 236 |
+
fallback_symbol = binance_symbol if binance_symbol else f"{save_as}USDT"
|
| 237 |
+
df = fetch_training_data_binance(fallback_symbol)
|
| 238 |
|
| 239 |
if df is None or len(df) < 250:
|
| 240 |
+
return {"error": f"Insufficient data for {save_as}. Need at least 250 days, got {len(df) if df is not None else 0}."}
|
| 241 |
|
| 242 |
# Engineer features
|
| 243 |
df = engineer_features(df)
|
| 244 |
|
| 245 |
if len(df) < 200:
|
| 246 |
+
return {"error": f"Insufficient data after feature engineering for {save_as}. Got {len(df)} days."}
|
| 247 |
|
| 248 |
print(f"[ModelManager] Training on {len(df)} days of data...")
|
| 249 |
|
|
|
|
| 263 |
print("[ModelManager] Training SVR model...")
|
| 264 |
svr_model, svr_scaler = train_svr_model(df)
|
| 265 |
|
| 266 |
+
# Prepare model data for saving (use save_as for consistent base symbol)
|
| 267 |
model_data = {
|
| 268 |
'hmm_model': hmm_model,
|
| 269 |
'svr_model': svr_model,
|
|
|
|
| 273 |
'n_states': n_states,
|
| 274 |
'train_days': len(df),
|
| 275 |
'trained_at': datetime.now().isoformat(),
|
| 276 |
+
'symbol': save_as
|
| 277 |
}
|
| 278 |
|
| 279 |
+
# Save to disk using base symbol (e.g., BTC not BTCUSDT or BTC-USD)
|
| 280 |
+
model_path = get_model_path(save_as)
|
| 281 |
joblib.dump(model_data, model_path)
|
| 282 |
|
| 283 |
+
# Update cache with base symbol
|
| 284 |
+
_model_cache[save_as.upper()] = model_data
|
| 285 |
|
| 286 |
print(f"[ModelManager] β
Model saved to {model_path}")
|
| 287 |
print(f"[ModelManager] States: 0=Low Vol (Safe), {n_states-1}=High Vol (Crash)")
|
|
|
|
| 289 |
|
| 290 |
return {
|
| 291 |
"success": True,
|
| 292 |
+
"symbol": save_as,
|
| 293 |
"trained_at": model_data['trained_at'],
|
| 294 |
"train_days": len(df),
|
| 295 |
"avg_train_vol": avg_train_vol,
|
|
|
|
| 422 |
symbol: str,
|
| 423 |
recent_data: pd.DataFrame,
|
| 424 |
short_window: int = 12,
|
| 425 |
+
long_window: int = 26,
|
| 426 |
+
lookback_window: int = 252
|
| 427 |
) -> Optional[Dict[str, Any]]:
|
| 428 |
"""
|
| 429 |
+
Calculate trading signal and position sizing with walk-forward logic.
|
| 430 |
+
Enhanced to match backtest methodology more closely.
|
| 431 |
|
| 432 |
Returns:
|
| 433 |
+
Dict with signal, target_position_size, regime info, stability metrics, etc.
|
| 434 |
"""
|
| 435 |
# Get regime and volatility prediction
|
| 436 |
prediction = predict_regime_and_volatility(symbol, recent_data)
|
|
|
|
| 440 |
model_data = load_model(symbol)
|
| 441 |
n_states = model_data['n_states']
|
| 442 |
|
| 443 |
+
# Use sliding window for EMA calculation (matches backtest)
|
| 444 |
df = recent_data.copy()
|
| 445 |
+
lookback_data = df.iloc[-lookback_window:] if len(df) > lookback_window else df
|
|
|
|
| 446 |
|
| 447 |
+
# Calculate EMAs on sliding window only (not full history)
|
| 448 |
+
lookback_data_copy = lookback_data.copy()
|
| 449 |
+
lookback_data_copy['EMA_Short'] = lookback_data_copy['Close'].ewm(span=short_window).mean()
|
| 450 |
+
lookback_data_copy['EMA_Long'] = lookback_data_copy['Close'].ewm(span=long_window).mean()
|
| 451 |
+
|
| 452 |
+
latest = lookback_data_copy.iloc[-1]
|
| 453 |
|
| 454 |
# EMA Crossover Signal (1 = bullish, 0 = bearish)
|
| 455 |
ema_signal = 1 if latest['EMA_Short'] > latest['EMA_Long'] else 0
|
| 456 |
|
| 457 |
+
# Calculate signal stability (how long has signal been consistent?)
|
| 458 |
+
if len(lookback_data_copy) >= 5:
|
| 459 |
+
recent_5_days = lookback_data_copy.tail(5)
|
| 460 |
+
recent_signals = (recent_5_days['EMA_Short'] > recent_5_days['EMA_Long']).astype(int)
|
| 461 |
+
signal_stability = recent_signals.sum() / 5.0 # 1.0 = all bullish, 0.0 = all bearish
|
| 462 |
+
else:
|
| 463 |
+
signal_stability = 0.5
|
| 464 |
+
|
| 465 |
# Determine position size based on regime and risk
|
| 466 |
regime = prediction['regime']
|
| 467 |
risk_ratio = prediction['risk_ratio']
|
| 468 |
|
| 469 |
+
# Enhanced 5-level leverage logic:
|
| 470 |
+
# - 0x: Crash regime OR bearish trend
|
| 471 |
+
# - 0.5x: High risk in normal regime (defensive)
|
| 472 |
+
# - 1x: Standard bullish position
|
| 473 |
+
# - 2x: Safe regime with moderate risk OR normal regime with low risk
|
| 474 |
+
# - 3x: Safe regime + very low risk (sniper mode)
|
| 475 |
+
|
| 476 |
+
position_size = 1.0 # Default: Standard position
|
| 477 |
|
| 478 |
+
# Debug logging to verify conditions
|
| 479 |
+
print(f"[Leverage Logic] {symbol}: Regime={regime}/{n_states-1}, Risk={risk_ratio:.3f}, EMA_Signal={ema_signal}")
|
|
|
|
| 480 |
|
| 481 |
+
# CRASH PROTOCOL: Override to 0x if crash regime detected
|
| 482 |
if regime == n_states - 1:
|
| 483 |
position_size = 0.0
|
| 484 |
+
print(f"[Leverage Logic] {symbol}: CRASH REGIME β 0x position")
|
| 485 |
+
|
| 486 |
+
# SAFE REGIME (Lowest volatility)
|
| 487 |
+
elif regime == 0:
|
| 488 |
+
if risk_ratio < 0.5:
|
| 489 |
+
position_size = 3.0 # π SNIPER MODE
|
| 490 |
+
print(f"[Leverage Logic] {symbol}: SNIPER MODE (Safe + Very Low Risk) β 3x")
|
| 491 |
+
elif risk_ratio < 0.85:
|
| 492 |
+
position_size = 2.0 # π Strong position
|
| 493 |
+
print(f"[Leverage Logic] {symbol}: Safe + Moderate Risk β 2x")
|
| 494 |
+
else:
|
| 495 |
+
position_size = 1.0 # β
Standard
|
| 496 |
+
print(f"[Leverage Logic] {symbol}: Safe + High Risk β 1x")
|
| 497 |
+
|
| 498 |
+
# NORMAL REGIME (Middle volatility)
|
| 499 |
+
elif regime == 1:
|
| 500 |
+
if risk_ratio < 0.5:
|
| 501 |
+
position_size = 2.0 # π Favorable
|
| 502 |
+
print(f"[Leverage Logic] {symbol}: Normal + Low Risk β 2x")
|
| 503 |
+
elif risk_ratio > 1.2:
|
| 504 |
+
position_size = 0.5 # β οΈ Defensive
|
| 505 |
+
print(f"[Leverage Logic] {symbol}: Normal + High Risk β 0.5x (defensive)")
|
| 506 |
+
else:
|
| 507 |
+
position_size = 1.0 # β
Standard
|
| 508 |
+
print(f"[Leverage Logic] {symbol}: Normal + Moderate Risk β 1x")
|
| 509 |
+
|
| 510 |
+
# Otherwise: Standard 1x position (if bullish) or 0x (if bearish)
|
| 511 |
|
| 512 |
# Target position = Signal * Position Size
|
| 513 |
# Signal of 0 means no position regardless of size
|
| 514 |
target_position = ema_signal * position_size
|
| 515 |
|
| 516 |
+
# Enhanced reasoning with stability context
|
| 517 |
+
reasoning = _get_signal_reasoning(
|
| 518 |
+
ema_signal, regime, risk_ratio, position_size, n_states, signal_stability
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
return {
|
| 522 |
'ema_signal': ema_signal,
|
| 523 |
'ema_short': float(latest['EMA_Short']),
|
|
|
|
| 529 |
'position_size_multiplier': position_size,
|
| 530 |
'target_position': target_position, # 0, 1, or 3
|
| 531 |
'close_price': float(latest['Close']),
|
| 532 |
+
'signal_stability': signal_stability, # NEW: How stable is the signal?
|
| 533 |
+
'reasoning': reasoning,
|
| 534 |
+
'ema_gap_percent': ((latest['EMA_Short'] - latest['EMA_Long']) / latest['EMA_Long'] * 100) # NEW: Strength of trend
|
| 535 |
}
|
| 536 |
|
| 537 |
|
| 538 |
def _get_signal_reasoning(ema_signal: int, regime: int, risk_ratio: float,
|
| 539 |
+
position_size: float, n_states: int, signal_stability: float = 0.5) -> str:
|
| 540 |
+
"""Generate human-readable reasoning for the signal with stability context."""
|
| 541 |
reasons = []
|
| 542 |
|
| 543 |
+
# EMA reasoning with stability
|
| 544 |
if ema_signal == 1:
|
| 545 |
+
stability_text = "STRONG" if signal_stability > 0.8 else ("WEAK" if signal_stability < 0.4 else "MODERATE")
|
| 546 |
+
reasons.append(f"β
Trend UP (12-EMA > 26-EMA) [{stability_text}]")
|
| 547 |
else:
|
| 548 |
+
stability_text = "STRONG" if signal_stability < 0.2 else ("WEAK" if signal_stability > 0.6 else "MODERATE")
|
| 549 |
+
reasons.append(f"π Trend DOWN (12-EMA < 26-EMA) [{stability_text}]")
|
| 550 |
|
| 551 |
# Regime reasoning
|
| 552 |
if regime == 0:
|
| 553 |
+
reasons.append("π‘οΈ Safe Regime (Low Volatility)")
|
| 554 |
elif regime == n_states - 1:
|
| 555 |
+
reasons.append("π¨ CRASH REGIME (High Volatility)")
|
| 556 |
else:
|
| 557 |
+
reasons.append(f"βοΈ Normal Regime (Neutral Volatility)")
|
| 558 |
|
| 559 |
# Risk reasoning
|
| 560 |
if risk_ratio < 0.5:
|
| 561 |
+
reasons.append(f"π€οΈ Future Looks Calm (risk: {risk_ratio:.2f})")
|
| 562 |
elif risk_ratio > 1.5:
|
| 563 |
+
reasons.append(f"β οΈ High Future Risk (risk: {risk_ratio:.2f})")
|
| 564 |
else:
|
| 565 |
+
reasons.append(f"π Normal Risk (ratio: {risk_ratio:.2f})")
|
| 566 |
|
| 567 |
+
# Position reasoning with enhanced logic
|
| 568 |
if position_size == 3.0:
|
| 569 |
+
reasons.append("β π MAX LEVERAGE 3x (Sniper Mode!)")
|
| 570 |
+
elif position_size == 2.0:
|
| 571 |
+
reasons.append("β π MEDIUM LEVERAGE 2x (Favorable)")
|
| 572 |
+
elif position_size == 1.0:
|
| 573 |
+
reasons.append("β β
Standard 1x Position")
|
| 574 |
+
elif position_size == 0.5:
|
| 575 |
+
reasons.append("β β οΈ REDUCED 0.5x (Defensive)")
|
| 576 |
elif position_size == 0.0:
|
| 577 |
+
if regime == n_states - 1:
|
| 578 |
+
reasons.append("β π CASH (Crash Protocol Override)")
|
| 579 |
+
else:
|
| 580 |
+
reasons.append("β π CASH (Bearish Trend)")
|
| 581 |
else:
|
| 582 |
+
reasons.append(f"β Position: {position_size:.1f}x")
|
| 583 |
|
| 584 |
return " | ".join(reasons)
|
| 585 |
|
requirements.txt
CHANGED
|
Binary files a/requirements.txt and b/requirements.txt differ
|
|
|
simulated_trading.py
CHANGED
|
@@ -135,25 +135,27 @@ class SimulatedTradingSession:
|
|
| 135 |
return
|
| 136 |
|
| 137 |
# Get signal from HMM-SVR model
|
| 138 |
-
signal = self.handler.get_signal(price)
|
| 139 |
|
| 140 |
# Log check
|
| 141 |
elapsed_hours = (datetime.now() - self.start_time).total_seconds() / 3600
|
| 142 |
position_str = self.position or 'NONE'
|
| 143 |
-
print(f"[HMM-SVR Bot] β° Check | {elapsed_hours:.1f}h | ${price:,.2f} | {signal} | Pos: {position_str}")
|
| 144 |
|
| 145 |
# Execute based on signal (long-only strategy)
|
| 146 |
if signal == "BUY" and self.position is None:
|
| 147 |
-
self._open_long_position(price)
|
| 148 |
elif signal == "SELL" and self.position == "LONG":
|
| 149 |
self._close_position(price)
|
| 150 |
|
| 151 |
except Exception as e:
|
| 152 |
print(f"[HMM-SVR Bot] β Error: {e}")
|
| 153 |
|
| 154 |
-
def _open_long_position(self, price: float):
|
| 155 |
-
"""Open a LONG position (BUY)"""
|
| 156 |
-
|
|
|
|
|
|
|
| 157 |
|
| 158 |
success, trade_info = simulated_exchange.execute_buy(
|
| 159 |
symbol=self.base_asset,
|
|
@@ -166,7 +168,8 @@ class SimulatedTradingSession:
|
|
| 166 |
self.position = "LONG"
|
| 167 |
self.entry_price = price
|
| 168 |
self._save_trade_to_db(trade_info)
|
| 169 |
-
|
|
|
|
| 170 |
else:
|
| 171 |
print(f"[HMM-SVR Bot] β Failed to open position")
|
| 172 |
|
|
|
|
| 135 |
return
|
| 136 |
|
| 137 |
# Get signal from HMM-SVR model
|
| 138 |
+
signal, position_size = self.handler.get_signal(price)
|
| 139 |
|
| 140 |
# Log check
|
| 141 |
elapsed_hours = (datetime.now() - self.start_time).total_seconds() / 3600
|
| 142 |
position_str = self.position or 'NONE'
|
| 143 |
+
print(f"[HMM-SVR Bot] β° Check | {elapsed_hours:.1f}h | ${price:,.2f} | {signal} {position_size}x | Pos: {position_str}")
|
| 144 |
|
| 145 |
# Execute based on signal (long-only strategy)
|
| 146 |
if signal == "BUY" and self.position is None:
|
| 147 |
+
self._open_long_position(price, position_size)
|
| 148 |
elif signal == "SELL" and self.position == "LONG":
|
| 149 |
self._close_position(price)
|
| 150 |
|
| 151 |
except Exception as e:
|
| 152 |
print(f"[HMM-SVR Bot] β Error: {e}")
|
| 153 |
|
| 154 |
+
def _open_long_position(self, price: float, position_size: float = 1.0):
|
| 155 |
+
"""Open a LONG position (BUY) with leverage multiplier"""
|
| 156 |
+
# Apply position size multiplier (0x, 1x, or 3x)
|
| 157 |
+
leveraged_amount = self.trade_amount * position_size
|
| 158 |
+
quantity = leveraged_amount / price
|
| 159 |
|
| 160 |
success, trade_info = simulated_exchange.execute_buy(
|
| 161 |
symbol=self.base_asset,
|
|
|
|
| 168 |
self.position = "LONG"
|
| 169 |
self.entry_price = price
|
| 170 |
self._save_trade_to_db(trade_info)
|
| 171 |
+
leverage_str = f" ({position_size}x)" if position_size != 1.0 else ""
|
| 172 |
+
print(f"[HMM-SVR Bot] π LONG opened: {quantity:.8f} {self.base_asset} @ ${price:,.2f}{leverage_str}")
|
| 173 |
else:
|
| 174 |
print(f"[HMM-SVR Bot] β Failed to open position")
|
| 175 |
|
strategy_handlers.py
CHANGED
|
@@ -72,21 +72,21 @@ class HMMSVRStrategyHandler:
|
|
| 72 |
except Exception as e:
|
| 73 |
print(f"[HMM-SVR] β οΈ Error loading historical data: {e}")
|
| 74 |
|
| 75 |
-
def get_signal(self, price: float) -> str:
|
| 76 |
"""
|
| 77 |
Generate trading signal using HMM-SVR model.
|
| 78 |
|
| 79 |
Returns:
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
"""
|
| 84 |
# Add current price to buffer
|
| 85 |
self.price_buffer.append(float(price))
|
| 86 |
|
| 87 |
# Need at least 100 data points
|
| 88 |
if len(self.price_buffer) < 100:
|
| 89 |
-
return "HOLD"
|
| 90 |
|
| 91 |
try:
|
| 92 |
# Convert buffer to DataFrame for model
|
|
@@ -104,7 +104,7 @@ class HMMSVRStrategyHandler:
|
|
| 104 |
|
| 105 |
if result is None or 'error' in result:
|
| 106 |
print(f"[HMM-SVR] Error: {result}")
|
| 107 |
-
return "HOLD"
|
| 108 |
|
| 109 |
# Extract results
|
| 110 |
target_position = result.get('target_position_size', 0.0)
|
|
@@ -123,8 +123,8 @@ class HMMSVRStrategyHandler:
|
|
| 123 |
signal = "HOLD"
|
| 124 |
|
| 125 |
self.last_position_size = target_position
|
| 126 |
-
return signal
|
| 127 |
|
| 128 |
except Exception as e:
|
| 129 |
print(f"[HMM-SVR] Error generating signal: {e}")
|
| 130 |
-
return "HOLD"
|
|
|
|
| 72 |
except Exception as e:
|
| 73 |
print(f"[HMM-SVR] β οΈ Error loading historical data: {e}")
|
| 74 |
|
| 75 |
+
def get_signal(self, price: float) -> tuple[str, float]:
|
| 76 |
"""
|
| 77 |
Generate trading signal using HMM-SVR model.
|
| 78 |
|
| 79 |
Returns:
|
| 80 |
+
tuple: (signal, position_size)
|
| 81 |
+
- signal: "BUY", "SELL", or "HOLD"
|
| 82 |
+
- position_size: 0.0 (no position), 1.0 (normal), 3.0 (high conviction)
|
| 83 |
"""
|
| 84 |
# Add current price to buffer
|
| 85 |
self.price_buffer.append(float(price))
|
| 86 |
|
| 87 |
# Need at least 100 data points
|
| 88 |
if len(self.price_buffer) < 100:
|
| 89 |
+
return "HOLD", 0.0
|
| 90 |
|
| 91 |
try:
|
| 92 |
# Convert buffer to DataFrame for model
|
|
|
|
| 104 |
|
| 105 |
if result is None or 'error' in result:
|
| 106 |
print(f"[HMM-SVR] Error: {result}")
|
| 107 |
+
return "HOLD", self.last_position_size
|
| 108 |
|
| 109 |
# Extract results
|
| 110 |
target_position = result.get('target_position_size', 0.0)
|
|
|
|
| 123 |
signal = "HOLD"
|
| 124 |
|
| 125 |
self.last_position_size = target_position
|
| 126 |
+
return signal, target_position
|
| 127 |
|
| 128 |
except Exception as e:
|
| 129 |
print(f"[HMM-SVR] Error generating signal: {e}")
|
| 130 |
+
return "HOLD", self.last_position_size
|