From 37b75effb4954ab3b2baf0f9b1a794a8031a0962 Mon Sep 17 00:00:00 2001 From: Dicken Date: Sun, 12 Jul 2026 12:55:20 +0200 Subject: [PATCH] feat: neues Tool Wettrechner (Surebet/Arbitrage-Rechner) unter Freizeit --- frontend/src/icons.jsx | 9 ++ frontend/src/toolRegistry.js | 11 ++- frontend/src/tools/wettrechner.jsx | 132 +++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 frontend/src/tools/wettrechner.jsx diff --git a/frontend/src/icons.jsx b/frontend/src/icons.jsx index 668af7a..5b5b285 100644 --- a/frontend/src/icons.jsx +++ b/frontend/src/icons.jsx @@ -208,3 +208,12 @@ export const GeoIcon = ({size=20,color='currentColor',sw}) => base(<> , size, color, sw); + +export const ScaleIcon = ({size=20,color='currentColor',sw}) => base(<> + + + + + + +, size, color, sw); diff --git a/frontend/src/toolRegistry.js b/frontend/src/toolRegistry.js index de14ca7..272f5aa 100644 --- a/frontend/src/toolRegistry.js +++ b/frontend/src/toolRegistry.js @@ -14,7 +14,8 @@ import Whiteboard from './tools/whiteboard.jsx'; import Gebietseroberung from './tools/gebietseroberung.jsx'; import Schocken from './tools/schocken.jsx'; import Koepi from './tools/koepi.jsx'; -import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon, CodeIcon, SkizzeIcon, WrenchIcon, FilmIcon, PaywallIcon, ChartIcon, KanbanIcon, WhiteboardIcon, GeoIcon, SchockenIcon, KoepiIcon } from './icons.jsx'; +import Wettrechner from './tools/wettrechner.jsx'; +import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon, CodeIcon, SkizzeIcon, WrenchIcon, FilmIcon, PaywallIcon, ChartIcon, KanbanIcon, WhiteboardIcon, GeoIcon, SchockenIcon, KoepiIcon, ScaleIcon } from './icons.jsx'; export const TOOLS = [ { @@ -89,6 +90,14 @@ export const TOOLS = [ group: 'Freizeit', component: Koepi, }, + { + id: 'wettrechner', + Icon: ScaleIcon, + label: 'Wettrechner', + navLabel: 'Wetten', + group: 'Freizeit', + component: Wettrechner, + }, { id: 'dateien', Icon: DatabaseIcon, diff --git a/frontend/src/tools/wettrechner.jsx b/frontend/src/tools/wettrechner.jsx new file mode 100644 index 0000000..a5eedc3 --- /dev/null +++ b/frontend/src/tools/wettrechner.jsx @@ -0,0 +1,132 @@ +import { useState } from 'react'; +import { S } from '../lib.js'; + +// ── Wettrechner (Surebet / Arbitrage) ──────────────────────────────────────── +// Prinzip: bei 3 sich gegenseitig ausschließenden Ergebnissen mit Quoten +// o1,o2,o3 existiert ein garantierter Gewinn, wenn 1/o1 + 1/o2 + 1/o3 < 1 +// ("Arbitrage-Prozentsatz" < 100%). Der Einsatz pro Ergebnis wird proportional +// zu 1/o_i verteilt — dadurch ist die Auszahlung bei JEDEM der drei Ausgänge +// exakt gleich hoch. + +function parseOdd(v) { + const n = parseFloat(String(v).replace(',', '.')); + return Number.isFinite(n) && n > 1 ? n : null; +} +function parseMoney(v) { + const n = parseFloat(String(v).replace(',', '.')); + return Number.isFinite(n) && n > 0 ? n : null; +} +const fmtEUR = n => n.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' }); +const fmtPct = n => n.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' %'; + +export default function Wettrechner() { + const [stake, setStake] = useState('100'); + const [outcomes, setOutcomes] = useState([ + { label: '', odd: '' }, + { label: '', odd: '' }, + { label: '', odd: '' }, + ]); + + const setOutcome = (i, field, value) => { + setOutcomes(prev => prev.map((o, idx) => idx === i ? { ...o, [field]: value } : o)); + }; + + const budget = parseMoney(stake); + const odds = outcomes.map(o => parseOdd(o.odd)); + const allOddsValid = odds.every(o => o !== null); + const canCalculate = allOddsValid && budget !== null; + + let result = null; + if (canCalculate) { + const invProbs = odds.map(o => 1 / o); + const sumInv = invProbs.reduce((a, b) => a + b, 0); + const arbitragePercent = sumInv * 100; + const isProfitable = sumInv < 1; + const stakes = invProbs.map(p => budget * p / sumInv); + const payout = budget / sumInv; + const profit = payout - budget; + const profitPercent = (payout / budget - 1) * 100; + result = { arbitragePercent, isProfitable, stakes, payout, profit, profitPercent }; + } + + return ( +
+
WETTRECHNER
+
+ Trag die Quoten für 3 sich gegenseitig ausschließende Ergebnisse ein (z.B. Sieg A / Unentschieden / Sieg B). + Der Rechner sagt dir, wie viel du auf jedes Ergebnis setzen musst, damit die Auszahlung immer gleich hoch ist — + egal welches Ergebnis eintritt. +
+ +
+
GESAMTEINSATZ
+
+ + setStake(e.target.value)} inputMode="decimal" + placeholder="100" style={{ ...S.inp, maxWidth: 160 }} /> +
+ + {outcomes.map((o, i) => ( +
+ setOutcome(i, 'label', e.target.value)} + placeholder={`Ergebnis ${i + 1} (optional, z.B. "Sieg A")`} + style={{ ...S.inp, flex: 2, minWidth: 160 }} /> + setOutcome(i, 'odd', e.target.value)} inputMode="decimal" + placeholder="Quote, z.B. 2.50" style={{ ...S.inp, flex: 1, minWidth: 100 }} /> +
+ ))} +
+ + {!canCalculate && ( +
+ Bitte Gesamteinsatz und alle drei Quoten (jeweils größer als 1) eintragen. +
+ )} + + {result && ( +
+
+ {result.isProfitable + ? `✓ Garantierter Gewinn möglich (Arbitrage: ${fmtPct(result.arbitragePercent)})` + : `✕ Kein garantierter Gewinn möglich (Summe der Quoten-Kehrwerte: ${fmtPct(result.arbitragePercent)}, muss unter 100 % liegen)`} +
+ + {outcomes.map((o, i) => ( +
+ + {o.label || `Ergebnis ${i + 1}`} (Quote {odds[i]}) + + + {fmtEUR(result.stakes[i])} + +
+ ))} + +
+
+
Auszahlung (immer gleich)
+
+ {fmtEUR(result.payout)} +
+
+
+
Gewinn / Verlust
+
+ {result.profit >= 0 ? '+' : ''}{fmtEUR(result.profit)} ({result.profitPercent >= 0 ? '+' : ''}{fmtPct(result.profitPercent)}) +
+
+
+
+ )} +
+ ); +}