Files
2026FIFAWorldCup/platform/web/components/BetSizingSlider.tsx

169 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { FormEvent, useEffect, useMemo, useState } from 'react';
import { calculateKellyRecommendation } from '@/lib/betting-utils';
import { TW_DOLLAR } from '@/lib/betting-utils';
import { calculateKelly, type KellyResponse } from '@/lib/analytics-api';
export function BetSizingSlider() {
const [bankroll, setBankroll] = useState(5000);
const [odds, setOdds] = useState(2.4);
const [probability, setProbability] = useState(0.55);
const [fractional, setFractional] = useState(0.25);
const [riskTolerance, setRiskTolerance] = useState(1);
const [kellyProfile, setKellyProfile] = useState<KellyResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const profile = useMemo(() => {
try {
return calculateKellyRecommendation({
odds,
trueProb: probability,
bankroll,
fractionalKelly: fractional,
riskTolerance,
});
} catch {
return {
recommendedStake: 0,
recommendedFraction: 0,
};
}
}, [odds, probability, bankroll, fractional, riskTolerance]);
const backendProfile = useMemo(
() => ({
odds,
true_prob: probability,
bankroll,
fractional_kelly_factor: fractional,
risk_tolerance_factor: riskTolerance,
}),
[bankroll, fractional, odds, probability, riskTolerance],
);
const showFractionPercent = kellyProfile ? kellyProfile.recommended_fraction : profile.recommendedFraction;
const showStake = kellyProfile ? kellyProfile.recommended_stake : profile.recommendedStake;
function onBankrollSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
}
useEffect(() => {
const handle = new AbortController();
setIsLoading(true);
setErrorMessage('');
const runCalculation = async () => {
try {
const data = await calculateKelly(backendProfile);
if (!handle.signal.aborted) {
setKellyProfile(data);
}
} catch (error) {
if (handle.signal.aborted) {
return;
}
setErrorMessage(error instanceof Error ? error.message : '凱利計算暫時失敗,使用本機備援值');
} finally {
if (!handle.signal.aborted) {
setIsLoading(false);
}
}
};
runCalculation();
return () => {
handle.abort();
};
}, [backendProfile]);
return (
<section className="panel-glow rounded-2xl p-4">
<h3 className="dot-matrix text-xl text-[#7d2a15]"></h3>
<form onSubmit={onBankrollSubmit} className="mt-3 space-y-4">
<label className="block text-sm text-[#7a5b46]">
USD
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
value={bankroll}
min={100}
step={100}
onChange={(e) => setBankroll(Math.max(100, Number(e.target.value)))}
/>
</label>
<label className="block text-sm text-[#7a5b46]">
Decimal
<input
className="mt-2 w-full rounded-lg border border-[#e0c6a8] bg-white/70 px-3 py-2"
type="number"
value={odds}
min={1.01}
step={0.05}
onChange={(e) => setOdds(Math.max(1.01, Number(e.target.value)))}
/>
</label>
<label className="block text-sm text-[#7a5b46]">
<input
className="mt-2 w-full"
type="range"
min={0}
max={1}
step={0.01}
value={probability}
onChange={(e) => setProbability(Number(e.target.value))}
/>
<p className="text-xs text-[#7a5b46]">{(probability * 100).toFixed(1)}%</p>
</label>
<label className="block text-sm text-[#7a5b46]">
Fractional
<input
className="mt-2 w-full"
type="range"
min={0.1}
max={1}
step={0.05}
value={fractional}
onChange={(e) => setFractional(Number(e.target.value))}
/>
<p className="text-xs text-[#7a5b46]">{(fractional * 100).toFixed(0)}%</p>
</label>
<label className="block text-sm text-[#7a5b46]">
<input
className="mt-2 w-full"
type="range"
min={0.5}
max={1.8}
step={0.05}
value={riskTolerance}
onChange={(e) => setRiskTolerance(Number(e.target.value))}
/>
<p className="text-xs text-[#7a5b46]">{riskTolerance.toFixed(2)}x</p>
</label>
</form>
<div className="mt-4 rounded-xl bg-[#fff0d9] p-4">
<p className="dot-matrix text-lg text-[#7d2a15]"></p>
<p className="mt-2 text-3xl text-[#b83822]">{TW_DOLLAR.format(profile.recommendedStake)}</p>
<p className="mt-1 text-sm text-[#7a5b46]">
{showFractionPercent.toFixed(1)}%
</p>
<p className="mt-1 text-sm text-[#7a5b46]">
{TW_DOLLAR.format(showStake)}
</p>
{isLoading ? <p className="mt-2 text-xs text-[#a16b4f]"></p> : null}
{errorMessage ? <p className="mt-2 text-xs text-[#8c2f2f]">{errorMessage}</p> : null}
</div>
</section>
);
}