feat: cream terminal redesign, English-only UI, autopilot launch fixes

- Redesign dashboard into a cream-paper + vermilion IBM Plex Mono terminal
  (live L2 order book, cost/liq map, WS K-line, signal matrix, orchestration
  topology, risk radar, execution log, current positions, equity curve)
- Convert all user-facing UI and backend strings/prompts from Chinese to
  English (multi-language retained, default English)
- Add /api/statistics/full endpoint + full-stats frontend wiring
- Fix Autopilot launch: reuse the existing trader instead of creating
  duplicates (eliminates repeat ~35s create cost and stale-trader 404s);
  launch sends 5m scan interval
- Fix unreadable toasts: cream theme with high-contrast text + per-type accent
- Silence background dashboard polls (getTraderConfig) to stop error-toast spam
This commit is contained in:
tinkle-community
2026-06-30 16:03:52 +08:00
parent eba28bcf0e
commit 110bf52908
149 changed files with 6835 additions and 3611 deletions

View File

@@ -27,10 +27,10 @@ func TestKlineDaily(t *testing.T) {
t.Fatal(err)
}
t.Log("=== BTCUSDT 日线 K线数据 (coinank_api 免费接口) ===")
t.Log("=== BTCUSDT daily K-line data (coinank_api free endpoint) ===")
for i, k := range resp {
startTime := time.UnixMilli(k.StartTime).Format("2006-01-02 15:04:05")
t.Logf("\n[%d] 时间: %s", i, startTime)
t.Logf("\n[%d] Time: %s", i, startTime)
t.Logf(" Open: %.2f", k.Open)
t.Logf(" High: %.2f", k.High)
t.Logf(" Low: %.2f", k.Low)
@@ -39,15 +39,15 @@ func TestKlineDaily(t *testing.T) {
t.Logf(" Quantity: %.4f (k[7])", k.Quantity)
t.Logf(" Count: %.0f (k[8])", k.Count)
// 计算验证
// Calculation verification
if k.Close > 0 && k.Volume > 0 {
t.Logf(" --- 验证 ---")
t.Logf(" --- verification ---")
t.Logf(" Volume × Close = %.2f", k.Volume*k.Close)
t.Logf(" Quantity / Close = %.4f", k.Quantity/k.Close)
}
}
// 打印原始 JSON
// Print raw JSON
res, _ := json.MarshalIndent(resp, "", " ")
fmt.Printf("\n原始 JSON:\n%s\n", res)
fmt.Printf("\nRaw JSON:\n%s\n", res)
}

View File

