blackmistcode's picture
Add files using upload-large-folder tool
dfe11f8 verified
Raw
History Blame Contribute Delete
3.62 kB
/**
* Criterio de Kelly para sizing de posiciones, limitado al 25%.
*
* Formula:
* odds = (1 / price) - 1
* k = (odds * pWin - pLose) / odds
* return min(max(k, 0), 0.25)
*
* Parametros:
* - price : precio actual del outcome (0 < price < 1).
* - pWin : probabilidad de ganar (usamos confidence de la senal IA).
*
* Devuelve:
* Fraccion del capital virtual recomendada (0.0 a 0.25).
*/
export function kellyFraction(price, pWin) {
if (!price || price <= 0 || price >= 1 || !pWin) return 0;
const pLose = 1 - pWin;
const odds = 1 / price - 1;
const k = (odds * pWin - pLose) / odds;
return Math.min(Math.max(k, 0), 0.25);
}
const SPREAD_ILLIQUID_THRESHOLD = 0.05; // 5c bid/ask → mercado ilíquido
const KELLY_CONSERVATIVE_FACTOR = 0.25; // Quarter-Kelly por seguridad (LLM no calibrada)
const MAX_BANKROLL_FRACTION = 0.25; // No mas del 25% del bankroll por mercado
/**
* Calcula una sugerencia de tamano de posicion *aware* del spread bid/ask.
*
* Resta el coste de ejecucion (spread) al edge bruto que reporta la IA. Si el
* edge neto es <= 0, devuelve 0 (no recomienda apostar). Marca ilíquidos los
* mercados con spread > 5c para que el frontend pueda deshabilitar el boton.
*
* @param {Object} args
* @param {number} args.yesPrice precio YES actual (0-1)
* @param {number} args.noPrice precio NO actual (0-1)
* @param {number} args.spread bid/ask spread (0-1) reportado por Polymarket
* @param {Object} args.signal { signal, confidence, edgePoints, fairProb }
* @param {number} [args.bankroll=1000] capital virtual base (€)
* @returns {{ outcome:'YES'|'NO'|null, fraction:number, amountEur:number,
* edgeNet:number, illiquid:boolean, note:string }}
*/
export function suggestSize({ yesPrice, noPrice, spread = 0, signal, bankroll = 1000 }) {
const illiquid = (spread ?? 0) > SPREAD_ILLIQUID_THRESHOLD;
if (!signal || signal.edgePoints == null) {
return {
outcome: null,
fraction: 0,
amountEur: 0,
edgeNet: 0,
illiquid,
note: illiquid
? `Mercado ilíquido (spread ${Math.round((spread ?? 0) * 100)}¢). Compra desaconsejada.`
: 'Sin señal IA disponible. No se puede calcular sugerencia.',
};
}
const rawEdge = Math.abs(signal.edgePoints) / 100; // 0-1 (probabilidad)
const netEdge = rawEdge - (spread ?? 0);
const outcome = signal.edgePoints > 0 ? 'YES' : 'NO';
const price = outcome === 'YES' ? yesPrice : noPrice;
if (netEdge <= 0.005 || !price || price <= 0 || price >= 1) {
return {
outcome: null,
fraction: 0,
amountEur: 0,
edgeNet: netEdge,
illiquid,
note: illiquid
? `Mercado ilíquido (spread ${Math.round((spread ?? 0) * 100)}¢).`
: `Sin edge neto tras spread (${(rawEdge * 100).toFixed(1)}pp − ${((spread ?? 0) * 100).toFixed(1)}pp). No apostar.`,
};
}
// Probabilidad efectiva = implied + edgeNet, capada en [0,1)
const pWin = Math.max(0, Math.min(0.99, price + netEdge));
// Quarter-Kelly por seguridad: la confianza del LLM no esta calibrada.
const k = kellyFraction(price, pWin) * KELLY_CONSERVATIVE_FACTOR;
const fraction = Math.min(MAX_BANKROLL_FRACTION, Math.max(0, k));
const amountEur = Math.round(bankroll * fraction);
return {
outcome,
fraction,
amountEur,
edgeNet: netEdge,
illiquid,
note: illiquid
? `Mercado ilíquido (spread ${Math.round((spread ?? 0) * 100)}¢). Compra desaconsejada.`
: `Kelly conservador (¼) sobre edge neto ${(netEdge * 100).toFixed(1)}pp: €${amountEur} en ${outcome}.`,
};
}