Files
bet/common/bet_tools.py
2025-10-25 19:20:28 +08:00

133 lines
5.6 KiB
Python
Raw 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.

import math
from scipy.optimize import fsolve # 导入 fsolve 函数用于数值求解
def moneyline_to_prob(moneyline_odds: int) -> float:
"""将 Moneyline 赔率转换为隐含概率."""
if moneyline_odds == 0:
raise ValueError("Moneyline odds cannot be 0")
elif moneyline_odds > 0:
# 正赔率 +X -> 隐含概率 = 100 / (100 + X)
return 100 / (moneyline_odds + 100)
else: # moneyline_odds <= 0
# 负赔率 -X -> 隐含概率 = X / (X + 100)
return abs(moneyline_odds) / (abs(moneyline_odds) + 100)
def prob_to_moneyline(probability: float) -> int:
"""将概率转换为 Moneyline 赔率 (四舍五入到最接近的整数)."""
if not 0 < probability < 1:
# 概率为 0 或 1 对应无限或 -100 的 Moneyline 赔率,这里简化处理,实际中极少遇到精确的 0 或 1
if math.isclose(probability, 0):
return float("inf")
if math.isclose(probability, 1):
return (
-100
) # 或者 raise ValueError("Probability must be between 0 and 1 (exclusive)")
raise ValueError("Probability must be between 0 and 1 (exclusive)")
if probability <= 0.5:
# 概率 <= 0.5 对应正 Moneyline 赔率 (Decimal >= 2.0)
# Decimal Odds = 1 / probability
# Moneyline = (Decimal Odds - 1) * 100
return round((1 / probability - 1) * 100, 2)
else:
# 概率 > 0.5 对应负 Moneyline 赔率 (Decimal < 2.0)
# Decimal Odds = 1 / probability
# Moneyline = -100 / (Decimal Odds - 1)
return round(-100 / (1 / probability - 1), 2)
def calculate_no_vig_moneyline_power(moneyline_odds_list: list[int]) -> list[int]:
"""
使用 Power Method (根据提供的文献描述) 计算无 vigorish 的 Moneyline 赔率。
该方法通过寻找 k 使得 sum(implied_prob^k) = 1 来调整概率。
参数:
moneyline_odds_list (list): 包含所有可能结果的 Moneyline 整数赔率列表 (例如, [+116, -156])。
返回:
list: 包含所有可能结果的无 vigorish Moneyline 整数赔率列表。
"""
if not moneyline_odds_list:
return []
# 1. 将 Moneyline 赔率转换为隐含概率 (pi)
implied_probabilities = [moneyline_to_prob(odds) for odds in moneyline_odds_list]
# 确保所有隐含概率都大于 0否则无法进行幂运算或取对数 (数值求解时可能涉及)
if any(p <= 0 for p in implied_probabilities):
raise ValueError("All implied probabilities must be positive.")
total_implied_probability = sum(implied_probabilities)
# 如果总概率 <= 1说明没有 vig 或 vig 极少,直接返回原始赔率
if total_implied_probability <= 1:
print(
"Warning: Input odds already have little or no vig. Returning original odds."
)
return moneyline_odds_list
# 2. 定义需要找到根的函数 f(k) = sum(pi^k) - 1
# 我们要找到 k 使得 sum(pi^k) = 1
# 由于 sum(pi) > 1 且 pi < 1, 我们需要 k > 1 才能让 pi^k < pi, 从而降低总和至 1。
def sum_pi_pow_k_minus_1(k):
# fsolve 传入的 k 是一个数组,我们需要取其第一个元素
k_val = k[0] if isinstance(k, (list, tuple)) else k
# 计算 sum(pi^k)
sum_val = sum(p**k_val for p in implied_probabilities)
return sum_val - 1 # 我们的目标是让这个函数等于 0
# 3. 寻找 k 使得 f(k) = 0
# 我们知道当 k=1 时,总和是 total_implied_probability (>1)。
# 当 k 增大时sum(pi^k) 会减小。所以根 k 应该大于 1。
# 提供一个合理的初始猜测值给 fsolve例如 1.1 或 1.5
initial_k_guess = [1.1] # fsolve 期望一个数组作为初始猜测
# 使用 fsolve 寻找 k
# fsolve 返回一个数组,即使只有一个解
k_solution = fsolve(sum_pi_pow_k_minus_1, initial_k_guess)
# 提取求解到的 k 值
k = k_solution[0]
# 4. 计算无 Vig 概率 pi_novig = pi^k
no_vig_probabilities = [p**k for p in implied_probabilities]
# 由于浮点数精度和数值求解的限制,最终的概率之和可能不严格等于 1。
# 虽然理论上由 k 的定义保证总和为 1但实践中检查一下是有益的。
final_sum_check = sum(no_vig_probabilities)
if not math.isclose(final_sum_check, 1.0, abs_tol=1e-9):
print(
f"Warning: Final no-vig probabilities sum to {final_sum_check:.6f}, expected 1.0. Sum may need slight re-normalization."
)
# 理论上 Power Method 的定义保证了总和为 1但如果因为数值误差偏离较多
# 可以选择在这里进行最后的比例调整,但严格遵循方法定义是不需要的。
# 5. 将无 Vig 概率转换回 Moneyline 赔率
no_vig_moneyline_odds = [
prob_to_moneyline(p_novig) for p_novig in no_vig_probabilities
]
return no_vig_moneyline_odds
# 示例
if __name__ == "__main__":
odds_list = [+150, -200, +300, -120]
for odds in odds_list:
prob = moneyline_to_prob(odds)
print(f"赔率 {odds}: 概率 {prob:.4f}")
odds = [+116, -156]
# 计算无 Vig 赔率使用 Power Method
no_vig_odds_power = calculate_no_vig_moneyline_power(odds)
print(f"原始 Moneyline 赔率: {odds}")
print(f"无 Vig Moneyline 赔率 (Power Method): {no_vig_odds_power}")
# 可选: 验证无 vig 赔率对应的概率之和是否接近 1
if no_vig_odds_power:
novig_probs_power = [moneyline_to_prob(o) for o in no_vig_odds_power]
print(f"无 Vig 概率之和 (基于计算出的赔率): {sum(novig_probs_power):.6f}")