From 3758049dcb1be8b8a8573569af657882558c3a7e Mon Sep 17 00:00:00 2001 From: ZHU MINCHAO <48154958+MinchaoZhu@users.noreply.github.com> Date: Thu, 5 Nov 2026 07:09:05 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E5=8D=81=E9=98=B6=E6=AE=B5=20stage=20?= =?UTF-8?q?2=EF=BC=88=E4=B8=80=EF=BC=89=EF=BC=9A=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E5=AF=B9=E6=94=B9=E4=B8=BA=20X/CRC=20=E6=B1=A0=EF=BC=8C?= =?UTF-8?q?=E4=BB=BB=E6=84=8F=E5=B8=81=E4=BA=92=E6=8D=A2=E7=BB=8F=20CRC=20?= =?UTF-8?q?=E4=B8=A4=E8=B7=B3=E8=B7=AF=E7=94=B1=EF=BC=8C=E5=B8=81=E4=BB=B7?= =?UTF-8?q?=E8=A1=A8=E6=94=B9=E4=B8=BA=20CRC=20=E4=BB=B7=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Decentralized.Domain/Market.cs | 44 ++++++++- src/Decentralized.Domain/Tuning.cs | 2 +- src/Decentralized.Domain/TuningTable.cs | 1 + .../ModFiles/Localization/de.csv | 8 +- .../ModFiles/Localization/en.csv | 8 +- .../ModFiles/Localization/es.csv | 8 +- .../ModFiles/Localization/fr.csv | 8 +- .../ModFiles/Localization/ja.csv | 8 +- .../ModFiles/Localization/pl.csv | 8 +- .../ModFiles/Localization/ru.csv | 8 +- .../ModFiles/Localization/zh.csv | 8 +- src/Decentralized/Session.cs | 3 +- src/Decentralized/TerminalWindow.cs | 97 +++++++++++++++---- tests/Decentralized.Domain.Tests/Program.cs | 33 +++++-- 14 files changed, 196 insertions(+), 48 deletions(-) diff --git a/src/Decentralized.Domain/Market.cs b/src/Decentralized.Domain/Market.cs index c3ef71c..939b7d1 100644 --- a/src/Decentralized.Domain/Market.cs +++ b/src/Decentralized.Domain/Market.cs @@ -4,6 +4,14 @@ using System.Linq; namespace Decentralized.Domain { + /// 一次互换的报价:得到数量、经过几个池、相对中间价的损失(手续费加滑点)。 + public sealed class SwapQuote + { + public string From, To; + public double In, Out, Impact; + public int Hops; + } + /// /// 影响行情的游戏事件种类。接入层把游戏的新闻事件与派系行动映射到这里(例如全站停电、电力短缺 → Blackout)。 /// @@ -64,14 +72,43 @@ namespace Decentralized.Domain 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.PSC, Assets.CRC, p, depth * SideDepthShare, Tuning.DexFee)); m.Pools.Add(Pool.Create(Venue.Dex, Assets.CRT, Assets.CRC, 1, depth * SideDepthShare, Tuning.DexFee)); + m.Pools.Add(Pool.Create(Venue.Dex, Assets.CRA, Assets.CRC, 1, depth * SideDepthShare, Tuning.DexFee)); + m.Pools.Add(Pool.Create(Venue.Dex, Assets.SPARK, Assets.CRC, Tuning.SparkStart, depth * Tuning.SparkDepthShare, Tuning.DexFee)); return m; } public Pool Main => Pools[0]; + /// DEX 上 X/CRC 的池(所有币都以 CRC 计价)。 + public Pool DexPool(string asset) => Find(Venue.Dex, asset, Assets.CRC); + + /// + /// 任意两币互换的报价:CRC 在一端走一个池,否则 X → CRC → Y 两跳(两次手续费、滑点叠加)。 + /// 不改池子;没有路由(同币、没有池)返回 null。 + /// + public SwapQuote Quote(string from, string to, double amount) + { + if (from == to || amount <= 0) return null; + var a = from == Assets.CRC ? null : DexPool(from); + var b = to == Assets.CRC ? null : DexPool(to); + if ((from != Assets.CRC && a == null) || (to != Assets.CRC && b == null)) return null; + double crc = a == null ? amount : a.QuoteOut(amount); + double mid = a == null ? amount : amount * a.Price; + double outAmt = b == null ? crc : b.BaseOut(crc); + double ideal = b == null ? mid : mid / b.Price; + return new SwapQuote { From = from, To = to, In = amount, Out = outAmt, Hops = (a != null ? 1 : 0) + (b != null ? 1 : 0), Impact = ideal > 0 ? Math.Max(0, 1 - outAmt / ideal) : 0 }; + } + + /// 成交:按 的路由改动池子,返回得到的数量(没有路由返回 0)。 + public double Swap(string from, string to, double amount) + { + if (Quote(from, to, amount) == null) return 0; + double crc = from == Assets.CRC ? amount : DexPool(from).SellBase(amount); + return to == Assets.CRC ? crc : DexPool(to).BuyBase(crc); + } + public Pool Find(Venue venue, string baseAsset, string quote) => Pools.FirstOrDefault(x => x.Venue == venue && x.Base == baseAsset && x.Quote == quote); @@ -152,7 +189,8 @@ namespace Decentralized.Domain 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); + double share = pool.Base == Assets.SPARK ? Tuning.SparkDepthShare : SideDepthShare; + pool.Revert(baseCredits / quoteCredits, ArbRate, depth * share / quoteCredits); } } } diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index 8dc059b..57694aa 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -97,7 +97,7 @@ namespace Decentralized.Domain /// 外观折价(占价值的比例):叶片积灰 / 缺角,金手指发暗 / 氧化,挡板划痕,封条缺失。 public const double CardFanDust = 0.04, CardFanChipped = 0.12, CardFingersDark = 0.04, CardFingersOxidized = 0.12, CardBracketScratch = 0.02, CardSealMissing = 0.06; /// 非主交易对每晚被拉回参考价的比例,及其深度占主交易对的比例。 - public const double SideArbRate = 0.75, SideDepthShare = 0.3; + public const double SideArbRate = 0.75, SideDepthShare = 0.3, SparkDepthShare = 0.15; /// Web3 黑市:开放所需黑市声望,中 / 高档货所需声望,现货每周刷新的天数与基础件数(每高 BmStockPerRep 声望多一件),现货溢价。 public const int BmOpenRep = 10, BmMidRep = 40, BmHighRep = 80, BmRefreshDays = 7, BmStockBase = 4; public const double BmStockPerRep = 40, BmPremium = 0.30; diff --git a/src/Decentralized.Domain/TuningTable.cs b/src/Decentralized.Domain/TuningTable.cs index 35dad1b..d65c34f 100644 --- a/src/Decentralized.Domain/TuningTable.cs +++ b/src/Decentralized.Domain/TuningTable.cs @@ -324,6 +324,7 @@ namespace Decentralized.Domain ["SentimentVol"] = Tuning.SentimentVol, ["SideArbRate"] = Tuning.SideArbRate, ["SideDepthShare"] = Tuning.SideDepthShare, + ["SparkDepthShare"] = Tuning.SparkDepthShare, ["SparkFee"] = Tuning.SparkFee, ["SparkMaxMult"] = Tuning.SparkMaxMult, ["SparkMinMult"] = Tuning.SparkMinMult, diff --git a/src/Decentralized/ModFiles/Localization/de.csv b/src/Decentralized/ModFiles/Localization/de.csv index 9ee0bfb..788cccd 100644 --- a/src/Decentralized/ModFiles/Localization/de.csv +++ b/src/Decentralized/ModFiles/Localization/de.csv @@ -149,8 +149,6 @@ bank.ops,Ein-/Auszahlung poin.account.h,Poinbase-Konto trade.h,Handel web3.wallet.h,On-Chain-Wallet -col.peg,Preis -col.status,Status stable.ok,Gekoppelt stable.depeg,Entkoppelt stable.warn,Warnung: Staking-Subventionspool knapp @@ -902,3 +900,9 @@ rig.st.power,Verbrauch rig.st.hash,Hashrate rig.st.kj,{0} kJ/Tag lbl.point.model,Modell beanstanden +col.crcprice,CRC-Preis +swap.pay,Zahlen +swap.get,Erhalten +swap.preview,"Ca. {0} {1} {2} Pool(s), Verlust {3} %" +swap.go,Tauschen +swap.same,Zahlungs- und Empfangswährung müssen verschieden sein. diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index 193f3dc..ed8a0bd 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -149,8 +149,6 @@ bank.ops,Deposit / withdraw poin.account.h,Poinbase account trade.h,Trade web3.wallet.h,On-chain wallet -col.peg,Price -col.status,Status stable.ok,Pegged stable.depeg,Depegged stable.warn,Warning: staking subsidy pool low @@ -902,3 +900,9 @@ rig.st.power,Power rig.st.hash,Hashrate rig.st.kj,{0} kJ/day lbl.point.model,Point out model +col.crcprice,CRC price +swap.pay,Pay +swap.get,Receive +swap.preview,"Receive about {0} {1} {2} pool(s), loss {3}%" +swap.go,Swap +swap.same,Pay and receive must be different coins. diff --git a/src/Decentralized/ModFiles/Localization/es.csv b/src/Decentralized/ModFiles/Localization/es.csv index 9bb25b6..3d948c4 100644 --- a/src/Decentralized/ModFiles/Localization/es.csv +++ b/src/Decentralized/ModFiles/Localization/es.csv @@ -149,8 +149,6 @@ bank.ops,Depósito / retiro poin.account.h,Cuenta de Poinbase trade.h,Comercio web3.wallet.h,Billetera en cadena -col.peg,Precio -col.status,Estado stable.ok,Anclado stable.depeg,Desanclado stable.warn,Advertencia: fondo de subsidio de staking bajo @@ -902,3 +900,9 @@ rig.st.power,Consumo rig.st.hash,Tasa de hash rig.st.kj,{0} kJ/día lbl.point.model,Señalar modelo +col.crcprice,Precio en CRC +swap.pay,Pagar +swap.get,Recibir +swap.preview,"Aprox. {0} {1} {2} pool(s), pérdida {3} %" +swap.go,Canjear +swap.same,Las monedas a pagar y recibir deben ser distintas. diff --git a/src/Decentralized/ModFiles/Localization/fr.csv b/src/Decentralized/ModFiles/Localization/fr.csv index 957b3b9..9428b72 100644 --- a/src/Decentralized/ModFiles/Localization/fr.csv +++ b/src/Decentralized/ModFiles/Localization/fr.csv @@ -149,8 +149,6 @@ bank.ops,Dépôt / Retrait poin.account.h,Compte Poinbase trade.h,Trading web3.wallet.h,Portefeuille on-chain -col.peg,Prix -col.status,Statut stable.ok,Arrimé stable.depeg,Désarrimé stable.warn,Attention : pool de subvention de staking au plus bas @@ -902,3 +900,9 @@ rig.st.power,Conso. rig.st.hash,Hachage rig.st.kj,{0} kJ/j lbl.point.model,Signaler le modèle +col.crcprice,Prix en CRC +swap.pay,Payer +swap.get,Recevoir +swap.preview,"Environ {0} {1} {2} pool(s), perte {3} %" +swap.go,Échanger +swap.same,Les monnaies payée et reçue doivent différer. diff --git a/src/Decentralized/ModFiles/Localization/ja.csv b/src/Decentralized/ModFiles/Localization/ja.csv index 761a2c5..e5cf5bf 100644 --- a/src/Decentralized/ModFiles/Localization/ja.csv +++ b/src/Decentralized/ModFiles/Localization/ja.csv @@ -149,8 +149,6 @@ bank.ops,入出金 poin.account.h,Poinbase口座 trade.h,取引 web3.wallet.h,オンチェーンウォレット -col.peg,価格 -col.status,状態 stable.ok,ペグ正常 stable.depeg,デペグ stable.warn,警告:ステーキング補助金プール残高低下 @@ -902,3 +900,9 @@ rig.st.power,消費電力 rig.st.hash,ハッシュレート rig.st.kj,{0} kJ/日 lbl.point.model,型番を指摘する +col.crcprice,CRC 価格 +swap.pay,支払い +swap.get,受取 +swap.preview,約 {0} {1} プール {2} 個、損失 {3}% +swap.go,スワップ +swap.same,支払いと受取は別の通貨にしてください。 diff --git a/src/Decentralized/ModFiles/Localization/pl.csv b/src/Decentralized/ModFiles/Localization/pl.csv index 076e9bd..aa1b017 100644 --- a/src/Decentralized/ModFiles/Localization/pl.csv +++ b/src/Decentralized/ModFiles/Localization/pl.csv @@ -149,8 +149,6 @@ bank.ops,Wpłata / wypłata poin.account.h,Konto Poinbase trade.h,Handel web3.wallet.h,Portfel on-chain -col.peg,Cena -col.status,Status stable.ok,W parytecie stable.depeg,Utrata parytetu stable.warn,Ostrzeżenie: niska pula dotacji stakingu @@ -902,3 +900,9 @@ rig.st.power,Pobór rig.st.hash,Moc obl. rig.st.kj,{0} kJ/dzień lbl.point.model,Wskaż model +col.crcprice,Cena w CRC +swap.pay,Płacę +swap.get,Otrzymuję +swap.preview,"Około {0} {1} pule: {2}, strata {3}%" +swap.go,Wymień +swap.same,Waluty płatności i odbioru muszą się różnić. diff --git a/src/Decentralized/ModFiles/Localization/ru.csv b/src/Decentralized/ModFiles/Localization/ru.csv index edaa9e8..8b3cce8 100644 --- a/src/Decentralized/ModFiles/Localization/ru.csv +++ b/src/Decentralized/ModFiles/Localization/ru.csv @@ -149,8 +149,6 @@ bank.ops,Депозит / вывод poin.account.h,Счёт Poinbase trade.h,Торговля web3.wallet.h,Ончейн-кошелёк -col.peg,Цена -col.status,Статус stable.ok,Привязан stable.depeg,Отвязан stable.warn,Внимание: пул субсидий стейкинга на исходе @@ -902,3 +900,9 @@ rig.st.power,Расход rig.st.hash,Хешрейт rig.st.kj,{0} кДж/день lbl.point.model,Указать модель +col.crcprice,Цена в CRC +swap.pay,Отдаю +swap.get,Получаю +swap.preview,"Около {0} {1} пулов: {2}, потери {3}%" +swap.go,Обменять +swap.same,Монеты оплаты и получения должны различаться. diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv index 97bb46c..6965684 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -148,8 +148,6 @@ bank.ops,"存取" poin.account.h,"Poinbase 账户" trade.h,"交易" web3.wallet.h,"链上钱包" -col.peg,"价格" -col.status,"状态" stable.ok,"锚定正常" stable.depeg,"脱锚" stable.warn,"预兆:质押补贴池告急" @@ -901,3 +899,9 @@ rig.st.power,耗电 rig.st.hash,算力 rig.st.kj,{0} kJ/天 lbl.point.model,指出型号 +col.crcprice,CRC 价格 +swap.pay,支付 +swap.get,获得 +swap.preview,约得 {0} {1} 经 {2} 个池,损耗 {3}% +swap.go,兑换 +swap.same,支付和获得的币种不能相同。 diff --git a/src/Decentralized/Session.cs b/src/Decentralized/Session.cs index 213f13e..f20ec0c 100644 --- a/src/Decentralized/Session.cs +++ b/src/Decentralized/Session.cs @@ -136,7 +136,8 @@ namespace Decentralized } catch (Exception ex) { Log.Error($"读取存档数据失败,重新开始:{ex.Message}"); } if (Data == null) Data = new SaveData(); - if (Data.Market == null) Data.Market = Market.CreateDefault(StoreStation.GetDayCounter()); + // 池子表在第十阶段改过(全部以 CRC 计价);旧测试档里的表缺池子,重建成默认值(价格从起始价重来) + if (Data.Market == null || Data.Market.DexPool(Assets.SPARK) == null) Data.Market = Market.CreateDefault(StoreStation.GetDayCounter()); // 旧存档里的商家地址没有校验位:换成合法地址 foreach (var s in Data.Black.Sellers) if (!ChainAddress.Valid(s.Name)) s.Name = ChainAddress.Make(Data.Rng.NextUInt()); if (Data.Story == null) Data.Story = new StoryMods(); diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs index 5303bd9..29510da 100644 --- a/src/Decentralized/TerminalWindow.cs +++ b/src/Decentralized/TerminalWindow.cs @@ -231,9 +231,6 @@ namespace Decentralized b.AddLabel(L.T("aml.taint", (int)Math.Round(taintRatio * 100)), taintRatio > 0.05 ? "aml_note" : "aml_dim"); } - private static readonly float[] StableWBase = new[] { 80f, 100f, 220f }; - private static float[] StableW => Sc(StableWBase); - private static string StableStatus(Stablecoins st, string asset, int day) { double p = st.Price(asset); @@ -242,15 +239,78 @@ namespace Decentralized return p < 0.99 ? L.T("stable.depeg") : L.T("stable.ok"); } - /// 稳定币行情:价格与状态(脱锚、预兆)。 - private static void StableTable(CustomUIBuilder b) + private static readonly float[] PriceWBase = new[] { 100f, 160f }; + private static float[] PriceW => Sc(PriceWBase); + private static readonly string[] SwapCoins = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA, Assets.SPARK }; + private static string _swapFrom = Assets.CRC, _swapTo = Assets.PSC; + + /// 币价表:各币在 DEX 池里以 CRC 计的价格。 + private static void PriceTable(CustomUIBuilder b) { - var st = Session.Data.Stable; - int day = StoreStation.GetDayCounter(); + var m = Session.Data.Market; b.AddSeparator(); - TableHeader(b, StableW, L.T("col.asset"), L.T("col.peg"), L.T("col.status")); - foreach (var a in new[] { Assets.CRC, Assets.CRT, Assets.CRA }) - TableRow(b, StableW, a, Fmt.Num(st.Price(a)), StableStatus(st, a, day)); + TableHeader(b, PriceW, L.T("col.asset"), L.T("col.crcprice")); + foreach (var a in SwapCoins.Where(x => x != Assets.CRC)) + TableRow(b, PriceW, a, Fmt.Num(m.DexPool(a).Price)); + } + + /// 互换:任意币对任意币,实际经 CRC 路由;预览显示得到数量、经过几个池、损耗。 + private static void SwapBlock(CustomUIBuilder b) + { + var m = Session.Data.Market; + Action picker = (label, cur, from) => + { + b.AddLabel(label); + b.BeginRow(4f); + foreach (var c in SwapCoins) + { + var coin = c; + PledgerSkin.Pick(b, coin, coin == cur, () => + { + if (from) { _swapFrom = coin; if (_swapTo == coin) _swapTo = SwapCoins.First(x => x != coin); } + else { _swapTo = coin; if (_swapFrom == coin) _swapFrom = SwapCoins.First(x => x != coin); } + Open(); + }, _picked); + } + b.End(); + }; + AmountRow(b); + picker(L.T("swap.pay"), _swapFrom, true); + picker(L.T("swap.get"), _swapTo, false); + double v; + bool ok = double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) && v > 0; + var q = ok ? m.Quote(_swapFrom, _swapTo, v) : null; + if (q != null) b.AddLabel(L.T("swap.preview", Fmt.Num(q.Out), _swapTo, q.Hops, (q.Impact * 100).ToString("0.0", CultureInfo.InvariantCulture)), "swap_preview_dim"); + b.AddButton(L.T("swap.go"), () => Swap()); + } + + /// 互换:DEX 是合约调用,要在硬件钱包上签名。 + private static void Swap() + { + if (!ReadAmount(out double v)) return; + string from = _swapFrom, to = _swapTo; + var q = Session.Data.Market.Quote(from, to, v); + if (q == null) { _message = L.T("swap.same"); Open(); return; } + var w = Session.Data.Hot; + if (w.Avail(from) + 1e-9 < v) { _message = L.T(frozenPay(w, from) ? "msg.frozen" : "msg.balance"); Open(); return; } + _message = L.T("hw.wait"); + Open(); + WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Asset = from, Amount = v, Summary = L.T("hw.swap", Fmt.Num(v), from, Fmt.Num(q.Out), to), Execute = pw => DoSwap(from, to, v, pw) }); + } + + private static void DoSwap(string from, string to, double v, Wallet w) + { + if (w.Avail(from) + 1e-9 < v) { _message = L.T(frozenPay(w, from) ? "msg.frozen" : "msg.balance"); Open(); return; } + double got = Session.Data.Market.Swap(from, to, v); + if (got <= 0) { _message = L.T("swap.same"); Open(); return; } + double spent = v * Session.Price(from); + CandleChart.Trade(from, spent); CandleChart.Trade(to, spent); + w.Add(from, -v); + w.Add(to, got); + w.RecordBuy(to, got, spent); + _message = L.T("msg.trade", Fmt.Num(v), from, Fmt.Num(got), to); + Session.Save(); + Open(); } private static void TradeRows(CustomUIBuilder b, Venue venue) @@ -1193,14 +1253,13 @@ namespace Decentralized l.AddSeparator(); l.AddLabel(L.T("wallet.total", Fmt.Credits(w.Value(Session.Price)))); if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)), "taint_note"); - StableTable(l); + PriceTable(l); l.AddSeparator(); CandleChart.AddPicker(l, "web3", Assets.PSC, Open, _picked); }, r => { r.AddHeader(L.T("trade.h")); - AmountRow(r); - TradeRows(r, Venue.Dex); + SwapBlock(r); if (Unlock.BlackUnlocked && BlackMarket.DeskOpen(StoreReputation.GetBMReputation())) PoneroBlock(r); }); } @@ -2311,18 +2370,14 @@ namespace Decentralized Open(); } - /// 交易:Poinbase 是托管账户,直接成交;DEX 是合约调用,要在硬件钱包上签名。 + /// 交易:Poinbase 是托管账户,直接成交。 private static void Trade(Pool p, bool buy) { if (!ReadAmount(out double v)) return; - var w = p.Venue == Venue.Dex ? Session.Data.Hot : Session.Data.Poinbase; - string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote; + var w = Session.Data.Poinbase; + string pay = buy ? p.Quote : p.Base; if (w.Avail(pay) + 1e-9 < v) { _message = L.T(frozenPay(w, pay) ? "msg.frozen" : "msg.balance"); Open(); return; } - if (p.Venue != Venue.Dex) { DoTrade(p, buy, v, w); return; } - double quote = buy ? v / p.Price : v * p.Price; - _message = L.T("hw.wait"); - Open(); - WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Asset = pay, Amount = v, Summary = L.T("hw.swap", Fmt.Num(v), pay, Fmt.Num(quote), get), Execute = pw => DoTrade(p, buy, v, pw) }); + DoTrade(p, buy, v, w); } private static void DoTrade(Pool p, bool buy, double v, Wallet w) diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index ef1a175..f9371cf 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -318,17 +318,38 @@ internal static class Program Check(n.Anchor(new MarketEvent[0]) > Tuning.PscStartPrice * 1.05, "锚随之上移"); }); - Test("市场:其他交易对每晚被套利拉向参考价,三角价差收敛", () => + Test("市场:其他交易对每晚被套利拉向参考价,价差收敛", () => { var m = Market.CreateDefault(1); - var crt = m.Find(Venue.Dex, Assets.PSC, Assets.CRT); - crt.SetPrice(130, crt.QuoteReserve); + var dexPsc = m.DexPool(Assets.PSC); + dexPsc.SetPrice(130, dexPsc.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); + double gap = Math.Abs(dexPsc.Price / m.Price - 1); + Check(gap < 0.03, $"五晚后 DEX 的 PSC/CRC 与参考价相差 {gap:P1}"); + var crtCrc = m.DexPool(Assets.CRT); 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}"); + var m2 = Market.CreateDefault(1); + var spark = m2.DexPool(Assets.SPARK); + m2.Night(new MarketEvent[0], new Rng(11), 1, a => a == Assets.SPARK ? 20 : 1); + Check(spark.Price > Tuning.SparkStart * 1.3, $"SPARK/CRC 向 SPARK 价格靠拢:{spark.Price:0.00}"); + }); + + Test("市场:任意两币互换经 CRC 路由,两跳付两次手续费", () => + { + var m = Market.CreateDefault(1); + var oneHop = m.Quote(Assets.CRC, Assets.PSC, 100); + var twoHop = m.Quote(Assets.CRA, Assets.PSC, 100); + Check(oneHop != null && oneHop.Hops == 1 && twoHop != null && twoHop.Hops == 2, "CRC 在一端走一个池,否则两个池"); + Check(m.Quote(Assets.PSC, Assets.PSC, 1) == null && m.Quote(Assets.CRC, Assets.CRC, 1) == null && m.Quote(Assets.CRC, Assets.Ponero, 1) == null, "同币、没有池没有路由"); + Check(twoHop.Impact > oneHop.Impact && twoHop.Impact > Tuning.DexFee * 1.9, $"两跳损失更大:{oneHop.Impact:P2} 对 {twoHop.Impact:P2}"); + double expect = m.DexPool(Assets.PSC).BaseOut(m.DexPool(Assets.CRA).QuoteOut(100)); + Check(Near(twoHop.Out, expect, 1e-9), "两跳的数量 = 先卖成 CRC 再买"); + double got = m.Swap(Assets.CRA, Assets.PSC, 100); + Check(Near(got, twoHop.Out, 1e-9), "成交数量与报价一致"); + Check(m.DexPool(Assets.CRA).Price < 1 && m.DexPool(Assets.PSC).Price > Tuning.PscStartPrice, "成交推动两个池的价格"); + var big = Market.CreateDefault(1).Quote(Assets.CRC, Assets.SPARK, 20000); + Check(big.Impact > 0.2, $"SPARK 池比较薄,大单滑点大:{big.Impact:P0}"); }); Test("稳定币:CRC 脱锚与减记,CRT 脱锚与冻结概率", () =>