结合伤停信息调仓
场景分析
伤停信息(球员伤病、停赛)主要影响体育博彩量化、体育类股票/基金(如俱乐部股票、体育用品股)以及预测市场,下面以足球/篮球博彩量化策略为例,给出完整的 Java 实现思路。

核心逻辑:
- 伤停信息发布 → 球队实力下降 → 赔率/概率变化 → 触发调仓(下注调整或持仓调整)
- 需要处理:信息时效性、信息可信度、影响权重、风控约束
领域模型设计
1 伤停信息实体
import java.time.LocalDateTime;
import java.util.Objects;
/**
* 伤停信息
*/
public class InjuryInfo {
private String playerId; // 球员ID
private String teamId; // 球队ID
private InjuryType type; // 伤病类型
private Severity severity; // 严重程度
private double impactScore; // 对球队实力的影响分(0~1)
private LocalDateTime publishTime; // 发布时间
private String source; // 信息来源
private double sourceReliability; // 来源可信度 0~1
public enum InjuryType { INJURY, SUSPENSION, ILLNESS, REST }
public enum Severity { MINOR, MODERATE, SEVERE, OUT_FOR_SEASON }
// 综合影响度
public double effectiveImpact() {
return impactScore * sourceReliability
* decayFactor(LocalDateTime.now());
}
/** 时间衰减:越新越可信 */
private double decayFactor(LocalDateTime now) {
long hours = java.time.Duration.between(publishTime, now).toHours();
return Math.exp(-hours / 24.0); // 24小时半衰
}
// getters / setters 省略
// equals & hashCode based on playerId + publishTime
}
2 球队实力模型
public class TeamStrength {
private String teamId;
private double baseAttack; // 基础进攻力
private double baseDefense; // 基础防守力
/** 根据伤停调整后的实力 */
public double adjustedAttack(List<InjuryInfo> injuries) {
double penalty = injuries.stream()
.mapToDouble(InjuryInfo::effectiveImpact)
.sum();
// 影响递减(避免多个伤病线性叠加)
return baseAttack * (1 - 1 + Math.exp(-penalty));
}
public double adjustedDefense(List<InjuryInfo> injuries) {
double penalty = injuries.stream()
.mapToDouble(InjuryInfo::effectiveImpact)
.sum();
return baseDefense * Math.exp(-penalty);
}
}
调仓引擎
1 目标仓位计算
public class PositionSizer {
/** 根据模型概率与市场赔率计算凯利仓位 */
public double kellyPosition(double modelProb, double marketOdds,
double bankroll, double kellyFraction) {
double b = marketOdds - 1.0;
double p = modelProb;
double q = 1 - p;
double kelly = (b * p - q) / b;
if (kelly <= 0) return 0;
return bankroll * kelly * kellyFraction; // 保守凯利
}
/** 结合伤停调整概率 */
public double adjustProb(double baseProb,
TeamStrength home, TeamStrength away,
List<InjuryInfo> homeInjuries,
List<InjuryInfo> awayInjuries) {
double homeAdj = home.adjustedAttack(homeInjuries) / home.baseAttack;
double awayAdj = away.adjustedAttack(awayInjuries) / away.baseAttack;
// 相对强度变化映射到概率
double delta = (homeAdj - awayAdj) * 0.3; // 敏感系数
return clamp(baseProb + delta, 0.01, 0.99);
}
private double clamp(double v, double lo, double hi) {
return Math.max(lo, Math.min(hi, v));
}
}
2 调仓决策器
public class RebalanceEngine {
private final PositionSizer sizer = new PositionSizer();
private final double kellyFraction = 0.25; // 1/4 凯利
private final double maxPositionPct = 0.05; // 单场最大仓位 5%
public Order decide(MatchContext ctx) {
double baseProb = ctx.getModelBaseProb();
double adjProb = sizer.adjustProb(
baseProb, ctx.getHome(), ctx.getAway(),
ctx.getHomeInjuries(), ctx.getAwayInjuries());
double bankroll = ctx.getBankroll();
double odds = ctx.getMarketOdds();
double target = sizer.kellyPosition(adjProb, odds, bankroll, kellyFraction);
target = Math.min(target, bankroll * maxPositionPct);
double current = ctx.getCurrentPosition();
double delta = target - current;
// 小于阈值不动,避免频繁交易
if (Math.abs(delta) < bankroll * 0.002) {
return Order.hold(ctx.getMatchId());
}
return delta > 0
? Order.buy(ctx.getMatchId(), delta, odds)
: Order.sell(ctx.getMatchId(), -delta, odds);
}
}
3 订单结构
public class Order {
public enum Side { BUY, SELL, HOLD }
private Side side;
private String matchId;
private double amount;
private double price;
public static Order buy(String id, double amt, double px) {
Order o = new Order(); o.side = Side.BUY;
o.matchId = id; o.amount = amt; o.price = px; return o;
}
public static Order sell(String id, double amt, double px) {
Order o = new Order(); o.side = Side.SELL;
o.matchId = id; o.amount = amt; o.price = px; return o;
}
public static Order hold(String id) {
Order o = new Order(); o.side = Side.HOLD;
o.matchId = id; o.amount = 0; return o;
}
// getters 省略
}
事件驱动集成
伤停信息是异步事件,需要挂接到主流程:
public class InjuryEventListener {
private final RebalanceEngine engine;
private final PositionRepository positionRepo;
private final OrderService orderService;
private final MatchContextCache contextCache;
public void onInjuryUpdate(InjuryInfo injury) {
// 1. 更新内存中的球队伤停列表
String teamId = injury.getTeamId();
contextCache.updateInjuries(teamId, injury);
// 2. 找出该球队相关的所有比赛
List<String> matchIds = contextCache.matchesOfTeam(teamId);
// 3. 逐场重新评估
for (String matchId : matchIds) {
MatchContext ctx = contextCache.build(matchId);
Order order = engine.decide(ctx);
if (order.getSide() != Order.Side.HOLD) {
// 4. 风控再校验(总敞口、单队集中度等)
if (RiskChecker.pass(order, positionRepo)) {
orderService.submit(order);
positionRepo.update(matchId, order);
}
}
}
}
}
关键工程要点
| 要点 | 说明 |
|---|---|
| 时效性 | 伤停信息有半衰期,用指数衰减;突发消息(官宣)应权重最高 |
| 来源可信度 | 官方公告 0.95、主流媒体 0.8、小道消息 0.4,加权叠加 |
| 去重 | 同一球员同一事件多源报道需合并(用 playerId + 时间窗去重) |
| 非线性叠加 | 多球员伤停不是简单相加,用 1-exp(-Σ) 形式避免过度反应 |
| 调仓阈值 | 小变动不动仓,防止手续费侵蚀收益 |
| 风控硬约束 | 凯利分数缩放 + 单场/单队仓位上限 + 日调仓次数上限 |
| 回测陷阱 | 必须用伤停信息发布时间而非比赛时间,防止未来函数 |
| 幂等 | 同一条伤停事件重复推送不能重复调仓,用事件ID去重 |
更贴近股票的版本(体育俱乐部股)
如果标的是尤文图斯股票、曼联股票等,思路平移:
public class StockRebalanceStrategy {
public TradeSignal onInjuryNews(InjuryInfo injury, Position pos) {
// 核心球员重伤 → 预期战绩下滑 → 减仓
double impact = injury.effectiveImpact();
double keyPlayerWeight = playerImportance(injury.getPlayerId());
double targetReduction = impact * keyPlayerWeight * 0.5; // 最多减半仓
double targetWeight = pos.getWeight() * (1 - targetReduction);
if (Math.abs(targetWeight - pos.getWeight()) < 0.01) {
return TradeSignal.HOLD;
}
return TradeSignal.rebalanceTo(targetWeight);
}
private double playerImportance(String playerId) {
// 从阵容数据、历史贡献加权
return 0.0; // 省略实现
}
}
要点:
- 股票流动性好,可实时调仓,但要考虑消息是否已被市场消化(可用成交量/价差判断)
- 多球员伤停叠加时可分批减仓
- 加入反向信号:核心球员回归 → 加仓
总结公式
目标仓位 = clip(
Kelly( modelProb ⊕ f(伤停影响) , marketOdds ) * kellyFraction,
0,
bankroll * maxPositionPct
)
伤停影响 = Σ( impact_i × reliability_i × exp(-Δt_i / τ) ) 则实力乘子 = exp(-影响)
核心思想:伤停信息 → 修正胜率/概率 → 重算凯利仓位 → 与当前仓位比较 → 触发调仓,中间用时间衰减、来源可信度和风控约束做工程加固。
需要我针对具体场景(足彩、篮球盘口、股票)展开更详细的代码或回测框架吗?