mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
- Add oi_low as independent source_type for short opportunities - Redesign Mixed mode with card-based selector (2x2 grid) - Show combination summary with total coin limit - Support both Chinese and English languages - Change default limits to 10 for OI Top and OI Low
238 lines
7.6 KiB
Go
238 lines
7.6 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("## 持仓量变化排行 (%s)\n\n", data.Duration))
|
|
|
|
if len(data.TopPositions) > 0 {
|
|
sb.WriteString("### 持仓增加榜\n")
|
|
sb.WriteString("资金流入,趋势延续或新仓建立信号:\n\n")
|
|
sb.WriteString("| 排名 | 币种 | 持仓变化(USDT) | OI变化% | 价格变化% |\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("### 持仓减少榜\n")
|
|
sb.WriteString("资金流出,趋势反转或仓位平仓信号:\n\n")
|
|
sb.WriteString("| 排名 | 币种 | 持仓变化(USDT) | OI变化% | 价格变化% |\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("**解读**: OI增+价涨=多头主导 | OI增+价跌=空头主导 | OI减+价涨=空头平仓 | OI减+价跌=多头平仓\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()
|
|
}
|