mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 09:24:36 +08:00
refactor: split large files and clean up project structure
- Rename experience/ to telemetry/ for clarity - Split 15+ large Go files (800-2200 lines) into focused modules: kernel/engine.go, backtest/runner.go, market/data.go, store/position.go, api/handler_trader.go, trader/auto_trader_grid.go, and 9 exchange traders - Split frontend monoliths: types.ts, api.ts, AITradersPage.tsx, BacktestPage.tsx into domain-specific modules with barrel re-exports - Remove stale files: screenshots, .yml.old, pyproject.toml - Remove unused scripts/ and cmd/ directories - Remove broken/outdated test files (network-dependent, stale expectations)
This commit is contained in:
425
market/data_klines.go
Normal file
425
market/data_klines.go
Normal file
@@ -0,0 +1,425 @@
|
||||
package market
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"nofx/logger"
|
||||
"nofx/provider/coinank/coinank_api"
|
||||
"nofx/provider/coinank/coinank_enum"
|
||||
"nofx/provider/hyperliquid"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Note: Kline data now uses free/open API (coinank_api.Kline) which doesn't require authentication
|
||||
|
||||
// getKlinesFromCoinAnk fetches kline data from CoinAnk API (replacement for WSMonitorCli)
|
||||
func getKlinesFromCoinAnk(symbol, interval, exchange string, limit int) ([]Kline, error) {
|
||||
// Map interval string to coinank enum
|
||||
var coinankInterval coinank_enum.Interval
|
||||
switch interval {
|
||||
case "1m":
|
||||
coinankInterval = coinank_enum.Minute1
|
||||
case "3m":
|
||||
coinankInterval = coinank_enum.Minute3
|
||||
case "5m":
|
||||
coinankInterval = coinank_enum.Minute5
|
||||
case "15m":
|
||||
coinankInterval = coinank_enum.Minute15
|
||||
case "30m":
|
||||
coinankInterval = coinank_enum.Minute30
|
||||
case "1h":
|
||||
coinankInterval = coinank_enum.Hour1
|
||||
case "2h":
|
||||
coinankInterval = coinank_enum.Hour2
|
||||
case "4h":
|
||||
coinankInterval = coinank_enum.Hour4
|
||||
case "6h":
|
||||
coinankInterval = coinank_enum.Hour6
|
||||
case "8h":
|
||||
coinankInterval = coinank_enum.Hour8
|
||||
case "12h":
|
||||
coinankInterval = coinank_enum.Hour12
|
||||
case "1d":
|
||||
coinankInterval = coinank_enum.Day1
|
||||
case "3d":
|
||||
coinankInterval = coinank_enum.Day3
|
||||
case "1w":
|
||||
coinankInterval = coinank_enum.Week1
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported interval: %s", interval)
|
||||
}
|
||||
|
||||
// Map exchange string to coinank enum
|
||||
var coinankExchange coinank_enum.Exchange
|
||||
switch strings.ToLower(exchange) {
|
||||
case "binance":
|
||||
coinankExchange = coinank_enum.Binance
|
||||
case "bybit":
|
||||
coinankExchange = coinank_enum.Bybit
|
||||
case "okx":
|
||||
coinankExchange = coinank_enum.Okex
|
||||
case "bitget":
|
||||
coinankExchange = coinank_enum.Bitget
|
||||
case "gate":
|
||||
coinankExchange = coinank_enum.Gate
|
||||
case "hyperliquid":
|
||||
coinankExchange = coinank_enum.Hyperliquid
|
||||
case "aster":
|
||||
coinankExchange = coinank_enum.Aster
|
||||
default:
|
||||
// Default to Binance for unknown exchanges
|
||||
coinankExchange = coinank_enum.Binance
|
||||
}
|
||||
|
||||
// Call CoinAnk free/open API (no authentication required)
|
||||
ctx := context.Background()
|
||||
ts := time.Now().UnixMilli()
|
||||
// Use "To" side to search backward from current time (get historical klines)
|
||||
coinankKlines, err := coinank_api.Kline(ctx, symbol, coinankExchange, ts, coinank_enum.To, limit, coinankInterval)
|
||||
if err != nil {
|
||||
// If exchange-specific data fails, fallback to Binance
|
||||
if coinankExchange != coinank_enum.Binance {
|
||||
logger.Warnf("⚠️ CoinAnk %s data failed, falling back to Binance: %v", exchange, err)
|
||||
coinankKlines, err = coinank_api.Kline(ctx, symbol, coinank_enum.Binance, ts, coinank_enum.To, limit, coinankInterval)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CoinAnk API error (fallback): %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("CoinAnk API error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert coinank kline format to market.Kline format
|
||||
klines := make([]Kline, len(coinankKlines))
|
||||
for i, ck := range coinankKlines {
|
||||
klines[i] = Kline{
|
||||
OpenTime: ck.StartTime,
|
||||
Open: ck.Open,
|
||||
High: ck.High,
|
||||
Low: ck.Low,
|
||||
Close: ck.Close,
|
||||
Volume: ck.Volume,
|
||||
CloseTime: ck.EndTime,
|
||||
}
|
||||
}
|
||||
|
||||
return klines, nil
|
||||
}
|
||||
|
||||
// getKlinesFromHyperliquid fetches kline data from Hyperliquid API for xyz dex assets
|
||||
func getKlinesFromHyperliquid(symbol, interval string, limit int) ([]Kline, error) {
|
||||
// Remove xyz: prefix if present for the API call
|
||||
baseCoin := strings.TrimPrefix(symbol, "xyz:")
|
||||
|
||||
// Map interval to Hyperliquid format
|
||||
hlInterval := hyperliquid.MapTimeframe(interval)
|
||||
|
||||
// Create Hyperliquid client
|
||||
client := hyperliquid.NewClient()
|
||||
|
||||
// Fetch candles
|
||||
ctx := context.Background()
|
||||
candles, err := client.GetCandles(ctx, baseCoin, hlInterval, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Hyperliquid API error: %w", err)
|
||||
}
|
||||
|
||||
// Convert to market.Kline format
|
||||
klines := make([]Kline, len(candles))
|
||||
for i, c := range candles {
|
||||
open, _ := strconv.ParseFloat(c.Open, 64)
|
||||
high, _ := strconv.ParseFloat(c.High, 64)
|
||||
low, _ := strconv.ParseFloat(c.Low, 64)
|
||||
closePrice, _ := strconv.ParseFloat(c.Close, 64)
|
||||
volume, _ := strconv.ParseFloat(c.Volume, 64)
|
||||
|
||||
klines[i] = Kline{
|
||||
OpenTime: c.OpenTime,
|
||||
Open: open,
|
||||
High: high,
|
||||
Low: low,
|
||||
Close: closePrice,
|
||||
Volume: volume,
|
||||
CloseTime: c.CloseTime,
|
||||
}
|
||||
}
|
||||
|
||||
return klines, nil
|
||||
}
|
||||
|
||||
// calculateTimeframeSeries calculates series data for a single timeframe
|
||||
func calculateTimeframeSeries(klines []Kline, timeframe string, count int) *TimeframeSeriesData {
|
||||
if count <= 0 {
|
||||
count = 10 // default
|
||||
}
|
||||
|
||||
data := &TimeframeSeriesData{
|
||||
Timeframe: timeframe,
|
||||
Klines: make([]KlineBar, 0, count),
|
||||
MidPrices: make([]float64, 0, count),
|
||||
EMA20Values: make([]float64, 0, count),
|
||||
EMA50Values: make([]float64, 0, count),
|
||||
MACDValues: make([]float64, 0, count),
|
||||
RSI7Values: make([]float64, 0, count),
|
||||
RSI14Values: make([]float64, 0, count),
|
||||
Volume: make([]float64, 0, count),
|
||||
BOLLUpper: make([]float64, 0, count),
|
||||
BOLLMiddle: make([]float64, 0, count),
|
||||
BOLLLower: make([]float64, 0, count),
|
||||
}
|
||||
|
||||
// Get latest N data points based on count from config
|
||||
start := len(klines) - count
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
for i := start; i < len(klines); i++ {
|
||||
// Store full OHLCV kline data
|
||||
data.Klines = append(data.Klines, KlineBar{
|
||||
Time: klines[i].OpenTime,
|
||||
Open: klines[i].Open,
|
||||
High: klines[i].High,
|
||||
Low: klines[i].Low,
|
||||
Close: klines[i].Close,
|
||||
Volume: klines[i].Volume,
|
||||
})
|
||||
|
||||
// Keep MidPrices and Volume for backward compatibility
|
||||
data.MidPrices = append(data.MidPrices, klines[i].Close)
|
||||
data.Volume = append(data.Volume, klines[i].Volume)
|
||||
|
||||
// Calculate EMA20 for each point
|
||||
if i >= 19 {
|
||||
ema20 := calculateEMA(klines[:i+1], 20)
|
||||
data.EMA20Values = append(data.EMA20Values, ema20)
|
||||
}
|
||||
|
||||
// Calculate EMA50 for each point
|
||||
if i >= 49 {
|
||||
ema50 := calculateEMA(klines[:i+1], 50)
|
||||
data.EMA50Values = append(data.EMA50Values, ema50)
|
||||
}
|
||||
|
||||
// Calculate MACD for each point
|
||||
if i >= 25 {
|
||||
macd := calculateMACD(klines[:i+1])
|
||||
data.MACDValues = append(data.MACDValues, macd)
|
||||
}
|
||||
|
||||
// Calculate RSI for each point
|
||||
if i >= 7 {
|
||||
rsi7 := calculateRSI(klines[:i+1], 7)
|
||||
data.RSI7Values = append(data.RSI7Values, rsi7)
|
||||
}
|
||||
if i >= 14 {
|
||||
rsi14 := calculateRSI(klines[:i+1], 14)
|
||||
data.RSI14Values = append(data.RSI14Values, rsi14)
|
||||
}
|
||||
|
||||
// Calculate Bollinger Bands (period 20, std dev multiplier 2)
|
||||
if i >= 19 {
|
||||
upper, middle, lower := calculateBOLL(klines[:i+1], 20, 2.0)
|
||||
data.BOLLUpper = append(data.BOLLUpper, upper)
|
||||
data.BOLLMiddle = append(data.BOLLMiddle, middle)
|
||||
data.BOLLLower = append(data.BOLLLower, lower)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate ATR14
|
||||
data.ATR14 = calculateATR(klines, 14)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// calculatePriceChangeByBars calculates how many K-lines to look back for price change based on timeframe
|
||||
func calculatePriceChangeByBars(klines []Kline, timeframe string, targetMinutes int) float64 {
|
||||
if len(klines) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Parse timeframe to minutes
|
||||
tfMinutes := parseTimeframeToMinutes(timeframe)
|
||||
if tfMinutes <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Calculate how many K-lines to look back
|
||||
barsBack := targetMinutes / tfMinutes
|
||||
if barsBack < 1 {
|
||||
barsBack = 1
|
||||
}
|
||||
|
||||
currentPrice := klines[len(klines)-1].Close
|
||||
idx := len(klines) - 1 - barsBack
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
}
|
||||
|
||||
oldPrice := klines[idx].Close
|
||||
if oldPrice > 0 {
|
||||
return ((currentPrice - oldPrice) / oldPrice) * 100
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// parseTimeframeToMinutes parses timeframe string to minutes
|
||||
func parseTimeframeToMinutes(tf string) int {
|
||||
switch tf {
|
||||
case "1m":
|
||||
return 1
|
||||
case "3m":
|
||||
return 3
|
||||
case "5m":
|
||||
return 5
|
||||
case "15m":
|
||||
return 15
|
||||
case "30m":
|
||||
return 30
|
||||
case "1h":
|
||||
return 60
|
||||
case "2h":
|
||||
return 120
|
||||
case "4h":
|
||||
return 240
|
||||
case "6h":
|
||||
return 360
|
||||
case "8h":
|
||||
return 480
|
||||
case "12h":
|
||||
return 720
|
||||
case "1d":
|
||||
return 1440
|
||||
case "3d":
|
||||
return 4320
|
||||
case "1w":
|
||||
return 10080
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// calculateIntradaySeries calculates intraday series data
|
||||
func calculateIntradaySeries(klines []Kline) *IntradayData {
|
||||
data := &IntradayData{
|
||||
MidPrices: make([]float64, 0, 10),
|
||||
EMA20Values: make([]float64, 0, 10),
|
||||
MACDValues: make([]float64, 0, 10),
|
||||
RSI7Values: make([]float64, 0, 10),
|
||||
RSI14Values: make([]float64, 0, 10),
|
||||
Volume: make([]float64, 0, 10),
|
||||
}
|
||||
|
||||
// Get latest 10 data points
|
||||
start := len(klines) - 10
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
for i := start; i < len(klines); i++ {
|
||||
data.MidPrices = append(data.MidPrices, klines[i].Close)
|
||||
data.Volume = append(data.Volume, klines[i].Volume)
|
||||
|
||||
// Calculate EMA20 for each point
|
||||
if i >= 19 {
|
||||
ema20 := calculateEMA(klines[:i+1], 20)
|
||||
data.EMA20Values = append(data.EMA20Values, ema20)
|
||||
}
|
||||
|
||||
// Calculate MACD for each point
|
||||
if i >= 25 {
|
||||
macd := calculateMACD(klines[:i+1])
|
||||
data.MACDValues = append(data.MACDValues, macd)
|
||||
}
|
||||
|
||||
// Calculate RSI for each point
|
||||
if i >= 7 {
|
||||
rsi7 := calculateRSI(klines[:i+1], 7)
|
||||
data.RSI7Values = append(data.RSI7Values, rsi7)
|
||||
}
|
||||
if i >= 14 {
|
||||
rsi14 := calculateRSI(klines[:i+1], 14)
|
||||
data.RSI14Values = append(data.RSI14Values, rsi14)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate 3m ATR14
|
||||
data.ATR14 = calculateATR(klines, 14)
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// calculateLongerTermData calculates longer-term data
|
||||
func calculateLongerTermData(klines []Kline) *LongerTermData {
|
||||
data := &LongerTermData{
|
||||
MACDValues: make([]float64, 0, 10),
|
||||
RSI14Values: make([]float64, 0, 10),
|
||||
}
|
||||
|
||||
// Calculate EMA
|
||||
data.EMA20 = calculateEMA(klines, 20)
|
||||
data.EMA50 = calculateEMA(klines, 50)
|
||||
|
||||
// Calculate ATR
|
||||
data.ATR3 = calculateATR(klines, 3)
|
||||
data.ATR14 = calculateATR(klines, 14)
|
||||
|
||||
// Calculate volume
|
||||
if len(klines) > 0 {
|
||||
data.CurrentVolume = klines[len(klines)-1].Volume
|
||||
// Calculate average volume
|
||||
sum := 0.0
|
||||
for _, k := range klines {
|
||||
sum += k.Volume
|
||||
}
|
||||
data.AverageVolume = sum / float64(len(klines))
|
||||
}
|
||||
|
||||
// Calculate MACD and RSI series
|
||||
start := len(klines) - 10
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
for i := start; i < len(klines); i++ {
|
||||
if i >= 25 {
|
||||
macd := calculateMACD(klines[:i+1])
|
||||
data.MACDValues = append(data.MACDValues, macd)
|
||||
}
|
||||
if i >= 14 {
|
||||
rsi14 := calculateRSI(klines[:i+1], 14)
|
||||
data.RSI14Values = append(data.RSI14Values, rsi14)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// GetBoxData fetches 1h klines and calculates box data for a symbol
|
||||
func GetBoxData(symbol string) (*BoxData, error) {
|
||||
symbol = Normalize(symbol)
|
||||
|
||||
// Fetch 500 1h klines
|
||||
var klines []Kline
|
||||
var err error
|
||||
|
||||
if IsXyzDexAsset(symbol) {
|
||||
klines, err = getKlinesFromHyperliquid(symbol, "1h", LongBoxPeriod)
|
||||
} else {
|
||||
klines, err = getKlinesFromCoinAnk(symbol, "1h", "binance", LongBoxPeriod)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get 1h klines: %w", err)
|
||||
}
|
||||
|
||||
if len(klines) == 0 {
|
||||
return nil, fmt.Errorf("no kline data available")
|
||||
}
|
||||
|
||||
currentPrice := klines[len(klines)-1].Close
|
||||
|
||||
return calculateBoxData(klines, currentPrice), nil
|
||||
}
|
||||
Reference in New Issue
Block a user