feat: realistic budget, EUR trade size in Discord, confidence analytics

- 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 <noreply@anthropic.com>
This commit is contained in:
sylyx 2026-05-14 08:30:45 +02:00
parent 7a0eccbcef
commit be1adfb94c
4 changed files with 61 additions and 8 deletions

View File

@ -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)

View File

@ -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.600.65", "0.650.75", "0.750.85", "0.851.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 "")

View File

@ -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 "",

View File

@ -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