feat: neues Tool Wettrechner (Surebet/Arbitrage-Rechner) unter Freizeit

This commit is contained in:
2026-07-12 12:55:20 +02:00
parent a28b96b424
commit 37b75effb4
3 changed files with 151 additions and 1 deletions

View File

@@ -208,3 +208,12 @@ export const GeoIcon = ({size=20,color='currentColor',sw}) => base(<>
<rect x="11" y="3" width="10" height="5" rx="1" stroke={color} strokeWidth={sw||1.2} fill="none"/>
<rect x="3" y="11" width="5" height="10" rx="1" stroke={color} strokeWidth={sw||1.2} fill="none"/>
</>, size, color, sw);
export const ScaleIcon = ({size=20,color='currentColor',sw}) => base(<>
<path d="M12 3v18" stroke={color} strokeWidth={sw||1.5}/>
<path d="M7 21h10" stroke={color} strokeWidth={sw||1.5}/>
<path d="M5 7h14" stroke={color} strokeWidth={sw||1.5}/>
<path d="M12 3l3 4" stroke={color} strokeWidth={sw||1.5}/>
<path d="M2 12l3-5 3 5a3 3 0 0 1-6 0z" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinejoin="round"/>
<path d="M16 12l3-5 3 5a3 3 0 0 1-6 0z" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinejoin="round"/>
</>, size, color, sw);

View File

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

View File

@@ -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 (
<div style={{ padding: 20, maxWidth: 640 }}>
<div style={{ ...S.head, marginBottom: 4 }}>WETTRECHNER</div>
<div style={{ ...S.sub, marginBottom: 16 }}>
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.
</div>
<div style={S.card}>
<div style={{ ...S.sub, marginBottom: 6 }}>GESAMTEINSATZ</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
<span style={{ color: 'rgba(255,255,255,0.4)', fontFamily: "'Space Mono',monospace" }}></span>
<input value={stake} onChange={e => setStake(e.target.value)} inputMode="decimal"
placeholder="100" style={{ ...S.inp, maxWidth: 160 }} />
</div>
{outcomes.map((o, i) => (
<div key={i} style={{ display: 'flex', gap: 8, marginBottom: 10, flexWrap: 'wrap' }}>
<input value={o.label} onChange={e => setOutcome(i, 'label', e.target.value)}
placeholder={`Ergebnis ${i + 1} (optional, z.B. "Sieg A")`}
style={{ ...S.inp, flex: 2, minWidth: 160 }} />
<input value={o.odd} onChange={e => setOutcome(i, 'odd', e.target.value)} inputMode="decimal"
placeholder="Quote, z.B. 2.50" style={{ ...S.inp, flex: 1, minWidth: 100 }} />
</div>
))}
</div>
{!canCalculate && (
<div style={{ ...S.sub, marginTop: 12 }}>
Bitte Gesamteinsatz und alle drei Quoten (jeweils größer als 1) eintragen.
</div>
)}
{result && (
<div style={{ ...S.card, marginTop: 16, borderColor: result.isProfitable ? 'rgba(74,222,128,0.35)' : 'rgba(248,113,113,0.35)' }}>
<div style={{
fontFamily: "'Space Mono',monospace", fontSize: 13, fontWeight: 700, marginBottom: 14,
color: result.isProfitable ? '#4ade80' : '#f87171',
}}>
{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)`}
</div>
{outcomes.map((o, i) => (
<div key={i} style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
padding: '8px 0', borderBottom: i < 2 ? '1px solid rgba(255,255,255,0.06)' : 'none',
}}>
<span style={{ color: 'rgba(255,255,255,0.7)', fontFamily: 'monospace', fontSize: 13 }}>
{o.label || `Ergebnis ${i + 1}`} <span style={{ color: 'rgba(255,255,255,0.35)' }}>(Quote {odds[i]})</span>
</span>
<span style={{ color: '#ffe66d', fontFamily: "'Space Mono',monospace", fontSize: 16, fontWeight: 700 }}>
{fmtEUR(result.stakes[i])}
</span>
</div>
))}
<div style={{ marginTop: 14, paddingTop: 14, borderTop: '1px solid rgba(255,255,255,0.1)', display: 'flex', flexWrap: 'wrap', gap: 20 }}>
<div>
<div style={{ ...S.sub, marginBottom: 2 }}>Auszahlung (immer gleich)</div>
<div style={{ color: '#4ecdc4', fontFamily: "'Space Mono',monospace", fontSize: 16, fontWeight: 700 }}>
{fmtEUR(result.payout)}
</div>
</div>
<div>
<div style={{ ...S.sub, marginBottom: 2 }}>Gewinn / Verlust</div>
<div style={{
color: result.isProfitable ? '#4ade80' : '#f87171',
fontFamily: "'Space Mono',monospace", fontSize: 16, fontWeight: 700,
}}>
{result.profit >= 0 ? '+' : ''}{fmtEUR(result.profit)} ({result.profitPercent >= 0 ? '+' : ''}{fmtPct(result.profitPercent)})
</div>
</div>
</div>
</div>
)}
</div>
);
}