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

+91 -2
View File
@@ -349,6 +349,93 @@ namespace Decentralized
});
}
private static int _predPick;
private static readonly float[] PredW = { 330f, 90f };
/// <summary>预测页:左边盘口列表和最近结果,右边选中盘口的买卖。资金走 Poinbase 的 CRC 账户,不用签名。</summary>
private static void PredictPage(CustomUIBuilder b)
{
var d = Session.Data;
var bk = d.Predict;
var acct = d.Poinbase;
int day = StoreStation.GetDayCounter();
var open = bk.Markets.Where(m => m.Outcome < 0).OrderBy(m => m.FromDay).ThenBy(m => m.Id).ToList();
var sel = open.Find(m => m.Id == _predPick) ?? open.FirstOrDefault();
_predPick = sel?.Id ?? 0;
Columns(b, l =>
{
l.AddHeader(L.T("predict.h"));
l.AddLabel(L.T("predict.acct", Fmt.Num(acct.Avail(Assets.CRC))));
l.AddSeparator();
if (open.Count == 0) l.AddLabel(L.T("predict.none"));
foreach (var m in open)
{
var mk = m;
var pos = bk.PosOf(m.Id);
string q = PredictGame.Question(m);
l.BeginRow(4f);
l.AddButton(m.Id == _predPick ? "[" + q + "]" : q, () => { _predPick = mk.Id; _message = null; Open(); });
l.End();
string state = m.IsOpen(day) ? "" : " " + L.T("predict.closed");
l.AddLabel(L.T("predict.row", (m.PYes * 100).ToString("0"), ((1 - m.PYes) * 100).ToString("0")) + (pos != null ? " " + L.T("predict.held", L.T(pos.Yes ? "predict.yes" : "predict.no"), Fmt.Num(pos.Shares)) : "") + state);
}
if (bk.Recent.Count > 0)
{
l.AddSeparator();
foreach (var r in Enumerable.Reverse(bk.Recent))
l.AddLabel(L.T("predict.result", r.Day, r.Title, r.Outcome == 2 ? L.T("predict.void") : L.T(r.Outcome == 1 ? "predict.yes" : "predict.no"), r.Held ? (r.Pnl >= 0 ? "+" : "") + Fmt.Num(r.Pnl) : "—"));
}
}, r =>
{
if (sel == null) { r.AddHeader(L.T("predict.h")); return; }
r.AddHeader(L.T("predict.pick"));
r.AddLabel(PredictGame.Question(sel));
r.AddLabel(L.T("predict.price", (sel.PYes * 100).ToString("0.0"), ((1 - sel.PYes) * 100).ToString("0.0")));
var pos = bk.PosOf(sel.Id);
if (pos != null)
{
r.AddLabel(L.T("predict.pos", L.T(pos.Yes ? "predict.yes" : "predict.no"), Fmt.Num(pos.Shares), Fmt.Num(pos.Cost)));
r.AddLabel(L.T("predict.pos2", Fmt.Num(sel.SellValue(pos.Yes, pos.Shares) * (1 - Tuning.PredictFee))));
}
if (!sel.IsOpen(day)) { r.AddLabel(L.T("predict.closed.note")); return; }
AmountRow(r);
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double v) && v > 0 && v < 1e7)
{
foreach (bool yes in new[] { true, false })
{
double shares = sel.SharesFor(yes, v / (1 + Tuning.PredictFee));
r.AddLabel(L.T("predict.preview", L.T(yes ? "predict.yes" : "predict.no"), Fmt.Num(shares), Fmt.Num(shares - v)));
}
}
r.BeginRow();
r.AddButton(L.T("predict.buy.yes"), () => PredictBuy(sel, true));
r.AddButton(L.T("predict.buy.no"), () => PredictBuy(sel, false));
r.End();
r.AddButton(L.T("predict.sell"), () => PredictSell(sel));
});
}
private static void PredictBuy(PredictMarket m, bool yes)
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
int day = StoreStation.GetDayCounter();
bool insider = PredictGame.Known(m, yes);
var res = d.Predict.Buy(d.Poinbase, m, yes, v, day, Session.Price, insider, out double shares);
_message = res == PredictResult.Ok ? L.T("predict.bought", L.T(yes ? "predict.yes" : "predict.no"), Fmt.Num(shares), Fmt.Num(v)) : L.T("predict.err." + res);
if (res == PredictResult.Ok) { Log.Info($"预测下注 #{m.Id} {(yes ? "是" : "否")} {shares:0.0} 份,花 {v:0.##},内幕 {insider}"); Session.Save(); }
Open();
}
private static void PredictSell(PredictMarket m)
{
var d = Session.Data;
var res = d.Predict.SellAll(d.Poinbase, m, StoreStation.GetDayCounter(), out double got);
_message = res == PredictResult.Ok ? L.T("predict.sold", Fmt.Num(got)) : L.T("predict.err." + res);
if (res == PredictResult.Ok) Session.Save();
Open();
}
private static string PerpErr(PerpResult res) => L.T("perp.err." + res);
private static void PerpOpen()
@@ -494,16 +581,18 @@ namespace Decentralized
{
if (_web3Tab == Web3Tab.Mixer && !Unlock.MixerUnlocked) _web3Tab = Web3Tab.Trade;
if (_web3Tab == Web3Tab.Black && !Unlock.BlackUnlocked) _web3Tab = Web3Tab.Trade;
if (_web3Tab == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter())) _web3Tab = Web3Tab.Trade;
b.BeginRow();
foreach (Web3Tab t in Enum.GetValues(typeof(Web3Tab)))
{
if ((t == Web3Tab.Mixer && !Unlock.MixerUnlocked) || (t == Web3Tab.Black && !Unlock.BlackUnlocked)) continue;
if ((t == Web3Tab.Mixer && !Unlock.MixerUnlocked) || (t == Web3Tab.Black && !Unlock.BlackUnlocked) || (t == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter()))) continue;
var tab = t;
string name = L.T("web3.sub." + t.ToString().ToLowerInvariant());
b.AddButton(tab == _web3Tab ? $"[{name}]" : name, () => { _web3Tab = tab; _message = null; _edit = null; Open(); });
}
b.End();
if (_web3Tab == Web3Tab.Stake) { StakePage(b); return; }
if (_web3Tab == Web3Tab.Predict) { PredictPage(b); return; }
if (_web3Tab == Web3Tab.Mining) { ReportPage(b); return; }
if (_web3Tab == Web3Tab.Mixer) { MixerPage(b); return; }
if (_web3Tab == Web3Tab.Black) { BlackPage(b); return; }
@@ -968,7 +1057,7 @@ namespace Decentralized
Open();
}
private enum Web3Tab { Trade, Stake, Mining, Mixer, Black }
private enum Web3Tab { Trade, Stake, Predict, Mining, Mixer, Black }
private static Web3Tab _web3Tab = Web3Tab.Trade;
private static string _mixAsset = Assets.PSC;
private static int _mixRounds = 2;