From 033adb87da3b6bedaccc88138a74b853e7351d46 Mon Sep 17 00:00:00 2001 From: spm Date: Fri, 30 Oct 2026 09:06:25 +0000 Subject: [PATCH] Upload files to "/" --- .env.example | 10 +++ Dockerfile | 14 +++ README.md | 68 +++++++++++++++ analysis.py | 209 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 50 +++++++++++ 5 files changed, 351 insertions(+) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 analysis.py create mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4014406 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# Kraken API (maak aan via: https://www.kraken.com/u/security/api) +# Permissies nodig: Query Funds, Query Open Orders, Create/Modify Orders +KRAKEN_API_KEY=jouw_api_key_hier +KRAKEN_SECRET=jouw_secret_hier + +# CryptoPanic API (gratis aan te maken op: https://cryptopanic.com/developers/api/) +CRYPTOPANIC_API_KEY=jouw_cryptopanic_key_hier + +# Jouw domein voor Traefik routing +DOMAIN=jouwdomein.nl diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..27ebae4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5216b7 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Crypto Radar + +Marktanalyse dashboard voor Kraken — toont top 6 tradeable assets op basis van +technische analyse + nieuws sentiment. + +## Vereisten + +- Docker + docker-compose +- Traefik + Authentik draaien als `traefik-proxy` network +- Kraken account met API keys +- CryptoPanic API key (gratis) + +## Setup + +```bash +# 1. Clone / kopieer de map +cp .env.example .env + +# 2. Vul .env in met jouw keys + +# 3. Start +docker-compose up -d + +# Dashboard bereikbaar op: https://crypto.jouwdomein.nl +# API bereikbaar op: https://crypto-api.jouwdomein.nl +``` + +## API endpoints + +| Endpoint | Beschrijving | +|---|---| +| `GET /api/assets/ranked` | Top 6 ranked assets (gecached 60s) | +| `GET /api/assets/{symbol}` | Detail + candles voor één asset | +| `GET /api/news/{coin}` | Nieuws + sentiment voor één coin | +| `GET /api/health` | Health check | + +## Projectstructuur + +``` +crypto-bot/ +├── backend/ +│ ├── main.py # FastAPI routes + caching +│ ├── exchange.py # Kraken connector (CCXT) +│ ├── analysis.py # TA indicatoren + scoring +│ ├── sentiment.py # CryptoPanic sentiment engine +│ ├── ranker.py # Asset ranker (TA + sentiment) +│ └── requirements.txt +├── frontend/ +│ ├── index.html # Dashboard +│ └── nginx.conf +├── docker-compose.yml +└── .env.example +``` + +## Indicatoren gebruikt + +- **RSI (14)** — koopzone detecitie (40-55) +- **EMA 9/21/50** — trend richting en crossover +- **VWAP** — prijs vs. institutioneel gemiddelde +- **Bollinger Bands (20, 2σ)** — volatiliteitsregime +- **ATR (14)** — volatiliteitsfilter (>5% = skip) +- **Volume ratio** — bevestiging van beweging + +## Volgende stap + +De trading bot zelf (order execution, DCA-in logica, R:R management) +is bewust nog niet ingebouwd. Valideer eerst de signalen via dit dashboard +voordat je live orders plaatst. diff --git a/analysis.py b/analysis.py new file mode 100644 index 0000000..a513243 --- /dev/null +++ b/analysis.py @@ -0,0 +1,209 @@ +""" +analysis.py — Technische analyse engine +Berekent: RSI, EMA, VWAP, Bollinger Bands, ATR, volume score +Geeft per asset een opportunity score terug (0-100) +""" +import pandas as pd +import pandas_ta as ta +import numpy as np +import logging +from dataclasses import dataclass + +logger = logging.getLogger(__name__) + + +@dataclass +class AssetScore: + symbol: str + opportunity_score: float # 0-100, hoger = interessanter + rsi: float + rsi_signal: str # "oversold" | "neutral" | "overbought" + ema9: float + ema21: float + ema_signal: str # "bullish" | "bearish" | "neutral" + vwap: float + vwap_signal: str # "above" | "below" + bb_position: float # 0.0 = lower band, 1.0 = upper band + atr_pct: float # ATR als % van prijs + volume_score: float # 0-100, relatief volume + trend_strength: str # "strong" | "moderate" | "weak" + tradeable: bool # False als ATR te hoog of signalen ontbreken + + +def calculate_indicators(df: pd.DataFrame) -> dict: + """ + Bereken alle indicatoren op een OHLCV dataframe. + Geeft dict terug met indicator waarden. + """ + if df.empty or len(df) < 30: + return {} + + try: + close = df["close"] + high = df["high"] + low = df["low"] + volume = df["volume"] + + # --- RSI --- + rsi_series = ta.rsi(close, length=14) + rsi = float(rsi_series.iloc[-1]) if rsi_series is not None else 50.0 + + # --- EMA --- + ema9 = float(ta.ema(close, length=9).iloc[-1]) + ema21 = float(ta.ema(close, length=21).iloc[-1]) + ema50 = float(ta.ema(close, length=50).iloc[-1]) + + # EMA50 slope: positief = uptrend + ema50_series = ta.ema(close, length=50) + ema50_slope = float(ema50_series.iloc[-1] - ema50_series.iloc[-5]) if len(ema50_series) >= 5 else 0 + + # --- VWAP --- + typical_price = (high + low + close) / 3 + cumulative_tp_vol = (typical_price * volume).cumsum() + cumulative_vol = volume.cumsum() + vwap = float((cumulative_tp_vol / cumulative_vol).iloc[-1]) + + # --- Bollinger Bands --- + bbands = ta.bbands(close, length=20, std=2) + if bbands is not None and not bbands.empty: + bb_upper = float(bbands.iloc[-1, 2]) # BBU + bb_lower = float(bbands.iloc[-1, 0]) # BBL + bb_mid = float(bbands.iloc[-1, 1]) # BBM + current = float(close.iloc[-1]) + bb_range = bb_upper - bb_lower + bb_pos = (current - bb_lower) / bb_range if bb_range > 0 else 0.5 + else: + bb_upper = bb_lower = bb_mid = 0 + bb_pos = 0.5 + + # --- ATR --- + atr_series = ta.atr(high, low, close, length=14) + atr = float(atr_series.iloc[-1]) if atr_series is not None else 0 + atr_pct = (atr / float(close.iloc[-1])) * 100 + + # --- Volume score --- + vol_mean = float(volume.rolling(20).mean().iloc[-1]) + vol_current = float(volume.iloc[-1]) + vol_ratio = vol_current / vol_mean if vol_mean > 0 else 1.0 + vol_score = min(100, vol_ratio * 50) # Genormaliseerd 0-100 + + return { + "rsi": rsi, + "ema9": ema9, + "ema21": ema21, + "ema50": ema50, + "ema50_slope": ema50_slope, + "vwap": vwap, + "bb_upper": bb_upper, + "bb_lower": bb_lower, + "bb_mid": bb_mid, + "bb_position": bb_pos, + "atr": atr, + "atr_pct": atr_pct, + "volume_score": vol_score, + "close": float(close.iloc[-1]), + } + + except Exception as e: + logger.error(f"Indicator berekening mislukt: {e}") + return {} + + +def score_asset(symbol: str, indicators: dict) -> AssetScore: + """ + Vertaal indicator waarden naar een opportunity score (0-100). + Hogere score = interessanter voor trading. + + Scoring breakdown: + - RSI in koopzone (40-55): +25 punten + - EMA bullish crossover: +20 punten + - Prijs onder VWAP: +20 punten + - BB positie laag (< 0.4): +15 punten + - Volume boven gemiddeld: +10 punten + - Uptrend (EMA50 slope +): +10 punten + Totaal max: 100 + """ + if not indicators: + return AssetScore( + symbol=symbol, opportunity_score=0, rsi=0, rsi_signal="unknown", + ema9=0, ema21=0, ema_signal="unknown", vwap=0, vwap_signal="unknown", + bb_position=0.5, atr_pct=0, volume_score=0, + trend_strength="unknown", tradeable=False + ) + + score = 0.0 + rsi = indicators["rsi"] + close = indicators["close"] + + # RSI beoordeling + if 40 <= rsi <= 55: + score += 25 + rsi_signal = "koopzone" + elif rsi < 30: + score += 15 # Oversold maar gevaarlijk — minder punten + rsi_signal = "oversold" + elif rsi > 70: + rsi_signal = "overbought" + else: + score += 10 + rsi_signal = "neutral" + + # EMA crossover + ema9 = indicators["ema9"] + ema21 = indicators["ema21"] + if ema9 > ema21: + score += 20 + ema_signal = "bullish" + elif ema9 < ema21 * 0.995: + ema_signal = "bearish" + else: + score += 8 + ema_signal = "neutral" + + # VWAP positie + vwap = indicators["vwap"] + if close < vwap: + score += 20 + vwap_signal = "onder_vwap" # Potentiële reversion kans + else: + score += 5 + vwap_signal = "boven_vwap" + + # Bollinger Band positie + bb_pos = indicators["bb_position"] + if bb_pos < 0.25: + score += 15 # Dicht bij lower band + elif bb_pos < 0.4: + score += 10 + + # Volume + vol_score = indicators["volume_score"] + score += (vol_score / 100) * 10 + + # EMA50 trend + if indicators["ema50_slope"] > 0: + score += 10 + trend_strength = "strong" if indicators["ema50_slope"] > indicators["close"] * 0.001 else "moderate" + else: + trend_strength = "weak" + + # ATR filter — te volatiel = niet tradeable + atr_pct = indicators["atr_pct"] + tradeable = atr_pct < 5.0 # Max 5% ATR + + return AssetScore( + symbol=symbol, + opportunity_score=round(min(100, score), 1), + rsi=round(rsi, 1), + rsi_signal=rsi_signal, + ema9=round(ema9, 4), + ema21=round(ema21, 4), + ema_signal=ema_signal, + vwap=round(vwap, 4), + vwap_signal=vwap_signal, + bb_position=round(bb_pos, 3), + atr_pct=round(atr_pct, 2), + volume_score=round(vol_score, 1), + trend_strength=trend_strength, + tradeable=tradeable, + ) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8537f16 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,50 @@ +services: + backend: + build: ./backend + container_name: crypto-bot-backend + restart: unless-stopped + environment: + - KRAKEN_API_KEY=${KRAKEN_API_KEY} + - KRAKEN_SECRET=${KRAKEN_SECRET} + - CRYPTOPANIC_API_KEY=${CRYPTOPANIC_API_KEY} + - REDIS_URL=redis://redis:6379 + volumes: + - ./data:/app/data + depends_on: + - redis + labels: + - "traefik.enable=true" + - "traefik.http.routers.crypto-api.rule=Host(`crypto-api.${DOMAIN}`)" + - "traefik.http.routers.crypto-api.entrypoints=websecure" + - "traefik.http.routers.crypto-api.tls=true" + - "traefik.http.services.crypto-api.loadbalancer.server.port=8000" + - "traefik.http.routers.crypto-api.middlewares=authentik@docker" + + frontend: + build: ./frontend + container_name: crypto-bot-frontend + restart: unless-stopped + depends_on: + - backend + labels: + - "traefik.enable=true" + - "traefik.http.routers.crypto-dash.rule=Host(`crypto.${DOMAIN}`)" + - "traefik.http.routers.crypto-dash.entrypoints=websecure" + - "traefik.http.routers.crypto-dash.tls=true" + - "traefik.http.services.crypto-dash.loadbalancer.server.port=80" + - "traefik.http.routers.crypto-dash.middlewares=authentik@docker" + + redis: + image: redis:7-alpine + container_name: crypto-bot-redis + restart: unless-stopped + volumes: + - redis_data:/data + +volumes: + redis_data: + +networks: + default: + external: true + name: traefik-proxy