@@ -16,10 +16,10 @@ func TestGetCandles_BTC(t *testing.T) {
t.Fatal(err)
}
t.Log("=== BTC 日线数据 (Hyperliquid) ===")
t.Log("=== BTC daily data (Hyperliquid) ===")
for i, c := range candles {
openTime := time.UnixMilli(c.OpenTime).Format("2006-01-02 15:04:05")
t.Logf("\n[%d] 时间: %s", i, openTime)
t.Logf("\n[%d] Time: %s", i, openTime)
t.Logf(" Symbol: %s", c.Symbol)
t.Logf(" Interval: %s", c.Interval)
t.Logf(" Open: %s", c.Open)
@@ -30,24 +30,24 @@ func TestGetCandles_BTC(t *testing.T) {
t.Logf(" TradeCount: %d", c.TradeCount)
}
// 打印原始 JSON
// Print raw JSON
res, _ := json.MarshalIndent(candles, "", " ")
fmt.Printf("\n原始 JSON:\n%s\n", res)
fmt.Printf("\nRaw JSON:\n%s\n", res)
}
func TestGetCandles_TSLA(t *testing.T) {
client := NewClient()
// 测试股票永续合约 - 使用 xyz dex
// Test stock perpetual contracts - using xyz dex
candles, err := client.GetCandles(context.TODO(), "TSLA", "1d", 5)
if err != nil {
t.Fatal(err)
}
t.Log("=== TSLA 日线数据 (Hyperliquid xyz dex) ===")
t.Log("=== TSLA daily data (Hyperliquid xyz dex) ===")
for i, c := range candles {
openTime := time.UnixMilli(c.OpenTime).Format("2006-01-02 15:04:05")
t.Logf("\n[%d] 时间: %s", i, openTime)
t.Logf("\n[%d] Time: %s", i, openTime)
t.Logf(" Symbol: %s", c.Symbol)
t.Logf(" Interval: %s", c.Interval)
t.Logf(" Open: %s", c.Open)
@@ -58,33 +58,33 @@ func TestGetCandles_TSLA(t *testing.T) {
t.Logf(" TradeCount: %d", c.TradeCount)
}
// 打印原始 JSON
// Print raw JSON
res, _ := json.MarshalIndent(candles, "", " ")
fmt.Printf("\n原始 JSON:\n%s\n", res)
fmt.Printf("\nRaw JSON:\n%s\n", res)
}
func TestGetCandles_StockPerps(t *testing.T) {
client := NewClient()
// 测试多个股票永续合约 (xyz dex)
// Test multiple stock perpetual contracts (xyz dex)
symbols := []string{"TSLA", "NVDA", "AAPL", "MSFT"}
for _, symbol := range symbols {
t.Logf("\n=== %s 日线数据 ===", symbol)
t.Logf("\n=== %s daily data ===", symbol)
candles, err := client.GetCandles(context.TODO(), symbol, "1d", 3)
if err != nil {
t.Errorf("%s 获取失败: %v", symbol, err)
t.Errorf("%s fetch failed: %v", symbol, err)
continue
}
if len(candles) == 0 {
t.Logf("%s: 无数据", symbol)
t.Logf("%s: no data", symbol)
continue
}
latest := candles[len(candles)-1]
openTime := time.UnixMilli(latest.OpenTime).Format("2006-01-02")
t.Logf("%s 最新: %s Open=%s High=%s Low=%s Close=%s Vol=%s",
t.Logf("%s latest: %s Open=%s High=%s Low=%s Close=%s Vol=%s",
symbol, openTime, latest.Open, latest.High, latest.Low, latest.Close, latest.Volume)
}
}
@@ -97,19 +97,19 @@ func TestGetAllMids(t *testing.T) {
t.Fatal(err)
}
t.Log("=== 加密货币资产中间价 (默认 dex) ===")
t.Log("=== Crypto asset mid prices (default dex) ===")
// 显示一些主要加密货币资产
// Show some major crypto assets
cryptoAssets := []string{"BTC", "ETH", "SOL", "DOGE", "XRP"}
for _, asset := range cryptoAssets {
if mid, ok := mids[asset]; ok {
t.Logf("%s: %s", asset, mid)
} else {
t.Logf("%s: 不存在", asset)
t.Logf("%s: not found", asset)
}
}
t.Logf("\n总共 %d 个加密货币交易对", len(mids))
t.Logf("\nTotal %d crypto trading pairs", len(mids))
}
func TestGetAllMidsXYZ(t *testing.T) {
@@ -120,14 +120,14 @@ func TestGetAllMidsXYZ(t *testing.T) {
t.Fatal(err)
}
t.Log("=== xyz dex 资产中间价 (股票、外汇、大宗商品) ===")
t.Log("=== xyz dex asset mid prices (stocks, forex, commodities) ===")
// 显示所有 xyz dex 资产
// Show all xyz dex assets
for symbol, mid := range mids {
t.Logf("%s: %s", symbol, mid)
}
t.Logf("\n总共 %d xyz dex 交易对", len(mids))
t.Logf("\nTotal %d xyz dex trading pairs", len(mids))
}
func TestGetMeta(t *testing.T) {
@@ -138,11 +138,11 @@ func TestGetMeta(t *testing.T) {
t.Fatal(err)
}
t.Log("=== 资产元数据 ===")
t.Logf("总共 %d 个资产", len(meta.Universe))
t.Log("=== Asset metadata ===")
t.Logf("Total %d assets", len(meta.Universe))
// 显示股票永续合约
t.Log("\n股票永续合约:")
// Show stock perpetual contracts
t.Log("\nStock perpetual contracts:")
for _, asset := range meta.Universe {
if IsStockPerp(asset.Name) {
t.Logf(" %s: szDecimals=%d, maxLeverage=%d", asset.Name, asset.SzDecimals, asset.MaxLeverage)

View File

@@ -12,7 +12,7 @@ type QuantData struct {
Symbol string `json:"symbol"`
Price float64 `json:"price"`
Netflow *NetflowData `json:"netflow,omitempty"`
OI map[string]*OIData `json:"oi,omitempty"` // keyed by exchange: "binance", "bybit"
OI map[string]*OIData `json:"oi,omitempty"` // keyed by exchange: "binance", "bybit"
PriceChange map[string]float64 `json:"price_change,omitempty"` // keyed by duration: "1h", "4h", etc.
}
@@ -118,11 +118,11 @@ func FormatQuantDataForAI(symbol string, data *QuantData, lang Language) string
func formatQuantDataZH(symbol string, data *QuantData) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("### %s 量化数据\n", symbol))
sb.WriteString(fmt.Sprintf("价格: $%.4f\n\n", data.Price))
sb.WriteString(fmt.Sprintf("### %s Quant Data\n", symbol))
sb.WriteString(fmt.Sprintf("Price: $%.4f\n\n", data.Price))
if len(data.PriceChange) > 0 {
sb.WriteString("**价格变化**:\n")
sb.WriteString("**Price Change**:\n")
durations := []string{"1h", "4h", "8h", "12h", "24h"}
for _, d := range durations {
if change, ok := data.PriceChange[d]; ok {
@@ -135,14 +135,14 @@ func formatQuantDataZH(symbol string, data *QuantData) string {
if len(data.OI) > 0 {
for exchange, oiData := range data.OI {
if oiData != nil {
sb.WriteString(fmt.Sprintf("**%s持仓**:\n", strings.ToUpper(exchange)))
sb.WriteString(fmt.Sprintf("**%s Open Interest**:\n", strings.ToUpper(exchange)))
sb.WriteString(fmt.Sprintf("- OI: %.2f\n", oiData.CurrentOI))
if oiData.NetLong > 0 || oiData.NetShort > 0 {
sb.WriteString(fmt.Sprintf("- 多头: %.2f, 空头: %.2f\n", oiData.NetLong, oiData.NetShort))
sb.WriteString(fmt.Sprintf("- Long: %.2f, Short: %.2f\n", oiData.NetLong, oiData.NetShort))
}
if oiData.Delta != nil {
if delta, ok := oiData.Delta["1h"]; ok && delta != nil {
sb.WriteString(fmt.Sprintf("- 1h变化: %s (%.2f%%)\n",
sb.WriteString(fmt.Sprintf("- 1h Change: %s (%.2f%%)\n",
formatValue(delta.OIDeltaValue), delta.OIDeltaPercent))
}
}
@@ -152,7 +152,7 @@ func formatQuantDataZH(symbol string, data *QuantData) string {
}
if data.Netflow != nil && data.Netflow.Institution != nil && data.Netflow.Institution.Future != nil {
sb.WriteString("**机构资金流**:\n")
sb.WriteString("**Institution Net Flow**:\n")
durations := []string{"1h", "4h", "24h"}
for _, d := range durations {
if flow, ok := data.Netflow.Institution.Future[d]; ok {

View File

@@ -22,8 +22,8 @@ type NetFlowResponse struct {
Data struct {
Netflows []NetFlowPosition `json:"netflows"`
Count int `json:"count"`
Type string `json:"type"` // institution or personal
Trade string `json:"trade"` // futures or spot
Type string `json:"type"` // institution or personal
Trade string `json:"trade"` // futures or spot
TimeRange string `json:"time_range"`
RankType string `json:"rank_type"` // top or low
Limit int `json:"limit"`
@@ -131,13 +131,13 @@ func FormatNetFlowRankingForAI(data *NetFlowRankingData, lang Language) string {
func formatNetFlowRankingZH(data *NetFlowRankingData) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("## 资金流向排行 (%s)\n\n", data.Duration))
sb.WriteString(fmt.Sprintf("## Net Flow Ranking (%s)\n\n", data.Duration))
// Institution inflow
if len(data.InstitutionFutureTop) > 0 {
sb.WriteString("### 机构资金流入榜\n")
sb.WriteString("Smart Money买入信号:\n\n")
sb.WriteString("| 排名 | 币种 | 流入金额(USDT) | 价格 |\n")
sb.WriteString("### Institution Inflow Ranking\n")
sb.WriteString("Smart Money buy signal:\n\n")
sb.WriteString("| Rank | Symbol | Inflow Amount(USDT) | Price |\n")
sb.WriteString("|------|------|----------------|------|\n")
for _, pos := range data.InstitutionFutureTop {
sb.WriteString(fmt.Sprintf("| %d | %s | %s | $%.4f |\n",
@@ -148,9 +148,9 @@ func formatNetFlowRankingZH(data *NetFlowRankingData) string {
// Institution outflow
if len(data.InstitutionFutureLow) > 0 {
sb.WriteString("### 机构资金流出榜\n")
sb.WriteString("Smart Money卖出信号:\n\n")
sb.WriteString("| 排名 | 币种 | 流出金额(USDT) | 价格 |\n")
sb.WriteString("### Institution Outflow Ranking\n")
sb.WriteString("Smart Money sell signal:\n\n")
sb.WriteString("| Rank | Symbol | Outflow Amount(USDT) | Price |\n")
sb.WriteString("|------|------|----------------|------|\n")
for _, pos := range data.InstitutionFutureLow {
sb.WriteString(fmt.Sprintf("| %d | %s | %s | $%.4f |\n",
@@ -161,9 +161,9 @@ func formatNetFlowRankingZH(data *NetFlowRankingData) string {
// Retail flow summary
if len(data.PersonalFutureTop) > 0 || len(data.PersonalFutureLow) > 0 {
sb.WriteString("### 散户资金动向\n")
sb.WriteString("### Retail Capital Movement\n")
if len(data.PersonalFutureTop) > 0 {
sb.WriteString("散户买入: ")
sb.WriteString("Retail buy: ")
for i, pos := range data.PersonalFutureTop {
if i >= 3 {
break
@@ -176,7 +176,7 @@ func formatNetFlowRankingZH(data *NetFlowRankingData) string {
sb.WriteString("\n")
}
if len(data.PersonalFutureLow) > 0 {
sb.WriteString("散户卖出: ")
sb.WriteString("Retail sell: ")
for i, pos := range data.PersonalFutureLow {
if i >= 3 {
break
@@ -191,7 +191,7 @@ func formatNetFlowRankingZH(data *NetFlowRankingData) string {
sb.WriteString("\n")
}
sb.WriteString("**解读**: 机构买入+散户卖出=强烈看多 | 机构卖出+散户买入=强烈看空\n\n")
sb.WriteString("**Interpretation**: Institution buy + Retail sell = strongly bullish | Institution sell + Retail buy = strongly bearish\n\n")
return sb.String()
}

View File

@@ -169,12 +169,12 @@ func FormatOIRankingForAI(data *OIRankingData, lang Language) string {
func formatOIRankingZH(data *OIRankingData) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("## 持仓量变化排行 (%s)\n\n", data.Duration))
sb.WriteString(fmt.Sprintf("## Open Interest Change Ranking (%s)\n\n", data.Duration))
if len(data.TopPositions) > 0 {
sb.WriteString("### 持仓增加榜\n")
sb.WriteString("资金流入,趋势延续或新仓建立信号:\n\n")
sb.WriteString("| 排名 | 币种 | 持仓变化(USDT) | OI变化% | 价格变化% |\n")
sb.WriteString("### OI Increase Ranking\n")
sb.WriteString("Capital inflow, trend continuation or new position signal:\n\n")
sb.WriteString("| Rank | Symbol | OI Change(USDT) | OI Change% | Price Change% |\n")
sb.WriteString("|------|------|----------------|---------|----------|\n")
for _, pos := range data.TopPositions {
sb.WriteString(fmt.Sprintf("| %d | %s | %s | %+.2f%% | %+.2f%% |\n",
@@ -185,9 +185,9 @@ func formatOIRankingZH(data *OIRankingData) string {
}
if len(data.LowPositions) > 0 {
sb.WriteString("### 持仓减少榜\n")
sb.WriteString("资金流出,趋势反转或仓位平仓信号:\n\n")
sb.WriteString("| 排名 | 币种 | 持仓变化(USDT) | OI变化% | 价格变化% |\n")
sb.WriteString("### OI Decrease Ranking\n")
sb.WriteString("Capital outflow, trend reversal or position close signal:\n\n")
sb.WriteString("| Rank | Symbol | OI Change(USDT) | OI Change% | Price Change% |\n")
sb.WriteString("|------|------|----------------|---------|----------|\n")
for _, pos := range data.LowPositions {
sb.WriteString(fmt.Sprintf("| %d | %s | %s | %+.2f%% | %+.2f%% |\n",
@@ -197,7 +197,7 @@ func formatOIRankingZH(data *OIRankingData) string {
sb.WriteString("\n")
}
sb.WriteString("**解读**: OI增+价涨=多头主导 | OI增+价跌=空头主导 | OI减+价涨=空头平仓 | OI减+价跌=多头平仓\n\n")
sb.WriteString("**Interpretation**: OI up + Price up = longs dominant | OI up + Price down = shorts dominant | OI down + Price up = shorts closing | OI down + Price down = longs closing\n\n")
return sb.String()
}

View File

@@ -12,7 +12,7 @@ import (
type PriceRankingItem struct {
Pair string `json:"pair"`
Symbol string `json:"symbol"`
PriceDelta float64 `json:"price_delta"` // Decimal format: 0.0723 = 7.23%
PriceDelta float64 `json:"price_delta"` // Decimal format: 0.0723 = 7.23%
Price float64 `json:"price"`
FutureFlow float64 `json:"future_flow"`
SpotFlow float64 `json:"spot_flow"`
@@ -98,7 +98,7 @@ func FormatPriceRankingForAI(data *PriceRankingData, lang Language) string {
func formatPriceRankingZH(data *PriceRankingData) string {
var sb strings.Builder
sb.WriteString("## 涨跌幅排行\n\n")
sb.WriteString("## Price Change Ranking\n\n")
durationOrder := []string{"1h", "4h", "24h"}
for _, duration := range durationOrder {
@@ -107,11 +107,11 @@ func formatPriceRankingZH(data *PriceRankingData) string {
continue
}
sb.WriteString(fmt.Sprintf("### %s 涨跌幅\n\n", duration))
sb.WriteString(fmt.Sprintf("### %s Price Change\n\n", duration))
if len(durationData.Top) > 0 {
sb.WriteString("**涨幅榜**\n")
sb.WriteString("| 币种 | 涨幅 | 价格 | 资金流 | OI变化 |\n")
sb.WriteString("**Gainers**\n")
sb.WriteString("| Symbol | Gain | Price | Net Flow | OI Change |\n")
sb.WriteString("|------|------|------|--------|--------|\n")
for _, item := range durationData.Top {
sb.WriteString(fmt.Sprintf("| %s | %+.2f%% | $%.4f | %s | %s |\n",
@@ -122,8 +122,8 @@ func formatPriceRankingZH(data *PriceRankingData) string {
}
if len(durationData.Low) > 0 {
sb.WriteString("**跌幅榜**\n")
sb.WriteString("| 币种 | 跌幅 | 价格 | 资金流 | OI变化 |\n")
sb.WriteString("**Losers**\n")
sb.WriteString("| Symbol | Loss | Price | Net Flow | OI Change |\n")
sb.WriteString("|------|------|------|--------|--------|\n")
for _, item := range durationData.Low {
sb.WriteString(fmt.Sprintf("| %s | %.2f%% | $%.4f | %s | %s |\n",
@@ -134,7 +134,7 @@ func formatPriceRankingZH(data *PriceRankingData) string {
}
}
sb.WriteString("**解读**: 涨幅大+资金流入+OI增加=强势上涨 | 跌幅大+资金流出+OI减少=弱势下跌\n\n")
sb.WriteString("**Interpretation**: Large gain + capital inflow + OI increase = strong uptrend | Large loss + capital outflow + OI decrease = weak downtrend\n\n")
return sb.String()
}

View File

@@ -27,6 +27,7 @@ const (
SignalRankingPath = "/api/v1/vergex/signal-ranking"
SignalLabPath = "/api/v1/vergex/signal-lab"
CostLiquidationHeatmapPath = "/api/v1/vergex/cost-liquidation-heatmap"
FlowMarketsPath = "/api/v1/vergex/flow-markets"
)
type Client struct {
@@ -128,6 +129,24 @@ func (c *Client) GetCostLiquidationHeatmap(ctx context.Context, q Query) (json.R
return c.doGET(ctx, CostLiquidationHeatmapPath, params)
}
// GetFlowMarkets fetches the Vergex net-flow market ranking via the paid
// claw402 x402 endpoint. Params mirror the public API: chain (e.g. "mainnet"),
// window (e.g. "1h"), and limit. The raw JSON is returned for the caller to
// pass through — the response shape is owned by Vergex.
func (c *Client) GetFlowMarkets(ctx context.Context, chain, window string, limit int) (json.RawMessage, error) {
params := url.Values{}
if v := strings.TrimSpace(chain); v != "" {
params.Set("chain", v)
}
if v := strings.TrimSpace(window); v != "" {
params.Set("window", v)
}
if limit > 0 {
params.Set("limit", fmt.Sprintf("%d", limit))
}
return c.doGET(ctx, FlowMarketsPath, params)
}
func addQueryDefaults(params url.Values, q Query, includeMarket bool) {
if includeMarket {
if q.MarketType != "" {