kronos-ai-forecast / kronos_server.py
CamelotLabs's picture
Upload folder using huggingface_hub
9a344a3 verified
Raw
History Blame Contribute Delete
9.62 kB
"""
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 ───────────────────────────────────────────────────────────
@app.route('/forecast')
def forecast():
symbol = request.args.get('symbol', 'BTCUSDT').upper()
result = compute_forecast(symbol)
return jsonify(result)
@app.route('/prices')
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'})
@app.route('/health')
def health():
return jsonify({'status': 'ok', 'kronos_loaded': KRONOS_OK, 'cached': list(_cache.keys()),
'data_source': 'CryptoCompare + Binance fallback'})
@app.route('/cache/clear')
def clear_cache():
_cache.clear()
return jsonify({'status': 'cache cleared'})
@app.route('/')
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)