mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +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:
-648
@@ -1,15 +1,11 @@
|
||||
package market
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"nofx/logger"
|
||||
"nofx/provider/coinank/coinank_api"
|
||||
"nofx/provider/coinank/coinank_enum"
|
||||
"nofx/provider/hyperliquid"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -28,143 +24,6 @@ var (
|
||||
frCacheTTL = 1 * time.Hour
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Get retrieves market data for the specified token (uses Binance data by default)
|
||||
func Get(symbol string) (*Data, error) {
|
||||
return GetWithExchange(symbol, "binance")
|
||||
@@ -396,398 +255,6 @@ func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe stri
|
||||
}, 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
|
||||
}
|
||||
}
|
||||
|
||||
// calculateEMA calculates EMA
|
||||
func calculateEMA(klines []Kline, period int) float64 {
|
||||
if len(klines) < period {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Calculate SMA as initial EMA
|
||||
sum := 0.0
|
||||
for i := 0; i < period; i++ {
|
||||
sum += klines[i].Close
|
||||
}
|
||||
ema := sum / float64(period)
|
||||
|
||||
// Calculate EMA
|
||||
multiplier := 2.0 / float64(period+1)
|
||||
for i := period; i < len(klines); i++ {
|
||||
ema = (klines[i].Close-ema)*multiplier + ema
|
||||
}
|
||||
|
||||
return ema
|
||||
}
|
||||
|
||||
// calculateMACD calculates MACD
|
||||
func calculateMACD(klines []Kline) float64 {
|
||||
if len(klines) < 26 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Calculate 12-period and 26-period EMA
|
||||
ema12 := calculateEMA(klines, 12)
|
||||
ema26 := calculateEMA(klines, 26)
|
||||
|
||||
// MACD = EMA12 - EMA26
|
||||
return ema12 - ema26
|
||||
}
|
||||
|
||||
// calculateRSI calculates RSI
|
||||
func calculateRSI(klines []Kline, period int) float64 {
|
||||
if len(klines) <= period {
|
||||
return 0
|
||||
}
|
||||
|
||||
gains := 0.0
|
||||
losses := 0.0
|
||||
|
||||
// Calculate initial average gain/loss
|
||||
for i := 1; i <= period; i++ {
|
||||
change := klines[i].Close - klines[i-1].Close
|
||||
if change > 0 {
|
||||
gains += change
|
||||
} else {
|
||||
losses += -change
|
||||
}
|
||||
}
|
||||
|
||||
avgGain := gains / float64(period)
|
||||
avgLoss := losses / float64(period)
|
||||
|
||||
// Use Wilder smoothing method to calculate subsequent RSI
|
||||
for i := period + 1; i < len(klines); i++ {
|
||||
change := klines[i].Close - klines[i-1].Close
|
||||
if change > 0 {
|
||||
avgGain = (avgGain*float64(period-1) + change) / float64(period)
|
||||
avgLoss = (avgLoss * float64(period-1)) / float64(period)
|
||||
} else {
|
||||
avgGain = (avgGain * float64(period-1)) / float64(period)
|
||||
avgLoss = (avgLoss*float64(period-1) + (-change)) / float64(period)
|
||||
}
|
||||
}
|
||||
|
||||
if avgLoss == 0 {
|
||||
return 100
|
||||
}
|
||||
|
||||
rs := avgGain / avgLoss
|
||||
rsi := 100 - (100 / (1 + rs))
|
||||
|
||||
return rsi
|
||||
}
|
||||
|
||||
// calculateATR calculates ATR
|
||||
func calculateATR(klines []Kline, period int) float64 {
|
||||
if len(klines) <= period {
|
||||
return 0
|
||||
}
|
||||
|
||||
trs := make([]float64, len(klines))
|
||||
for i := 1; i < len(klines); i++ {
|
||||
high := klines[i].High
|
||||
low := klines[i].Low
|
||||
prevClose := klines[i-1].Close
|
||||
|
||||
tr1 := high - low
|
||||
tr2 := math.Abs(high - prevClose)
|
||||
tr3 := math.Abs(low - prevClose)
|
||||
|
||||
trs[i] = math.Max(tr1, math.Max(tr2, tr3))
|
||||
}
|
||||
|
||||
// Calculate initial ATR
|
||||
sum := 0.0
|
||||
for i := 1; i <= period; i++ {
|
||||
sum += trs[i]
|
||||
}
|
||||
atr := sum / float64(period)
|
||||
|
||||
// Wilder smoothing
|
||||
for i := period + 1; i < len(klines); i++ {
|
||||
atr = (atr*float64(period-1) + trs[i]) / float64(period)
|
||||
}
|
||||
|
||||
return atr
|
||||
}
|
||||
|
||||
// calculateBOLL calculates Bollinger Bands (upper, middle, lower)
|
||||
// period: typically 20, multiplier: typically 2
|
||||
func calculateBOLL(klines []Kline, period int, multiplier float64) (upper, middle, lower float64) {
|
||||
if len(klines) < period {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
// Calculate SMA (middle band)
|
||||
sum := 0.0
|
||||
for i := len(klines) - period; i < len(klines); i++ {
|
||||
sum += klines[i].Close
|
||||
}
|
||||
sma := sum / float64(period)
|
||||
|
||||
// Calculate standard deviation
|
||||
variance := 0.0
|
||||
for i := len(klines) - period; i < len(klines); i++ {
|
||||
diff := klines[i].Close - sma
|
||||
variance += diff * diff
|
||||
}
|
||||
stdDev := math.Sqrt(variance / float64(period))
|
||||
|
||||
// Calculate bands
|
||||
middle = sma
|
||||
upper = sma + multiplier*stdDev
|
||||
lower = sma - multiplier*stdDev
|
||||
|
||||
return upper, middle, lower
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// getOpenInterestData retrieves OI data
|
||||
func getOpenInterestData(symbol string) (*OIData, error) {
|
||||
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/openInterest?symbol=%s", symbol)
|
||||
@@ -1227,118 +694,3 @@ func isStaleData(klines []Kline, symbol string) bool {
|
||||
logger.Infof("⚠️ %s detected extreme price stability (no fluctuation for %d consecutive periods), but volume is normal", symbol, stalePriceThreshold)
|
||||
return false
|
||||
}
|
||||
|
||||
// ========== 导出的指标计算函数(供测试使用) ==========
|
||||
|
||||
// ExportCalculateEMA exports calculateEMA for testing
|
||||
func ExportCalculateEMA(klines []Kline, period int) float64 {
|
||||
return calculateEMA(klines, period)
|
||||
}
|
||||
|
||||
// ExportCalculateMACD exports calculateMACD for testing
|
||||
func ExportCalculateMACD(klines []Kline) float64 {
|
||||
return calculateMACD(klines)
|
||||
}
|
||||
|
||||
// ExportCalculateRSI exports calculateRSI for testing
|
||||
func ExportCalculateRSI(klines []Kline, period int) float64 {
|
||||
return calculateRSI(klines, period)
|
||||
}
|
||||
|
||||
// ExportCalculateATR exports calculateATR for testing
|
||||
func ExportCalculateATR(klines []Kline, period int) float64 {
|
||||
return calculateATR(klines, period)
|
||||
}
|
||||
|
||||
// ExportCalculateBOLL exports calculateBOLL for testing
|
||||
func ExportCalculateBOLL(klines []Kline, period int, multiplier float64) (upper, middle, lower float64) {
|
||||
return calculateBOLL(klines, period, multiplier)
|
||||
}
|
||||
|
||||
// calculateDonchian calculates Donchian channel (highest high, lowest low) for given period
|
||||
func calculateDonchian(klines []Kline, period int) (upper, lower float64) {
|
||||
if len(klines) == 0 || period <= 0 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Use all available klines if period > len(klines)
|
||||
start := len(klines) - period
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
upper = klines[start].High
|
||||
lower = klines[start].Low
|
||||
|
||||
for i := start + 1; i < len(klines); i++ {
|
||||
if klines[i].High > upper {
|
||||
upper = klines[i].High
|
||||
}
|
||||
if klines[i].Low < lower {
|
||||
lower = klines[i].Low
|
||||
}
|
||||
}
|
||||
|
||||
return upper, lower
|
||||
}
|
||||
|
||||
// ExportCalculateDonchian exports calculateDonchian for testing
|
||||
func ExportCalculateDonchian(klines []Kline, period int) (float64, float64) {
|
||||
return calculateDonchian(klines, period)
|
||||
}
|
||||
|
||||
// Box period constants (in 1h candles)
|
||||
const (
|
||||
ShortBoxPeriod = 72 // 3 days of 1h candles
|
||||
MidBoxPeriod = 240 // 10 days of 1h candles
|
||||
LongBoxPeriod = 500 // ~21 days of 1h candles
|
||||
)
|
||||
|
||||
// calculateBoxData calculates multi-period box data from klines
|
||||
func calculateBoxData(klines []Kline, currentPrice float64) *BoxData {
|
||||
box := &BoxData{
|
||||
CurrentPrice: currentPrice,
|
||||
}
|
||||
|
||||
if len(klines) == 0 {
|
||||
return box
|
||||
}
|
||||
|
||||
box.ShortUpper, box.ShortLower = calculateDonchian(klines, ShortBoxPeriod)
|
||||
box.MidUpper, box.MidLower = calculateDonchian(klines, MidBoxPeriod)
|
||||
box.LongUpper, box.LongLower = calculateDonchian(klines, LongBoxPeriod)
|
||||
|
||||
return box
|
||||
}
|
||||
|
||||
// ExportCalculateBoxData exports calculateBoxData for testing
|
||||
func ExportCalculateBoxData(klines []Kline, currentPrice float64) *BoxData {
|
||||
return calculateBoxData(klines, currentPrice)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
package market
|
||||
|
||||
import "math"
|
||||
|
||||
// calculateEMA calculates EMA
|
||||
func calculateEMA(klines []Kline, period int) float64 {
|
||||
if len(klines) < period {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Calculate SMA as initial EMA
|
||||
sum := 0.0
|
||||
for i := 0; i < period; i++ {
|
||||
sum += klines[i].Close
|
||||
}
|
||||
ema := sum / float64(period)
|
||||
|
||||
// Calculate EMA
|
||||
multiplier := 2.0 / float64(period+1)
|
||||
for i := period; i < len(klines); i++ {
|
||||
ema = (klines[i].Close-ema)*multiplier + ema
|
||||
}
|
||||
|
||||
return ema
|
||||
}
|
||||
|
||||
// calculateMACD calculates MACD
|
||||
func calculateMACD(klines []Kline) float64 {
|
||||
if len(klines) < 26 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Calculate 12-period and 26-period EMA
|
||||
ema12 := calculateEMA(klines, 12)
|
||||
ema26 := calculateEMA(klines, 26)
|
||||
|
||||
// MACD = EMA12 - EMA26
|
||||
return ema12 - ema26
|
||||
}
|
||||
|
||||
// calculateRSI calculates RSI
|
||||
func calculateRSI(klines []Kline, period int) float64 {
|
||||
if len(klines) <= period {
|
||||
return 0
|
||||
}
|
||||
|
||||
gains := 0.0
|
||||
losses := 0.0
|
||||
|
||||
// Calculate initial average gain/loss
|
||||
for i := 1; i <= period; i++ {
|
||||
change := klines[i].Close - klines[i-1].Close
|
||||
if change > 0 {
|
||||
gains += change
|
||||
} else {
|
||||
losses += -change
|
||||
}
|
||||
}
|
||||
|
||||
avgGain := gains / float64(period)
|
||||
avgLoss := losses / float64(period)
|
||||
|
||||
// Use Wilder smoothing method to calculate subsequent RSI
|
||||
for i := period + 1; i < len(klines); i++ {
|
||||
change := klines[i].Close - klines[i-1].Close
|
||||
if change > 0 {
|
||||
avgGain = (avgGain*float64(period-1) + change) / float64(period)
|
||||
avgLoss = (avgLoss * float64(period-1)) / float64(period)
|
||||
} else {
|
||||
avgGain = (avgGain * float64(period-1)) / float64(period)
|
||||
avgLoss = (avgLoss*float64(period-1) + (-change)) / float64(period)
|
||||
}
|
||||
}
|
||||
|
||||
if avgLoss == 0 {
|
||||
return 100
|
||||
}
|
||||
|
||||
rs := avgGain / avgLoss
|
||||
rsi := 100 - (100 / (1 + rs))
|
||||
|
||||
return rsi
|
||||
}
|
||||
|
||||
// calculateATR calculates ATR
|
||||
func calculateATR(klines []Kline, period int) float64 {
|
||||
if len(klines) <= period {
|
||||
return 0
|
||||
}
|
||||
|
||||
trs := make([]float64, len(klines))
|
||||
for i := 1; i < len(klines); i++ {
|
||||
high := klines[i].High
|
||||
low := klines[i].Low
|
||||
prevClose := klines[i-1].Close
|
||||
|
||||
tr1 := high - low
|
||||
tr2 := math.Abs(high - prevClose)
|
||||
tr3 := math.Abs(low - prevClose)
|
||||
|
||||
trs[i] = math.Max(tr1, math.Max(tr2, tr3))
|
||||
}
|
||||
|
||||
// Calculate initial ATR
|
||||
sum := 0.0
|
||||
for i := 1; i <= period; i++ {
|
||||
sum += trs[i]
|
||||
}
|
||||
atr := sum / float64(period)
|
||||
|
||||
// Wilder smoothing
|
||||
for i := period + 1; i < len(klines); i++ {
|
||||
atr = (atr*float64(period-1) + trs[i]) / float64(period)
|
||||
}
|
||||
|
||||
return atr
|
||||
}
|
||||
|
||||
// calculateBOLL calculates Bollinger Bands (upper, middle, lower)
|
||||
// period: typically 20, multiplier: typically 2
|
||||
func calculateBOLL(klines []Kline, period int, multiplier float64) (upper, middle, lower float64) {
|
||||
if len(klines) < period {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
// Calculate SMA (middle band)
|
||||
sum := 0.0
|
||||
for i := len(klines) - period; i < len(klines); i++ {
|
||||
sum += klines[i].Close
|
||||
}
|
||||
sma := sum / float64(period)
|
||||
|
||||
// Calculate standard deviation
|
||||
variance := 0.0
|
||||
for i := len(klines) - period; i < len(klines); i++ {
|
||||
diff := klines[i].Close - sma
|
||||
variance += diff * diff
|
||||
}
|
||||
stdDev := math.Sqrt(variance / float64(period))
|
||||
|
||||
// Calculate bands
|
||||
middle = sma
|
||||
upper = sma + multiplier*stdDev
|
||||
lower = sma - multiplier*stdDev
|
||||
|
||||
return upper, middle, lower
|
||||
}
|
||||
|
||||
// calculateDonchian calculates Donchian channel (highest high, lowest low) for given period
|
||||
func calculateDonchian(klines []Kline, period int) (upper, lower float64) {
|
||||
if len(klines) == 0 || period <= 0 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Use all available klines if period > len(klines)
|
||||
start := len(klines) - period
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
upper = klines[start].High
|
||||
lower = klines[start].Low
|
||||
|
||||
for i := start + 1; i < len(klines); i++ {
|
||||
if klines[i].High > upper {
|
||||
upper = klines[i].High
|
||||
}
|
||||
if klines[i].Low < lower {
|
||||
lower = klines[i].Low
|
||||
}
|
||||
}
|
||||
|
||||
return upper, lower
|
||||
}
|
||||
|
||||
// Box period constants (in 1h candles)
|
||||
const (
|
||||
ShortBoxPeriod = 72 // 3 days of 1h candles
|
||||
MidBoxPeriod = 240 // 10 days of 1h candles
|
||||
LongBoxPeriod = 500 // ~21 days of 1h candles
|
||||
)
|
||||
|
||||
// calculateBoxData calculates multi-period box data from klines
|
||||
func calculateBoxData(klines []Kline, currentPrice float64) *BoxData {
|
||||
box := &BoxData{
|
||||
CurrentPrice: currentPrice,
|
||||
}
|
||||
|
||||
if len(klines) == 0 {
|
||||
return box
|
||||
}
|
||||
|
||||
box.ShortUpper, box.ShortLower = calculateDonchian(klines, ShortBoxPeriod)
|
||||
box.MidUpper, box.MidLower = calculateDonchian(klines, MidBoxPeriod)
|
||||
box.LongUpper, box.LongLower = calculateDonchian(klines, LongBoxPeriod)
|
||||
|
||||
return box
|
||||
}
|
||||
|
||||
// ========== Exported indicator calculation functions (for testing) ==========
|
||||
|
||||
// ExportCalculateEMA exports calculateEMA for testing
|
||||
func ExportCalculateEMA(klines []Kline, period int) float64 {
|
||||
return calculateEMA(klines, period)
|
||||
}
|
||||
|
||||
// ExportCalculateMACD exports calculateMACD for testing
|
||||
func ExportCalculateMACD(klines []Kline) float64 {
|
||||
return calculateMACD(klines)
|
||||
}
|
||||
|
||||
// ExportCalculateRSI exports calculateRSI for testing
|
||||
func ExportCalculateRSI(klines []Kline, period int) float64 {
|
||||
return calculateRSI(klines, period)
|
||||
}
|
||||
|
||||
// ExportCalculateATR exports calculateATR for testing
|
||||
func ExportCalculateATR(klines []Kline, period int) float64 {
|
||||
return calculateATR(klines, period)
|
||||
}
|
||||
|
||||
// ExportCalculateBOLL exports calculateBOLL for testing
|
||||
func ExportCalculateBOLL(klines []Kline, period int, multiplier float64) (upper, middle, lower float64) {
|
||||
return calculateBOLL(klines, period, multiplier)
|
||||
}
|
||||
|
||||
// ExportCalculateDonchian exports calculateDonchian for testing
|
||||
func ExportCalculateDonchian(klines []Kline, period int) (float64, float64) {
|
||||
return calculateDonchian(klines, period)
|
||||
}
|
||||
|
||||
// ExportCalculateBoxData exports calculateBoxData for testing
|
||||
func ExportCalculateBoxData(klines []Kline, currentPrice float64) *BoxData {
|
||||
return calculateBoxData(klines, currentPrice)
|
||||
}
|
||||
@@ -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