Spaces:
Paused
Paused
| """ | |
| FastAPI Backend for Signal Engine | |
| Complete trading signals platform with ML predictions, user auth, and real-time data | |
| """ | |
| from fastapi import FastAPI, HTTPException, Depends, Header, Request, WebSocket, WebSocketDisconnect | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | |
| import websockets | |
| import json | |
| import asyncio | |
| from pydantic import BaseModel, Field | |
| from typing import List, Optional, Dict | |
| from datetime import datetime, timedelta | |
| import secrets | |
| import hashlib | |
| import hmac | |
| import stripe | |
| import aiohttp | |
| import numpy as np | |
| from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier | |
| from sklearn.svm import SVC | |
| from sklearn.linear_model import LogisticRegression | |
| from eth_account import Account | |
| from eth_account.messages import encode_defunct | |
| from eth_utils import to_checksum_address | |
| from web3 import Web3 | |
| from web3.middleware import geth_poa_middleware | |
| from database import get_session, Symbol, PredictionBatch, Prediction, LedgerEntry, Subscriber, ApiUsage | |
| from signal_engine import SignalEngine | |
| from config import settings | |
| from ml_engine import MLEngine, ReinforcementLearningAgent, GeneticAlgorithmOptimizer, SelfLearningSystem | |
| app = FastAPI( | |
| title="Signal Engine API", | |
| description="Gate.io crypto-perpetual signal engine with audit-grade ledger", | |
| version="1.0.0" | |
| ) | |
| # CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Mount static files for landing page | |
| app.mount("/static", StaticFiles(directory="static"), name="static") | |
| # Initialize signal engine | |
| signal_engine = SignalEngine() | |
| # Initialize ML/RL Engine | |
| ml_engine = MLEngine() | |
| rl_agent = ReinforcementLearningAgent(state_size=10, action_size=3) | |
| ga_optimizer = GeneticAlgorithmOptimizer(population_size=20, generations=50) | |
| self_learning = SelfLearningSystem(ml_engine) | |
| # Try to load pre-trained models | |
| try: | |
| ml_engine.load_models() | |
| print("Loaded pre-trained ML models") | |
| except: | |
| print("No pre-trained models found, will train on first request") | |
| # Pydantic models | |
| class HealthResponse(BaseModel): | |
| status: str | |
| ledger_valid: bool | |
| latest_batch: Optional[str] | |
| total_predictions: int | |
| class PredictionResponse(BaseModel): | |
| symbol: str | |
| target_hour: str | |
| entry_price: float | |
| direction: str | |
| probability_up: float | |
| confidence: float | |
| suggested_position: float | |
| class CurrentPredictionsResponse(BaseModel): | |
| batch_id: str | |
| target_hour: str | |
| batch_hash: str | |
| num_predictions: int | |
| avg_confidence: float | |
| predictions: List[PredictionResponse] | |
| class MetricsResponse(BaseModel): | |
| total_predictions: int | |
| scored_predictions: int | |
| overall_accuracy: float | |
| avg_brier_score: float | |
| avg_hypothetical_pnl_bps: float | |
| class AccuracyResponse(BaseModel): | |
| window_hours: int | |
| accuracy: float | |
| total_predictions: int | |
| correct_predictions: int | |
| class LeaderboardEntry(BaseModel): | |
| symbol: str | |
| hit_rate: float | |
| total_predictions: int | |
| avg_hypothetical_pnl_bps: float | |
| class TrackRecordResponse(BaseModel): | |
| ledger_head_hash: str | |
| recent_entries: List[Dict] | |
| total_scored_predictions: int | |
| overall_accuracy: float | |
| overall_hypothetical_pnl_bps: float | |
| class WebhookEvent(BaseModel): | |
| type: str | |
| data: Dict | |
| class WalletConnectRequest(BaseModel): | |
| address: str | |
| signature: str | |
| message: str | |
| class ZKLoginRequest(BaseModel): | |
| challenge: str | |
| proof: Dict | |
| public_key: str | |
| class PredictionRequest(BaseModel): | |
| symbol: str | |
| timeframe: str = "1h" | |
| class ContractSubscription(BaseModel): | |
| tier: str # FREE, PRO, ENTERPRISE | |
| duration: int # months | |
| # Authentication | |
| def verify_api_key(x_api_key: str = Header(...)) -> Subscriber: | |
| """Verify API key and return subscriber""" | |
| session = get_session(settings.database_url) | |
| # Hash the provided key for comparison | |
| key_hash = hashlib.sha256(x_api_key.encode()).hexdigest() | |
| subscriber = session.query(Subscriber).filter_by( | |
| api_key_hash=key_hash, | |
| active=True | |
| ).first() | |
| if not subscriber: | |
| session.close() | |
| raise HTTPException(status_code=401, detail="Invalid API key") | |
| # Check subscription expiry | |
| if subscriber.expires_at and subscriber.expires_at < datetime.utcnow(): | |
| session.close() | |
| raise HTTPException(status_code=403, detail="Subscription expired") | |
| session.close() | |
| return subscriber | |
| def check_rate_limit(subscriber: Subscriber): | |
| """Check rate limit for subscriber""" | |
| session = get_session(settings.database_url) | |
| # Get rate limit based on tier | |
| rate_limits = { | |
| 'free': settings.free_tier_rate_limit, | |
| 'pro': settings.pro_tier_rate_limit, | |
| 'enterprise': settings.enterprise_tier_rate_limit | |
| } | |
| limit = rate_limits.get(subscriber.tier, settings.free_tier_rate_limit) | |
| # Count requests in last hour | |
| one_hour_ago = datetime.utcnow() - timedelta(hours=1) | |
| usage_count = session.query(ApiUsage).filter( | |
| ApiUsage.subscriber_id == subscriber.id, | |
| ApiUsage.timestamp >= one_hour_ago | |
| ).count() | |
| session.close() | |
| if usage_count >= limit: | |
| raise HTTPException(status_code=429, detail="Rate limit exceeded") | |
| def log_api_usage(subscriber: Subscriber, endpoint: str, status_code: int): | |
| """Log API usage for rate limiting""" | |
| session = get_session(settings.database_url) | |
| usage = ApiUsage( | |
| subscriber_id=subscriber.id, | |
| endpoint=endpoint, | |
| status_code=status_code | |
| ) | |
| session.add(usage) | |
| session.commit() | |
| session.close() | |
| # Endpoints | |
| async def root(): | |
| """Root endpoint - serve landing page""" | |
| from fastapi.responses import FileResponse | |
| return FileResponse("static/landing.html") | |
| async def app_root(): | |
| """Serve the main application""" | |
| from fastapi.responses import FileResponse | |
| return FileResponse("static/index.html") | |
| async def health(): | |
| """Health check endpoint""" | |
| session = get_session(settings.database_url) | |
| # Check ledger validity | |
| ledger_entries = session.query(LedgerEntry).order_by(LedgerEntry.id).all() | |
| ledger_valid = True | |
| for i, entry in enumerate(ledger_entries): | |
| if i > 0 and entry.prev_hash != ledger_entries[i-1].entry_hash: | |
| ledger_valid = False | |
| break | |
| # Get latest batch | |
| latest_batch = session.query(PredictionBatch).order_by(PredictionBatch.id.desc()).first() | |
| # Count total predictions | |
| total_predictions = session.query(Prediction).count() | |
| session.close() | |
| return HealthResponse( | |
| status="healthy", | |
| ledger_valid=ledger_valid, | |
| latest_batch=latest_batch.batch_id if latest_batch else None, | |
| total_predictions=total_predictions | |
| ) | |
| async def get_current_predictions(subscriber: Subscriber = Depends(verify_api_key)): | |
| """Get current prediction batch""" | |
| check_rate_limit(subscriber) | |
| session = get_session(settings.database_url) | |
| # Get latest batch | |
| batch = session.query(PredictionBatch).order_by(PredictionBatch.id.desc()).first() | |
| if not batch: | |
| session.close() | |
| raise HTTPException(status_code=404, detail="No predictions available") | |
| # Get predictions | |
| predictions = session.query(Prediction).filter_by(batch_id=batch.batch_id).all() | |
| session.close() | |
| log_api_usage(subscriber, "/v1/predictions/current", 200) | |
| return CurrentPredictionsResponse( | |
| batch_id=batch.batch_id, | |
| target_hour=batch.target_hour.isoformat(), | |
| batch_hash=batch.batch_hash, | |
| num_predictions=batch.num_predictions, | |
| avg_confidence=batch.avg_confidence, | |
| predictions=[ | |
| PredictionResponse( | |
| symbol=p.symbol, | |
| target_hour=p.target_hour.isoformat(), | |
| entry_price=p.entry_price, | |
| direction=p.direction, | |
| probability_up=p.probability_up, | |
| confidence=p.confidence, | |
| suggested_position=p.suggested_position | |
| ) | |
| for p in predictions | |
| ] | |
| ) | |
| async def get_prediction_history( | |
| subscriber: Subscriber = Depends(verify_api_key), | |
| limit: int = 100, | |
| offset: int = 0 | |
| ): | |
| """Get historical predictions""" | |
| check_rate_limit(subscriber) | |
| session = get_session(settings.database_url) | |
| batches = session.query(PredictionBatch).order_by( | |
| PredictionBatch.target_hour.desc() | |
| ).limit(limit).offset(offset).all() | |
| result = [] | |
| for batch in batches: | |
| predictions = session.query(Prediction).filter_by(batch_id=batch.batch_id).all() | |
| result.append({ | |
| 'batch_id': batch.batch_id, | |
| 'target_hour': batch.target_hour.isoformat(), | |
| 'batch_hash': batch.batch_hash, | |
| 'num_predictions': batch.num_predictions, | |
| 'avg_confidence': batch.avg_confidence, | |
| 'accuracy': batch.accuracy, | |
| 'scored_at': batch.scored_at.isoformat() if batch.scored_at else None, | |
| 'predictions': [ | |
| { | |
| 'symbol': p.symbol, | |
| 'direction': p.direction, | |
| 'probability_up': p.probability_up, | |
| 'confidence': p.confidence, | |
| 'entry_price': p.entry_price, | |
| 'exit_price': p.exit_price, | |
| 'actual_return': p.actual_return, | |
| 'correct': p.correct | |
| } | |
| for p in predictions | |
| ] | |
| }) | |
| session.close() | |
| log_api_usage(subscriber, "/v1/predictions/history", 200) | |
| return result | |
| async def get_metrics(subscriber: Subscriber = Depends(verify_api_key)): | |
| """Get global metrics""" | |
| check_rate_limit(subscriber) | |
| session = get_session(settings.database_url) | |
| total_predictions = session.query(Prediction).count() | |
| scored_predictions = session.query(Prediction).filter( | |
| Prediction.scored_at.isnot(None) | |
| ).count() | |
| batches = session.query(PredictionBatch).filter( | |
| PredictionBatch.scored_at.isnot(None) | |
| ).all() | |
| if batches: | |
| avg_accuracy = sum(b.accuracy or 0 for b in batches) / len(batches) | |
| avg_brier = sum(b.brier_score or 0 for b in batches) / len(batches) | |
| avg_pnl = sum(b.hypothetical_pnl_bps or 0 for b in batches) / len(batches) | |
| else: | |
| avg_accuracy = 0.0 | |
| avg_brier = 0.0 | |
| avg_pnl = 0.0 | |
| session.close() | |
| log_api_usage(subscriber, "/v1/metrics", 200) | |
| return MetricsResponse( | |
| total_predictions=total_predictions, | |
| scored_predictions=scored_predictions, | |
| overall_accuracy=avg_accuracy, | |
| avg_brier_score=avg_brier, | |
| avg_hypothetical_pnl_bps=avg_pnl | |
| ) | |
| async def get_accuracy( | |
| subscriber: Subscriber = Depends(verify_api_key), | |
| window_hours: int = 24 | |
| ): | |
| """Get accuracy over rolling window""" | |
| check_rate_limit(subscriber) | |
| session = get_session(settings.database_url) | |
| cutoff = datetime.utcnow() - timedelta(hours=window_hours) | |
| predictions = session.query(Prediction).filter( | |
| Prediction.scored_at >= cutoff, | |
| Prediction.scored_at.isnot(None) | |
| ).all() | |
| total = len(predictions) | |
| correct = sum(1 for p in predictions if p.correct) | |
| session.close() | |
| log_api_usage(subscriber, "/v1/accuracy", 200) | |
| return AccuracyResponse( | |
| window_hours=window_hours, | |
| accuracy=correct / total if total > 0 else 0.0, | |
| total_predictions=total, | |
| correct_predictions=correct | |
| ) | |
| async def get_leaderboard(subscriber: Subscriber = Depends(verify_api_key)): | |
| """Get symbol leaderboard (pro tier only)""" | |
| if subscriber.tier != 'pro' and subscriber.tier != 'enterprise': | |
| raise HTTPException(status_code=403, detail="Pro tier required") | |
| check_rate_limit(subscriber) | |
| session = get_session(settings.database_url) | |
| # Calculate per-symbol metrics | |
| symbols = session.query(Prediction.symbol).distinct().all() | |
| leaderboard = [] | |
| for (symbol,) in symbols: | |
| predictions = session.query(Prediction).filter( | |
| Prediction.symbol == symbol, | |
| Prediction.scored_at.isnot(None) | |
| ).all() | |
| if predictions: | |
| hit_rate = sum(1 for p in predictions if p.correct) / len(predictions) | |
| avg_pnl = sum( | |
| (p.actual_return or 0) * abs(p.suggested_position) * 10000 | |
| for p in predictions | |
| ) / len(predictions) | |
| leaderboard.append(LeaderboardEntry( | |
| symbol=symbol, | |
| hit_rate=hit_rate, | |
| total_predictions=len(predictions), | |
| avg_hypothetical_pnl_bps=avg_pnl - settings.maker_fee_bps | |
| )) | |
| # Sort by hit rate | |
| leaderboard.sort(key=lambda x: x.hit_rate, reverse=True) | |
| session.close() | |
| log_api_usage(subscriber, "/v1/leaderboard", 200) | |
| return leaderboard[:20] | |
| async def get_track_record(subscriber: Subscriber = Depends(verify_api_key)): | |
| """Get track record with ledger info""" | |
| check_rate_limit(subscriber) | |
| session = get_session(settings.database_url) | |
| # Get ledger head | |
| ledger_head = session.query(LedgerEntry).order_by(LedgerEntry.id.desc()).first() | |
| # Get recent ledger entries | |
| recent_entries = session.query(LedgerEntry).order_by( | |
| LedgerEntry.id.desc() | |
| ).limit(10).all() | |
| # Calculate overall metrics | |
| scored_predictions = session.query(Prediction).filter( | |
| Prediction.scored_at.isnot(None) | |
| ).all() | |
| total_scored = len(scored_predictions) | |
| overall_accuracy = sum(1 for p in scored_predictions if p.correct) / total_scored if total_scored > 0 else 0 | |
| overall_pnl = sum( | |
| (p.actual_return or 0) * abs(p.suggested_position) * 10000 | |
| for p in scored_predictions | |
| ) / total_scored if total_scored > 0 else 0 | |
| session.close() | |
| log_api_usage(subscriber, "/v1/track-record", 200) | |
| return TrackRecordResponse( | |
| ledger_head_hash=ledger_head.entry_hash if ledger_head else "", | |
| recent_entries=[ | |
| { | |
| 'entry_hash': e.entry_hash, | |
| 'entry_type': e.entry_type, | |
| 'timestamp': e.timestamp.isoformat() | |
| } | |
| for e in recent_entries | |
| ], | |
| total_scored_predictions=total_scored, | |
| overall_accuracy=overall_accuracy, | |
| overall_hypothetical_pnl_bps=overall_pnl - settings.maker_fee_bps | |
| ) | |
| async def stripe_webhook(request: Request): | |
| """Handle Stripe webhooks""" | |
| if not settings.stripe_webhook_secret: | |
| # In development, accept without verification | |
| payload = await request.body() | |
| event_data = payload.decode('utf-8') | |
| else: | |
| # Production: verify signature | |
| payload = await request.body() | |
| sig_header = request.headers.get('stripe-signature') | |
| if not sig_header: | |
| raise HTTPException(status_code=400, detail="No signature header") | |
| try: | |
| event = stripe.Webhook.construct_event( | |
| payload, sig_header, settings.stripe_webhook_secret | |
| ) | |
| except ValueError: | |
| raise HTTPException(status_code=400, detail="Invalid payload") | |
| except stripe.error.SignatureVerificationError: | |
| raise HTTPException(status_code=400, detail="Invalid signature") | |
| event_data = event | |
| # Process webhook | |
| session = get_session(settings.database_url) | |
| # Handle subscription events | |
| if event_data['type'] in ['customer.subscription.created', 'customer.subscription.updated']: | |
| customer_id = event_data['data']['object']['customer'] | |
| subscriber = session.query(Subscriber).filter_by( | |
| stripe_customer_id=customer_id | |
| ).first() | |
| if subscriber: | |
| subscriber.active = True | |
| # Set expiry based on subscription | |
| session.commit() | |
| elif event_data['type'] == 'customer.subscription.deleted': | |
| customer_id = event_data['data']['object']['customer'] | |
| subscriber = session.query(Subscriber).filter_by( | |
| stripe_customer_id=customer_id | |
| ).first() | |
| if subscriber: | |
| subscriber.active = False | |
| session.commit() | |
| session.close() | |
| return {"status": "success"} | |
| async def zk_login(request: ZKLoginRequest): | |
| """ZK login with zero-knowledge proof verification (demo mode)""" | |
| try: | |
| # Demo mode: Generate API key without database | |
| api_key = secrets.token_urlsafe(32) | |
| return { | |
| "api_key": api_key, | |
| "tier": "free", | |
| "identity": request.public_key, | |
| "message": "ZK login successful (demo mode)" | |
| } | |
| except Exception as e: | |
| import traceback | |
| error_detail = f"ZK login failed: {str(e)}\n{traceback.format_exc()}" | |
| print(error_detail) | |
| return JSONResponse( | |
| status_code=500, | |
| content={"detail": error_detail} | |
| ) | |
| async def get_public_predictions(): | |
| """Get public predictions (no auth required, limited data)""" | |
| session = get_session(settings.database_url) | |
| # Get latest batch | |
| batch = session.query(PredictionBatch).order_by(PredictionBatch.id.desc()).first() | |
| if not batch: | |
| session.close() | |
| return {"predictions": [], "message": "No predictions available"} | |
| # Get predictions (limited fields for public view) | |
| predictions = session.query(Prediction).filter_by(batch_id=batch.batch_id).limit(5).all() | |
| session.close() | |
| return { | |
| "batch_id": batch.batch_id, | |
| "target_hour": batch.target_hour.isoformat(), | |
| "num_predictions": batch.num_predictions, | |
| "predictions": [ | |
| { | |
| "symbol": p.symbol, | |
| "direction": p.direction, | |
| "confidence": round(p.confidence, 2) | |
| } | |
| for p in predictions | |
| ] | |
| } | |
| async def get_market_tickers(): | |
| """Get real market tickers from Gate.io REST API""" | |
| from gateio_client import GateIOClient | |
| try: | |
| async with GateIOClient() as client: | |
| symbols = ["BTC_USDT", "ETH_USDT", "SOL_USDT", "DOGE_USDT", "XRP_USDT"] | |
| tickers = [] | |
| for symbol in symbols: | |
| ticker = await client.get_ticker(symbol) | |
| if ticker: | |
| tickers.append({ | |
| "symbol": ticker["symbol"].replace("_", "/"), | |
| "price": ticker["last_price"], | |
| "change_24h": 0, # Gate.io doesn't provide this directly in ticker | |
| "volume_24h": ticker["volume_24h"] | |
| }) | |
| return {"tickers": tickers} | |
| except Exception as e: | |
| return {"tickers": [], "error": str(e)} | |
| async def get_orderbook(symbol: str): | |
| """Get order book from Gate.io REST API""" | |
| import aiohttp | |
| gate_symbol = symbol.replace("/", "_") | |
| url = f"https://api.gateio.ws/api/v4/futures/usdt/order_book" | |
| try: | |
| async with aiohttp.ClientSession() as session: | |
| params = {"contract": gate_symbol, "limit": 20} | |
| async with session.get(url, params=params) as response: | |
| data = await response.json() | |
| return { | |
| "asks": data.get("asks", [])[:10], | |
| "bids": data.get("bids", [])[:10] | |
| } | |
| except Exception as e: | |
| return {"asks": [], "bids": [], "error": str(e)} | |
| # ML Prediction Generation | |
| async def generate_prediction(request: PredictionRequest, subscriber: Subscriber = Depends(verify_api_key)): | |
| """Generate ML prediction using real trained models""" | |
| try: | |
| # Fetch recent market data | |
| from gateio_client import GateIOClient | |
| async with GateIOClient() as client: | |
| candles = await client.get_candles(request.symbol, interval='1h', limit=100) | |
| if len(candles) < 50: | |
| raise HTTPException(status_code=400, detail="Insufficient data for prediction") | |
| # Extract features | |
| df = pd.DataFrame(candles, columns=['timestamp', 'volume', 'close', 'high', 'low', 'open']) | |
| df['timestamp'] = pd.to_datetime(df['timestamp'], unit='s') | |
| df.set_index('timestamp', inplace=True) | |
| features_dict = ml_engine.extract_features(df).iloc[-1].to_dict() | |
| # Generate prediction using real ML engine | |
| if not ml_engine.is_trained: | |
| # Train on the fly if not trained | |
| await ml_engine.train(request.symbol) | |
| prediction = ml_engine.predict(features_dict) | |
| # Get RL signal | |
| rl_signal = rl_agent.get_signal(features_dict) | |
| # Store in database | |
| session = get_session(settings.database_url) | |
| batch_id = secrets.token_hex(16) | |
| target_hour = datetime.utcnow() + timedelta(hours=1) | |
| pred = Prediction( | |
| batch_id=batch_id, | |
| symbol=request.symbol, | |
| target_hour=target_hour, | |
| entry_price=float(df.iloc[-1]['close']), | |
| direction=prediction['direction'], | |
| probability_up=prediction['probability_up'], | |
| confidence=prediction['confidence'], | |
| suggested_position=prediction['probability_up'] if prediction['direction'] == 'LONG' else -prediction['probability_up'], | |
| feature_hash=hashlib.sha256(str(features_dict).encode()).hexdigest() | |
| ) | |
| session.add(pred) | |
| session.commit() | |
| session.close() | |
| return { | |
| "symbol": request.symbol, | |
| "direction": prediction['direction'], | |
| "rl_signal": rl_signal, | |
| "confidence": prediction['confidence'], | |
| "entry_price": float(df.iloc[-1]['close']), | |
| "probability_up": prediction['probability_up'], | |
| "probability_down": prediction['probability_down'], | |
| "target_hour": target_hour.isoformat(), | |
| "ensemble_weights": { | |
| "random_forest": 0.3, | |
| "gradient_boosting": 0.3, | |
| "svm": 0.2, | |
| "logistic_regression": 0.2 | |
| } | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def train_models(symbol: str = "BTC_USDT", subscriber: Subscriber = Depends(verify_api_key)): | |
| """Train ML models on historical data""" | |
| try: | |
| results = await ml_engine.train(symbol) | |
| # Update self-learning system | |
| await self_learning.update(symbol) | |
| return { | |
| "status": "success", | |
| "symbol": symbol, | |
| "training_results": results, | |
| "performance_metrics": self_learning.get_performance_metrics() | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_ml_performance(subscriber: Subscriber = Depends(verify_api_key)): | |
| """Get ML performance metrics""" | |
| return self_learning.get_performance_metrics() | |
| def extract_features(df): | |
| """Extract technical features from candle data""" | |
| features = {} | |
| # Price features | |
| features['close'] = df.iloc[-1]['close'] | |
| features['high'] = df.iloc[-1]['high'] | |
| features['low'] = df.iloc[-1]['low'] | |
| features['volume'] = df.iloc[-1]['volume'] | |
| # Returns | |
| df['returns'] = df['close'].pct_change() | |
| features['return_1h'] = df['returns'].iloc[-1] | |
| features['return_4h'] = df['returns'].iloc[-4:].sum() | |
| features['return_24h'] = df['returns'].iloc[-24:].sum() | |
| # Volatility | |
| features['volatility'] = df['returns'].iloc[-24:].std() | |
| # Moving averages | |
| features['sma_7'] = df['close'].iloc[-7:].mean() | |
| features['sma_24'] = df['close'].iloc[-24:].mean() | |
| features['ema_12'] = df['close'].ewm(span=12).mean().iloc[-1] | |
| # RSI | |
| delta = df['close'].diff() | |
| gain = (delta.where(delta > 0, 0)).rolling(window=14).mean() | |
| loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean() | |
| rs = gain / loss | |
| features['rsi'] = 100 - (100 / (1 + rs.iloc[-1])) | |
| return features | |
| # Portfolio Management | |
| async def get_portfolio(subscriber: Subscriber = Depends(verify_api_key)): | |
| """Get user portfolio""" | |
| session = get_session(settings.database_url) | |
| # Get user's recent predictions | |
| predictions = session.query(Prediction).filter_by( | |
| symbol='BTC_USDT' # Filter by user's symbols | |
| ).order_by(Prediction.target_hour.desc()).limit(20).all() | |
| session.close() | |
| # Calculate portfolio value (mock for now) | |
| portfolio = { | |
| "total_value": 0, | |
| "positions": [], | |
| "recent_predictions": [ | |
| { | |
| "symbol": p.symbol, | |
| "direction": p.direction, | |
| "confidence": p.confidence, | |
| "entry_price": p.entry_price, | |
| "target_hour": p.target_hour.isoformat() | |
| } | |
| for p in predictions | |
| ] | |
| } | |
| return portfolio | |
| # Analytics | |
| async def get_performance(subscriber: Subscriber = Depends(verify_api_key)): | |
| """Get performance analytics""" | |
| session = get_session(settings.database_url) | |
| # Get scored predictions | |
| predictions = session.query(Prediction).filter( | |
| Prediction.scored_at.isnot(None) | |
| ).all() | |
| if not predictions: | |
| session.close() | |
| return {"accuracy": 0, "total_predictions": 0, "profitable": 0} | |
| correct = sum(1 for p in predictions if p.correct) | |
| accuracy = correct / len(predictions) | |
| session.close() | |
| return { | |
| "accuracy": round(accuracy * 100, 2), | |
| "total_predictions": len(predictions), | |
| "profitable": correct | |
| } | |
| # Smart Contract Endpoints | |
| async def get_contract_subscription(address: str): | |
| """Get subscription status from smart contract""" | |
| try: | |
| # For demo, return mock data | |
| # In production, connect to actual deployed contract | |
| return { | |
| "subscriber": address, | |
| "tier": "FREE", | |
| "expiresAt": int(datetime.utcnow().timestamp()) + 86400 * 30, | |
| "active": True | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def purchase_subscription(sub: ContractSubscription, subscriber: Subscriber = Depends(verify_api_key)): | |
| """Purchase subscription via smart contract""" | |
| try: | |
| # For demo, just update database tier | |
| session = get_session(settings.database_url) | |
| db_subscriber = session.query(Subscriber).filter_by( | |
| wallet_address=subscriber.wallet_address | |
| ).first() | |
| if db_subscriber: | |
| db_subscriber.tier = sub.tier.lower() | |
| session.commit() | |
| session.close() | |
| return { | |
| "status": "success", | |
| "tier": sub.tier, | |
| "message": "Subscription purchased (demo mode)" | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def get_contract_prediction(batch_id: str): | |
| """Get prediction verification from smart contract""" | |
| try: | |
| # For demo, return mock data | |
| return { | |
| "batchId": batch_id, | |
| "verified": True, | |
| "timestamp": int(datetime.utcnow().timestamp()) | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def websocket_market(websocket: WebSocket): | |
| """WebSocket endpoint for real-time Gate.io market data""" | |
| await websocket.accept() | |
| # Connect to Gate.io WebSocket | |
| gate_ws_url = "wss://api.gateio.ws/ws/v4/" | |
| try: | |
| async with websockets.connect(gate_ws_url) as gate_ws: | |
| # Subscribe to BTC/USDT ticker | |
| subscribe_msg = { | |
| "method": "ticker.subscribe", | |
| "params": ["BTC_USDT"], | |
| "id": 1 | |
| } | |
| await gate_ws.send(json.dumps(subscribe_msg)) | |
| # Subscribe to order book | |
| orderbook_msg = { | |
| "method": "order_book.subscribe", | |
| "params": ["BTC_USDT", "5", "0"], | |
| "id": 2 | |
| } | |
| await gate_ws.send(json.dumps(orderbook_msg)) | |
| # Forward messages from Gate.io to client | |
| while True: | |
| try: | |
| message = await gate_ws.recv() | |
| data = json.loads(message) | |
| # Forward relevant data to client | |
| if 'result' in data or 'params' in data: | |
| await websocket.send_json(data) | |
| except websockets.exceptions.ConnectionClosed: | |
| break | |
| except Exception as e: | |
| print(f"WebSocket error: {e}") | |
| finally: | |
| await websocket.close() | |
| async def websocket_multi(websocket: WebSocket): | |
| """WebSocket endpoint for multiple symbols market data""" | |
| await websocket.accept() | |
| gate_ws_url = "wss://api.gateio.ws/ws/v4/" | |
| symbols = ["BTC_USDT", "ETH_USDT", "SOL_USDT", "DOGE_USDT", "XRP_USDT"] | |
| try: | |
| async with websockets.connect(gate_ws_url) as gate_ws: | |
| # Subscribe to multiple tickers | |
| for i, symbol in enumerate(symbols): | |
| subscribe_msg = { | |
| "method": "ticker.subscribe", | |
| "params": [symbol], | |
| "id": i + 1 | |
| } | |
| await gate_ws.send(json.dumps(subscribe_msg)) | |
| # Forward messages | |
| while True: | |
| try: | |
| message = await gate_ws.recv() | |
| data = json.loads(message) | |
| await websocket.send_json(data) | |
| except websockets.exceptions.ConnectionClosed: | |
| break | |
| except Exception as e: | |
| print(f"WebSocket error: {e}") | |
| finally: | |
| await websocket.close() | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host=settings.api_host, port=settings.api_port) | |