From be1adfb94c6926e08094c53d35099babe177f46a Mon Sep 17 00:00:00 2001 From: sylyx Date: Thu, 14 May 2026 08:30:45 +0200 Subject: [PATCH] feat: realistic budget, EUR trade size in Discord, confidence analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config: starting equity 10k→100 EUR, max_position_pct 20%→10%, min_order 25→5 EUR - discord: show invested EUR amount (💶 Einsatz) in trade_open embed - dashboard: add Analytics tab with win-rate by confidence bucket Co-Authored-By: Claude Sonnet 4.6 --- config.yaml | 6 ++-- src/aitrader/dashboard/app.py | 55 +++++++++++++++++++++++++++++++-- src/aitrader/notify/discord.py | 6 ++-- src/aitrader/trader/executor.py | 2 +- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/config.yaml b/config.yaml index 67b70d2..e40e3f6 100644 --- a/config.yaml +++ b/config.yaml @@ -3,15 +3,15 @@ interval_minutes: 15 timeframes: [15m, 1h, 4h] ohlcv_limit: 200 -starting_equity_eur: 10000 # Demo-Startkapital (nur für lokale Buchführung) +starting_equity_eur: 100 # Demo-Startkapital (nur für lokale Buchführung) risk: - max_position_pct: 0.20 + max_position_pct: 0.10 max_open_positions: 2 stop_loss_atr_mult: 2.0 take_profit_atr_mult: 3.0 daily_loss_limit_pct: 0.05 - min_order_eur: 25 + min_order_eur: 5 ai: mode: ensemble # ensemble (beide Voter müssen sich einig sein) | single (nur voter_a) diff --git a/src/aitrader/dashboard/app.py b/src/aitrader/dashboard/app.py index 85a7bfb..6ae0075 100644 --- a/src/aitrader/dashboard/app.py +++ b/src/aitrader/dashboard/app.py @@ -23,8 +23,8 @@ def load_df(stmt) -> pd.DataFrame: return pd.DataFrame([r.model_dump() for r in rows]) -tab_overview, tab_trades, tab_decisions, tab_ai = st.tabs( - ["Overview", "Trades", "Decisions", "AI-Vergleich"] +tab_overview, tab_trades, tab_decisions, tab_ai, tab_analytics = st.tabs( + ["Overview", "Trades", "Decisions", "AI-Vergleich", "Analytics"] ) with tab_overview: @@ -110,3 +110,54 @@ with tab_ai: st.caption( "Hinweis: Nur Ticks bei denen beide BUY/SELL mit ausreichender Confidence stimmen führen zu Trades." ) + +with tab_analytics: + st.subheader("Confidence-Analytics") + df_d = load_df(select(Decision)) + df_t = load_df(select(Trade)) + if df_d.empty or df_t.empty: + st.info("Noch zu wenig Daten — mindestens ein abgeschlossener Trade nötig.") + else: + # Decisions mit abgeschlossenen Trades joinen + closed = df_t[df_t["status"] == "closed"][["decision_id", "pnl_eur"]].dropna() + merged = df_d.merge(closed, left_on="id", right_on="decision_id", how="inner") + + if merged.empty: + st.info("Noch keine abgeschlossenen Trades mit verknüpften Decisions.") + else: + merged["won"] = merged["pnl_eur"] > 0 + merged["avg_confidence"] = (merged["voter_a_confidence"] + merged["voter_b_confidence"]) / 2 + + # Confidence-Buckets + bins = [0.0, 0.65, 0.75, 0.85, 1.01] + labels = ["0.60–0.65", "0.65–0.75", "0.75–0.85", "0.85–1.0"] + merged["conf_bucket"] = pd.cut(merged["avg_confidence"], bins=bins, labels=labels) + bucket_stats = ( + merged.groupby("conf_bucket", observed=True) + .agg(trades=("won", "count"), win_rate=("won", "mean"), avg_pnl=("pnl_eur", "mean")) + .reset_index() + ) + bucket_stats["win_rate"] = (bucket_stats["win_rate"] * 100).round(1) + bucket_stats["avg_pnl"] = bucket_stats["avg_pnl"].round(2) + + st.markdown("#### Win-Rate nach Confidence-Bucket (Ø beider Voter)") + st.dataframe(bucket_stats, use_container_width=True) + + fig_conf = px.bar( + bucket_stats, + x="conf_bucket", + y="win_rate", + text="win_rate", + labels={"conf_bucket": "Ø Confidence", "win_rate": "Win-Rate (%)"}, + color="win_rate", + color_continuous_scale=["#E74C3C", "#F1C40F", "#2ECC71"], + ) + fig_conf.update_traces(texttemplate="%{text:.1f}%", textposition="outside") + st.plotly_chart(fig_conf, use_container_width=True) + + # Gesamtstatistik + st.markdown("#### Gesamt") + col1, col2, col3 = st.columns(3) + col1.metric("Trades mit Decision", len(merged)) + col2.metric("Ø Win-Rate", f"{merged['won'].mean() * 100:.1f}%") + col3.metric("Ø Confidence bei Wins", f"{merged[merged['won']]['avg_confidence'].mean():.2f}" if merged["won"].any() else "—") diff --git a/src/aitrader/notify/discord.py b/src/aitrader/notify/discord.py index 2d0f672..2a1c42c 100644 --- a/src/aitrader/notify/discord.py +++ b/src/aitrader/notify/discord.py @@ -91,10 +91,11 @@ def notify_decision(settings: Settings, symbol: str, ensemble, label_a: str, lab ) -def notify_trade_opened(settings: Settings, trade) -> None: +def notify_trade_opened(settings: Settings, trade, qty_eur: float = 0.0) -> None: if not _should(settings, "trade_open"): return side_emoji = "📈" if trade.side == "buy" else "📉" + invested = qty_eur or trade.qty * trade.entry_price _post( settings, { @@ -102,8 +103,9 @@ def notify_trade_opened(settings: Settings, trade) -> None: "color": COLOR_GREEN if trade.side == "buy" else COLOR_RED, "fields": [ {"name": "Side", "value": trade.side.upper(), "inline": True}, + {"name": "💶 Einsatz", "value": f"**{invested:.2f} EUR**", "inline": True}, {"name": "Qty", "value": f"{trade.qty:.6f}", "inline": True}, - {"name": "Entry", "value": f"{trade.entry_price:.2f} EUR", "inline": True}, + {"name": "Entry", "value": f"{trade.entry_price:.2f} USD", "inline": True}, { "name": "Stop-Loss", "value": f"{trade.stop_loss:.2f}" if trade.stop_loss else "—", diff --git a/src/aitrader/trader/executor.py b/src/aitrader/trader/executor.py index c18b4d8..40709cd 100644 --- a/src/aitrader/trader/executor.py +++ b/src/aitrader/trader/executor.py @@ -69,5 +69,5 @@ def execute_trade( sl=sl, tp=tp, ) - discord.notify_trade_opened(settings, trade) + discord.notify_trade_opened(settings, trade, qty_eur=qty_eur) return trade