足球博彩中怎么进行套利交易(四)
上一篇讲的是数学模型:收益向量,返还矩阵,下注比例,以及线性方程求解.这篇讲这套模型在实时策略脚本里怎样运行.
我的策略不是手工输入几条赔率再计算,而是从标准化后的赔率库里读取多家公司的实时盘口,按比赛聚合,筛选候选组合,求解下注比例,再把当前最好的结果输出到结果表.
问:策略的输入是什么?
策略的主要输入来自两类数据:
- 已经完成球队名称和比赛归一化的 match links.
- 已经完成格式统一的 normalized odds.
第一类数据解决“这些赔率是不是同一场比赛”的问题.第二类数据解决“每家公司当前给出了什么盘口和赔率”的问题.
脚本读取赔率以后,会先做新鲜度过滤.过旧的报价,校验失败的报价,或者无法追溯来源的报价,不应该进入套利计算.因为套利策略对时间非常敏感,一个已经过期的高赔率会制造出假的套利空间.
经过过滤以后,每一条市场数据会被拆成标准下注腿.
例如 moneyline 会拆成三条:
| 原始市场 | 拆出的下注腿 |
|---|---|
| moneyline | moneyline_home |
| moneyline | moneyline_draw |
| moneyline | moneyline_away |
亚洲让球盘会拆成两条:
| 原始市场 | 拆出的下注腿 |
|---|---|
| asiahandicap | asiahandicap_home |
| asiahandicap | asiahandicap_away |
大小球会按方向拆成:
| 原始市场 | 拆出的下注腿 |
|---|---|
| over | over |
| under | under |
| total | total |
每条下注腿最终都被统一成:
[bettype, handicap, dealer, odd]
例如:
["over", "2.5", "m88", "2.05"]
["under", "2.5", "ps888", "1.98"]
这个统一格式非常重要.后面的收益矩阵并不关心它来自哪张原始表,只关心它是哪一种规则,盘口线是多少,属于哪家公司,赔率是多少.
问:不同公司赔率如何进入同一个比赛?
脚本按统一后的 event key 聚合数据.同一场比赛下,可能同时存在竞彩,北单,M88,PS888,Betway,HaoBo 等多个来源.
聚合以后,一场比赛的数据大致长这样:
event = "世界杯|阿根廷VS佛得角"
[
["moneyline_home", "0", "m88", "2.30"],
["moneyline_draw", "0", "ps888", "3.70"],
["moneyline_away", "0", "betway", "4.20"],
["over", "2.5", "m88", "2.05"],
["under", "2.5", "ps888", "1.98"]
]
在进入计算前,策略还会记录每条下注腿的来源信息,包括 source event id, source game id, quote timestamp, quote age, snapshot run id 等字段.这些字段不会改变套利数学,但会影响后续排查:如果一个结果异常,需要知道它来自哪一次抓取,哪一个原始市场.
问:候选组合怎样从理论矩阵落到当前比赛?
理论上,所有玩法和盘口线都可以预先生成收益向量.但一场具体比赛只会出现其中一部分盘口.所以策略先把当前比赛实际出现的下注腿收集起来,形成 available keys.
每个标准下注腿都有一个稳定的 leg id,例如:
("moneyline_home", 0)
("asiahandicap_home", "-0.25")
("over", "2.5")
策略用这些 leg id 构建实时收益矩阵.矩阵的行是下注腿,列是 49 个比分状态,单元格仍然是 0,50,100,150,200 这样的结算代码.
然后为每一行生成一个 coverage mask.如果某一腿在某个比分状态下返还代码大于 0,这个状态就被标记为 covered.
对于一组候选腿:
\[ L_1,L_2,\ldots,L_n \]
它们的覆盖状态通过位运算合并:
\[ \text{mask}(L_1)\lor\text{mask}(L_2)\lor\cdots\lor\text{mask}(L_n) \]
如果合并后的 mask 等于全比分状态的 mask,说明这组盘口覆盖了所有比分状态,可以进入线性求解.
这一步只判断“规则上是否覆盖”,还不判断“赔率上是否赚钱”.它相当于先排除那些无论怎么分配金额都不可能覆盖全场结果的组合.
问:为什么只枚举 2 到 4 条腿?
策略当前只枚举 2,3,4 条腿的组合.原因很现实:
- 腿数越少,越容易执行.
- 腿数越多,越容易因为某一家公司赔率变化导致整组失效.
- 多腿组合虽然可能提高理论覆盖能力,但会显著增加限额,下单顺序和资金占用问题.
在套利交易里,数学上成立不等于交易上可执行.如果一个组合需要 7 条腿,其中任意一腿下单失败,整个组合都可能从套利变成裸露风险.所以策略宁愿优先搜索短组合.
问:同一个盘口有多家公司报价时怎样处理?
同一个下注腿可能被多家公司同时报价,例如多家公司都有 Over 2.5.策略不会把全部报价都拿去穷举,而是按 dealer 去重,每家公司保留该下注腿下最好的赔率.
之后,同一个下注腿只取赔率最高的前几个候选.当前策略默认每个下注腿取 top 2.这一步是一个工程加速,背后的假设是:如果同一条下注腿的低赔率版本不能提供更好收益,通常没有必要进入昂贵的组合求解.
例如:
| 下注腿 | 公司 | 赔率 |
|---|---|---|
| Over 2.5 | A | 2.01 |
| Over 2.5 | B | 2.05 |
| Over 2.5 | C | 1.96 |
策略会优先保留 2.05,再保留 2.01,而不是把全部都放进组合.
此外,策略要求一组输出至少来自两家公司.如果所有下注腿都来自同一个 dealer,即使数学上形成覆盖,也不作为跨公司套利结果输出.
问:赔率怎样进入返还矩阵?
候选组合通过 coverage mask 以后,会进入真实赔率求解.此时结算代码才会被转换成返还倍数:
| 代码 | 转换 |
|---|---|
| 0 | 0 |
| 50 | 0.5 |
| 100 | 1 |
| 150 | \((O+1)/2\) |
| 200 | \(O\) |
这一步会把形如:
[200, 0]
[0, 200]
转换成:
[2.05, 0]
[0, 1.98]
也会把亚盘里的半赢半输转换成真实返还倍数:
150 -> (odd + 1) / 2
50 -> 0.5
只有在这个阶段,赔率才真正参与计算.
问:线性求解器做了什么?
对于每一个候选组合,策略会生成:
odd_list
payout_rows
其中 payout_rows 是去重后的返还矩阵行.然后求解:
\[ (p_j-p_1)\cdot x=0 \]
以及:
\[ \sum_i x_i=1,\quad x_i\ge0 \]
如果能解出非负的下注比例 \(x\),就得到一个候选结果:
amount_rate = [x1, x2, ..., xn]
rate_back = p1 dot x
amount_rate 是投注比例,不是绝对金额.rate_back 是不考虑返佣时的统一返还倍数.
为了加速,策略使用了几个缓存和批处理:
- 相同 odds 和 payout rows 的求解结果会缓存.
- 多个候选可以批量交给 Fortran 求解器.
- 同一场比赛内部会复用已经求过的 LP case.
- 磁盘上会保存一份 LP cache,下次运行可以复用.
这不会改变策略逻辑,只是减少重复计算.
问:返佣怎样进入 EV?
如果某家公司有返佣,它相当于对下注金额的一部分返还.对于下注比例 \(x_i\),dealer 返佣率为 \(b_i\),返佣贡献为:
\[ b_i x_i \]
整组组合的返佣贡献为:
\[ B=\sum_i b_i x_i \]
所以策略最终使用的有效返还倍数是:
\[ EV=R+B \]
其中 \(R\) 是赔率结算带来的返还倍数,\(B\) 是返佣贡献.
例如一组组合的赔率返还倍数为 0.998,看起来不够套利.但如果对应下注金额能获得 0.5% 的综合返佣,则:
\[ EV=0.998+0.005=1.003 \]
这时它从纯赔率角度不是套利,但从实际交易回报角度可能变成正收益.
需要注意,返佣不是口号,必须按具体 dealer,具体账号,具体结算规则确认.如果返佣无法稳定到账,就不能把它作为安全边际.
问:最后怎样筛选结果?
一场比赛可能产生很多候选组合.策略不会把全部候选都输出,而是做几层过滤:
- 至少来自两家公司.
- 有效返还倍数达到配置阈值.
- 同一场比赛只保留 EV 最高的一组.
- 按 EV 从高到低排序.
这样输出表里每场比赛最多只有一个推荐组合,避免同一场比赛下出现多个互相冲突的下注建议.
到这里,策略已经完成了从实时赔率到候选套利组合的计算.下一篇继续写最后一步:结果表里的每一列是什么意思,如何把 amount 比例换成真实投注金额,以及为什么输出结果仍然需要执行层面的二次确认.