saadrizvi09 commited on
Commit
2b05b19
Β·
1 Parent(s): 6c0f223

Sync with backend: 5-level dynamic leverage system, enhanced signal analysis

Browse files
Files changed (8) hide show
  1. .gitignore +6 -3
  2. README_HF.md +85 -0
  3. live_trading.py +0 -900
  4. main.py +153 -100
  5. model_manager.py +119 -41
  6. requirements.txt +0 -0
  7. simulated_trading.py +10 -7
  8. 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 LiveTradingRequest(BaseModel):
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
- # --- LIVE TRADING ROUTES ---
337
-
338
- @app.get("/api/live/strategies")
339
- def list_strategies(current_user: str = Depends(get_current_user)):
340
- """Get list of available trading strategies"""
341
- return {"strategies": AVAILABLE_STRATEGIES}
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
- result = start_live_trading(
388
- user_email=current_user,
389
- strategy=req.strategy,
390
- symbol=req.symbol,
391
- trade_amount=req.trade_amount,
392
- duration_minutes=duration_minutes,
393
- duration_unit=req.duration_unit,
394
- short_window=req.short_window,
395
- long_window=req.long_window,
396
- interval=req.interval
397
- )
398
- if "error" in result:
399
- raise HTTPException(status_code=400, detail=result["error"])
400
- return result
401
-
402
-
403
- @app.post("/api/live/stop/{session_id}")
404
- def stop_trading(session_id: str, current_user: str = Depends(get_current_user)):
405
- """Stop an active trading session"""
406
- result = stop_live_trading(session_id)
407
- if "error" in result:
408
- raise HTTPException(status_code=404, detail=result["error"])
409
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
 
411
 
412
- @app.get("/api/live/session/{session_id}")
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: LiveTradingRequest, current_user: str = Depends(get_current_user)):
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 {symbol}")
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
- df = fetch_training_data_binance(symbol)
 
 
224
 
225
  if df is None or len(df) < 250:
226
- return {"error": f"Insufficient data for {symbol}. Need at least 250 days, got {len(df) if df is not None else 0}."}
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 {symbol}. Got {len(df)} days."}
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': symbol
263
  }
264
 
265
- # Save to disk
266
- model_path = get_model_path(symbol)
267
  joblib.dump(model_data, model_path)
268
 
269
- # Update cache
270
- _model_cache[symbol.upper()] = model_data
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": 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
- # Calculate EMAs on recent data
428
  df = recent_data.copy()
429
- df['EMA_Short'] = df['Close'].ewm(span=short_window).mean()
430
- df['EMA_Long'] = df['Close'].ewm(span=long_window).mean()
431
 
432
- latest = df.iloc[-1]
 
 
 
 
 
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
- # Default position size
442
- position_size = 1.0
 
 
 
 
 
 
443
 
444
- # Boost: 3x in safe regime with low risk
445
- if regime == 0 and risk_ratio < 0.5:
446
- position_size = 3.0
447
 
448
- # Cut: 0x in crash regime (highest volatility state)
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
- 'reasoning': _get_signal_reasoning(ema_signal, regime, risk_ratio, position_size, n_states)
 
 
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
- reasons.append("EMA crossover bullish (short > long)")
 
479
  else:
480
- reasons.append("EMA crossover bearish (short < long)")
 
481
 
482
  # Regime reasoning
483
  if regime == 0:
484
- reasons.append("Safe regime detected (low volatility)")
485
  elif regime == n_states - 1:
486
- reasons.append("⚠️ Crash regime detected (high volatility)")
487
  else:
488
- reasons.append(f"Normal regime ({regime})")
489
 
490
  # Risk reasoning
491
  if risk_ratio < 0.5:
492
- reasons.append(f"Low risk (ratio: {risk_ratio:.2f})")
493
  elif risk_ratio > 1.5:
494
- reasons.append(f"High risk (ratio: {risk_ratio:.2f})")
495
  else:
496
- reasons.append(f"Moderate risk (ratio: {risk_ratio:.2f})")
497
 
498
- # Position reasoning
499
  if position_size == 3.0:
500
- reasons.append("β†’ Leveraging 3x (safe + low risk)")
 
 
 
 
 
 
501
  elif position_size == 0.0:
502
- reasons.append("β†’ Position cut to 0 (crash regime)")
 
 
 
503
  else:
504
- reasons.append("β†’ Standard 1x position")
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
- quantity = self.trade_amount / price
 
 
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
- print(f"[HMM-SVR Bot] πŸ“ˆ LONG opened: {quantity:.8f} {self.base_asset} @ ${price:,.2f}")
 
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
- "BUY" - open/increase position
81
- "SELL" - close position
82
- "HOLD" - maintain current state
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