Stage 7.1: prediction markets (LMSR, event/faction/purity/price markets, insider check)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-04 07:26:27 +08:00
1 parent 7d13ae2fe8
commit e5a6e3e22c
11 files changed
+810 -4

No files matched your search

+143
View File
@@ -1043,6 +1043,149 @@ internal static class Program
Check(o.State == OrderState.Refunded && Math.Abs(b.Get(Assets.CRT) - bBefore) < 1e-6 && a.Get(Assets.CRT) == aBefore, $"骗子托管退款回 B:{o.State}");
});
Test("预测市场:LMSR 定价、最大亏损、买卖", () =>
{
foreach (double p0 in new[] { 0.5, 0.08, 0.9 })
{
var m = PredictMarket.Create(1, PredictKind.Event, "x", "t", 0, 10, 11, 12, p0);
Check(Math.Abs(m.PYes - p0) < 1e-9, $"初始价 = 先验 {p0}:{m.PYes:0.0000}");
Check(Math.Abs(m.MaxLoss - PredictMarket.LossFor(10)) < 1e-6, "最大亏损等于预设");
double cost = 100, sh = m.SharesFor(true, cost);
Check(Math.Abs(m.BuyCost(true, sh) - cost) < 1e-6, $"闭式反解和成本函数一致({p0})");
Check(Math.Abs(m.SellValue(true, 0)) < 1e-9, "卖 0 份收回 0");
// 系统最大亏损:玩家买满一边,赢了系统最多亏 MaxLoss
double paid = m.BuyCost(true, 5000);
Check(5000 - paid <= m.MaxLoss + 1e-6, $"买得再多系统也亏不过最大亏损:{5000 - paid:0.0} ≤ {m.MaxLoss:0.0}");
var m2 = PredictMarket.Create(1, PredictKind.Event, "x", "t", 0, 10, 11, 12, p0);
double paidNo = m2.BuyCost(false, 5000);
Check(5000 - paidNo <= m2.MaxLoss + 1e-6, "反方向同理");
// 买了再卖:只损失手续费之外的价差为 0
var m3 = PredictMarket.Create(1, PredictKind.Event, "x", "t", 0, 10, 11, 12, p0);
double s3 = m3.SharesFor(true, 50);
m3.QYes += s3;
Check(Math.Abs(m3.SellValue(true, s3) - 50) < 1e-6, "原路卖回收回成本");
}
});
Test("预测市场:买入、卖出、手续费、污点、持仓限制", () =>
{
Func<string, double> pr = a => 1;
var acct = new Wallet { Address = "poinbase" };
acct.Add(Assets.CRC, 1000); acct.Taint = 400; acct.Basis = 1000;
var bk = new PredictBook();
var m = PredictMarket.Create(++bk.Seq, PredictKind.Purity, "gutter", "t", 8500, 5, 6, 6, 0.5);
bk.Markets.Add(m);
Check(bk.Buy(acct, m, true, 0, 5, pr, false, out _) == PredictResult.Amount, "金额为 0 被拒");
Check(bk.Buy(acct, m, true, 2000, 5, pr, false, out _) == PredictResult.Funds, "钱不够被拒");
Check(bk.Buy(acct, m, true, 100, 6, pr, false, out _) == PredictResult.Closed, "窗口开始后封盘");
var res = bk.Buy(acct, m, true, 102, 5, pr, false, out double sh);
Check(res == PredictResult.Ok && sh > 0, "买入成功");
var pos = bk.PosOf(m.Id);
Check(Math.Abs(pos.Cost - 100) < 1e-6 && Math.Abs(acct.Get(Assets.CRC) - 898) < 1e-6, $"成本 100(手续费 2):{pos.Cost:0.00}");
Check(Math.Abs(pos.Taint - 40.8) < 1e-6 && Math.Abs(acct.Taint - 359.2) < 1e-6, $"污点按花掉的价值比例带进持仓:{pos.Taint:0.00}");
Check(bk.Buy(acct, m, false, 10, 5, pr, false, out _) == PredictResult.Opposite, "不能同盘反方向");
// 卖出:价差为 0,只付 2% 手续费
double before = acct.Get(Assets.CRC);
Check(bk.SellAll(acct, m, 5, out double got) == PredictResult.Ok && Math.Abs(got - 98) < 1e-6, $"原价卖回只扣手续费:{got:0.00}");
Check(Math.Abs(acct.Get(Assets.CRC) - before - got) < 1e-6 && bk.PosOf(m.Id) == null, "回到账户");
Check(acct.Taint < 400 && acct.Taint > 359.2, "卖回时污点按退回的比例回来");
// 持仓上限
var big = new Wallet { Address = "x" }; big.Add(Assets.CRC, 1e7);
Check(bk.Buy(big, m, true, 1e6, 5, pr, false, out _) == PredictResult.Cap, "超过份额上限被拒");
Check(bk.SellAll(acct, m, 5, out _) == PredictResult.NoPosition, "没持仓不能卖");
});
Test("预测市场:结算、作废、内幕被查、观测", () =>
{
Func<string, double> pr = a => 1;
var rng = new Rng(5);
var acct = new Wallet { Address = "poinbase" };
acct.Add(Assets.CRC, 100000);
var bk = new PredictBook();
// 观测:第一晚没有 Prev,不算 Started
bk.Night(4, new[] { "oxymoreWave" }, m => null, acct, rng);
Check(!bk.Started.ContainsKey(4), "第一晚没有对照,不记新出现的事件");
var win = PredictMarket.Create(++bk.Seq, PredictKind.Event, "powerShortage", "停电", 0, 4, 5, 6, 0.2);
var lose = PredictMarket.Create(++bk.Seq, PredictKind.Event, "blackout", "x", 0, 4, 5, 6, 0.2);
var voided = PredictMarket.Create(++bk.Seq, PredictKind.Faction, "FACTION_SECURITY", "镇压", 0, 4, 5, 8, 0.2);
bk.Markets.AddRange(new[] { win, lose, voided });
bk.Buy(acct, win, true, 51, 4, pr, false, out double sw);
bk.Buy(acct, lose, true, 51, 4, pr, false, out _);
bk.Buy(acct, voided, true, 51, 4, pr, false, out _);
double cash = acct.Get(Assets.CRC);
bk.Night(5, new[] { "oxymoreWave", "powerShortage" }, bk.Happened, acct, rng);
Check(win.Outcome < 0 && lose.Outcome < 0, "窗口没结束不结算");
var evs = bk.Night(6, new[] { "oxymoreWave", "powerShortage" }, bk.Happened, acct, rng);
Check(win.Outcome == -1 || bk.Markets.All(x => x.Id != win.Id), "到期的盘口移出列表");
Check(evs.Any(e => e.Kind == "win" && e.Title == "停电" && Math.Abs(e.Amount - sw) < 1e-6), "押中兑付份数");
Check(evs.Any(e => e.Kind == "lose" && e.Title == "x"), "押错没收");
Check(Math.Abs(acct.Get(Assets.CRC) - cash - sw) < 1e-6, "账户只多了赢的那份");
Check(bk.Markets.Count == 1 && bk.Markets[0].Id == voided.Id, "后面的窗口还没到");
// 缺观测:8 号之前 7、8 的记录缺,到期作废退本金
double c2 = acct.Get(Assets.CRC);
var ev2 = bk.Night(9, new string[0], bk.Happened, acct, rng);
Check(ev2.Any(e => e.Kind == "void") && Math.Abs(acct.Get(Assets.CRC) - c2 - 50) < 1e-6, $"缺数据作废并退本金:{acct.Get(Assets.CRC) - c2:0.00}");
// 内幕被查:持仓达到上限的 40% 就有概率;200 次里大约一半
int caught = 0, wins = 0;
for (int i = 0; i < 400; i++)
{
var b2 = new PredictBook(); var a2 = new Wallet { Address = "p" }; a2.Add(Assets.CRC, 1e6);
var mk = PredictMarket.Create(++b2.Seq, PredictKind.Event, "e", "t", 0, 4, 5, 5, 0.1); b2.Markets.Add(mk);
double target = Tuning.PredictInsiderShare * mk.ShareCap * 1.05, spend = 0;
spend = (mk.BuyCost(true, target)) * (1 + Tuning.PredictFee);
b2.Buy(a2, mk, true, spend, 4, pr, true, out _);
var e = b2.Night(5, new string[0], mm => true, a2, new Rng((ulong)(i + 1)));
if (e[0].Kind == "caught") { caught++; Check(e[0].Fraud > 0 && e[0].Fraud <= Tuning.PredictFraudCap, "罪证有上限"); }
else wins++;
}
Check(caught > 140 && caught < 260, $"内幕大单约一半被查:{caught}/400");
// 小额内幕不会被查
var b3 = new PredictBook(); var a3 = new Wallet { Address = "p" }; a3.Add(Assets.CRC, 1e6);
var m3 = PredictMarket.Create(++b3.Seq, PredictKind.Event, "e", "t", 0, 4, 5, 5, 0.1); b3.Markets.Add(m3);
b3.Buy(a3, m3, true, m3.BuyCost(true, 0.2 * m3.ShareCap) * 1.02, 4, pr, true, out _);
Check(b3.Night(5, new string[0], mm => true, a3, new Rng(1))[0].Kind == "win", "小额内幕安全");
// 被查:只退本金
var b4 = new PredictBook(); var a4 = new Wallet { Address = "p" }; a4.Add(Assets.CRC, 1e6);
var m4 = PredictMarket.Create(++b4.Seq, PredictKind.Event, "e", "t", 0, 4, 5, 5, 0.1); b4.Markets.Add(m4);
double s4 = m4.BuyCost(true, 0.5 * m4.ShareCap) * 1.02;
b4.Buy(a4, m4, true, s4, 4, pr, true, out _);
double c4 = a4.Get(Assets.CRC);
for (ulong sd = 1; sd < 50; sd++)
{
var bb = new PredictBook(); bb.Markets.Add(m4); bb.Positions.AddRange(b4.Positions.Select(x => new PredictPos { Market = x.Market, Yes = x.Yes, Shares = x.Shares, Cost = x.Cost, Insider = x.Insider }));
var aa = new Wallet { Address = "p" };
m4.Outcome = -1;
var ee = bb.Night(5, new string[0], mm => true, aa, new Rng(sd));
if (ee[0].Kind == "caught") { Check(Math.Abs(aa.Get(Assets.CRC) - (b4.Positions[0].Cost)) < 1e-6, "被查只退本金"); break; }
}
});
Test("预测市场:先验概率", () =>
{
Check(Math.Abs(PredictMath.PurityAtLeast(8500, 8000, 8999) - 0.5) < 1e-9, "纯度 8500 以上一半");
Check(PredictMath.PurityAtLeast(7000, 8000, 8999) == 1 && PredictMath.PurityAtLeast(9500, 8000, 8999) == 0, "越界取 0 和 1");
double p3 = PredictMath.EventProb(0.4, 15, 150, 3);
Check(Math.Abs(p3 - (1 - Math.Pow(1 - 0.04, 3))) < 1e-12, "事件概率:每天的概率连乘");
Check(PredictMath.FactionProb(1) > 0 && PredictMath.FactionProb(3) > PredictMath.FactionProb(1), "天数越长概率越大");
Check(Math.Abs(PredictMath.Phi(0) - 0.5) < 1e-6 && PredictMath.Phi(1.96) > 0.974 && PredictMath.Phi(1.96) < 0.976, "正态分布函数");
// 行情先验和 Market 的真实过程对得上
foreach (var (mult, nights) in new[] { (1.0, 1), (1.1, 2), (0.9, 3), (1.2, 4) })
{
int hit = 0, n = 3000;
for (int i = 0; i < n; i++)
{
var mk = Market.CreateDefault(20);
var rng = new Rng((ulong)(i * 7 + 3));
double start = mk.Price, anchor = mk.Anchor(new List<MarketEvent>());
double thr = start * mult;
for (int k = 0; k < nights; k++) { mk.Night(new List<MarketEvent>(), rng, 20 + k, a => 1); mk.Open(new List<MarketEvent>(), rng, 21 + k); }
if (mk.Price >= thr) hit++;
}
double sim = hit / (double)n, calc = PredictMath.PriceAtLeast(Market.CreateDefault(20).Price, Market.CreateDefault(20).Anchor(new List<MarketEvent>()), Tuning.PscVol, Tuning.Revert, nights, Market.CreateDefault(20).Price * mult);
Check(Math.Abs(sim - calc) < 0.04, $"行情先验 ×{mult} {nights} 晚:模拟 {sim:0.000} vs 公式 {calc:0.000}");
}
});
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
return _failed == 0 ? 0 : 1;
}