diff --git a/CHANGELOG.md b/CHANGELOG.md index cb6db3b..f27ba51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,3 +3,4 @@ ## 未发布 - 纯逻辑层:带种子的随机数、资产登记、地址账本(污点与成本随币走)、恒定乘积交易对、挖矿(显卡、矿机、全网算力、减半、损耗与暴毙)。 +- 纯逻辑层:市场(价格锚、情绪、游戏事件冲击、NPC 大户、多交易对套利回归、当日均价),三种稳定币(CRC 脱锚与减记、CRT 脱锚与冻结概率、CRA 补贴池与死亡螺旋),合规出金与 AML(大额与累计阈值、污点比例、轻度与重度命中、只对收益计税)。 diff --git a/src/Decentralized.Domain/Exchange.cs b/src/Decentralized.Domain/Exchange.cs new file mode 100644 index 0000000..05d6fdd --- /dev/null +++ b/src/Decentralized.Domain/Exchange.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Decentralized.Domain +{ + public enum AmlOutcome { Clear, Light, Heavy } + + /// 一次合规出金的结果。罪证由接入层记进游戏的罪证账本(销赃)。 + public sealed class CashoutResult + { + public double Requested; + public double Received; + public double Confiscated; + public double Crime; + public double Tax; + public double HitChance; + public AmlOutcome Outcome; + } + + /// + /// 合规交易所出金:CRC 兑回信用点,过 AML。审查按单笔金额、7 天累计金额、污点比例、是否来自混币器或被标记地址判定。 + /// 轻度命中(污点 < 30%)没收污点部分、不记罪证;重度命中没收污点部分,并把污点额记为销赃罪证。 + /// + public sealed class Exchange + { + /// 最近的出金记录:(天, 金额)。 + public List> History = new List>(); + + public static double HitChance(double amount, double taint, double recentTotal, double rent, bool fromMixerOrFlagged) + { + if (amount <= 0) return 0; + double ratio = taint / amount; + double big = Tuning.AmlBigRents * rent, cum = Tuning.AmlCumRents * rent; + double p = 0; + if (ratio >= Tuning.AmlTaintRatio) p += ratio * 1.5; + p += Math.Max(0, (amount - big) / big) * 0.2; + p += Math.Max(0, (recentTotal + amount - cum) / cum) * 0.2; + if (fromMixerOrFlagged) p += Tuning.AmlMixerBonus; + return Math.Min(Tuning.AmlMaxChance, p); + } + + /// + /// 从地址里取出 amount 个 CRC 兑成信用点(按 CRC 当前价格,平时为 1;脱锚期间按市价),扣手续费。污点与成本按比例随取出部分计算; + /// 只对超出成本的部分计税。 + /// + public CashoutResult Cashout(Wallet wallet, double amount, Func price, int day, double rent, bool fromMixer, Rng rng) + { + amount = Math.Min(amount, wallet.Get(Assets.CRC)); + var r = new CashoutResult { Requested = amount }; + if (amount <= 0) return r; + double total = wallet.Value(price); + double share = total > 0 ? Math.Min(1, amount * price(Assets.CRC) / total) : 0; + double taint = wallet.Taint * share, basis = wallet.Basis * share; + History.RemoveAll(h => h.Key <= day - Tuning.AmlWindowDays); + double recent = History.Sum(h => h.Value); + double value = amount * price(Assets.CRC); + r.HitChance = HitChance(value, taint, recent, rent, fromMixer || wallet.Flagged); + wallet.Add(Assets.CRC, -amount); + wallet.Taint -= taint; + wallet.Basis -= basis; + History.Add(new KeyValuePair(day, value)); + double got = value * (1 - Tuning.ExchangeFee); + if (rng.Chance(r.HitChance)) + { + r.Outcome = taint / value >= Tuning.AmlHeavyRatio ? AmlOutcome.Heavy : AmlOutcome.Light; + r.Confiscated = Math.Min(got, taint); + got -= r.Confiscated; + if (r.Outcome == AmlOutcome.Heavy) r.Crime = taint; + } + r.Tax = Math.Max(0, got - basis) * Tuning.TaxRate; + r.Received = got; + return r; + } + } +} diff --git a/src/Decentralized.Domain/Market.cs b/src/Decentralized.Domain/Market.cs new file mode 100644 index 0000000..80685dc --- /dev/null +++ b/src/Decentralized.Domain/Market.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Decentralized.Domain +{ + /// + /// 影响行情的游戏事件种类。接入层把游戏的新闻事件与派系行动映射到这里(例如全站停电、电力短缺 → Blackout)。 + /// + public enum MarketEvent { Blackout, Crackdown, GangWar, RevRaidUpper, LowerUnrest } + + /// + /// PSCoin 行情与全部交易对。PSCoin 有一个参考价(主交易对:合规交易所 PSC/CRC); + /// 夜间参考价向价格锚回归并加噪声,其余交易对由 NPC 套利者拉向参考价。 + /// + public sealed class Market + { + public double Sentiment = 1.0; + public List Pools = new List(); + /// 开门时的参考价,夜间用它和收盘价算当日均价(合约的标记价)。 + public double OpenPrice = Tuning.PscStartPrice; + + public const double ArbRate = 0.5; // 非主交易对每晚被拉回参考价的比例 + public const double SideDepthShare = 0.3; // 非主交易对的深度占主交易对的比例 + + public static Market CreateDefault(int day) + { + var m = new Market(); + double depth = Tuning.PoolDepth(day), p = Tuning.PscStartPrice; + m.Pools.Add(Pool.Create(Venue.Exchange, Assets.PSC, Assets.CRC, p, depth, Tuning.ExchangeFee)); + m.Pools.Add(Pool.Create(Venue.Dex, Assets.PSC, Assets.CRT, p, depth * SideDepthShare, Tuning.DexFee)); + m.Pools.Add(Pool.Create(Venue.Dex, Assets.PSC, Assets.CRA, p, depth * SideDepthShare, Tuning.DexFee)); + m.Pools.Add(Pool.Create(Venue.Dex, Assets.CRT, Assets.CRC, 1, depth * SideDepthShare, Tuning.DexFee)); + return m; + } + + public Pool Main => Pools[0]; + + public Pool Find(Venue venue, string baseAsset, string quote) => + Pools.FirstOrDefault(x => x.Venue == venue && x.Base == baseAsset && x.Quote == quote); + + /// PSCoin 的参考价(信用点)。 + public double Price => Main.Price; + + public double Anchor(ICollection active) + { + double a = Tuning.PscStartPrice * Sentiment; + if (active.Contains(MarketEvent.Blackout)) a *= 1.10; + if (active.Contains(MarketEvent.Crackdown)) a *= 0.92; + return a; + } + + public static double Volatility(ICollection active) + { + double v = Tuning.PscVol; + if (active.Contains(MarketEvent.GangWar)) v *= 2; + else if (active.Contains(MarketEvent.RevRaidUpper)) v *= 1.5; + return v; + } + + /// 开门:一次日内波动(幅度为日波动率的一半)。 + public void Open(ICollection active, Rng rng, int day) + { + double p = Price * Math.Exp(rng.Gauss() * Volatility(active) / 2); + Main.SetPrice(p, Tuning.PoolDepth(day)); + OpenPrice = p; + } + + /// NPC 大户:每周约 2 笔、每笔 2,000–8,000 信用点,随机买或卖。返回成交额(正为买入),没有交易时为 0。 + public double Whale(Rng rng) + { + if (!rng.Chance(2.0 / 7)) return 0; + double size = rng.Range(2000.0, 8000.0); + if (rng.Chance(0.5)) { Main.BuyBase(size); return size; } + Main.SellBase(size / Price); + return -size; + } + + /// 当日均价:合约强平与借贷估值用它,白天的插针不触发强平。 + public double Mark => (OpenPrice + Price) / 2; + + /// + /// 夜间:情绪做均值回归随机游走;参考价向锚回归 15% 并加噪声,限制在锚的 0.3–3 倍; + /// 其余交易对按各自计价币的价格拉向参考价。 + /// + public void Night(ICollection active, Rng rng, int day, Func stablePrice) + { + Sentiment *= Math.Exp(Tuning.SentimentKappa * -Math.Log(Sentiment) + rng.Gauss() * Tuning.SentimentVol); + double anchor = Anchor(active); + double lp = Math.Log(Price); + lp += Tuning.Revert * (Math.Log(anchor) - lp) + rng.Gauss() * Volatility(active); + double p = Math.Max(Tuning.PscStartPrice * Tuning.PriceFloor, Math.Min(Tuning.PscStartPrice * Tuning.PriceCeiling, Math.Exp(lp))); + double depth = Tuning.PoolDepth(day); + Main.SetPrice(p, depth); + foreach (var pool in Pools.Skip(1)) + { + double baseCredits = pool.Base == Assets.PSC ? p : stablePrice(pool.Base); + double quoteCredits = pool.Quote == Assets.PSC ? p : stablePrice(pool.Quote); + if (quoteCredits <= 0) continue; + pool.Revert(baseCredits / quoteCredits, ArbRate, depth * SideDepthShare / quoteCredits); + } + } + } +} diff --git a/src/Decentralized.Domain/Stablecoins.cs b/src/Decentralized.Domain/Stablecoins.cs new file mode 100644 index 0000000..e7f1fab --- /dev/null +++ b/src/Decentralized.Domain/Stablecoins.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; + +namespace Decentralized.Domain +{ + /// + /// 三种稳定币的价格与状态:CRC(上层,托管银行出事时脱锚、可能减记),CRT(黑市,镇压或火并时脱锚, + /// 镇压期间按黑市声望出卖客户),CRA(革命军,质押付息,补贴池告急或外生触发时进入三天死亡螺旋)。 + /// + public sealed class Stablecoins + { + public double Crc = 1, Crt = 1, Cra = 1; + public int CrcDepegEnd = -1, CrtDepegEnd = -1; + public bool CrcHaircutPending; + public double CraSubsidy = Tuning.CraSubsidy0; + public int CraWarnDay = -1, CraSpiralDay = -1; + public double RevPower = 50; + public List RevHistory = new List(); + + public double Price(string asset) + { + switch (asset) + { + case Assets.CRC: return Crc; + case Assets.CRT: return Crt; + case Assets.CRA: return Cra; + default: return 1; + } + } + + /// 减记发生时返回 CRC 持有量要乘的系数(其余情况为 1)。 + public double OnEventStart(MarketEvent ev, int day, Rng rng) + { + if (ev == MarketEvent.RevRaidUpper && CrcDepegEnd < 0 && rng.Chance(Tuning.CrcDepegChance)) + { + Crc = rng.Range(Tuning.CrcDepegMin, Tuning.CrcDepegMax); + CrcDepegEnd = day + rng.Range(Tuning.CrcDepegDaysMin, Tuning.CrcDepegDaysMax); + CrcHaircutPending = rng.Chance(Tuning.CrcHaircutChance); + } + if ((ev == MarketEvent.GangWar || ev == MarketEvent.Crackdown) && CrtDepegEnd < 0 && rng.Chance(Tuning.CrtDepegChance)) + { + Crt = rng.Range(Tuning.CrtDepegMin, Tuning.CrtDepegMax); + CrtDepegEnd = day + rng.Range(Tuning.CrtDepegDaysMin, Tuning.CrtDepegDaysMax); + } + return 1; + } + + /// 镇压开始时,玩家持有的 CRT 被冻结一半的概率:黑市声望 0 时 40%,50 以上为 0。 + public static double CrtFreezeChance(double blackMarketRep) => + Math.Max(0, Tuning.CrtFreezeBase - blackMarketRep * Tuning.CrtFreezePerRep); + + public bool CraSubsidyLow => CraSubsidy <= Tuning.CraSubsidy0 * Tuning.CraSubsidyWarn; + + /// CRA 的预兆:外生螺旋的新闻已出,或补贴池告急(利率下调)。螺旋开始后不再算预兆。 + public bool CraWarning(int day) => (CraWarnDay >= 0 && CraWarnDay <= day || CraSubsidyLow) && Cra >= 0.97; + + public double CraRate => CraSubsidyLow ? Tuning.CraRateLow : Tuning.CraRate; + + /// + /// 夜间结算。返回 CRC 持有量要乘的系数(减记兑现时为 0.95,否则为 1)。 + /// playerStaked:玩家的 CRA 质押额,用来算补贴支出(玩家的利息由调用方按 发放)。 + /// + public double Night(int day, Rng rng, double playerStaked) + { + double crcFactor = 1; + if (CrcDepegEnd == day) + { + Crc = 1; + if (CrcHaircutPending) crcFactor = Tuning.CrcHaircut; + CrcDepegEnd = -1; + CrcHaircutPending = false; + } + if (CrtDepegEnd == day) + { + Crt = 1; + CrtDepegEnd = -1; + } + RevHistory.Add(RevPower); + if (RevHistory.Count > 7) RevHistory.RemoveAt(0); + RevPower = Math.Max(0, Math.Min(100, RevPower + rng.Gauss() * 3)); + + if (CraSpiralDay < 0) + { + CraSubsidy -= (Tuning.CraNpcStake + playerStaked) * CraRate; + bool revFalling = RevHistory.Count == 7 && RevPower < RevHistory[0] - 5; + double weekly = Tuning.CraSpiralWeekly * (revFalling ? 3 : 1); + double daily = 1 - Math.Pow(1 - weekly, 1.0 / 7); + if (CraWarnDay < 0 && (rng.Chance(daily) || CraSubsidy <= 0)) + { + CraWarnDay = day; + CraSpiralDay = day + rng.Range(Tuning.CraWarnLeadMin, Tuning.CraWarnLeadMax); + } + } + if (CraSpiralDay > 0 && day >= CraSpiralDay) + Cra = Tuning.CraSpiralPath[Math.Min(day - CraSpiralDay, Tuning.CraSpiralPath.Length - 1)]; + return crcFactor; + } + } +} diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index fc3a849..bb858f4 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -28,6 +28,30 @@ namespace Decentralized.Domain public const double SentimentKappa = 0.02, SentimentVol = 0.02; public const double PriceFloor = 0.3, PriceCeiling = 3.0; + // 稳定币 + public const double CrcDepegChance = 0.30, CrcDepegMin = 0.88, CrcDepegMax = 0.93; + public const int CrcDepegDaysMin = 2, CrcDepegDaysMax = 3; + public const double CrcHaircutChance = 0.15, CrcHaircut = 0.95; + public const double CrtDepegChance = 0.40, CrtDepegMin = 0.92, CrtDepegMax = 0.96; + public const int CrtDepegDaysMin = 1, CrtDepegDaysMax = 2; + public const double CrtFreezeBase = 0.40, CrtFreezePerRep = 0.008, CrtFreezeShare = 0.5; + public const int CrtUnfreezeAfter = 3; + public const double CrtSwapFee = 0.02; + public const double CraRate = 0.005, CraRateLow = 0.002; + public const int CraUnlockDays = 4; + public const double CraSubsidy0 = 30000, CraSubsidyWarn = 0.2, CraNpcStake = 20000; + public const double CraSpiralWeekly = 0.02; + public const int CraWarnLeadMin = 4, CraWarnLeadMax = 7; + public static readonly double[] CraSpiralPath = { 0.85, 0.30, 0.02 }; + + // 合规出金与 AML(阈值以当前房租为单位) + public const double AmlBigRents = 5, AmlCumRents = 15; + public const int AmlWindowDays = 7; + public const double AmlTaintRatio = 0.10, AmlHeavyRatio = 0.30, AmlMixerBonus = 0.30, AmlMaxChance = 0.9; + public const double TaxRate = 0.08; + /// 单个金融违规事件记入的罪证上限。 + public const double CrimeEventCap = 400; + /// 主交易对(信用点一侧)的深度随天数增长。 public static double PoolDepth(int day) => PoolDepth0 * (1 + day / 60.0); } diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index 504cb1c..2a4de91 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Decentralized.Domain; @@ -158,6 +159,123 @@ internal static class Program $"PMD 20% / 76% / 95%:{q[0]:P0} / {q[1]:P0} / {q[2]:P0}"); }); + Test("市场:价格锚受停电、镇压影响;夜间向锚回归并限制在 0.3–3 倍", () => + { + var m = Market.CreateDefault(1); + var none = new MarketEvent[0]; + Check(Near(m.Anchor(none), 100, 1e-9) && Near(m.Anchor(new[] { MarketEvent.Blackout }), 110, 1e-9) + && Near(m.Anchor(new[] { MarketEvent.Crackdown }), 92, 1e-9), "锚:基准 100,停电 ×1.1,镇压 ×0.92"); + Check(Market.Volatility(new[] { MarketEvent.GangWar }) == Tuning.PscVol * 2, "火并时波动 ×2"); + m.Main.SetPrice(60, Tuning.PoolDepth(1)); + var rng = new Rng(5); + double sum = 0; + for (int i = 0; i < 400; i++) + { + var mm = Market.CreateDefault(1); + mm.Main.SetPrice(60, Tuning.PoolDepth(1)); + mm.Night(none, new Rng((ulong)i), 1, a => 1); + sum += Math.Log(mm.Price); + } + double expect = Math.Log(60) + 0.15 * (Math.Log(100) - Math.Log(60)); + Check(Near(sum / 400, expect, 0.01), $"从 60 起一晚平均回到 {Math.Exp(expect):0.0} 附近:{Math.Exp(sum / 400):0.0}"); + var hi = Market.CreateDefault(1); + hi.Main.SetPrice(1000, Tuning.PoolDepth(1)); + hi.Night(none, rng, 1, a => 1); + Check(hi.Price <= 300 + 1e-9, $"上限 3 倍:{hi.Price:0.0}"); + }); + + Test("市场:其他交易对每晚被套利拉向参考价,三角价差收敛", () => + { + var m = Market.CreateDefault(1); + var crt = m.Find(Venue.Dex, Assets.PSC, Assets.CRT); + crt.SetPrice(130, crt.QuoteReserve); + for (int d = 1; d <= 5; d++) m.Night(new MarketEvent[0], new Rng((ulong)d), 1, a => 1); + double gap = Math.Abs(crt.Price / m.Price - 1); + Check(gap < 0.03, $"五晚后 PSC/CRT 与参考价相差 {gap:P1}"); + var crtCrc = m.Find(Venue.Dex, Assets.CRT, Assets.CRC); + m.Night(new MarketEvent[0], new Rng(9), 1, a => a == Assets.CRT ? 0.9 : 1); + Check(crtCrc.Price < 0.98, $"CRT 脱锚时 CRT/CRC 向 0.9 靠拢:{crtCrc.Price:0.000}"); + }); + + Test("稳定币:CRC 脱锚与减记,CRT 脱锚与冻结概率", () => + { + int depegs = 0, haircuts = 0; + for (int i = 0; i < 2000; i++) + { + var s = new Stablecoins(); + var rng = new Rng((ulong)i); + s.OnEventStart(MarketEvent.RevRaidUpper, 10, rng); + if (s.Crc < 1) + { + depegs++; + Check(s.Crc >= 0.88 && s.Crc <= 0.93 && s.CrcDepegEnd >= 12 && s.CrcDepegEnd <= 13, "CRC 脱锚深度与持续"); + double f = 1; + for (int d = 10; d <= s.CrcDepegEnd; d++) f *= s.Night(d, rng, 0); + if (f < 1) haircuts++; + Check(s.Crc == 1, "到期恢复"); + } + } + Check(Near(depegs / 2000.0, 0.30, 0.03), $"革命军袭击上层时 30% 脱锚:{depegs / 2000.0:P0}"); + Check(Near(haircuts / (double)depegs, 0.15, 0.04), $"其中 15% 减记:{haircuts / (double)depegs:P0}"); + Check(Near(Stablecoins.CrtFreezeChance(0), 0.40, 1e-9) && Stablecoins.CrtFreezeChance(50) == 0 && Near(Stablecoins.CrtFreezeChance(25), 0.2, 1e-9), + "CRT 冻结概率:声望 0 为 40%,25 为 20%,50 为 0"); + }); + + Test("稳定币:CRA 补贴池告急即为预兆;螺旋三天 0.85 / 0.30 / 0.02", () => + { + var s = new Stablecoins { CraSubsidy = Tuning.CraSubsidy0 * 0.19 }; + Check(s.CraWarning(1) && s.CraRate == Tuning.CraRateLow, "补贴池低于 20%:预兆出现、利率降到 0.2%"); + var t = new Stablecoins { CraSubsidy = 1 }; + var rng = new Rng(3); + t.Night(1, rng, 100000); + Check(t.CraWarnDay == 1 && t.CraSpiralDay >= 5 && t.CraSpiralDay <= 8, $"补贴池吃空触发螺旋,提前 4–7 天:第 {t.CraSpiralDay} 天开始"); + var path = new List(); + for (int d = 2; d <= t.CraSpiralDay + 3; d++) { t.Night(d, rng, 0); if (d >= t.CraSpiralDay) path.Add(t.Cra); } + Check(path.Take(3).SequenceEqual(new[] { 0.85, 0.30, 0.02 }), $"螺旋路径:{string.Join(" / ", path)}"); + Check(!t.CraWarning(t.CraSpiralDay + 1), "螺旋开始后不再算预兆"); + }); + + Test("合规出金:干净小额不查;污点高时重度命中记罪证;轻度命中只没收污点部分;只对收益计税", () => + { + Func px = a => 1; + Check(Exchange.HitChance(500, 0, 0, 200, false) == 0, "干净、低于 5 倍房租:不查"); + Check(Near(Exchange.HitChance(2000, 0, 0, 200, false), 0.2, 1e-9), "单笔为大额阈值 2 倍:20%"); + Check(Near(Exchange.HitChance(1000, 500, 0, 200, false), 0.75, 1e-9), "污点 50%:75%"); + Check(Exchange.HitChance(100000, 100000, 1e6, 200, true) == Tuning.AmlMaxChance, "上限 90%"); + int heavy = 0; + double crime = 0; + for (int i = 0; i < 1000; i++) + { + var w = new Wallet { Taint = 500, Basis = 1000 }; + w.Add(Assets.CRC, 1000); + var r = new Exchange().Cashout(w, 1000, px, 1, 200, false, new Rng((ulong)i)); + if (r.Outcome == AmlOutcome.Heavy) { heavy++; crime = r.Crime; Check(Near(r.Received, 998 - 500, 1e-6), "重度:没收污点部分"); } + else Check(Near(r.Received, 998, 1e-6) && r.Tax == 0, "未命中:全额到账;本金不计税"); + } + Check(Near(heavy / 1000.0, 0.75, 0.04) && crime == 500, $"污点 50% 时约 75% 重度命中,罪证 = 污点额:{heavy / 1000.0:P0}"); + var light = new Wallet { Taint = 150 }; + light.Add(Assets.CRC, 1000); + CashoutResult lr = null; + for (ulong i = 0; lr == null || lr.Outcome == AmlOutcome.Clear; i++) + { + var w = new Wallet { Taint = 150 }; + w.Add(Assets.CRC, 1000); + lr = new Exchange().Cashout(w, 1000, px, 1, 200, false, new Rng(i)); + } + Check(lr.Outcome == AmlOutcome.Light && lr.Crime == 0 && Near(lr.Confiscated, 150, 1e-6), "污点 15%:轻度命中,没收 150、不记罪证"); + Check(Near(lr.Tax, (998 - 150) * Tuning.TaxRate, 1e-6), "成本为 0 时到账部分全额计税"); + var ex = new Exchange(); + var big = new Wallet(); + big.Add(Assets.CRC, 4000); + ex.Cashout(big, 2000, px, 1, 200, false, new Rng(1)); + Check(Near(Exchange.HitChance(2000, 0, ex.History.Sum(h => h.Value), 200, false), 0.2 + 0.2 * 1000 / 3000.0, 1e-9), + "7 天内累计超过 15 倍房租也会提高命中率"); + var dep = new Wallet { Basis = 1000 }; + dep.Add(Assets.CRC, 1000); + var dr = new Exchange().Cashout(dep, 1000, a => 0.9, 1, 200, false, new Rng(1)); + Check(Near(dr.Received, 900 * (1 - Tuning.ExchangeFee), 1e-6), "CRC 脱锚时按市价兑出"); + }); + Console.WriteLine($"\n{_passed} 通过,{_failed} 失败"); return _failed == 0 ? 0 : 1; }