Stage 6.2: Poinbase perpetual futures (PSC, SPARK) with slippage, funding, insurance fund, ADL, debt and seizure

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-04 00:26:50 +08:00
1 parent 98e80412e6
commit b249747855
10 files changed
+578 -2

No files matched your search

+123
View File
@@ -228,10 +228,22 @@ namespace Decentralized
}
/// <summary>Poinbase:合规交易所。左边账户持仓,右边交易;信用点的存取在银行。</summary>
private enum PoinTab { Spot, Perp }
private static PoinTab _poinTab = PoinTab.Spot;
private static void ExchangePage(CustomUIBuilder b, Venue venue)
{
var d = Session.Data;
var w = d.Poinbase;
b.BeginRow();
foreach (PoinTab t in Enum.GetValues(typeof(PoinTab)))
{
var tab = t;
string name = L.T("poin.sub." + t.ToString().ToLowerInvariant());
b.AddButton(tab == _poinTab ? $"[{name}]" : name, () => { _poinTab = tab; _message = null; Open(); });
}
b.End();
if (_poinTab == PoinTab.Perp) { PerpPage(b); return; }
Columns(b, l =>
{
l.AddHeader(L.T("poin.account.h"));
@@ -261,6 +273,116 @@ namespace Decentralized
private static string _poinAsset = Assets.CRC;
private static string _perpProduct = "PSC";
private static bool _perpLong = true;
private static int _perpLev = 3;
private static readonly float[] PerpW = { 118f, 60f, 104f, 60f, 58f };
private static double PerpSpot(string p) => p == "SPARK" ? Session.Data.Stable.Spark : Session.Data.Market.Price;
/// <summary>永续页:保证金账户、仓位、资金费与保险基金;开仓、追加保证金、平仓。Poinbase 是托管账户,不用签名。</summary>
private static void PerpPage(CustomUIBuilder b)
{
var d = Session.Data;
var bk = d.Perps;
var acct = d.Poinbase;
int day = StoreStation.GetDayCounter();
if (_perpLev > PerpBook.MaxLeverage(_perpProduct)) _perpLev = PerpBook.MaxLeverage(_perpProduct);
Columns(b, l =>
{
l.AddHeader(L.T("perp.h"));
l.AddLabel(L.T("perp.acct", Fmt.Num(acct.Avail(Assets.CRC))));
if (bk.Debt > 0) l.AddLabel(L.T("perp.debt", Fmt.Credits(bk.Debt), Math.Max(0, Tuning.PerpDebtDays - (day - bk.DebtSince))));
if (bk.Banned(day)) l.AddLabel(L.T("perp.banned", bk.BannedUntil - day + 1));
l.AddSeparator();
if (bk.Positions.Count == 0) l.AddLabel(L.T("perp.none"));
else
{
TableHeader(l, PerpW, L.T("perp.col.pos"), L.T("perp.col.notional"), L.T("perp.col.price"), L.T("perp.col.pnl"), L.T("perp.col.liq"));
foreach (var p in bk.Positions)
{
double spot = PerpSpot(p.Product);
TableRow(l, PerpW, p.Product + " " + L.T(p.Long ? "perp.long" : "perp.short") + " " + Math.Round(p.Leverage, 1) + "×", Fmt.Credits(p.Notional(spot)),
Fmt.Num(p.Entry) + " → " + Fmt.Num(spot), Fmt.Credits(p.Pnl(spot)), Fmt.Num(p.LiqPrice));
}
}
l.AddSeparator();
foreach (var p in PerpBook.Products)
l.AddLabel(L.T("perp.rate", p, Fmt.Num(PerpSpot(p)), (bk.Rate(p) * 100).ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture)));
l.AddLabel(L.T("perp.fund", Fmt.Credits(bk.InsuranceFund)));
}, r =>
{
r.AddHeader(L.T("perp.open"));
r.BeginRow();
foreach (var p in PerpBook.Products)
{
var product = p;
r.AddButton(product == _perpProduct ? $"[{product}]" : product, () => { _perpProduct = product; Open(); });
}
r.End();
r.BeginRow();
r.AddButton(_perpLong ? "[" + L.T("perp.long") + "]" : L.T("perp.long"), () => { _perpLong = true; Open(); });
r.AddButton(!_perpLong ? "[" + L.T("perp.short") + "]" : L.T("perp.short"), () => { _perpLong = false; Open(); });
r.End();
r.BeginRow();
foreach (int lev in new[] { 1, 2, 3, 5 })
{
if (lev > PerpBook.MaxLeverage(_perpProduct)) continue;
int lv = lev;
r.AddButton(lv == _perpLev ? $"[{lv}×]" : lv + "×", () => { _perpLev = lv; Open(); });
}
r.End();
r.AddLabel(L.T("perp.margin"));
AmountRow(r);
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double m) && m > 0)
{
double notional = m * _perpLev, spot = PerpSpot(_perpProduct), exec = bk.Exec(spot, notional, _perpLong, day);
var ghost = new PerpPosition { Long = _perpLong, Entry = exec, Size = notional / exec, Margin = m };
r.AddLabel(L.T("perp.preview", Fmt.Credits(notional), Fmt.Num(exec)));
r.AddLabel(L.T("perp.preview2", Fmt.Num(notional * Tuning.PerpFee), Fmt.Num(ghost.LiqPrice)));
}
r.BeginRow();
r.AddButton(L.T("perp.do.open"), PerpOpen);
r.AddButton(L.T("perp.do.add"), PerpAdd);
r.AddButton(L.T("perp.do.close"), PerpClose);
r.End();
});
}
private static string PerpErr(PerpResult res) => L.T("perp.err." + res);
private static void PerpOpen()
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
var res = d.Perps.Open(d.Poinbase, _perpProduct, _perpLong, v, _perpLev, PerpSpot(_perpProduct), StoreStation.GetDayCounter(), Session.Price);
_message = res == PerpResult.Ok ? L.T("perp.opened", _perpProduct, L.T(_perpLong ? "perp.long" : "perp.short"), Fmt.Num(v), _perpLev) : PerpErr(res);
if (res == PerpResult.Ok) Session.Save();
Open();
}
private static void PerpAdd()
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
var res = d.Perps.AddMargin(d.Poinbase, _perpProduct, v, Session.Price);
_message = res == PerpResult.Ok ? L.T("perp.added", Fmt.Num(v)) : PerpErr(res);
if (res == PerpResult.Ok) Session.Save();
Open();
}
private static void PerpClose()
{
var d = Session.Data;
var pos = d.Perps.Of(_perpProduct);
if (pos == null) { _message = PerpErr(PerpResult.NoPosition); Open(); return; }
double margin = pos.Margin;
double paid = d.Perps.Close(d.Poinbase, _perpProduct, PerpSpot(_perpProduct), StoreStation.GetDayCounter(), out double deficit);
_message = L.T("perp.closed", Fmt.Num(paid), (paid - margin >= 0 ? "+" : "") + Fmt.Num(paid - margin)) + (deficit > 0 ? "\n" + L.T("perp.deficit", Fmt.Credits(deficit)) : "");
Session.Save();
Open();
}
/// <summary>Poinbase 提到当前钱包:托管账户直接转,不用签名。污点按比例随币走。</summary>
private static void PoinbaseOut()
{
@@ -1061,6 +1183,7 @@ namespace Decentralized
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
if (d.Perps.Debt > 0) { _message = L.T("perp.debt.block"); Open(); return; }
if (d.Poinbase.Avail(Assets.CRC) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
var r = d.Exchange.Cashout(d.Poinbase, v, Session.Price, StoreStation.GetDayCounter(), Session.Rent(), d.Mixer.RecentlyMixed(StoreStation.GetDayCounter()), d.Rng);
int got = (int)Math.Floor(r.Received - r.Tax);