Spaces:
Running
Running
| """ | |
| Kronos AI Forecast Microservice — Render Deployment | |
| Merlijn Signaal Labo — Camelot Finance | |
| Endpoint: GET /forecast?symbol=BTCUSDT | |
| Health: GET /health | |
| """ | |
| import os | |
| import requests | |
| from flask import Flask, jsonify, request | |
| import pandas as pd | |
| import time | |
| app = Flask(__name__) | |
| # ── Model laden (eenmalig bij opstart) ─────────────────────────────── | |
| print("[Kronos] Model laden...") | |
| try: | |
| from model import Kronos, KronosTokenizer, KronosPredictor | |
| tokenizer = KronosTokenizer.from_pretrained("NeoQuasar/Kronos-Tokenizer-base") | |
| model_k = Kronos.from_pretrained("NeoQuasar/Kronos-small") | |
| predictor = KronosPredictor(model_k, tokenizer, max_context=512) | |
| print("[Kronos] Model geladen") | |
| KRONOS_OK = True | |
| except Exception as e: | |
| print(f"[Kronos] Model fout: {e}") | |
| KRONOS_OK = False | |
| # ── Cache: max 1 forecast per symbol per 30 min ───────────────────── | |
| _cache = {} | |
| CACHE_TTL = 1800 | |
| # Symbool mapping naar CryptoCompare formaat | |
| SYMBOL_MAP = { | |
| 'HBARUSDT': ('HBAR', 'USDT'), | |
| 'XRPUSDT': ('XRP', 'USDT'), | |
| 'VETUSDT': ('VET', 'USDT'), | |
| 'BTCUSDT': ('BTC', 'USDT'), | |
| 'PAXGUSDT': ('PAXG', 'USDT'), | |
| 'XAGUSD': ('PAXG', 'USDT'), | |
| 'XAUUSD': ('PAXG', 'USDT'), | |
| } | |
| def get_cc_pair(symbol): | |
| """Vertaal symbol naar CryptoCompare fsym/tsym.""" | |
| s = symbol.upper() | |
| if s in SYMBOL_MAP: | |
| return SYMBOL_MAP[s] | |
| # Probeer automatisch te splitsen | |
| if s.endswith('USDT'): | |
| return (s[:-4], 'USDT') | |
| if s.endswith('USD'): | |
| return (s[:-3], 'USD') | |
| return (s, 'USDT') | |
| def fetch_ohlcv(symbol, lookback=450): | |
| """Haal OHLCV data op via CryptoCompare (geen geo-restricties).""" | |
| fsym, tsym = get_cc_pair(symbol) | |
| errors = [] | |
| # CryptoCompare: 4h candles = hourly met aggregate=4 | |
| # Max 2000 per call, we vragen lookback candles | |
| url = f'https://min-api.cryptocompare.com/data/v2/histohour?fsym={fsym}&tsym={tsym}&limit={lookback * 4}&aggregate=1' | |
| try: | |
| resp = requests.get(url, timeout=15) | |
| data = resp.json() | |
| if data.get('Response') == 'Success' and data.get('Data', {}).get('Data'): | |
| raw = data['Data']['Data'] | |
| # Groepeer per 4 uur (OHLCV aggregatie) | |
| candles = [] | |
| for i in range(0, len(raw) - 3, 4): | |
| chunk = raw[i:i+4] | |
| if len(chunk) < 4: | |
| break | |
| candles.append({ | |
| 'timestamp': chunk[0]['time'] * 1000, | |
| 'open': chunk[0]['open'], | |
| 'high': max(c['high'] for c in chunk), | |
| 'low': min(c['low'] for c in chunk), | |
| 'close': chunk[-1]['close'], | |
| 'volume': sum(c['volumefrom'] for c in chunk), | |
| }) | |
| if len(candles) >= 50: | |
| df = pd.DataFrame(candles) | |
| df['timestamps'] = pd.to_datetime(df['timestamp'], unit='ms') | |
| print(f"[Kronos] OHLCV via CryptoCompare: {fsym}/{tsym} ({len(df)} 4h candles)") | |
| return df | |
| else: | |
| errors.append(f"CryptoCompare: te weinig data ({len(candles)} candles)") | |
| else: | |
| errors.append(f"CryptoCompare: {data.get('Message', 'onbekende fout')}") | |
| except Exception as e: | |
| errors.append(f"CryptoCompare: {e}") | |
| # Fallback: Binance data-api (proxy, soms minder geo-restrictief) | |
| binance_urls = [ | |
| f'https://data-api.binance.vision/api/v3/klines?symbol={symbol.upper()}&interval=4h&limit={lookback}', | |
| f'https://api.binance.com/api/v3/klines?symbol={symbol.upper()}&interval=4h&limit={lookback}', | |
| ] | |
| for burl in binance_urls: | |
| try: | |
| resp = requests.get(burl, timeout=10) | |
| if resp.status_code == 200: | |
| raw = resp.json() | |
| if isinstance(raw, list) and len(raw) > 0: | |
| df = pd.DataFrame(raw, columns=[ | |
| 'timestamp', 'open', 'high', 'low', 'close', 'volume', | |
| 'close_time', 'quote_vol', 'trades', 'taker_buy_base', 'taker_buy_quote', 'ignore' | |
| ]) | |
| df = df[['timestamp', 'open', 'high', 'low', 'close', 'volume']].astype(float) | |
| df['timestamp'] = df['timestamp'].astype(int) | |
| df['timestamps'] = pd.to_datetime(df['timestamp'], unit='ms') | |
| src = 'Binance-data-api' if 'data-api' in burl else 'Binance' | |
| print(f"[Kronos] OHLCV via {src}: {symbol} ({len(df)} candles)") | |
| return df | |
| except Exception as e: | |
| errors.append(f"Binance: {e}") | |
| raise Exception(f"Alle data bronnen gefaald voor {symbol}: {'; '.join(errors)}") | |
| def fetch_multi_exchange_price(symbol): | |
| """Haal huidige prijs op via CryptoCompare (multi-exchange gemiddelde).""" | |
| fsym, tsym = get_cc_pair(symbol) | |
| try: | |
| url = f'https://min-api.cryptocompare.com/data/pricemultifull?fsyms={fsym}&tsyms={tsym}' | |
| resp = requests.get(url, timeout=10) | |
| data = resp.json() | |
| raw = data.get('RAW', {}).get(fsym, {}).get(tsym, {}) | |
| if not raw: | |
| return None | |
| price = float(raw.get('PRICE', 0)) | |
| high24 = float(raw.get('HIGH24HOUR', 0)) | |
| low24 = float(raw.get('LOW24HOUR', 0)) | |
| volume = float(raw.get('VOLUME24HOUR', 0)) | |
| market = raw.get('LASTMARKET', 'unknown') | |
| return { | |
| 'prices': {market: price}, | |
| 'avg': round(price, 6), | |
| 'spread': round(high24 - low24, 6), | |
| 'spread_pct': round((high24 - low24) / price * 100, 3) if price > 0 else 0, | |
| 'high24': round(high24, 6), | |
| 'low24': round(low24, 6), | |
| 'volume24': round(volume, 2), | |
| } | |
| except Exception as e: | |
| print(f"[Kronos] Prijs fout {symbol}: {e}") | |
| return None | |
| def compute_forecast(symbol): | |
| now = time.time() | |
| if symbol in _cache and now - _cache[symbol]['ts'] < CACHE_TTL: | |
| return _cache[symbol]['data'] | |
| if not KRONOS_OK: | |
| return {'symbol': symbol, 'direction': 'neutral', 'pct': 0.0, 'score': 0} | |
| try: | |
| df = fetch_ohlcv(symbol) | |
| lookback = 400 | |
| pred_len = 24 | |
| if len(df) < lookback + pred_len: | |
| return {'symbol': symbol, 'direction': 'neutral', 'pct': 0.0, 'score': 0, | |
| 'error': f'Onvoldoende data: {len(df)} rijen'} | |
| df = df.iloc[-(lookback + pred_len):].reset_index(drop=True) | |
| x_df = df.iloc[:lookback][['open', 'high', 'low', 'close', 'volume']] | |
| x_timestamp = df.iloc[:lookback]['timestamps'] | |
| y_timestamp = df.iloc[lookback:lookback + pred_len]['timestamps'] | |
| pred_df = predictor.predict( | |
| df = x_df, | |
| x_timestamp = x_timestamp, | |
| y_timestamp = y_timestamp, | |
| pred_len = pred_len, | |
| T = 1.0, | |
| top_p = 0.9, | |
| sample_count = 3 | |
| ) | |
| current_close = df.iloc[lookback - 1]['close'] | |
| forecast_close = pred_df['close'].iloc[-1] | |
| pct_change = (forecast_close - current_close) / current_close * 100 | |
| raw_score = pct_change * 1.5 | |
| score = int(max(-15, min(15, round(raw_score)))) | |
| if pct_change > 1.5: | |
| direction = 'bullish' | |
| elif pct_change < -1.5: | |
| direction = 'bearish' | |
| else: | |
| direction = 'neutral' | |
| # Multi-exchange prijsvergelijking | |
| multi = fetch_multi_exchange_price(symbol) | |
| result = { | |
| 'symbol': symbol, | |
| 'direction': direction, | |
| 'pct': round(pct_change, 2), | |
| 'score': score, | |
| 'forecast': round(float(forecast_close), 6), | |
| 'current': round(float(current_close), 6), | |
| 'exchanges': multi, | |
| } | |
| _cache[symbol] = {'ts': now, 'data': result} | |
| print(f"[Kronos] {symbol}: {direction} {pct_change:+.2f}% score {score:+d}") | |
| return result | |
| except Exception as e: | |
| print(f"[Kronos] Fout {symbol}: {e}") | |
| return {'symbol': symbol, 'direction': 'neutral', 'pct': 0.0, 'score': 0, 'error': str(e)} | |
| # ── Routes ─────────────────────────────────────────────────────────── | |
| def forecast(): | |
| symbol = request.args.get('symbol', 'BTCUSDT').upper() | |
| result = compute_forecast(symbol) | |
| return jsonify(result) | |
| def prices(): | |
| symbol = request.args.get('symbol', 'BTCUSDT').upper() | |
| result = fetch_multi_exchange_price(symbol) | |
| if result: | |
| return jsonify({'symbol': symbol, **result}) | |
| return jsonify({'symbol': symbol, 'error': 'Geen prijzen beschikbaar'}) | |
| def health(): | |
| return jsonify({'status': 'ok', 'kronos_loaded': KRONOS_OK, 'cached': list(_cache.keys()), | |
| 'data_source': 'CryptoCompare + Binance fallback'}) | |
| def clear_cache(): | |
| _cache.clear() | |
| return jsonify({'status': 'cache cleared'}) | |
| def index(): | |
| return jsonify({'service': 'Kronos AI Forecast', 'status': 'ok', | |
| 'endpoints': ['/forecast?symbol=BTCUSDT', '/prices?symbol=BTCUSDT', '/health']}) | |
| if __name__ == '__main__': | |
| port = int(os.environ.get('PORT', 5001)) | |
| app.run(host='0.0.0.0', port=port, debug=False) | |