mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
7e96c5d0f2
* feat: add AI grid trading and market regime classification - Add GridTrader interface with PlaceLimitOrder, CancelOrder, GetOrderBook - Implement GridTrader for all exchanges (Binance, Bybit, OKX, Bitget, Hyperliquid, Aster, Lighter) - Add grid engine with ATR-based boundary calculation and fund distribution - Add market regime classification documents (Chinese/English) - Add GridConfigEditor component for frontend configuration * fix: implement GetOpenOrders for Lighter exchange * debug: add logging for Lighter GetActiveOrders API call * fix: correct Lighter API response parsing for GetOpenOrders - Changed response field from 'data' to 'orders' to match Lighter API - Updated OrderResponse struct to match Lighter's actual field names - Fixed field types: price/quantity as strings, is_ask for side * feat: implement GetOpenOrders for Aster, OKX, Bitget exchanges - Aster: uses /fapi/v3/openOrders endpoint - OKX: uses /api/v5/trade/orders-pending and orders-algo-pending - Bitget: uses /api/v2/mix/order/orders-pending and orders-plan-pending * fix: address code review issues for GetOpenOrders - Add error logging for OKX/Bitget API failures (was silently swallowed) - Fix Lighter position side logic to handle reduce-only orders - Change verbose debug logs from Infof to Debugf level * fix: provide FromAccountIndex and ApiKeyIndex for Lighter nonce auto-fetch Root cause: SDK requires these fields to fetch nonce from API, otherwise nonce gets cached/stuck * fix: use auth query parameter instead of Authorization header for Lighter API * test: add Lighter API authentication tests and diagnostic tools * fix(grid): add leverage setting before order placement CRITICAL BUG FIX: - Call SetLeverage() in GridTraderAdapter.PlaceLimitOrder() - Set leverage during grid initialization - Log leverage setting results * fix(grid): prevent CancelOrder from canceling all orders CRITICAL BUG FIX: - CancelOrder no longer calls CancelAllOrders - Try exchange-specific CancelOrder if available - Return error if individual cancellation not supported * fix(grid): add total position value limit check CRITICAL: Prevent excessive position accumulation - New checkTotalPositionLimit() function - Checks current + pending + new order value - Rejects orders that would exceed TotalInvestment x Leverage - Logs clear error messages when limit exceeded * feat(grid): implement stop loss execution CRITICAL: Add code-level stop loss protection - New checkAndExecuteStopLoss() function - Checks each filled level against StopLossPct - Automatically closes positions exceeding stop loss - Called during every grid state sync * feat(grid): add breakout detection and auto-pause CRITICAL: Detect price breakout from grid range - New checkBreakout() function to detect upper/lower breakouts - Auto-pause grid on significant breakout (>2%) - Cancel all orders when breakout detected - Prevent continued losses in trending market - Minor breakouts (1-2%) logged for AI consideration * feat(grid): enforce max drawdown limit with emergency exit CRITICAL: Add drawdown protection - New checkMaxDrawdown() function tracks peak equity - emergencyExit() closes all positions and cancels orders - Auto-pause grid when MaxDrawdownPct exceeded - Protect capital from excessive losses * feat(grid): enforce daily loss limit - Add checkDailyLossLimit() function to check if daily loss exceeds limit - Track daily PnL with auto-reset at midnight - Pause grid when DailyLossLimitPct exceeded - Add updateDailyPnL() helper for realized PnL tracking - Prevent excessive single-day losses * fix(grid): update daily PnL when stop loss is executed The updateDailyPnL() function was added but never called, leaving DailyPnL always at 0 and preventing daily loss limit checks from triggering. This fix updates DailyPnL and TotalProfit directly in checkAndExecuteStopLoss() when a stop loss is executed. We update directly rather than calling updateDailyPnL() because the mutex is already held in that function. * feat(grid): add automatic grid adjustment - New checkGridSkew() detects imbalanced grid - autoAdjustGrid() reinitializes around current price - Prevents grid from becoming ineffective after drift - Triggers when one side is 3x more filled than other * fix(grid): recalculate bounds in autoAdjustGrid before reinitializing levels Critical fix for grid auto-adjustment: - Recalculate grid bounds (UpperPrice, LowerPrice, GridSpacing) centered on current price before reinitializing grid levels - Preserve filled positions during adjustment by saving and restoring them to the closest new level after reinitialization - Hold mutex lock for the entire adjustment operation to ensure atomicity - Add locked variants of calculateDefaultBounds, calculateATRBounds, and initializeGridLevels to use during adjustment Without this fix, autoAdjustGrid was using old boundaries when creating new grid levels, defeating the purpose of auto-adjustment when price moved significantly. * fix(grid): improve order state sync logic - Don't assume missing orders are filled - Compare position size to determine fill vs cancel - Properly reset cancelled orders to empty state - More accurate grid state tracking * fix(grid): use actual PositionSize sum instead of count in syncGridState heuristic The position-based heuristic was using `float64(previousFilledCount) * level.OrderQuantity` which incorrectly assumed uniform order quantities. Since the grid uses weighted distribution (gaussian, pyramid, uniform) where orders have different quantities, this could lead to incorrect fill detection. Now sums the actual PositionSize from filled levels for accurate comparison. Also adds warning log when GetPositions() fails. * docs: add grid market regime detection design Design for enhanced market state recognition with: - Multi-dimensional indicators (ATR, Bollinger, EMA, MACD, RSI) - Multi-period box indicators (72/240/500 1h candles) - 4-level ranging classification - Breakout detection and handling - Frontend risk control panel * docs: add grid market regime implementation plan 20 tasks covering: - Donchian channel calculation - Box data types and API - Regime classification (4 levels) - Breakout detection and handling - False breakout recovery - Frontend risk panel - AI prompt updates * feat(market): add Donchian channel calculation Add calculateDonchian function to compute highest high and lowest low over a specified period. This is the foundation for box (range) detection in the multi-period box indicator system for grid trading. * fix(market): handle invalid period in calculateDonchian * feat(market): add BoxData and RegimeLevel types * feat(market): add GetBoxData for multi-period box calculation Adds calculateBoxData internal function and GetBoxData public API that fetches 1h klines and computes three Donchian box levels (short/mid/long). This will be used by the grid trading system to detect market regime. * feat(store): add box and regime fields to grid models * feat(trader): add regime classification and breakout detection Implements Tasks 6-9 for grid market regime awareness: - Task 6: classifyRegimeLevel with Bollinger/ATR thresholds - Task 7: detectBoxBreakout for multi-period box breakouts - Task 8: confirmBreakout with 3-candle confirmation logic - Task 9: getBreakoutAction mapping breakout levels to actions * feat(trader): integrate box breakout detection into grid cycle - Task 10: Add checkBoxBreakout with 3-candle confirmation - Task 11: Add checkFalseBreakoutRecovery for 50% position recovery - Task 12: Add box/breakout/regime fields to GridState * feat: add grid risk panel with API endpoint - Task 13: Add GridRiskInfo type to frontend - Task 14: Add /traders/:id/grid-risk API endpoint - Task 15: Add GetGridRiskInfo method to AutoTrader - Task 16: Create GridRiskPanel component with i18n * feat(kernel): add box indicators to AI prompt - Add BoxData field to GridContext - Add box indicator table to both zh/en prompts - Show breakout/warning alerts based on price position * feat(web): integrate GridRiskPanel into TraderDashboardPage * feat(lighter): improve API key validation and market caching - Add API key validation status tracking - Add market list caching to reduce API calls - Improve logging (debug vs info levels) - Add comprehensive integration tests - Update trader manager and store for lighter support * fix: remove hardcoded test wallet address * fix(grid): improve GridRiskPanel layout and fix liquidation data - Make panel collapsible with summary badges when collapsed - Use compact 2-column grid layout for detailed info - Fix auth token key (token -> auth_token) - Only calculate liquidation distance when position exists * fix(grid): add isRunning checks to prevent trades after Stop() is called
497 lines
19 KiB
Go
497 lines
19 KiB
Go
package store
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// StrategyStore strategy storage
|
||
type StrategyStore struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// Strategy strategy configuration
|
||
type Strategy struct {
|
||
ID string `gorm:"primaryKey" json:"id"`
|
||
UserID string `gorm:"column:user_id;not null;default:'';index" json:"user_id"`
|
||
Name string `gorm:"not null" json:"name"`
|
||
Description string `gorm:"default:''" json:"description"`
|
||
IsActive bool `gorm:"column:is_active;default:false;index" json:"is_active"`
|
||
IsDefault bool `gorm:"column:is_default;default:false" json:"is_default"`
|
||
IsPublic bool `gorm:"column:is_public;default:false;index" json:"is_public"` // whether visible in strategy market
|
||
ConfigVisible bool `gorm:"column:config_visible;default:true" json:"config_visible"` // whether config details are visible
|
||
Config string `gorm:"not null;default:'{}'" json:"config"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
func (Strategy) TableName() string { return "strategies" }
|
||
|
||
// StrategyConfig strategy configuration details (JSON structure)
|
||
type StrategyConfig struct {
|
||
// Strategy type: "ai_trading" (default) or "grid_trading"
|
||
StrategyType string `json:"strategy_type,omitempty"`
|
||
|
||
// language setting: "zh" for Chinese, "en" for English
|
||
// This determines the language used for data formatting and prompt generation
|
||
Language string `json:"language,omitempty"`
|
||
// coin source configuration
|
||
CoinSource CoinSourceConfig `json:"coin_source"`
|
||
// quantitative data configuration
|
||
Indicators IndicatorConfig `json:"indicators"`
|
||
// custom prompt (appended at the end)
|
||
CustomPrompt string `json:"custom_prompt,omitempty"`
|
||
// risk control configuration
|
||
RiskControl RiskControlConfig `json:"risk_control"`
|
||
// editable sections of System Prompt
|
||
PromptSections PromptSectionsConfig `json:"prompt_sections,omitempty"`
|
||
|
||
// Grid trading configuration (only used when StrategyType == "grid_trading")
|
||
GridConfig *GridStrategyConfig `json:"grid_config,omitempty"`
|
||
}
|
||
|
||
// GridStrategyConfig grid trading specific configuration
|
||
type GridStrategyConfig struct {
|
||
// Trading pair (e.g., "BTCUSDT")
|
||
Symbol string `json:"symbol"`
|
||
// Number of grid levels (5-50)
|
||
GridCount int `json:"grid_count"`
|
||
// Total investment in USDT
|
||
TotalInvestment float64 `json:"total_investment"`
|
||
// Leverage (1-20)
|
||
Leverage int `json:"leverage"`
|
||
// Upper price boundary (0 = auto-calculate from ATR)
|
||
UpperPrice float64 `json:"upper_price"`
|
||
// Lower price boundary (0 = auto-calculate from ATR)
|
||
LowerPrice float64 `json:"lower_price"`
|
||
// Use ATR to auto-calculate bounds
|
||
UseATRBounds bool `json:"use_atr_bounds"`
|
||
// ATR multiplier for bound calculation (default 2.0)
|
||
ATRMultiplier float64 `json:"atr_multiplier"`
|
||
// Position distribution: "uniform" | "gaussian" | "pyramid"
|
||
Distribution string `json:"distribution"`
|
||
// Maximum drawdown percentage before emergency exit
|
||
MaxDrawdownPct float64 `json:"max_drawdown_pct"`
|
||
// Stop loss percentage per position
|
||
StopLossPct float64 `json:"stop_loss_pct"`
|
||
// Daily loss limit percentage
|
||
DailyLossLimitPct float64 `json:"daily_loss_limit_pct"`
|
||
// Use maker-only orders for lower fees
|
||
UseMakerOnly bool `json:"use_maker_only"`
|
||
}
|
||
|
||
// PromptSectionsConfig editable sections of System Prompt
|
||
type PromptSectionsConfig struct {
|
||
// role definition (title + description)
|
||
RoleDefinition string `json:"role_definition,omitempty"`
|
||
// trading frequency awareness
|
||
TradingFrequency string `json:"trading_frequency,omitempty"`
|
||
// entry standards
|
||
EntryStandards string `json:"entry_standards,omitempty"`
|
||
// decision process
|
||
DecisionProcess string `json:"decision_process,omitempty"`
|
||
}
|
||
|
||
// CoinSourceConfig coin source configuration
|
||
type CoinSourceConfig struct {
|
||
// source type: "static" | "ai500" | "oi_top" | "mixed"
|
||
SourceType string `json:"source_type"`
|
||
// static coin list (used when source_type = "static")
|
||
StaticCoins []string `json:"static_coins,omitempty"`
|
||
// excluded coins list (filtered out from all sources)
|
||
ExcludedCoins []string `json:"excluded_coins,omitempty"`
|
||
// whether to use AI500 coin pool
|
||
UseAI500 bool `json:"use_ai500"`
|
||
// AI500 coin pool maximum count
|
||
AI500Limit int `json:"ai500_limit,omitempty"`
|
||
// whether to use OI Top
|
||
UseOITop bool `json:"use_oi_top"`
|
||
// OI Top maximum count
|
||
OITopLimit int `json:"oi_top_limit,omitempty"`
|
||
// Note: API URLs are now built automatically using NofxOSAPIKey from IndicatorConfig
|
||
}
|
||
|
||
// IndicatorConfig indicator configuration
|
||
type IndicatorConfig struct {
|
||
// K-line configuration
|
||
Klines KlineConfig `json:"klines"`
|
||
// raw kline data (OHLCV) - always enabled, required for AI analysis
|
||
EnableRawKlines bool `json:"enable_raw_klines"`
|
||
// technical indicator switches
|
||
EnableEMA bool `json:"enable_ema"`
|
||
EnableMACD bool `json:"enable_macd"`
|
||
EnableRSI bool `json:"enable_rsi"`
|
||
EnableATR bool `json:"enable_atr"`
|
||
EnableBOLL bool `json:"enable_boll"` // Bollinger Bands
|
||
EnableVolume bool `json:"enable_volume"`
|
||
EnableOI bool `json:"enable_oi"` // open interest
|
||
EnableFundingRate bool `json:"enable_funding_rate"` // funding rate
|
||
// EMA period configuration
|
||
EMAPeriods []int `json:"ema_periods,omitempty"` // default [20, 50]
|
||
// RSI period configuration
|
||
RSIPeriods []int `json:"rsi_periods,omitempty"` // default [7, 14]
|
||
// ATR period configuration
|
||
ATRPeriods []int `json:"atr_periods,omitempty"` // default [14]
|
||
// BOLL period configuration (period, standard deviation multiplier is fixed at 2)
|
||
BOLLPeriods []int `json:"boll_periods,omitempty"` // default [20] - can select multiple timeframes
|
||
// external data sources
|
||
ExternalDataSources []ExternalDataSource `json:"external_data_sources,omitempty"`
|
||
|
||
// ========== NofxOS Unified API Configuration ==========
|
||
// Unified API Key for all NofxOS data sources
|
||
NofxOSAPIKey string `json:"nofxos_api_key,omitempty"`
|
||
|
||
// quantitative data sources (capital flow, position changes, price changes)
|
||
EnableQuantData bool `json:"enable_quant_data"` // whether to enable quantitative data
|
||
EnableQuantOI bool `json:"enable_quant_oi"` // whether to show OI data
|
||
EnableQuantNetflow bool `json:"enable_quant_netflow"` // whether to show Netflow data
|
||
|
||
// OI ranking data (market-wide open interest increase/decrease rankings)
|
||
EnableOIRanking bool `json:"enable_oi_ranking"` // whether to enable OI ranking data
|
||
OIRankingDuration string `json:"oi_ranking_duration,omitempty"` // duration: 1h, 4h, 24h
|
||
OIRankingLimit int `json:"oi_ranking_limit,omitempty"` // number of entries (default 10)
|
||
|
||
// NetFlow ranking data (market-wide fund flow rankings - institution/personal)
|
||
EnableNetFlowRanking bool `json:"enable_netflow_ranking"` // whether to enable NetFlow ranking data
|
||
NetFlowRankingDuration string `json:"netflow_ranking_duration,omitempty"` // duration: 1h, 4h, 24h
|
||
NetFlowRankingLimit int `json:"netflow_ranking_limit,omitempty"` // number of entries (default 10)
|
||
|
||
// Price ranking data (market-wide gainers/losers)
|
||
EnablePriceRanking bool `json:"enable_price_ranking"` // whether to enable price ranking data
|
||
PriceRankingDuration string `json:"price_ranking_duration,omitempty"` // durations: "1h" or "1h,4h,24h"
|
||
PriceRankingLimit int `json:"price_ranking_limit,omitempty"` // number of entries per ranking (default 10)
|
||
}
|
||
|
||
// KlineConfig K-line configuration
|
||
type KlineConfig struct {
|
||
// primary timeframe: "1m", "3m", "5m", "15m", "1h", "4h"
|
||
PrimaryTimeframe string `json:"primary_timeframe"`
|
||
// primary timeframe K-line count
|
||
PrimaryCount int `json:"primary_count"`
|
||
// longer timeframe
|
||
LongerTimeframe string `json:"longer_timeframe,omitempty"`
|
||
// longer timeframe K-line count
|
||
LongerCount int `json:"longer_count,omitempty"`
|
||
// whether to enable multi-timeframe analysis
|
||
EnableMultiTimeframe bool `json:"enable_multi_timeframe"`
|
||
// selected timeframe list (new: supports multi-timeframe selection)
|
||
SelectedTimeframes []string `json:"selected_timeframes,omitempty"`
|
||
}
|
||
|
||
// ExternalDataSource external data source configuration
|
||
type ExternalDataSource struct {
|
||
Name string `json:"name"` // data source name
|
||
Type string `json:"type"` // type: "api" | "webhook"
|
||
URL string `json:"url"` // API URL
|
||
Method string `json:"method"` // HTTP method
|
||
Headers map[string]string `json:"headers,omitempty"`
|
||
DataPath string `json:"data_path,omitempty"` // JSON data path
|
||
RefreshSecs int `json:"refresh_secs,omitempty"` // refresh interval (seconds)
|
||
}
|
||
|
||
// RiskControlConfig risk control configuration
|
||
type RiskControlConfig struct {
|
||
// Max number of coins held simultaneously (CODE ENFORCED)
|
||
MaxPositions int `json:"max_positions"`
|
||
|
||
// BTC/ETH exchange leverage for opening positions (AI guided)
|
||
BTCETHMaxLeverage int `json:"btc_eth_max_leverage"`
|
||
// Altcoin exchange leverage for opening positions (AI guided)
|
||
AltcoinMaxLeverage int `json:"altcoin_max_leverage"`
|
||
|
||
// BTC/ETH single position max value = equity × this ratio (CODE ENFORCED, default: 5)
|
||
BTCETHMaxPositionValueRatio float64 `json:"btc_eth_max_position_value_ratio"`
|
||
// Altcoin single position max value = equity × this ratio (CODE ENFORCED, default: 1)
|
||
AltcoinMaxPositionValueRatio float64 `json:"altcoin_max_position_value_ratio"`
|
||
|
||
// Max margin utilization (e.g. 0.9 = 90%) (CODE ENFORCED)
|
||
MaxMarginUsage float64 `json:"max_margin_usage"`
|
||
// Min position size in USDT (CODE ENFORCED)
|
||
MinPositionSize float64 `json:"min_position_size"`
|
||
|
||
// Min take_profit / stop_loss ratio (AI guided)
|
||
MinRiskRewardRatio float64 `json:"min_risk_reward_ratio"`
|
||
// Min AI confidence to open position (AI guided)
|
||
MinConfidence int `json:"min_confidence"`
|
||
}
|
||
|
||
// NewStrategyStore creates a new StrategyStore
|
||
func NewStrategyStore(db *gorm.DB) *StrategyStore {
|
||
return &StrategyStore{db: db}
|
||
}
|
||
|
||
func (s *StrategyStore) initTables() error {
|
||
// AutoMigrate will add missing columns without dropping existing data
|
||
return s.db.AutoMigrate(&Strategy{})
|
||
}
|
||
|
||
func (s *StrategyStore) initDefaultData() error {
|
||
// No longer pre-populate strategies - create on demand when user configures
|
||
return nil
|
||
}
|
||
|
||
// GetDefaultStrategyConfig returns the default strategy configuration for the given language
|
||
func GetDefaultStrategyConfig(lang string) StrategyConfig {
|
||
// Normalize language to "zh" or "en"
|
||
normalizedLang := "en"
|
||
if lang == "zh" {
|
||
normalizedLang = "zh"
|
||
}
|
||
|
||
config := StrategyConfig{
|
||
Language: normalizedLang,
|
||
CoinSource: CoinSourceConfig{
|
||
SourceType: "ai500",
|
||
UseAI500: true,
|
||
AI500Limit: 10,
|
||
UseOITop: false,
|
||
OITopLimit: 20,
|
||
},
|
||
Indicators: IndicatorConfig{
|
||
Klines: KlineConfig{
|
||
PrimaryTimeframe: "5m",
|
||
PrimaryCount: 30,
|
||
LongerTimeframe: "4h",
|
||
LongerCount: 10,
|
||
EnableMultiTimeframe: true,
|
||
SelectedTimeframes: []string{"5m", "15m", "1h", "4h"},
|
||
},
|
||
EnableRawKlines: true, // Required - raw OHLCV data for AI analysis
|
||
EnableEMA: false,
|
||
EnableMACD: false,
|
||
EnableRSI: false,
|
||
EnableATR: false,
|
||
EnableBOLL: false,
|
||
EnableVolume: true,
|
||
EnableOI: true,
|
||
EnableFundingRate: true,
|
||
EMAPeriods: []int{20, 50},
|
||
RSIPeriods: []int{7, 14},
|
||
ATRPeriods: []int{14},
|
||
BOLLPeriods: []int{20},
|
||
// NofxOS unified API key
|
||
NofxOSAPIKey: "cm_568c67eae410d912c54c",
|
||
// Quant data
|
||
EnableQuantData: true,
|
||
EnableQuantOI: true,
|
||
EnableQuantNetflow: true,
|
||
// OI ranking data
|
||
EnableOIRanking: true,
|
||
OIRankingDuration: "1h",
|
||
OIRankingLimit: 10,
|
||
// NetFlow ranking data
|
||
EnableNetFlowRanking: true,
|
||
NetFlowRankingDuration: "1h",
|
||
NetFlowRankingLimit: 10,
|
||
// Price ranking data
|
||
EnablePriceRanking: true,
|
||
PriceRankingDuration: "1h,4h,24h",
|
||
PriceRankingLimit: 10,
|
||
},
|
||
RiskControl: RiskControlConfig{
|
||
MaxPositions: 3, // Max 3 coins simultaneously (CODE ENFORCED)
|
||
BTCETHMaxLeverage: 5, // BTC/ETH exchange leverage (AI guided)
|
||
AltcoinMaxLeverage: 5, // Altcoin exchange leverage (AI guided)
|
||
BTCETHMaxPositionValueRatio: 5.0, // BTC/ETH: max position = 5x equity (CODE ENFORCED)
|
||
AltcoinMaxPositionValueRatio: 1.0, // Altcoin: max position = 1x equity (CODE ENFORCED)
|
||
MaxMarginUsage: 0.9, // Max 90% margin usage (CODE ENFORCED)
|
||
MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED)
|
||
MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided)
|
||
MinConfidence: 75, // Min 75% confidence (AI guided)
|
||
},
|
||
}
|
||
|
||
if lang == "zh" {
|
||
config.PromptSections = PromptSectionsConfig{
|
||
RoleDefinition: `# 你是一个专业的加密货币交易AI
|
||
|
||
你的任务是根据提供的市场数据做出交易决策。你是一个经验丰富的量化交易员,擅长技术分析和风险管理。`,
|
||
TradingFrequency: `# ⏱️ 交易频率意识
|
||
|
||
- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔
|
||
- 每小时超过2笔 = 过度交易
|
||
- 单笔持仓时间 ≥ 30-60分钟
|
||
如果你发现自己每个周期都在交易 → 标准太低;如果持仓不到30分钟就平仓 → 太冲动。`,
|
||
EntryStandards: `# 🎯 入场标准(严格)
|
||
|
||
只在多个信号共振时入场。自由使用任何有效的分析方法,避免单一指标、信号矛盾、横盘震荡、或平仓后立即重新开仓等低质量行为。`,
|
||
DecisionProcess: `# 📋 决策流程
|
||
|
||
1. 检查持仓 → 是否止盈/止损
|
||
2. 扫描候选币种 + 多时间框架 → 是否存在强信号
|
||
3. 先写思维链,再输出结构化JSON`,
|
||
}
|
||
} else {
|
||
config.PromptSections = PromptSectionsConfig{
|
||
RoleDefinition: `# You are a professional cryptocurrency trading AI
|
||
|
||
Your task is to make trading decisions based on the provided market data. You are an experienced quantitative trader skilled in technical analysis and risk management.`,
|
||
TradingFrequency: `# ⏱️ Trading Frequency Awareness
|
||
|
||
- Excellent trader: 2-4 trades per day ≈ 0.1-0.2 trades per hour
|
||
- >2 trades per hour = overtrading
|
||
- Single position holding time ≥ 30-60 minutes
|
||
If you find yourself trading every cycle → standards are too low; if closing positions in <30 minutes → too impulsive.`,
|
||
EntryStandards: `# 🎯 Entry Standards (Strict)
|
||
|
||
Only enter positions when multiple signals resonate. Freely use any effective analysis methods, avoid low-quality behaviors such as single indicators, contradictory signals, sideways oscillation, or immediately restarting after closing positions.`,
|
||
DecisionProcess: `# 📋 Decision Process
|
||
|
||
1. Check positions → whether to take profit/stop loss
|
||
2. Scan candidate coins + multi-timeframe → whether strong signals exist
|
||
3. Write chain of thought first, then output structured JSON`,
|
||
}
|
||
}
|
||
|
||
return config
|
||
}
|
||
|
||
// Create create a strategy
|
||
func (s *StrategyStore) Create(strategy *Strategy) error {
|
||
return s.db.Create(strategy).Error
|
||
}
|
||
|
||
// Update update a strategy
|
||
func (s *StrategyStore) Update(strategy *Strategy) error {
|
||
return s.db.Model(&Strategy{}).
|
||
Where("id = ? AND user_id = ?", strategy.ID, strategy.UserID).
|
||
Updates(map[string]interface{}{
|
||
"name": strategy.Name,
|
||
"description": strategy.Description,
|
||
"config": strategy.Config,
|
||
"is_public": strategy.IsPublic,
|
||
"config_visible": strategy.ConfigVisible,
|
||
"updated_at": time.Now().UTC(),
|
||
}).Error
|
||
}
|
||
|
||
// Delete delete a strategy
|
||
func (s *StrategyStore) Delete(userID, id string) error {
|
||
// do not allow deleting system default strategy
|
||
var st Strategy
|
||
if err := s.db.Where("id = ?", id).First(&st).Error; err == nil && st.IsDefault {
|
||
return fmt.Errorf("cannot delete system default strategy")
|
||
}
|
||
|
||
return s.db.Where("id = ? AND user_id = ?", id, userID).Delete(&Strategy{}).Error
|
||
}
|
||
|
||
// List get user's strategy list
|
||
func (s *StrategyStore) List(userID string) ([]*Strategy, error) {
|
||
var strategies []*Strategy
|
||
err := s.db.Where("user_id = ? OR is_default = ?", userID, true).
|
||
Order("is_default DESC, created_at DESC").
|
||
Find(&strategies).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return strategies, nil
|
||
}
|
||
|
||
// ListPublic get all public strategies for the strategy market
|
||
func (s *StrategyStore) ListPublic() ([]*Strategy, error) {
|
||
var strategies []*Strategy
|
||
err := s.db.Where("is_public = ?", true).
|
||
Order("created_at DESC").
|
||
Find(&strategies).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return strategies, nil
|
||
}
|
||
|
||
// Get get a single strategy
|
||
func (s *StrategyStore) Get(userID, id string) (*Strategy, error) {
|
||
var st Strategy
|
||
err := s.db.Where("id = ? AND (user_id = ? OR is_default = ?)", id, userID, true).
|
||
First(&st).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &st, nil
|
||
}
|
||
|
||
// GetActive get user's currently active strategy
|
||
func (s *StrategyStore) GetActive(userID string) (*Strategy, error) {
|
||
var st Strategy
|
||
err := s.db.Where("user_id = ? AND is_active = ?", userID, true).First(&st).Error
|
||
if err == gorm.ErrRecordNotFound {
|
||
// no active strategy, return system default strategy
|
||
return s.GetDefault()
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &st, nil
|
||
}
|
||
|
||
// GetDefault get system default strategy
|
||
func (s *StrategyStore) GetDefault() (*Strategy, error) {
|
||
var st Strategy
|
||
err := s.db.Where("is_default = ?", true).First(&st).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &st, nil
|
||
}
|
||
|
||
// SetActive set active strategy (will first deactivate other strategies)
|
||
func (s *StrategyStore) SetActive(userID, strategyID string) error {
|
||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||
// first deactivate all strategies for the user
|
||
if err := tx.Model(&Strategy{}).Where("user_id = ?", userID).
|
||
Update("is_active", false).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// activate specified strategy
|
||
return tx.Model(&Strategy{}).
|
||
Where("id = ? AND (user_id = ? OR is_default = ?)", strategyID, userID, true).
|
||
Update("is_active", true).Error
|
||
})
|
||
}
|
||
|
||
// Duplicate duplicate a strategy (used to create custom strategy based on default strategy)
|
||
func (s *StrategyStore) Duplicate(userID, sourceID, newID, newName string) error {
|
||
// get source strategy
|
||
source, err := s.Get(userID, sourceID)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to get source strategy: %w", err)
|
||
}
|
||
|
||
// create new strategy
|
||
newStrategy := &Strategy{
|
||
ID: newID,
|
||
UserID: userID,
|
||
Name: newName,
|
||
Description: "Created based on [" + source.Name + "]",
|
||
IsActive: false,
|
||
IsDefault: false,
|
||
Config: source.Config,
|
||
}
|
||
|
||
return s.Create(newStrategy)
|
||
}
|
||
|
||
// ParseConfig parse strategy configuration JSON
|
||
func (s *Strategy) ParseConfig() (*StrategyConfig, error) {
|
||
var config StrategyConfig
|
||
if err := json.Unmarshal([]byte(s.Config), &config); err != nil {
|
||
return nil, fmt.Errorf("failed to parse strategy configuration: %w", err)
|
||
}
|
||
return &config, nil
|
||
}
|
||
|
||
// SetConfig set strategy configuration
|
||
func (s *Strategy) SetConfig(config *StrategyConfig) error {
|
||
data, err := json.Marshal(config)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to serialize strategy configuration: %w", err)
|
||
}
|
||
s.Config = string(data)
|
||
return nil
|
||
}
|