Upload files to "/"
This commit is contained in:
@@ -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
|
||||||
+14
@@ -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"]
|
||||||
@@ -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.
|
||||||
+209
@@ -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,
|
||||||
|
)
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user