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:
parent
7a0eccbcef
commit
be1adfb94c
@ -3,15 +3,15 @@ interval_minutes: 15
|
|||||||
timeframes: [15m, 1h, 4h]
|
timeframes: [15m, 1h, 4h]
|
||||||
ohlcv_limit: 200
|
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:
|
risk:
|
||||||
max_position_pct: 0.20
|
max_position_pct: 0.10
|
||||||
max_open_positions: 2
|
max_open_positions: 2
|
||||||
stop_loss_atr_mult: 2.0
|
stop_loss_atr_mult: 2.0
|
||||||
take_profit_atr_mult: 3.0
|
take_profit_atr_mult: 3.0
|
||||||
daily_loss_limit_pct: 0.05
|
daily_loss_limit_pct: 0.05
|
||||||
min_order_eur: 25
|
min_order_eur: 5
|
||||||
|
|
||||||
ai:
|
ai:
|
||||||
mode: ensemble # ensemble (beide Voter müssen sich einig sein) | single (nur voter_a)
|
mode: ensemble # ensemble (beide Voter müssen sich einig sein) | single (nur voter_a)
|
||||||
|
|||||||
@ -23,8 +23,8 @@ def load_df(stmt) -> pd.DataFrame:
|
|||||||
return pd.DataFrame([r.model_dump() for r in rows])
|
return pd.DataFrame([r.model_dump() for r in rows])
|
||||||
|
|
||||||
|
|
||||||
tab_overview, tab_trades, tab_decisions, tab_ai = st.tabs(
|
tab_overview, tab_trades, tab_decisions, tab_ai, tab_analytics = st.tabs(
|
||||||
["Overview", "Trades", "Decisions", "AI-Vergleich"]
|
["Overview", "Trades", "Decisions", "AI-Vergleich", "Analytics"]
|
||||||
)
|
)
|
||||||
|
|
||||||
with tab_overview:
|
with tab_overview:
|
||||||
@ -110,3 +110,54 @@ with tab_ai:
|
|||||||
st.caption(
|
st.caption(
|
||||||
"Hinweis: Nur Ticks bei denen beide BUY/SELL mit ausreichender Confidence stimmen führen zu Trades."
|
"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 "—")
|
||||||
|
|||||||
@ -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"):
|
if not _should(settings, "trade_open"):
|
||||||
return
|
return
|
||||||
side_emoji = "📈" if trade.side == "buy" else "📉"
|
side_emoji = "📈" if trade.side == "buy" else "📉"
|
||||||
|
invested = qty_eur or trade.qty * trade.entry_price
|
||||||
_post(
|
_post(
|
||||||
settings,
|
settings,
|
||||||
{
|
{
|
||||||
@ -102,8 +103,9 @@ def notify_trade_opened(settings: Settings, trade) -> None:
|
|||||||
"color": COLOR_GREEN if trade.side == "buy" else COLOR_RED,
|
"color": COLOR_GREEN if trade.side == "buy" else COLOR_RED,
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "Side", "value": trade.side.upper(), "inline": True},
|
{"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": "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",
|
"name": "Stop-Loss",
|
||||||
"value": f"{trade.stop_loss:.2f}" if trade.stop_loss else "—",
|
"value": f"{trade.stop_loss:.2f}" if trade.stop_loss else "—",
|
||||||
|
|||||||
@ -69,5 +69,5 @@ def execute_trade(
|
|||||||
sl=sl,
|
sl=sl,
|
||||||
tp=tp,
|
tp=tp,
|
||||||
)
|
)
|
||||||
discord.notify_trade_opened(settings, trade)
|
discord.notify_trade_opened(settings, trade, qty_eur=qty_eur)
|
||||||
return trade
|
return trade
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user