diff --git a/src/Decentralized.Domain/Wallet.cs b/src/Decentralized.Domain/Wallet.cs
index 0212fb3..9133c3a 100644
--- a/src/Decentralized.Domain/Wallet.cs
+++ b/src/Decentralized.Domain/Wallet.cs
@@ -19,11 +19,30 @@ namespace Decentralized.Domain
/// 被执法标记。
public bool Flagged;
+ /// 买入记录:每种币买入的数量与花费(信用点等值),用来显示买入均价。卖出或转出时按比例减少。
+ public Dictionary BuyQty = new Dictionary(), BuyCost = new Dictionary();
+
+ public void RecordBuy(string asset, double qty, double costCredits)
+ {
+ if (qty <= 0) return;
+ BuyQty[asset] = (BuyQty.TryGetValue(asset, out var q) ? q : 0) + qty;
+ BuyCost[asset] = (BuyCost.TryGetValue(asset, out var c) ? c : 0) + Math.Max(0, costCredits);
+ }
+
+ /// 买入均价(信用点);没有买入记录(例如全是挖出来的)返回 null。
+ public double? AvgBuy(string asset) => BuyQty.TryGetValue(asset, out var q) && q > 1e-9 ? BuyCost[asset] / q : (double?)null;
+
public double Get(string asset) => Balances.TryGetValue(asset, out var v) ? v : 0;
public void Add(string asset, double amount)
{
if (amount == 0) return;
+ if (amount < 0 && BuyQty.TryGetValue(asset, out var bq))
+ {
+ double before = Get(asset), keep = before > 1e-12 ? Math.Max(0, 1 + amount / before) : 0;
+ BuyQty[asset] = bq * keep; BuyCost[asset] *= keep;
+ if (BuyQty[asset] < 1e-9) { BuyQty.Remove(asset); BuyCost.Remove(asset); }
+ }
double v = Get(asset) + amount;
if (v < -1e-9) throw new InvalidOperationException($"{Address}: {asset} 余额不足");
if (Math.Abs(v) < 1e-12) Balances.Remove(asset);
diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv
index 9081b60..ad4f72d 100644
--- a/src/Decentralized/ModFiles/Localization/en.csv
+++ b/src/Decentralized/ModFiles/Localization/en.csv
@@ -148,3 +148,6 @@ bank.rent,"Rent: {0} credits, due in {1} days"
bank.crc,"Poinbase account (CRC): {0}"
bank.hint,"Deposit turns credits into CRC; withdrawing turns CRC back into credits and passes compliance review."
poin.account,"Poinbase account: CRC {0}, PSC {1}"
+avg.line,"{0} average buy price: {1} credits"
+avg.short,"avg. buy {0}"
+avg.none,"—"
diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv
index 6f85c3b..9fa0a11 100644
--- a/src/Decentralized/ModFiles/Localization/zh.csv
+++ b/src/Decentralized/ModFiles/Localization/zh.csv
@@ -147,3 +147,6 @@ bank.rent,"房租:{0} 信用点,{1} 天后支付"
bank.crc,"Poinbase 账户(CRC):{0}"
bank.hint,"存入把信用点换成 CRC,提取把 CRC 换回信用点,提取要过合规审查。"
poin.account,"Poinbase 账户:CRC {0},PSC {1}"
+avg.line,"{0} 买入均价:{1} 信用点"
+avg.short,"买入均价 {0}"
+avg.none,"—"
diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs
index 9ce6825..4e05092 100644
--- a/src/Decentralized/TerminalWindow.cs
+++ b/src/Decentralized/TerminalWindow.cs
@@ -56,7 +56,7 @@ namespace Decentralized
b.AddImage(ModInfo.Id + ":" + icon, 96f, 96f);
b.AddHeader(name);
b.AddLabel(line1);
- b.AddLabel(line2);
+ if (line2 != null) b.AddLabel(line2);
b.AddFlexibleSpace();
b.AddButton(L.T("home.enter"), () => Go(target));
b.End();
@@ -70,7 +70,7 @@ namespace Decentralized
b.AddSpace(6f);
b.BeginRow(8f, TextAnchor.UpperCenter);
Entry(b, "ui_bank", L.T("entry.bank"), L.T("entry.bank.l1"), L.T("entry.bank.l2", PlayerStore.Instance.playerCash), Page.Bank);
- Entry(b, "ui_poinbase", L.T("entry.poinbase"), L.T("entry.poinbase.l1"), L.T("entry.poinbase.l2", Fmt.Num(d.Market.Price)), Page.Poinbase);
+ Entry(b, "ui_poinbase", L.T("entry.poinbase"), L.T("entry.poinbase.l1"), null, Page.Poinbase);
Entry(b, "ui_web3", L.T("entry.web3"), L.T("entry.web3.l1"), L.T("entry.web3.l2", Fmt.Credits(chain)), Page.Web3);
b.End();
b.AddSpace(8f);
@@ -125,6 +125,7 @@ namespace Decentralized
var w = Session.Data.Hot;
b.AddHeader(L.T("entry.poinbase"));
b.AddLabel(L.T("poin.account", Fmt.Num(w.Get(Assets.CRC)), Fmt.Num(w.Get(Assets.PSC))));
+ b.AddLabel(L.T("avg.line", Assets.PSC, AvgText(w, Assets.PSC)));
b.AddLabel(L.T("trade.hint"));
AmountRow(b);
b.AddSeparator();
@@ -138,7 +139,7 @@ namespace Decentralized
b.AddHeader(L.T("entry.web3"));
if (w.Balances.Count == 0) b.AddLabel(L.T("wallet.empty"));
foreach (var kv in w.Balances.OrderBy(k => k.Key))
- b.AddLabel(L.T("wallet.line", kv.Key, Fmt.Num(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key))));
+ b.AddLabel(L.T("wallet.line", kv.Key, Fmt.Num(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key))) + " " + L.T("avg.short", AvgText(w, kv.Key)));
b.AddLabel(L.T("wallet.total", Fmt.Credits(w.Value(Session.Price))));
if (w.Taint > 0.5) b.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
b.AddSeparator();
@@ -148,6 +149,8 @@ namespace Decentralized
TradeRows(b, Venue.Dex);
}
+ private static string AvgText(Wallet w, string asset) => w.AvgBuy(asset) is double a ? Fmt.Num(a) : L.T("avg.none");
+
private static void ReportPage(CustomUIBuilder b)
{
var r = Session.Data.LastNight;
@@ -181,6 +184,7 @@ namespace Decentralized
ModHelper.AddCredit(-credits, false);
var w = Session.Data.Hot;
w.Add(Assets.CRC, credits / Session.Data.Stable.Crc);
+ w.RecordBuy(Assets.CRC, credits / Session.Data.Stable.Crc, credits);
w.Basis += credits;
_message = L.T("msg.deposit", credits);
Session.Save();
@@ -213,8 +217,10 @@ namespace Decentralized
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
if (w.Get(pay) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
double got = buy ? p.BuyBase(v) : p.SellBase(v);
+ double spent = v * Session.Price(pay);
w.Add(pay, -v);
w.Add(get, got);
+ w.RecordBuy(get, got, spent);
_message = L.T("msg.trade", Fmt.Num(v), pay, Fmt.Num(got), get);
Session.Save();
Open();
diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs
index 78e66ce..20938d6 100644
--- a/tests/Decentralized.Domain.Tests/Program.cs
+++ b/tests/Decentralized.Domain.Tests/Program.cs
@@ -248,6 +248,20 @@ internal static class Program
Check(hi.Price <= 300 + 1e-9, $"上限 3 倍:{hi.Price:0.0}");
});
+ Test("钱包:买入均价按买入记录算,卖出和转出按比例减少,挖出的币不计入", () =>
+ {
+ var w = new Wallet { Address = "x" };
+ w.Add("PSC", 10); // 挖出的 10 个,无买入记录
+ Check(w.AvgBuy("PSC") == null, "没有买入记录时没有均价");
+ w.Add("PSC", 5); w.RecordBuy("PSC", 5, 400);
+ w.Add("PSC", 5); w.RecordBuy("PSC", 5, 600);
+ Check(Near(w.AvgBuy("PSC").Value, 100, 1e-9), "买 5 个花 400 + 买 5 个花 600:均价 100");
+ w.Add("PSC", -10); // 卖掉一半(20 个里的 10 个)
+ Check(Near(w.AvgBuy("PSC").Value, 100, 1e-9) && Near(w.BuyQty["PSC"], 5, 1e-9), "卖出后均价不变,买入记录按比例减少");
+ w.Add("PSC", -10);
+ Check(w.AvgBuy("PSC") == null, "清仓后记录清除");
+ });
+
Test("市场:减半抬高价格锚(逐日靠拢),减半前一周有预期行情", () =>
{
Check(Near(Market.SupplyTarget(10, Tuning.BlockReward), 1.0, 1e-9), "减半前很远:目标因子 1");