feat: graceful fallback to single-voter when voter_a hits rate limit
- main.py: detect rate_limit_exhausted from voter_a, switch to single(voter_b) and notify Discord once per session - discord.py: add notify_voter_fallback() with yellow warning embed - config.py: add voter_fallback to default notify_on list - dashboard/app.py: show warning banner when fallback mode active (last 2h) - CLAUDE.md: refresh architecture docs, fix stale google-genai migration note Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2ec58d01f7
commit
680a4fcc5f
118
CLAUDE.md
118
CLAUDE.md
@ -1,71 +1,96 @@
|
|||||||
# aitrader — Projekt-Kontext für Claude
|
# CLAUDE.md
|
||||||
|
|
||||||
Diese Datei wird von Claude Code automatisch geladen. Sie beschreibt das Projekt so kompakt wie möglich, damit jede neue Session — lokal oder auf dem VPS — sofort produktiv ist.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## Was das ist
|
## Was das ist
|
||||||
|
|
||||||
Crypto-Trading-Bot, der **alle 15 Minuten** Marktdaten von **Kraken Futures Demo** holt, sie an **Gemini + Claude im Ensemble** schickt und nur bei Konsens (beide BUY/SELL ≥ 0.6 confidence) einen Trade ausführt. Alle Decisions/Trades landen in SQLite, ein Streamlit-Dashboard visualisiert PnL, ein Discord-Webhook benachrichtigt über Trades/Errors/Daily-Summary.
|
Crypto-Trading-Bot, der **alle 15 Minuten** Marktdaten von **Kraken Futures Demo** holt, sie an **zwei LLM-Voter im Ensemble** schickt und nur bei Konsens (beide BUY/SELL ≥ 0.6 confidence) einen Trade ausführt. Alle Decisions/Trades landen in SQLite, ein Streamlit-Dashboard visualisiert PnL, ein Discord-Webhook benachrichtigt über Trades/Errors/Daily-Summary.
|
||||||
|
|
||||||
**Zweck:** Empirisch herausfinden, ob LLM-basiertes Trading profitabel wäre — ohne reales Risiko (Demo-Account).
|
**Zweck:** Empirisch herausfinden, ob LLM-basiertes Trading profitabel wäre — ohne reales Risiko (Demo-Account).
|
||||||
|
|
||||||
## Architektur (kurz)
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Tests + Lint
|
||||||
|
.venv/bin/pytest -q # Unit-Tests
|
||||||
|
.venv/bin/pytest tests/test_indicators.py # einzelner Test
|
||||||
|
.venv/bin/ruff check src/ # Linting
|
||||||
|
|
||||||
|
# Smoke-Tests (erfordern .env mit echten Keys)
|
||||||
|
.venv/bin/python scripts/smoke_kraken.py # Demo-API erreichbar?
|
||||||
|
.venv/bin/python scripts/smoke_ai.py # AI-Voter liefern JSON?
|
||||||
|
.venv/bin/python scripts/smoke_discord.py # Webhook erreichbar?
|
||||||
|
|
||||||
|
# Ausführen
|
||||||
|
.venv/bin/python -m aitrader.main --once # Ein vollständiger Tick
|
||||||
|
aitrader --once # alternativ via installiertem Entrypoint
|
||||||
|
|
||||||
|
# Dashboard (lokal)
|
||||||
|
.venv/bin/streamlit run src/aitrader/dashboard/app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Package-Manager: `uv`. Dependencies installieren mit `uv pip install -e ".[dev]"`.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
```
|
```
|
||||||
src/aitrader/
|
src/aitrader/
|
||||||
├── main.py # Scheduler (APScheduler), --once Modus
|
├── main.py # run_tick(): vollständiger Tick-Flow von oben nach unten
|
||||||
├── config.py # YAML + ENV → Pydantic Settings
|
├── config.py # YAML (config.yaml) + .env → Pydantic Settings (lru_cache)
|
||||||
├── logging_setup.py # structlog
|
|
||||||
├── exchange/
|
├── exchange/
|
||||||
│ ├── kraken.py # ccxt Wrapper, sandbox=True für Demo
|
│ ├── kraken.py # ccxt-Wrapper, sandbox=True für Demo
|
||||||
│ └── market_data.py # OHLCV + Orderbook + Ticker Snapshots
|
│ └── market_data.py # OHLCV + Orderbook + Ticker → MarketSnapshot
|
||||||
├── features/
|
├── features/
|
||||||
│ ├── indicators.py # RSI/MACD/EMA/ATR (eigene Implementierung, kein pandas-ta)
|
│ ├── indicators.py # RSI/MACD/EMA/ATR — pandas-only, kein pandas-ta
|
||||||
│ └── orderbook.py # Spread, Imbalance
|
│ └── orderbook.py # Spread, Imbalance
|
||||||
├── news/sentiment.py # CryptoPanic + VADER (optional)
|
├── news/sentiment.py # CryptoPanic + VADER (optional)
|
||||||
├── ai/
|
├── ai/
|
||||||
│ ├── prompt.py # Prompt-Builder (System + User)
|
│ ├── registry.py # VoterConfig → konkreter Client (make_voter)
|
||||||
│ ├── schema.py # TradeDecision Pydantic + JSON-Schema
|
│ ├── prompt.py # System- + User-Prompt-Builder
|
||||||
│ ├── gemini.py # google-generativeai, response_schema
|
│ ├── schema.py # TradeDecision Pydantic-Modell + JSON-Schema
|
||||||
|
│ ├── gemini.py # google-genai SDK, response_schema
|
||||||
│ ├── claude.py # anthropic SDK, Tool-Use für strukturierten Output
|
│ ├── claude.py # anthropic SDK, Tool-Use für strukturierten Output
|
||||||
│ └── ensemble.py # Konsens-Logik
|
│ ├── openai_compat.py # OpenAI-kompatible Endpunkte (groq/deepseek/xai/openrouter/ollama)
|
||||||
|
│ └── ensemble.py # combine() / single() — HOLD bei Disagreement oder zu niedriger Confidence
|
||||||
├── trader/
|
├── trader/
|
||||||
│ ├── risk.py # Position-Cap, Daily-Loss-Limit, max offene Positionen
|
│ ├── risk.py # evaluate(): Position-Cap, Daily-Loss-Limit, max offene Positionen
|
||||||
│ ├── executor.py # Market-Order auf Kraken Demo + DB-Eintrag
|
│ ├── executor.py # Market-Order auf Kraken Demo + DB-Eintrag
|
||||||
│ └── portfolio.py # SL/TP-Check, Equity-Snapshot, Trade-Close
|
│ └── portfolio.py # SL/TP-Check, Equity-Snapshot, Trade-Close
|
||||||
├── notify/discord.py # Webhook-Notifier (Embeds)
|
├── notify/discord.py # Webhook-Notifier (Embeds)
|
||||||
├── storage/
|
├── storage/
|
||||||
│ ├── models.py # SQLModel-Tabellen: Decision, Trade, EquitySnapshot
|
│ ├── models.py # SQLModel-Tabellen: Decision, Trade, EquitySnapshot
|
||||||
│ └── db.py # SQLite-Engine + create_all
|
│ └── db.py # SQLite-Engine + create_all
|
||||||
└── dashboard/app.py # Streamlit
|
└── dashboard/app.py # Streamlit — absolute Imports wegen Script-Ausführung
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Tick-Flow** (`main.py:run_tick`): Marktdaten → Features/Indikatoren → Orderbook → News/Sentiment → Prompt-Builder → Voter A + B → Ensemble → DB → Discord → Risk-Check → Execute.
|
||||||
|
|
||||||
## AI-Voter
|
## AI-Voter
|
||||||
|
|
||||||
Statt fest verdrahtetem Gemini+Claude gibt es zwei generische **Voter-Slots** in `config.yaml` (`ai.voter_a`, `ai.voter_b`). Provider werden via `ai/registry.py` instanziiert. Default: `voter_a=gemini`, `voter_b=groq`. Wechsel auf andere Provider braucht nur Config-Änderung + den passenden ENV-Key.
|
Zwei generische **Voter-Slots** in `config.yaml` (`ai.voter_a`, `ai.voter_b`). Provider werden via `ai/registry.py` instanziiert. Default: `voter_a=gemini`, `voter_b=groq`. Wechsel braucht nur Config-Änderung + den passenden ENV-Key.
|
||||||
|
|
||||||
| provider | base_url | ENV-Key | Beispiel-Modell |
|
| provider | ENV-Key | Beispiel-Modell |
|
||||||
|---|---|---|---|
|
|---|---|---|
|
||||||
| gemini | (Google SDK) | `GEMINI_API_KEY` | `gemini-2.0-flash` |
|
| gemini | `GEMINI_API_KEY` | `gemini-2.0-flash` |
|
||||||
| claude | (Anthropic SDK) | `ANTHROPIC_API_KEY` | `claude-haiku-4-5-20251001` |
|
| claude | `ANTHROPIC_API_KEY` | `claude-haiku-4-5-20251001` |
|
||||||
| groq | api.groq.com | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
| groq | `GROQ_API_KEY` | `llama-3.3-70b-versatile` |
|
||||||
| deepseek | api.deepseek.com | `DEEPSEEK_API_KEY` | `deepseek-chat` |
|
| deepseek | `DEEPSEEK_API_KEY` | `deepseek-chat` |
|
||||||
| xai | api.x.ai | `XAI_API_KEY` | `grok-4-fast` |
|
| xai | `XAI_API_KEY` | `grok-4-fast` |
|
||||||
| openrouter | openrouter.ai | `OPENROUTER_API_KEY` | `meta-llama/llama-3.3-70b-instruct:free` |
|
| openrouter | `OPENROUTER_API_KEY` | `meta-llama/llama-3.3-70b-instruct:free` |
|
||||||
| ollama | localhost:11434 | (kein Key) | `llama3.3` |
|
| ollama | (kein Key) | `llama3.3` |
|
||||||
|
|
||||||
Modus `ai.mode: single` → nur `voter_a` wird gefragt, kein Konsens nötig.
|
`ai.mode: single` → nur `voter_a`, kein Konsens nötig.
|
||||||
|
|
||||||
## Wichtige Regeln & Gotchas
|
## Wichtige Regeln & Gotchas
|
||||||
|
|
||||||
- **Niemals** `exchange.sandbox` in `config.yaml` auf `false` ändern, ohne dass der User das explizit will. Das ist die Schutzlinie zum Live-Geld.
|
- **Niemals** `exchange.sandbox: false` ohne explizite User-Bestätigung. Das ist die einzige Schutzlinie zum Live-Geld.
|
||||||
- **Keine API-Keys** ins Repo. Alle gehen via `.env` → `config.py:get_settings()`.
|
- `exchange.paper_only: true` blockt Trade-Execution zusätzlich, auch bei sandbox=true.
|
||||||
- **Indikatoren** sind selbst implementiert (pandas-only) weil pandas-ta + numpy 2 broken war.
|
- **Indikatoren** sind selbst implementiert (pandas-only) weil pandas-ta + numpy 2 inkompatibel war.
|
||||||
- **Pair-Symbole**: Kraken Futures nutzt teils `PI_XBTUSD`-Format. Wenn `BTC/EUR` nicht gefunden wird, ist das die Ursache — `pairs:` in `config.yaml` anpassen.
|
- **Pair-Symbole**: Kraken Futures nutzt teils `PI_XBTUSD`-Format statt `BTC/EUR` — bei Symbol-Fehlern `pairs:` in `config.yaml` anpassen.
|
||||||
- **Ensemble-HOLD ist gewollt**: Bei Disagreement oder zu niedriger Confidence wird absichtlich nicht getradet.
|
- **Ensemble-HOLD ist gewollt**: Bei Disagreement oder zu niedriger Confidence wird absichtlich nicht getradet.
|
||||||
- **Decision-Notify ist standardmäßig AUS** in `config.yaml` (`discord.notify_on` enthält `decision` nicht), weil 192 Embeds/Tag spammig wären.
|
- **`discord.notify_on`** enthält `decision` standardmäßig nicht (192 Embeds/Tag wären spammig). Nur `trade_open`, `trade_close`, `error` etc.
|
||||||
- **SQLite-DB unter `data/aitrader.db`** (lokal) bzw. `/opt/aitrader/data/aitrader.db` (Server) — nicht löschen, da Backtest-Replay drauf basiert.
|
- **Daily-Loss-Limit (5%)** pausiert Trading automatisch — Reset um 00:00 UTC.
|
||||||
- **Daily-Loss-Limit (5%)** pausiert Trading automatisch — Reset um 00:00 UTC weil `daily_pnl_eur` nur seit Tagesbeginn summiert.
|
- **`get_settings()` ist `lru_cache`** — Änderungen an `.env`/`config.yaml` nach Start werden nicht übernommen ohne Neustart.
|
||||||
- **`run_in_background` für lange Operationen**: Smoke-Tests und Bot sind interaktiv okay, aber wenn du den Bot im Dauerbetrieb startest mach das via `systemctl`, nicht im Vordergrund.
|
|
||||||
|
|
||||||
## Wo läuft was
|
## Wo läuft was
|
||||||
|
|
||||||
@ -73,23 +98,13 @@ Modus `ai.mode: single` → nur `voter_a` wird gefragt, kein Konsens nötig.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Lokal (Dev) | `~/code/aitrader` | `.venv/bin/python -m aitrader.main --once` |
|
| Lokal (Dev) | `~/code/aitrader` | `.venv/bin/python -m aitrader.main --once` |
|
||||||
| Server (Prod) | `/opt/aitrader` | `systemctl status aitrader` |
|
| Server (Prod) | `/opt/aitrader` | `systemctl status aitrader` |
|
||||||
| DB | `data/aitrader.db` | SQLite — `sqlite3 data/aitrader.db ".tables"` |
|
| DB | `data/aitrader.db` | `sqlite3 data/aitrader.db ".tables"` |
|
||||||
| Logs (Server) | journald | `journalctl -u aitrader -f` |
|
| Logs (Server) | journald | `journalctl -u aitrader -f` |
|
||||||
| Dashboard (Server) | `http://<tailscale-host>:8501` | nur via Tailscale |
|
| Dashboard (Server) | `http://<tailscale-host>:8501` | nur via Tailscale |
|
||||||
|
|
||||||
## Test + Verifikation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
.venv/bin/pytest -q # Unit-Tests
|
|
||||||
.venv/bin/python scripts/smoke_kraken.py # Demo-API erreichbar?
|
|
||||||
.venv/bin/python scripts/smoke_ai.py # Gemini+Claude liefern JSON?
|
|
||||||
.venv/bin/python scripts/smoke_discord.py # Webhook erreichbar?
|
|
||||||
.venv/bin/python -m aitrader.main --once # Ein vollständiger Tick
|
|
||||||
```
|
|
||||||
|
|
||||||
## Update-Workflow (lokal → Server)
|
## Update-Workflow (lokal → Server)
|
||||||
|
|
||||||
1. Lokal Änderung → `git commit && git push`
|
1. `git commit && git push`
|
||||||
2. Auf Server: `cd /opt/aitrader && sudo -u aitrader git pull`
|
2. Auf Server: `cd /opt/aitrader && sudo -u aitrader git pull`
|
||||||
3. Bei neuen Dependencies: `sudo -u aitrader /home/aitrader/.local/bin/uv pip install -e .`
|
3. Bei neuen Dependencies: `sudo -u aitrader /home/aitrader/.local/bin/uv pip install -e .`
|
||||||
4. `sudo systemctl restart aitrader aitrader-dashboard`
|
4. `sudo systemctl restart aitrader aitrader-dashboard`
|
||||||
@ -97,17 +112,16 @@ Modus `ai.mode: single` → nur `voter_a` wird gefragt, kein Konsens nötig.
|
|||||||
|
|
||||||
Siehe `DEPLOY.md` für vollen Setup vom Frischserver inkl. Tailscale.
|
Siehe `DEPLOY.md` für vollen Setup vom Frischserver inkl. Tailscale.
|
||||||
|
|
||||||
## Ausstehende / sinnvolle Erweiterungen (nicht implementiert)
|
## Ausstehende Erweiterungen (nicht implementiert)
|
||||||
|
|
||||||
- Backtest-Modus (historische Daten replay durch Decisions-Tabelle)
|
- Backtest-Modus (historische Daten replay durch Decisions-Tabelle)
|
||||||
- Mehr Pairs / dynamisches Universe-Selection
|
- Mehr Pairs / dynamisches Universe-Selection
|
||||||
- Position-Sizing per Kelly oder Vol-Targeting
|
- Position-Sizing per Kelly oder Vol-Targeting
|
||||||
- Prompt-Tuning A/B (zwei Prompts, vergleichen welcher besser performt)
|
- Prompt-Tuning A/B (zwei Prompts, vergleichen welcher besser performt)
|
||||||
- Migration weg von `google-generativeai` (deprecated) hin zu `google-genai`
|
|
||||||
|
|
||||||
## Wenn Claude diese Datei liest
|
## Für Claude
|
||||||
|
|
||||||
- Kein Refactor "weil schöner" — der Code ist bewusst kompakt gehalten.
|
- Kein Refactor "weil schöner" — der Code ist bewusst kompakt gehalten.
|
||||||
- Bei Fragen zu Trading-Logik: `ai/ensemble.py` und `trader/risk.py` sind die zwei kritischen Dateien.
|
- Bei Trading-Logik: `ai/ensemble.py` und `trader/risk.py` sind die zwei kritischen Dateien.
|
||||||
- Bei Fragen zu Daten-Pipeline: `main.py:run_tick` ist der Flow von oben nach unten.
|
- Bei Daten-Pipeline: `main.py:run_tick` ist der Flow von oben nach unten.
|
||||||
- Niemals echtes Geld ohne explizite User-Bestätigung — das Projekt ist Demo-only.
|
- Niemals echtes Geld ohne explizite User-Bestätigung — das Projekt ist Demo-only.
|
||||||
|
|||||||
@ -57,6 +57,7 @@ class DiscordConfig(BaseModel):
|
|||||||
"error",
|
"error",
|
||||||
"daily_summary",
|
"daily_summary",
|
||||||
"news_alert",
|
"news_alert",
|
||||||
|
"voter_fallback",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
news_sentiment_threshold: float = 0.4
|
news_sentiment_threshold: float = 0.4
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
"""Streamlit-Dashboard: Equity, Trades, AI-Vergleich."""
|
"""Streamlit-Dashboard: Equity, Trades, AI-Vergleich."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import plotly.express as px
|
import plotly.express as px
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
@ -26,6 +28,24 @@ tab_overview, tab_trades, tab_decisions, tab_ai = st.tabs(
|
|||||||
)
|
)
|
||||||
|
|
||||||
with tab_overview:
|
with tab_overview:
|
||||||
|
# Fallback-Status: Decisions der letzten 2h mit rate_limit_exhausted
|
||||||
|
cutoff = datetime.now(timezone.utc) - timedelta(hours=2)
|
||||||
|
with dbm.session(settings.db_path) as _s:
|
||||||
|
_fb_rows = _s.exec(
|
||||||
|
select(Decision)
|
||||||
|
.where(Decision.voter_a_reasoning == "rate_limit_exhausted")
|
||||||
|
.where(Decision.ts >= cutoff)
|
||||||
|
).all()
|
||||||
|
if _fb_rows:
|
||||||
|
_providers = {r.voter_a_provider for r in _fb_rows}
|
||||||
|
_fallback = {r.voter_b_provider for r in _fb_rows}
|
||||||
|
st.warning(
|
||||||
|
f"⚠️ **Fallback-Modus aktiv** — {', '.join(_providers)} hat Rate-Limit erreicht. "
|
||||||
|
f"Bot läuft mit {', '.join(_fallback)} als Single-Voter "
|
||||||
|
f"({len(_fb_rows)} Ticks in den letzten 2h betroffen).",
|
||||||
|
icon="⚠️",
|
||||||
|
)
|
||||||
|
|
||||||
st.subheader("Equity-Kurve")
|
st.subheader("Equity-Kurve")
|
||||||
df_eq = load_df(select(EquitySnapshot).order_by(EquitySnapshot.ts))
|
df_eq = load_df(select(EquitySnapshot).order_by(EquitySnapshot.ts))
|
||||||
if df_eq.empty:
|
if df_eq.empty:
|
||||||
|
|||||||
@ -28,6 +28,9 @@ from .trader.executor import execute_trade
|
|||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
# Provider die in dieser Session bereits ihr Limit erreicht haben (Reset bei Neustart).
|
||||||
|
_exhausted_providers: set[str] = set()
|
||||||
|
|
||||||
|
|
||||||
def _current_position_summary(settings: Settings, symbol: str) -> dict | None:
|
def _current_position_summary(settings: Settings, symbol: str) -> dict | None:
|
||||||
open_trades = portfolio.open_trades_for_symbol(settings, symbol)
|
open_trades = portfolio.open_trades_for_symbol(settings, symbol)
|
||||||
@ -92,6 +95,13 @@ def run_tick(
|
|||||||
discord.notify_error(settings, f"voter_a={voter_a.provider} ({symbol})", str(e))
|
discord.notify_error(settings, f"voter_a={voter_a.provider} ({symbol})", str(e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
voter_a_exhausted = a.reasoning == "rate_limit_exhausted"
|
||||||
|
if voter_a_exhausted and voter_a.provider not in _exhausted_providers:
|
||||||
|
_exhausted_providers.add(voter_a.provider)
|
||||||
|
fb = voter_b.provider if voter_b else "—"
|
||||||
|
log.warning("voter_a.rate_limit_fallback", provider=voter_a.provider, fallback=fb)
|
||||||
|
discord.notify_voter_fallback(settings, voter_a.provider, fb)
|
||||||
|
|
||||||
if voter_b is not None:
|
if voter_b is not None:
|
||||||
try:
|
try:
|
||||||
b = voter_b.decide(prompt)
|
b = voter_b.decide(prompt)
|
||||||
@ -99,7 +109,7 @@ def run_tick(
|
|||||||
log.error("voter_b.failed", provider=voter_b.provider, error=str(e))
|
log.error("voter_b.failed", provider=voter_b.provider, error=str(e))
|
||||||
discord.notify_error(settings, f"voter_b={voter_b.provider} ({symbol})", str(e))
|
discord.notify_error(settings, f"voter_b={voter_b.provider} ({symbol})", str(e))
|
||||||
continue
|
continue
|
||||||
result = combine(a, b, settings.ai.min_confidence)
|
result = single(b, settings.ai.min_confidence) if voter_a_exhausted else combine(a, b, settings.ai.min_confidence)
|
||||||
else:
|
else:
|
||||||
b = a
|
b = a
|
||||||
result = single(a, settings.ai.min_confidence)
|
result = single(a, settings.ai.min_confidence)
|
||||||
|
|||||||
@ -189,6 +189,22 @@ def notify_daily_summary(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_voter_fallback(settings: Settings, exhausted_provider: str, fallback_provider: str) -> None:
|
||||||
|
if not _should(settings, "voter_fallback"):
|
||||||
|
return
|
||||||
|
_post(
|
||||||
|
settings,
|
||||||
|
{
|
||||||
|
"title": f"⚠️ {exhausted_provider} Rate-Limit — Fallback auf {fallback_provider}",
|
||||||
|
"color": COLOR_YELLOW,
|
||||||
|
"description": (
|
||||||
|
f"`{exhausted_provider}` hat das API-Tageslimit erreicht.\n"
|
||||||
|
f"Bot läuft jetzt im **Single-Voter-Modus** mit `{fallback_provider}`."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def notify_news_alert(settings: Settings, symbol: str, headlines: list[dict[str, Any]], avg_sentiment: float) -> None:
|
def notify_news_alert(settings: Settings, symbol: str, headlines: list[dict[str, Any]], avg_sentiment: float) -> None:
|
||||||
"""Postet wenn |avg_sentiment| über Schwellwert liegt."""
|
"""Postet wenn |avg_sentiment| über Schwellwert liegt."""
|
||||||
if not _should(settings, "news_alert"):
|
if not _should(settings, "news_alert"):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user