feat: neues Tool Wettrechner (Surebet/Arbitrage-Rechner) unter Freizeit
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
132
frontend/src/tools/wettrechner.jsx
Normal file
132
frontend/src/tools/wettrechner.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user