mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 04:50:57 +08:00
- 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)
193 lines
5.6 KiB
Go
193 lines
5.6 KiB
Go
package okx
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"nofx/logger"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// GetPositions gets all positions
|
|
func (t *OKXTrader) GetPositions() ([]map[string]interface{}, error) {
|
|
// Check cache
|
|
t.positionsCacheMutex.RLock()
|
|
if t.cachedPositions != nil && time.Since(t.positionsCacheTime) < t.cacheDuration {
|
|
t.positionsCacheMutex.RUnlock()
|
|
logger.Infof("✓ Using cached OKX positions")
|
|
return t.cachedPositions, nil
|
|
}
|
|
t.positionsCacheMutex.RUnlock()
|
|
|
|
logger.Infof("🔄 Calling OKX API to get positions...")
|
|
data, err := t.doRequest("GET", okxPositionPath+"?instType=SWAP", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get positions: %w", err)
|
|
}
|
|
|
|
var positions []struct {
|
|
InstId string `json:"instId"`
|
|
PosSide string `json:"posSide"`
|
|
Pos string `json:"pos"`
|
|
AvgPx string `json:"avgPx"`
|
|
MarkPx string `json:"markPx"`
|
|
Upl string `json:"upl"`
|
|
Lever string `json:"lever"`
|
|
LiqPx string `json:"liqPx"`
|
|
Margin string `json:"margin"`
|
|
MgnMode string `json:"mgnMode"` // Margin mode: "cross" or "isolated"
|
|
CTime string `json:"cTime"` // Position created time (ms)
|
|
UTime string `json:"uTime"` // Position last update time (ms)
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &positions); err != nil {
|
|
return nil, fmt.Errorf("failed to parse position data: %w", err)
|
|
}
|
|
|
|
logger.Infof("🔍 OKX raw positions response: %d positions", len(positions))
|
|
var result []map[string]interface{}
|
|
for _, pos := range positions {
|
|
logger.Infof("🔍 OKX raw position: instId=%s, posSide=%s, pos=%s, mgnMode=%s", pos.InstId, pos.PosSide, pos.Pos, pos.MgnMode)
|
|
contractCount, _ := strconv.ParseFloat(pos.Pos, 64)
|
|
if contractCount == 0 {
|
|
continue
|
|
}
|
|
|
|
entryPrice, _ := strconv.ParseFloat(pos.AvgPx, 64)
|
|
markPrice, _ := strconv.ParseFloat(pos.MarkPx, 64)
|
|
upl, _ := strconv.ParseFloat(pos.Upl, 64)
|
|
leverage, _ := strconv.ParseFloat(pos.Lever, 64)
|
|
liqPrice, _ := strconv.ParseFloat(pos.LiqPx, 64)
|
|
|
|
// Convert symbol format
|
|
symbol := t.convertSymbolBack(pos.InstId)
|
|
logger.Infof("🔍 OKX symbol conversion: %s → %s", pos.InstId, symbol)
|
|
|
|
// Determine direction and ensure contractCount is positive
|
|
side := "long"
|
|
if pos.PosSide == "short" {
|
|
side = "short"
|
|
}
|
|
// OKX short position's pos is negative, need to take absolute value
|
|
if contractCount < 0 {
|
|
contractCount = -contractCount
|
|
}
|
|
|
|
// Convert contract count to actual position amount (in base asset)
|
|
// positionAmt = contractCount * ctVal
|
|
inst, err := t.getInstrument(symbol)
|
|
posAmt := contractCount
|
|
if err == nil && inst.CtVal > 0 {
|
|
posAmt = contractCount * inst.CtVal
|
|
logger.Debugf(" 📊 OKX position %s: contracts=%.4f, ctVal=%.6f, posAmt=%.6f", symbol, contractCount, inst.CtVal, posAmt)
|
|
}
|
|
|
|
// Parse timestamps
|
|
cTime, _ := strconv.ParseInt(pos.CTime, 10, 64)
|
|
uTime, _ := strconv.ParseInt(pos.UTime, 10, 64)
|
|
|
|
// Default to cross margin mode if not specified
|
|
mgnMode := pos.MgnMode
|
|
if mgnMode == "" {
|
|
mgnMode = "cross"
|
|
}
|
|
|
|
posMap := map[string]interface{}{
|
|
"symbol": symbol,
|
|
"positionAmt": posAmt,
|
|
"entryPrice": entryPrice,
|
|
"markPrice": markPrice,
|
|
"unRealizedProfit": upl,
|
|
"leverage": leverage,
|
|
"liquidationPrice": liqPrice,
|
|
"side": side,
|
|
"mgnMode": mgnMode, // Margin mode: "cross" or "isolated"
|
|
"createdTime": cTime, // Position open time (ms)
|
|
"updatedTime": uTime, // Position last update time (ms)
|
|
}
|
|
result = append(result, posMap)
|
|
}
|
|
|
|
// Update cache
|
|
t.positionsCacheMutex.Lock()
|
|
t.cachedPositions = result
|
|
t.positionsCacheTime = time.Now()
|
|
t.positionsCacheMutex.Unlock()
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// InvalidatePositionCache clears the position cache to force fresh data on next call
|
|
func (t *OKXTrader) InvalidatePositionCache() {
|
|
t.positionsCacheMutex.Lock()
|
|
t.cachedPositions = nil
|
|
t.positionsCacheTime = time.Time{}
|
|
t.positionsCacheMutex.Unlock()
|
|
}
|
|
|
|
// getInstrument gets instrument info
|
|
func (t *OKXTrader) getInstrument(symbol string) (*OKXInstrument, error) {
|
|
instId := t.convertSymbol(symbol)
|
|
|
|
// Check cache
|
|
t.instrumentsCacheMutex.RLock()
|
|
if inst, ok := t.instrumentsCache[instId]; ok && time.Since(t.instrumentsCacheTime) < 5*time.Minute {
|
|
t.instrumentsCacheMutex.RUnlock()
|
|
return inst, nil
|
|
}
|
|
t.instrumentsCacheMutex.RUnlock()
|
|
|
|
// Get instrument info
|
|
path := fmt.Sprintf("%s?instType=SWAP&instId=%s", okxInstrumentsPath, instId)
|
|
data, err := t.doRequest("GET", path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var instruments []struct {
|
|
InstId string `json:"instId"`
|
|
CtVal string `json:"ctVal"`
|
|
CtMult string `json:"ctMult"`
|
|
LotSz string `json:"lotSz"`
|
|
MinSz string `json:"minSz"`
|
|
MaxMktSz string `json:"maxMktSz"` // Maximum market order size
|
|
TickSz string `json:"tickSz"`
|
|
CtType string `json:"ctType"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &instruments); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(instruments) == 0 {
|
|
return nil, fmt.Errorf("instrument info not found: %s", instId)
|
|
}
|
|
|
|
inst := instruments[0]
|
|
ctVal, _ := strconv.ParseFloat(inst.CtVal, 64)
|
|
ctMult, _ := strconv.ParseFloat(inst.CtMult, 64)
|
|
lotSz, _ := strconv.ParseFloat(inst.LotSz, 64)
|
|
minSz, _ := strconv.ParseFloat(inst.MinSz, 64)
|
|
maxMktSz, _ := strconv.ParseFloat(inst.MaxMktSz, 64)
|
|
tickSz, _ := strconv.ParseFloat(inst.TickSz, 64)
|
|
|
|
instrument := &OKXInstrument{
|
|
InstID: inst.InstId,
|
|
CtVal: ctVal,
|
|
CtMult: ctMult,
|
|
LotSz: lotSz,
|
|
MinSz: minSz,
|
|
MaxMktSz: maxMktSz,
|
|
TickSz: tickSz,
|
|
CtType: inst.CtType,
|
|
}
|
|
|
|
// Update cache
|
|
t.instrumentsCacheMutex.Lock()
|
|
t.instrumentsCache[instId] = instrument
|
|
t.instrumentsCacheTime = time.Now()
|
|
t.instrumentsCacheMutex.Unlock()
|
|
|
|
return instrument, nil
|
|
}
|