Files
nofx/provider/nofxos/oi.go
tinkle-community 110bf52908 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
2026-06-30 16:03:52 +08:00

238 lines
7.7 KiB
Go

package nofxos
import (
"encoding/json"
"fmt"
"log"
"strings"
"time"
)
// OIPosition represents open interest data for a single coin
type OIPosition struct {
Symbol string `json:"symbol"`
Rank int `json:"rank"`
Price float64 `json:"price"`
CurrentOI float64 `json:"current_oi"`
OIDelta float64 `json:"oi_delta"`
OIDeltaPercent float64 `json:"oi_delta_percent"` // Already x100 (5.0 = 5%)
OIDeltaValue float64 `json:"oi_delta_value"` // USDT value
PriceDeltaPercent float64 `json:"price_delta_percent"` // Already x100 (5.0 = 5%)
NetLong float64 `json:"net_long"`
NetShort float64 `json:"net_short"`
}
// OIRankingResponse is the API response structure for OI ranking
type OIRankingResponse struct {
Success bool `json:"success"`
Code int `json:"code"`
Data struct {
Positions []OIPosition `json:"positions"`
Count int `json:"count"`
Exchange string `json:"exchange"`
TimeRange string `json:"time_range"`
TimeRangeParam string `json:"time_range_param"`
RankType string `json:"rank_type"`
Limit int `json:"limit"`
} `json:"data"`
}
// OIRankingData contains both top and low OI rankings
type OIRankingData struct {
TimeRange string `json:"time_range"`
Duration string `json:"duration"`
TopPositions []OIPosition `json:"top_positions"`
LowPositions []OIPosition `json:"low_positions"`
FetchedAt time.Time `json:"fetched_at"`
}
// GetOIRanking retrieves OI ranking data (both top increase and low decrease)
func (c *Client) GetOIRanking(duration string, limit int) (*OIRankingData, error) {
if duration == "" {
duration = "1h"
}
if limit <= 0 {
limit = 20
}
result := &OIRankingData{
Duration: duration,
FetchedAt: time.Now(),
}
// Fetch top ranking (OI increase)
topPositions, timeRange, err := c.fetchOIRanking("top", duration, limit)
if err != nil {
log.Printf("⚠️ Failed to fetch OI top ranking: %v", err)
} else {
result.TopPositions = topPositions
result.TimeRange = timeRange
}
// Fetch low ranking (OI decrease)
lowPositions, _, err := c.fetchOIRanking("low", duration, limit)
if err != nil {
log.Printf("⚠️ Failed to fetch OI low ranking: %v", err)
} else {
result.LowPositions = lowPositions
}
log.Printf("✓ Fetched OI ranking data: %d top, %d low (duration: %s)",
len(result.TopPositions), len(result.LowPositions), duration)
return result, nil
}
func (c *Client) fetchOIRanking(rankType, duration string, limit int) ([]OIPosition, string, error) {
endpoint := fmt.Sprintf("/api/oi/%s-ranking?limit=%d&duration=%s", rankType, limit, duration)
body, err := c.doRequest(endpoint)
if err != nil {
return nil, "", fmt.Errorf("request failed: %w", err)
}
var response OIRankingResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, "", fmt.Errorf("JSON parsing failed: %w", err)
}
// Check for success (support both success field and code field)
if !response.Success && response.Code != 0 {
return nil, "", fmt.Errorf("API returned error code: %d", response.Code)
}
return response.Data.Positions, response.Data.TimeRange, nil
}
// GetOITopPositions retrieves top OI increase positions (legacy compatibility)
func (c *Client) GetOITopPositions() ([]OIPosition, error) {
positions, _, err := c.fetchOIRanking("top", "1h", 20)
if err != nil {
return nil, err
}
return positions, nil
}
// GetOITopSymbols retrieves OI top coin symbol list
func (c *Client) GetOITopSymbols() ([]string, error) {
positions, err := c.GetOITopPositions()
if err != nil {
return nil, err
}
var symbols []string
for _, pos := range positions {
symbol := NormalizeSymbol(pos.Symbol)
symbols = append(symbols, symbol)
}
return symbols, nil
}
// GetOILowPositions retrieves OI decrease positions (for short opportunities)
func (c *Client) GetOILowPositions() ([]OIPosition, error) {
positions, _, err := c.fetchOIRanking("low", "1h", 20)
if err != nil {
return nil, err
}
return positions, nil
}
// GetOILowSymbols retrieves OI low coin symbol list
func (c *Client) GetOILowSymbols() ([]string, error) {
positions, err := c.GetOILowPositions()
if err != nil {
return nil, err
}
var symbols []string
for _, pos := range positions {
symbol := NormalizeSymbol(pos.Symbol)
symbols = append(symbols, symbol)
}
return symbols, nil
}
// FormatOIRankingForAI formats OI ranking data for AI consumption
func FormatOIRankingForAI(data *OIRankingData, lang Language) string {
if data == nil {
return ""
}
if lang == LangChinese {
return formatOIRankingZH(data)
}
return formatOIRankingEN(data)
}
func formatOIRankingZH(data *OIRankingData) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("## Open Interest Change Ranking (%s)\n\n", data.Duration))
if len(data.TopPositions) > 0 {
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",
pos.Rank, pos.Symbol, formatValue(pos.OIDeltaValue),
pos.OIDeltaPercent, pos.PriceDeltaPercent))
}
sb.WriteString("\n")
}
if len(data.LowPositions) > 0 {
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",
pos.Rank, pos.Symbol, formatValue(pos.OIDeltaValue),
pos.OIDeltaPercent, pos.PriceDeltaPercent))
}
sb.WriteString("\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()
}
func formatOIRankingEN(data *OIRankingData) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("## Open Interest Changes (%s)\n\n", data.Duration))
if len(data.TopPositions) > 0 {
sb.WriteString("### OI Increase Ranking\n")
sb.WriteString("Capital inflow signals - trend continuation or new positions:\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",
pos.Rank, pos.Symbol, formatValue(pos.OIDeltaValue),
pos.OIDeltaPercent, pos.PriceDeltaPercent))
}
sb.WriteString("\n")
}
if len(data.LowPositions) > 0 {
sb.WriteString("### OI Decrease Ranking\n")
sb.WriteString("Capital outflow signals - trend reversal or position closing:\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",
pos.Rank, pos.Symbol, formatValue(pos.OIDeltaValue),
pos.OIDeltaPercent, pos.PriceDeltaPercent))
}
sb.WriteString("\n")
}
sb.WriteString("**Key**: OI up + Price up = Bulls dominant | OI up + Price down = Bears dominant | OI down + Price up = Short covering | OI down + Price down = Long liquidation\n\n")
return sb.String()
}