Terminal: buy-average price per coin in Poinbase and Web3; drop the price line on the Poinbase card

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-03 15:35:48 +08:00
1 parent 9441d4d1bd
commit a2dc1dd2f6
5 files changed
+48 -3

No files matched your search

+19
View File
@@ -19,11 +19,30 @@ namespace Decentralized.Domain
/// <summary>被执法标记。</summary>
public bool Flagged;
/// <summary>买入记录:每种币买入的数量与花费(信用点等值),用来显示买入均价。卖出或转出时按比例减少。</summary>
public Dictionary<string, double> BuyQty = new Dictionary<string, double>(), BuyCost = new Dictionary<string, double>();
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);
}
/// <summary>买入均价(信用点);没有买入记录(例如全是挖出来的)返回 null。</summary>
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);
@@ -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,"—"
1 # Probably Decentralized localization table: English (en)
148 bank.crc
149 bank.hint
150 poin.account
151 avg.line
152 avg.short
153 avg.none
@@ -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,"—"
1 # Probably Decentralized 本地化表:简体中文(zh)
147 bank.crc
148 bank.hint
149 poin.account
150 avg.line
151 avg.short
152 avg.none
+9 -3
View File
@@ -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();
@@ -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");