using Marathon.Domain.Entities;
using Marathon.Domain.ValueObjects;
namespace Marathon.Application.Betting;
///
/// Pure helper that computes Closing Line Value (CLV) for a placed bet.
///
///
///
/// CLV measures how much better (or worse) the rate the user took was compared
/// with the bookmaker's last pre-match price on the same selection. It is the
/// single best long-run indicator of betting skill — positive CLV correlates
/// with positive expected value regardless of any individual bet's outcome.
///
///
/// Formula (implied-probability delta):
///
/// - Taken implied probability: p_t = 1 / takenRate
/// - Closing implied probability: p_c = 1 / closeRate
/// - CLV = p_c − p_t
///
/// Positive CLV means the closing price implied higher probability for the
/// selection than the price the user took — i.e. the line moved in the user's
/// favour after they placed the bet.
///
///
/// Returns null when no matching bet (same Scope / Type / Side / Value)
/// can be found in the closing snapshot — typically because the market closed
/// before the bookmaker exposed a comparable line, or the snapshot store has
/// gaps. UI consumers must distinguish "no data" from "0% CLV".
///
///
public static class ClosingLineValueCalculator
{
///
/// Computes CLV (implied-probability delta) given the rate the user took
/// and the rate present in the closing pre-match snapshot for the same
/// selection. Both must be positive — invariants on
/// already guarantee this for inputs sourced from the domain.
///
public static decimal Compute(decimal takenRate, decimal closingRate)
{
if (takenRate <= 0m)
throw new ArgumentOutOfRangeException(nameof(takenRate), takenRate, "Must be positive.");
if (closingRate <= 0m)
throw new ArgumentOutOfRangeException(nameof(closingRate), closingRate, "Must be positive.");
var takenProb = 1m / takenRate;
var closingProb = 1m / closingRate;
// Round to 6 decimals — beyond that is noise from the round-trip.
return Math.Round(closingProb - takenProb, 6);
}
///
/// Convenience overload: finds the matching in
/// by Scope / Type / Side / Value, then
/// computes CLV against . Returns null
/// when no comparable bet is present.
///
public static decimal? TryCompute(
decimal takenRate,
Bet placedSelection,
OddsSnapshot? closingSnapshot)
{
ArgumentNullException.ThrowIfNull(placedSelection);
if (closingSnapshot is null) return null;
var match = closingSnapshot.Bets.FirstOrDefault(b =>
b.Scope.Equals(placedSelection.Scope) &&
b.Type == placedSelection.Type &&
b.Side == placedSelection.Side &&
NullableValuesEqual(b.Value, placedSelection.Value));
return match is null ? null : Compute(takenRate, match.Rate.Value);
}
private static bool NullableValuesEqual(OddsValue? a, OddsValue? b)
{
if (a is null && b is null) return true;
if (a is null || b is null) return false;
return a.Value == b.Value;
}
}