Initial commit: NOFX AI Trading System

- Multi-AI competition mode (Qwen vs DeepSeek)
- Binance Futures integration
- AI self-learning mechanism
- Professional web dashboard
- Complete risk management system
This commit is contained in:
tinkle-community
2025-10-28 15:47:34 +08:00
parent 976d6d74f6
commit 5aa50d35d7
12369 changed files with 1739210 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
# IDE 配置文件
.idea/
*.iml
*.xml
# 编译产物
nofx-auto
*.exe
# Go 相关
*.test
*.out
# 操作系统
.DS_Store
Thumbs.db
# 临时文件
*.log
*.tmp
*.bak
# 环境变量
.env
config.json
# 决策日志
decision_logs/
coin_pool_cache/
+394
View File
@@ -0,0 +1,394 @@
package api
import (
"fmt"
"log"
"net/http"
"nofx/manager"
"github.com/gin-gonic/gin"
)
// Server HTTP API服务器
type Server struct {
router *gin.Engine
traderManager *manager.TraderManager
port int
}
// NewServer 创建API服务器
func NewServer(traderManager *manager.TraderManager, port int) *Server {
// 设置为Release模式(减少日志输出)
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
// 启用CORS
router.Use(corsMiddleware())
s := &Server{
router: router,
traderManager: traderManager,
port: port,
}
// 设置路由
s.setupRoutes()
return s
}
// corsMiddleware CORS中间件
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusOK)
return
}
c.Next()
}
}
// setupRoutes 设置路由
func (s *Server) setupRoutes() {
// 健康检查
s.router.GET("/health", s.handleHealth)
// API路由组
api := s.router.Group("/api")
{
// 竞赛总览
api.GET("/competition", s.handleCompetition)
// Trader列表
api.GET("/traders", s.handleTraderList)
// 指定trader的数据(使用query参数 ?trader_id=xxx
api.GET("/status", s.handleStatus)
api.GET("/account", s.handleAccount)
api.GET("/positions", s.handlePositions)
api.GET("/decisions", s.handleDecisions)
api.GET("/decisions/latest", s.handleLatestDecisions)
api.GET("/statistics", s.handleStatistics)
api.GET("/equity-history", s.handleEquityHistory)
}
}
// handleHealth 健康检查
func (s *Server) handleHealth(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"time": c.Request.Context().Value("time"),
})
}
// getTraderFromQuery 从query参数获取trader
func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, string, error) {
traderID := c.Query("trader_id")
if traderID == "" {
// 如果没有指定trader_id,返回第一个trader
ids := s.traderManager.GetTraderIDs()
if len(ids) == 0 {
return nil, "", fmt.Errorf("没有可用的trader")
}
traderID = ids[0]
}
return s.traderManager, traderID, nil
}
// handleCompetition 竞赛总览(对比所有trader)
func (s *Server) handleCompetition(c *gin.Context) {
comparison, err := s.traderManager.GetComparisonData()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取对比数据失败: %v", err),
})
return
}
c.JSON(http.StatusOK, comparison)
}
// handleTraderList trader列表
func (s *Server) handleTraderList(c *gin.Context) {
traders := s.traderManager.GetAllTraders()
result := make([]map[string]interface{}, 0, len(traders))
for _, t := range traders {
result = append(result, map[string]interface{}{
"trader_id": t.GetID(),
"trader_name": t.GetName(),
"ai_model": t.GetAIModel(),
})
}
c.JSON(http.StatusOK, result)
}
// handleStatus 系统状态
func (s *Server) handleStatus(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
status := trader.GetStatus()
c.JSON(http.StatusOK, status)
}
// handleAccount 账户信息
func (s *Server) handleAccount(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
log.Printf("📊 收到账户信息请求 [%s]", trader.GetName())
account, err := trader.GetAccountInfo()
if err != nil {
log.Printf("❌ 获取账户信息失败 [%s]: %v", trader.GetName(), err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取账户信息失败: %v", err),
})
return
}
log.Printf("✓ 返回账户信息 [%s]: 净值=%.2f, 可用=%.2f, 盈亏=%.2f (%.2f%%)",
trader.GetName(),
account["total_equity"],
account["available_balance"],
account["total_pnl"],
account["total_pnl_pct"])
c.JSON(http.StatusOK, account)
}
// handlePositions 持仓列表
func (s *Server) handlePositions(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
positions, err := trader.GetPositions()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取持仓列表失败: %v", err),
})
return
}
c.JSON(http.StatusOK, positions)
}
// handleDecisions 决策日志列表
func (s *Server) handleDecisions(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// 获取所有历史决策记录(无限制)
records, err := trader.GetDecisionLogger().GetLatestRecords(10000)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取决策日志失败: %v", err),
})
return
}
c.JSON(http.StatusOK, records)
}
// handleLatestDecisions 最新决策日志(最近5条,最新的在前)
func (s *Server) handleLatestDecisions(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
records, err := trader.GetDecisionLogger().GetLatestRecords(5)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取决策日志失败: %v", err),
})
return
}
// 反转数组,让最新的在前面(用于列表显示)
// GetLatestRecords返回的是从旧到新(用于图表),这里需要从新到旧
for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 {
records[i], records[j] = records[j], records[i]
}
c.JSON(http.StatusOK, records)
}
// handleStatistics 统计信息
func (s *Server) handleStatistics(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
stats, err := trader.GetDecisionLogger().GetStatistics()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取统计信息失败: %v", err),
})
return
}
c.JSON(http.StatusOK, stats)
}
// handleEquityHistory 收益率历史数据
func (s *Server) handleEquityHistory(c *gin.Context) {
_, traderID, err := s.getTraderFromQuery(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
trader, err := s.traderManager.GetTrader(traderID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
// 获取尽可能多的历史数据(几天的数据)
// 每3分钟一个周期:10000条 = 约20天的数据
records, err := trader.GetDecisionLogger().GetLatestRecords(10000)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("获取历史数据失败: %v", err),
})
return
}
// 构建收益率历史数据点
type EquityPoint struct {
Timestamp string `json:"timestamp"`
TotalEquity float64 `json:"total_equity"` // 账户净值(wallet + unrealized
AvailableBalance float64 `json:"available_balance"` // 可用余额
TotalPnL float64 `json:"total_pnl"` // 总盈亏(相对初始余额)
TotalPnLPct float64 `json:"total_pnl_pct"` // 总盈亏百分比
PositionCount int `json:"position_count"` // 持仓数量
MarginUsedPct float64 `json:"margin_used_pct"` // 保证金使用率
CycleNumber int `json:"cycle_number"`
}
// 从AutoTrader获取初始余额(用于计算盈亏百分比)
initialBalance := 0.0
if status := trader.GetStatus(); status != nil {
if ib, ok := status["initial_balance"].(float64); ok && ib > 0 {
initialBalance = ib
}
}
// 如果无法从status获取,且有历史记录,则从第一条记录获取
if initialBalance == 0 && len(records) > 0 {
// 第一条记录的equity作为初始余额
initialBalance = records[0].AccountState.TotalBalance
}
// 如果还是无法获取,返回错误
if initialBalance == 0 {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "无法获取初始余额",
})
return
}
var history []EquityPoint
for _, record := range records {
// TotalBalance字段实际存储的是TotalEquity
totalEquity := record.AccountState.TotalBalance
// TotalUnrealizedProfit字段实际存储的是TotalPnL(相对初始余额)
totalPnL := record.AccountState.TotalUnrealizedProfit
// 计算盈亏百分比
totalPnLPct := 0.0
if initialBalance > 0 {
totalPnLPct = (totalPnL / initialBalance) * 100
}
history = append(history, EquityPoint{
Timestamp: record.Timestamp.Format("2006-01-02 15:04:05"),
TotalEquity: totalEquity,
AvailableBalance: record.AccountState.AvailableBalance,
TotalPnL: totalPnL,
TotalPnLPct: totalPnLPct,
PositionCount: record.AccountState.PositionCount,
MarginUsedPct: record.AccountState.MarginUsedPct,
CycleNumber: record.CycleNumber,
})
}
c.JSON(http.StatusOK, history)
}
// Start 启动服务器
func (s *Server) Start() error {
addr := fmt.Sprintf(":%d", s.port)
log.Printf("🌐 API服务器启动在 http://localhost%s", addr)
log.Printf("📊 API文档:")
log.Printf(" • GET /api/competition - 竞赛总览(对比所有trader")
log.Printf(" • GET /api/traders - Trader列表")
log.Printf(" • GET /api/status?trader_id=xxx - 指定trader的系统状态")
log.Printf(" • GET /api/account?trader_id=xxx - 指定trader的账户信息")
log.Printf(" • GET /api/positions?trader_id=xxx - 指定trader的持仓列表")
log.Printf(" • GET /api/decisions?trader_id=xxx - 指定trader的决策日志")
log.Printf(" • GET /api/decisions/latest?trader_id=xxx - 指定trader的最新决策")
log.Printf(" • GET /api/statistics?trader_id=xxx - 指定trader的统计信息")
log.Printf(" • GET /api/equity-history?trader_id=xxx - 指定trader的收益率历史数据")
log.Printf(" • GET /health - 健康检查")
log.Println()
return s.router.Run(addr)
}
+30
View File
@@ -0,0 +1,30 @@
{
"traders": [
{
"id": "qwen_trader",
"name": "Qwen AI Trader",
"ai_model": "qwen",
"binance_api_key": "YOUR_BINANCE_API_KEY_1",
"binance_secret_key": "YOUR_BINANCE_SECRET_KEY_1",
"qwen_key": "YOUR_QWEN_API_KEY",
"initial_balance": 1000.0,
"scan_interval_minutes": 3
},
{
"id": "deepseek_trader",
"name": "DeepSeek AI Trader",
"ai_model": "deepseek",
"binance_api_key": "YOUR_BINANCE_API_KEY_2",
"binance_secret_key": "YOUR_BINANCE_SECRET_KEY_2",
"deepseek_key": "YOUR_DEEPSEEK_API_KEY",
"initial_balance": 1000.0,
"scan_interval_minutes": 3
}
],
"coin_pool_api_url": "http://x.x.x.x:x/api/ai500/list?auth=",
"oi_top_api_url": "http://x.x.x.x:x/api/oi/top?auth=",
"api_server_port": 8080,
"max_daily_loss": 5.0,
"max_drawdown": 10.0,
"stop_trading_minutes": 30
}
+103
View File
@@ -0,0 +1,103 @@
package config
import (
"encoding/json"
"fmt"
"os"
"time"
)
// TraderConfig 单个trader的配置
type TraderConfig struct {
ID string `json:"id"`
Name string `json:"name"`
AIModel string `json:"ai_model"` // "qwen" or "deepseek"
BinanceAPIKey string `json:"binance_api_key"`
BinanceSecretKey string `json:"binance_secret_key"`
QwenKey string `json:"qwen_key,omitempty"`
DeepSeekKey string `json:"deepseek_key,omitempty"`
InitialBalance float64 `json:"initial_balance"`
ScanIntervalMinutes int `json:"scan_interval_minutes"`
}
// Config 总配置
type Config struct {
Traders []TraderConfig `json:"traders"`
CoinPoolAPIURL string `json:"coin_pool_api_url"`
OITopAPIURL string `json:"oi_top_api_url"`
APIServerPort int `json:"api_server_port"`
MaxDailyLoss float64 `json:"max_daily_loss"`
MaxDrawdown float64 `json:"max_drawdown"`
StopTradingMinutes int `json:"stop_trading_minutes"`
}
// LoadConfig 从文件加载配置
func LoadConfig(filename string) (*Config, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
// 验证配置
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("配置验证失败: %w", err)
}
return &config, nil
}
// Validate 验证配置有效性
func (c *Config) Validate() error {
if len(c.Traders) == 0 {
return fmt.Errorf("至少需要配置一个trader")
}
traderIDs := make(map[string]bool)
for i, trader := range c.Traders {
if trader.ID == "" {
return fmt.Errorf("trader[%d]: ID不能为空", i)
}
if traderIDs[trader.ID] {
return fmt.Errorf("trader[%d]: ID '%s' 重复", i, trader.ID)
}
traderIDs[trader.ID] = true
if trader.Name == "" {
return fmt.Errorf("trader[%d]: Name不能为空", i)
}
if trader.AIModel != "qwen" && trader.AIModel != "deepseek" {
return fmt.Errorf("trader[%d]: ai_model必须是 'qwen' 或 'deepseek'", i)
}
if trader.BinanceAPIKey == "" || trader.BinanceSecretKey == "" {
return fmt.Errorf("trader[%d]: 币安API密钥不能为空", i)
}
if trader.AIModel == "qwen" && trader.QwenKey == "" {
return fmt.Errorf("trader[%d]: 使用Qwen时必须配置qwen_key", i)
}
if trader.AIModel == "deepseek" && trader.DeepSeekKey == "" {
return fmt.Errorf("trader[%d]: 使用DeepSeek时必须配置deepseek_key", i)
}
if trader.InitialBalance <= 0 {
return fmt.Errorf("trader[%d]: initial_balance必须大于0", i)
}
if trader.ScanIntervalMinutes <= 0 {
trader.ScanIntervalMinutes = 3 // 默认3分钟
}
}
if c.APIServerPort <= 0 {
c.APIServerPort = 8080 // 默认8080端口
}
return nil
}
// GetScanInterval 获取扫描间隔
func (tc *TraderConfig) GetScanInterval() time.Duration {
return time.Duration(tc.ScanIntervalMinutes) * time.Minute
}
+46
View File
@@ -0,0 +1,46 @@
module nofx
go 1.24
require github.com/adshao/go-binance/v2 v2.8.7
require (
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.11.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)
+105
View File
@@ -0,0 +1,105 @@
github.com/adshao/go-binance/v2 v2.8.7 h1:n7jkhwIHMdtd/9ZU2gTqFV15XVSbUCjyFlOUAtTd8uU=
github.com/adshao/go-binance/v2 v2.8.7/go.mod h1:XkkuecSyJKPolaCGf/q4ovJYB3t0P+7RUYTbGr+LMGM=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+460
View File
@@ -0,0 +1,460 @@
package logger
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
)
// DecisionRecord 决策记录
type DecisionRecord struct {
Timestamp time.Time `json:"timestamp"` // 决策时间
CycleNumber int `json:"cycle_number"` // 周期编号
CoTTrace string `json:"cot_trace"` // AI思维链
DecisionJSON string `json:"decision_json"` // 决策JSON
AccountState AccountSnapshot `json:"account_state"` // 账户状态快照
Positions []PositionSnapshot `json:"positions"` // 持仓快照
CandidateCoins []string `json:"candidate_coins"` // 候选币种列表
Decisions []DecisionAction `json:"decisions"` // 执行的决策
ExecutionLog []string `json:"execution_log"` // 执行日志
Success bool `json:"success"` // 是否成功
ErrorMessage string `json:"error_message"` // 错误信息(如果有)
}
// AccountSnapshot 账户状态快照
type AccountSnapshot struct {
TotalBalance float64 `json:"total_balance"`
AvailableBalance float64 `json:"available_balance"`
TotalUnrealizedProfit float64 `json:"total_unrealized_profit"`
PositionCount int `json:"position_count"`
MarginUsedPct float64 `json:"margin_used_pct"`
}
// PositionSnapshot 持仓快照
type PositionSnapshot struct {
Symbol string `json:"symbol"`
Side string `json:"side"`
PositionAmt float64 `json:"position_amt"`
EntryPrice float64 `json:"entry_price"`
MarkPrice float64 `json:"mark_price"`
UnrealizedProfit float64 `json:"unrealized_profit"`
Leverage float64 `json:"leverage"`
LiquidationPrice float64 `json:"liquidation_price"`
}
// DecisionAction 决策动作
type DecisionAction struct {
Action string `json:"action"` // open_long, open_short, close_long, close_short
Symbol string `json:"symbol"` // 币种
Quantity float64 `json:"quantity"` // 数量
Leverage int `json:"leverage"` // 杠杆(开仓时)
Price float64 `json:"price"` // 执行价格
OrderID int64 `json:"order_id"` // 订单ID
Timestamp time.Time `json:"timestamp"` // 执行时间
Success bool `json:"success"` // 是否成功
Error string `json:"error"` // 错误信息
}
// DecisionLogger 决策日志记录器
type DecisionLogger struct {
logDir string
cycleNumber int
}
// NewDecisionLogger 创建决策日志记录器
func NewDecisionLogger(logDir string) *DecisionLogger {
if logDir == "" {
logDir = "decision_logs"
}
// 确保日志目录存在
if err := os.MkdirAll(logDir, 0755); err != nil {
fmt.Printf("⚠ 创建日志目录失败: %v\n", err)
}
return &DecisionLogger{
logDir: logDir,
cycleNumber: 0,
}
}
// LogDecision 记录决策
func (l *DecisionLogger) LogDecision(record *DecisionRecord) error {
l.cycleNumber++
record.CycleNumber = l.cycleNumber
record.Timestamp = time.Now()
// 生成文件名:decision_YYYYMMDD_HHMMSS_cycleN.json
filename := fmt.Sprintf("decision_%s_cycle%d.json",
record.Timestamp.Format("20060102_150405"),
record.CycleNumber)
filepath := filepath.Join(l.logDir, filename)
// 序列化为JSON(带缩进,方便阅读)
data, err := json.MarshalIndent(record, "", " ")
if err != nil {
return fmt.Errorf("序列化决策记录失败: %w", err)
}
// 写入文件
if err := ioutil.WriteFile(filepath, data, 0644); err != nil {
return fmt.Errorf("写入决策记录失败: %w", err)
}
fmt.Printf("📝 决策记录已保存: %s\n", filename)
return nil
}
// GetLatestRecords 获取最近N条记录(按时间正序:从旧到新)
func (l *DecisionLogger) GetLatestRecords(n int) ([]*DecisionRecord, error) {
files, err := ioutil.ReadDir(l.logDir)
if err != nil {
return nil, fmt.Errorf("读取日志目录失败: %w", err)
}
// 先按修改时间倒序收集(最新的在前)
var records []*DecisionRecord
count := 0
for i := len(files) - 1; i >= 0 && count < n; i-- {
file := files[i]
if file.IsDir() {
continue
}
filepath := filepath.Join(l.logDir, file.Name())
data, err := ioutil.ReadFile(filepath)
if err != nil {
continue
}
var record DecisionRecord
if err := json.Unmarshal(data, &record); err != nil {
continue
}
records = append(records, &record)
count++
}
// 反转数组,让时间从旧到新排列(用于图表显示)
for i, j := 0, len(records)-1; i < j; i, j = i+1, j-1 {
records[i], records[j] = records[j], records[i]
}
return records, nil
}
// GetRecordByDate 获取指定日期的所有记录
func (l *DecisionLogger) GetRecordByDate(date time.Time) ([]*DecisionRecord, error) {
dateStr := date.Format("20060102")
pattern := filepath.Join(l.logDir, fmt.Sprintf("decision_%s_*.json", dateStr))
files, err := filepath.Glob(pattern)
if err != nil {
return nil, fmt.Errorf("查找日志文件失败: %w", err)
}
var records []*DecisionRecord
for _, filepath := range files {
data, err := ioutil.ReadFile(filepath)
if err != nil {
continue
}
var record DecisionRecord
if err := json.Unmarshal(data, &record); err != nil {
continue
}
records = append(records, &record)
}
return records, nil
}
// CleanOldRecords 清理N天前的旧记录
func (l *DecisionLogger) CleanOldRecords(days int) error {
cutoffTime := time.Now().AddDate(0, 0, -days)
files, err := ioutil.ReadDir(l.logDir)
if err != nil {
return fmt.Errorf("读取日志目录失败: %w", err)
}
removedCount := 0
for _, file := range files {
if file.IsDir() {
continue
}
if file.ModTime().Before(cutoffTime) {
filepath := filepath.Join(l.logDir, file.Name())
if err := os.Remove(filepath); err != nil {
fmt.Printf("⚠ 删除旧记录失败 %s: %v\n", file.Name(), err)
continue
}
removedCount++
}
}
if removedCount > 0 {
fmt.Printf("🗑️ 已清理 %d 条旧记录(%d天前)\n", removedCount, days)
}
return nil
}
// GetStatistics 获取统计信息
func (l *DecisionLogger) GetStatistics() (*Statistics, error) {
files, err := ioutil.ReadDir(l.logDir)
if err != nil {
return nil, fmt.Errorf("读取日志目录失败: %w", err)
}
stats := &Statistics{}
for _, file := range files {
if file.IsDir() {
continue
}
filepath := filepath.Join(l.logDir, file.Name())
data, err := ioutil.ReadFile(filepath)
if err != nil {
continue
}
var record DecisionRecord
if err := json.Unmarshal(data, &record); err != nil {
continue
}
stats.TotalCycles++
for _, action := range record.Decisions {
if action.Success {
switch action.Action {
case "open_long", "open_short":
stats.TotalOpenPositions++
case "close_long", "close_short":
stats.TotalClosePositions++
}
}
}
if record.Success {
stats.SuccessfulCycles++
} else {
stats.FailedCycles++
}
}
return stats, nil
}
// Statistics 统计信息
type Statistics struct {
TotalCycles int `json:"total_cycles"`
SuccessfulCycles int `json:"successful_cycles"`
FailedCycles int `json:"failed_cycles"`
TotalOpenPositions int `json:"total_open_positions"`
TotalClosePositions int `json:"total_close_positions"`
}
// TradeOutcome 单笔交易结果
type TradeOutcome struct {
Symbol string // 币种
Side string // long/short
OpenPrice float64 // 开仓价
ClosePrice float64 // 平仓价
PnL float64 // 盈亏(USDT
PnLPct float64 // 盈亏百分比
Duration string // 持仓时长
OpenTime time.Time // 开仓时间
CloseTime time.Time // 平仓时间
WasStopLoss bool // 是否止损
}
// PerformanceAnalysis 交易表现分析
type PerformanceAnalysis struct {
TotalTrades int // 总交易数
WinningTrades int // 盈利交易数
LosingTrades int // 亏损交易数
WinRate float64 // 胜率
AvgWin float64 // 平均盈利
AvgLoss float64 // 平均亏损
ProfitFactor float64 // 盈亏比
RecentTrades []TradeOutcome // 最近N笔交易
SymbolStats map[string]*SymbolPerformance // 各币种表现
BestSymbol string // 表现最好的币种
WorstSymbol string // 表现最差的币种
}
// SymbolPerformance 币种表现统计
type SymbolPerformance struct {
Symbol string // 币种
TotalTrades int // 交易次数
WinningTrades int // 盈利次数
LosingTrades int // 亏损次数
WinRate float64 // 胜率
TotalPnL float64 // 总盈亏
AvgPnL float64 // 平均盈亏
}
// AnalyzePerformance 分析最近N个周期的交易表现
func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAnalysis, error) {
records, err := l.GetLatestRecords(lookbackCycles)
if err != nil {
return nil, fmt.Errorf("读取历史记录失败: %w", err)
}
if len(records) == 0 {
return &PerformanceAnalysis{
SymbolStats: make(map[string]*SymbolPerformance),
}, nil
}
analysis := &PerformanceAnalysis{
SymbolStats: make(map[string]*SymbolPerformance),
}
// 追踪持仓状态:symbol -> {side, openPrice, openTime}
openPositions := make(map[string]map[string]interface{})
// 遍历所有记录
for _, record := range records {
for _, action := range record.Decisions {
if !action.Success {
continue
}
symbol := action.Symbol
posKey := symbol // 使用symbol作为key(假设同一时间一个币种只有一个方向的仓位)
switch action.Action {
case "open_long", "open_short":
// 记录开仓
openPositions[posKey] = map[string]interface{}{
"side": action.Action[5:], // "long" or "short"
"openPrice": action.Price,
"openTime": action.Timestamp,
}
case "close_long", "close_short":
// 查找对应的开仓记录
if openPos, exists := openPositions[posKey]; exists {
openPrice := openPos["openPrice"].(float64)
openTime := openPos["openTime"].(time.Time)
side := openPos["side"].(string)
// 计算盈亏
pnl := 0.0
pnlPct := 0.0
if side == "long" {
pnlPct = ((action.Price - openPrice) / openPrice) * 100
} else {
pnlPct = ((openPrice - action.Price) / openPrice) * 100
}
pnl = pnlPct // 简化:用百分比代表盈亏
// 记录交易结果
outcome := TradeOutcome{
Symbol: symbol,
Side: side,
OpenPrice: openPrice,
ClosePrice: action.Price,
PnL: pnl,
PnLPct: pnlPct,
Duration: action.Timestamp.Sub(openTime).String(),
OpenTime: openTime,
CloseTime: action.Timestamp,
}
analysis.RecentTrades = append(analysis.RecentTrades, outcome)
analysis.TotalTrades++
if pnl > 0 {
analysis.WinningTrades++
analysis.AvgWin += pnl
} else {
analysis.LosingTrades++
analysis.AvgLoss += pnl
}
// 更新币种统计
if _, exists := analysis.SymbolStats[symbol]; !exists {
analysis.SymbolStats[symbol] = &SymbolPerformance{
Symbol: symbol,
}
}
stats := analysis.SymbolStats[symbol]
stats.TotalTrades++
stats.TotalPnL += pnl
if pnl > 0 {
stats.WinningTrades++
} else {
stats.LosingTrades++
}
// 移除已平仓记录
delete(openPositions, posKey)
}
}
}
}
// 计算统计指标
if analysis.TotalTrades > 0 {
analysis.WinRate = (float64(analysis.WinningTrades) / float64(analysis.TotalTrades)) * 100
if analysis.WinningTrades > 0 {
analysis.AvgWin /= float64(analysis.WinningTrades)
}
if analysis.LosingTrades > 0 {
analysis.AvgLoss /= float64(analysis.LosingTrades)
}
if analysis.AvgLoss != 0 {
analysis.ProfitFactor = analysis.AvgWin / (-analysis.AvgLoss)
}
}
// 计算各币种胜率和平均盈亏
bestPnL := -999999.0
worstPnL := 999999.0
for symbol, stats := range analysis.SymbolStats {
if stats.TotalTrades > 0 {
stats.WinRate = (float64(stats.WinningTrades) / float64(stats.TotalTrades)) * 100
stats.AvgPnL = stats.TotalPnL / float64(stats.TotalTrades)
if stats.TotalPnL > bestPnL {
bestPnL = stats.TotalPnL
analysis.BestSymbol = symbol
}
if stats.TotalPnL < worstPnL {
worstPnL = stats.TotalPnL
analysis.WorstSymbol = symbol
}
}
}
// 只保留最近的交易(倒序:最新的在前)
if len(analysis.RecentTrades) > 10 {
// 反转数组,让最新的在前
for i, j := 0, len(analysis.RecentTrades)-1; i < j; i, j = i+1, j-1 {
analysis.RecentTrades[i], analysis.RecentTrades[j] = analysis.RecentTrades[j], analysis.RecentTrades[i]
}
analysis.RecentTrades = analysis.RecentTrades[:10]
} else if len(analysis.RecentTrades) > 0 {
// 反转数组
for i, j := 0, len(analysis.RecentTrades)-1; i < j; i, j = i+1, j-1 {
analysis.RecentTrades[i], analysis.RecentTrades[j] = analysis.RecentTrades[j], analysis.RecentTrades[i]
}
}
return analysis, nil
}
+111
View File
@@ -0,0 +1,111 @@
package main
import (
"fmt"
"log"
"nofx/api"
"nofx/config"
"nofx/manager"
"nofx/pool"
"os"
"os/signal"
"strings"
"syscall"
)
func main() {
fmt.Println("╔════════════════════════════════════════════════════════════╗")
fmt.Println("║ 🏆 AI模型交易竞赛系统 - Qwen vs DeepSeek ║")
fmt.Println("╚════════════════════════════════════════════════════════════╝")
fmt.Println()
// 加载配置文件
configFile := "config.json"
if len(os.Args) > 1 {
configFile = os.Args[1]
}
log.Printf("📋 加载配置文件: %s", configFile)
cfg, err := config.LoadConfig(configFile)
if err != nil {
log.Fatalf("❌ 加载配置失败: %v", err)
}
log.Printf("✓ 配置加载成功,共%d个trader参赛", len(cfg.Traders))
fmt.Println()
// 设置币种池API URL
if cfg.CoinPoolAPIURL != "" {
pool.SetCoinPoolAPI(cfg.CoinPoolAPIURL)
log.Printf("✓ 已配置AI500币种池API")
}
if cfg.OITopAPIURL != "" {
pool.SetOITopAPI(cfg.OITopAPIURL)
log.Printf("✓ 已配置OI Top API")
}
// 创建TraderManager
traderManager := manager.NewTraderManager()
// 添加所有trader
for i, traderCfg := range cfg.Traders {
log.Printf("📦 [%d/%d] 初始化 %s (%s模型)...",
i+1, len(cfg.Traders), traderCfg.Name, strings.ToUpper(traderCfg.AIModel))
err := traderManager.AddTrader(
traderCfg,
cfg.CoinPoolAPIURL,
cfg.MaxDailyLoss,
cfg.MaxDrawdown,
cfg.StopTradingMinutes,
)
if err != nil {
log.Fatalf("❌ 初始化trader失败: %v", err)
}
}
fmt.Println()
fmt.Println("🏁 竞赛参赛者:")
for _, traderCfg := range cfg.Traders {
fmt.Printf(" • %s (%s) - 初始资金: %.0f USDT\n",
traderCfg.Name, strings.ToUpper(traderCfg.AIModel), traderCfg.InitialBalance)
}
fmt.Println()
fmt.Println("🤖 AI全权决策模式:")
fmt.Println(" • AI将自主决定每笔交易的杠杆倍数(山寨币1-20倍,BTC/ETH最高50倍)")
fmt.Println(" • AI将自主决定每笔交易的仓位大小")
fmt.Println(" • AI将自主设置止损和止盈价格")
fmt.Println(" • AI将基于市场数据、技术指标、账户状态做出全面分析")
fmt.Println()
fmt.Println("⚠️ 风险提示: AI自动交易有风险,建议小额资金测试!")
fmt.Println()
fmt.Println("按 Ctrl+C 停止运行")
fmt.Println(strings.Repeat("=", 60))
fmt.Println()
// 创建并启动API服务器
apiServer := api.NewServer(traderManager, cfg.APIServerPort)
go func() {
if err := apiServer.Start(); err != nil {
log.Printf("❌ API服务器错误: %v", err)
}
}()
// 设置优雅退出
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// 启动所有trader
traderManager.StartAll()
// 等待退出信号
<-sigChan
fmt.Println()
fmt.Println()
log.Println("📛 收到退出信号,正在停止所有trader...")
traderManager.StopAll()
fmt.Println()
fmt.Println("👋 感谢使用AI交易竞赛系统!")
}
+160
View File
@@ -0,0 +1,160 @@
package manager
import (
"fmt"
"log"
"nofx/config"
"nofx/trader"
"sync"
"time"
)
// TraderManager 管理多个trader实例
type TraderManager struct {
traders map[string]*trader.AutoTrader // key: trader ID
mu sync.RWMutex
}
// NewTraderManager 创建trader管理器
func NewTraderManager() *TraderManager {
return &TraderManager{
traders: make(map[string]*trader.AutoTrader),
}
}
// AddTrader 添加一个trader
func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int) error {
tm.mu.Lock()
defer tm.mu.Unlock()
if _, exists := tm.traders[cfg.ID]; exists {
return fmt.Errorf("trader ID '%s' 已存在", cfg.ID)
}
// 构建AutoTraderConfig
traderConfig := trader.AutoTraderConfig{
ID: cfg.ID,
Name: cfg.Name,
AIModel: cfg.AIModel,
BinanceAPIKey: cfg.BinanceAPIKey,
BinanceSecretKey: cfg.BinanceSecretKey,
CoinPoolAPIURL: coinPoolURL,
UseQwen: cfg.AIModel == "qwen",
DeepSeekKey: cfg.DeepSeekKey,
QwenKey: cfg.QwenKey,
ScanInterval: cfg.GetScanInterval(),
InitialBalance: cfg.InitialBalance,
MaxDailyLoss: maxDailyLoss,
MaxDrawdown: maxDrawdown,
StopTradingTime: time.Duration(stopTradingMinutes) * time.Minute,
}
// 创建trader实例
at, err := trader.NewAutoTrader(traderConfig)
if err != nil {
return fmt.Errorf("创建trader失败: %w", err)
}
tm.traders[cfg.ID] = at
log.Printf("✓ Trader '%s' (%s) 已添加", cfg.Name, cfg.AIModel)
return nil
}
// GetTrader 获取指定ID的trader
func (tm *TraderManager) GetTrader(id string) (*trader.AutoTrader, error) {
tm.mu.RLock()
defer tm.mu.RUnlock()
t, exists := tm.traders[id]
if !exists {
return nil, fmt.Errorf("trader ID '%s' 不存在", id)
}
return t, nil
}
// GetAllTraders 获取所有trader
func (tm *TraderManager) GetAllTraders() map[string]*trader.AutoTrader {
tm.mu.RLock()
defer tm.mu.RUnlock()
result := make(map[string]*trader.AutoTrader)
for id, t := range tm.traders {
result[id] = t
}
return result
}
// GetTraderIDs 获取所有trader ID列表
func (tm *TraderManager) GetTraderIDs() []string {
tm.mu.RLock()
defer tm.mu.RUnlock()
ids := make([]string, 0, len(tm.traders))
for id := range tm.traders {
ids = append(ids, id)
}
return ids
}
// StartAll 启动所有trader
func (tm *TraderManager) StartAll() {
tm.mu.RLock()
defer tm.mu.RUnlock()
log.Println("🚀 启动所有Trader...")
for id, t := range tm.traders {
go func(traderID string, at *trader.AutoTrader) {
log.Printf("▶️ 启动 %s...", at.GetName())
if err := at.Run(); err != nil {
log.Printf("❌ %s 运行错误: %v", at.GetName(), err)
}
}(id, t)
}
}
// StopAll 停止所有trader
func (tm *TraderManager) StopAll() {
tm.mu.RLock()
defer tm.mu.RUnlock()
log.Println("⏹ 停止所有Trader...")
for _, t := range tm.traders {
t.Stop()
}
}
// GetComparisonData 获取对比数据
func (tm *TraderManager) GetComparisonData() (map[string]interface{}, error) {
tm.mu.RLock()
defer tm.mu.RUnlock()
comparison := make(map[string]interface{})
traders := make([]map[string]interface{}, 0, len(tm.traders))
for _, t := range tm.traders {
account, err := t.GetAccountInfo()
if err != nil {
continue
}
status := t.GetStatus()
traders = append(traders, map[string]interface{}{
"trader_id": t.GetID(),
"trader_name": t.GetName(),
"ai_model": t.GetAIModel(),
"total_equity": account["total_equity"],
"total_pnl": account["total_pnl"],
"total_pnl_pct": account["total_pnl_pct"],
"position_count": account["position_count"],
"margin_used_pct": account["margin_used_pct"],
"call_count": status["call_count"],
"is_running": status["is_running"],
})
}
comparison["traders"] = traders
comparison["count"] = len(traders)
return comparison, nil
}
+641
View File
@@ -0,0 +1,641 @@
package market
import (
"encoding/json"
"fmt"
"log"
"nofx/pool"
"strings"
"time"
)
// PositionInfo 持仓信息
type PositionInfo struct {
Symbol string `json:"symbol"`
Side string `json:"side"` // "long" or "short"
EntryPrice float64 `json:"entry_price"`
MarkPrice float64 `json:"mark_price"`
Quantity float64 `json:"quantity"`
Leverage int `json:"leverage"`
UnrealizedPnL float64 `json:"unrealized_pnl"`
UnrealizedPnLPct float64 `json:"unrealized_pnl_pct"`
LiquidationPrice float64 `json:"liquidation_price"`
MarginUsed float64 `json:"margin_used"`
}
// AccountInfo 账户信息
type AccountInfo struct {
TotalEquity float64 `json:"total_equity"` // 账户净值
AvailableBalance float64 `json:"available_balance"` // 可用余额
TotalPnL float64 `json:"total_pnl"` // 总盈亏
TotalPnLPct float64 `json:"total_pnl_pct"` // 总盈亏百分比
MarginUsed float64 `json:"margin_used"` // 已用保证金
MarginUsedPct float64 `json:"margin_used_pct"` // 保证金使用率
PositionCount int `json:"position_count"` // 持仓数量
}
// CandidateCoin 候选币种(来自币种池)
type CandidateCoin struct {
Symbol string `json:"symbol"`
Sources []string `json:"sources"` // 来源: "ai500" 和/或 "oi_top"
}
// OITopData 持仓量增长Top数据(用于AI决策参考)
type OITopData struct {
Rank int // OI Top排名
OIDeltaPercent float64 // 持仓量变化百分比(1小时)
OIDeltaValue float64 // 持仓量变化价值
PriceDeltaPercent float64 // 价格变化百分比
NetLong float64 // 净多仓
NetShort float64 // 净空仓
}
// TradingContext 交易上下文(传递给AI的完整信息)
type TradingContext struct {
CurrentTime string `json:"current_time"`
RuntimeMinutes int `json:"runtime_minutes"`
CallCount int `json:"call_count"`
Account AccountInfo `json:"account"`
Positions []PositionInfo `json:"positions"`
CandidateCoins []CandidateCoin `json:"candidate_coins"`
MarketDataMap map[string]*MarketData `json:"-"` // 不序列化,但内部使用
OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射
Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis
}
// TradingDecision AI的交易决策
type TradingDecision struct {
Symbol string `json:"symbol"`
Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "hold", "wait"
Leverage int `json:"leverage,omitempty"`
PositionSizeUSD float64 `json:"position_size_usd,omitempty"`
StopLoss float64 `json:"stop_loss,omitempty"`
TakeProfit float64 `json:"take_profit,omitempty"`
Reasoning string `json:"reasoning"`
}
// AIFullDecision AI的完整决策(包含思维链)
type AIFullDecision struct {
CoTTrace string `json:"cot_trace"` // 思维链分析
Decisions []TradingDecision `json:"decisions"` // 具体决策列表
Timestamp time.Time `json:"timestamp"`
}
// GetFullTradingDecision 获取AI的完整交易决策(批量分析所有币种和持仓)
func GetFullTradingDecision(ctx *TradingContext) (*AIFullDecision, error) {
// 1. 为所有币种获取市场数据
if err := fetchMarketDataForContext(ctx); err != nil {
return nil, fmt.Errorf("获取市场数据失败: %w", err)
}
// 2. 构建AI提示
prompt := buildFullDecisionPrompt(ctx)
// 3. 调用AI API
aiResponse, err := callDeepSeekAPI(prompt)
if err != nil {
return nil, fmt.Errorf("调用AI API失败: %w", err)
}
// 4. 解析AI响应
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity)
if err != nil {
return nil, fmt.Errorf("解析AI响应失败: %w", err)
}
decision.Timestamp = time.Now()
return decision, nil
}
// fetchMarketDataForContext 为上下文中的所有币种获取市场数据和OI数据
func fetchMarketDataForContext(ctx *TradingContext) error {
ctx.MarketDataMap = make(map[string]*MarketData)
ctx.OITopDataMap = make(map[string]*OITopData)
// 收集所有需要获取数据的币种
symbolSet := make(map[string]bool)
// 1. 优先获取持仓币种的数据(这是必须的)
for _, pos := range ctx.Positions {
symbolSet[pos.Symbol] = true
}
// 2. 候选币种数量根据账户状态动态调整
maxCandidates := calculateMaxCandidates(ctx)
for i, coin := range ctx.CandidateCoins {
if i >= maxCandidates {
break
}
symbolSet[coin.Symbol] = true
}
// 并发获取市场数据
// 持仓币种集合(用于判断是否跳过OI检查)
positionSymbols := make(map[string]bool)
for _, pos := range ctx.Positions {
positionSymbols[pos.Symbol] = true
}
for symbol := range symbolSet {
data, err := GetMarketData(symbol)
if err != nil {
// 单个币种失败不影响整体,只记录错误
continue
}
// ⚠️ 流动性过滤:持仓价值低于15M USD的币种不做(多空都不做)
// 持仓价值 = 持仓量 × 当前价格
// 但现有持仓必须保留(需要决策是否平仓)
isExistingPosition := positionSymbols[symbol]
if !isExistingPosition && data.OpenInterest != nil && data.CurrentPrice > 0 {
// 计算持仓价值(USD)= 持仓量 × 当前价格
oiValue := data.OpenInterest.Latest * data.CurrentPrice
oiValueInMillions := oiValue / 1_000_000 // 转换为百万美元单位
if oiValueInMillions < 15 {
log.Printf("⚠️ %s 持仓价值过低(%.2fM USD < 15M),跳过此币种 [持仓量:%.0f × 价格:%.4f]",
symbol, oiValueInMillions, data.OpenInterest.Latest, data.CurrentPrice)
continue
}
}
ctx.MarketDataMap[symbol] = data
}
// 加载OI Top数据(不影响主流程)
oiPositions, err := pool.GetOITopPositions()
if err == nil {
for _, pos := range oiPositions {
// 标准化符号匹配
symbol := pos.Symbol
ctx.OITopDataMap[symbol] = &OITopData{
Rank: pos.Rank,
OIDeltaPercent: pos.OIDeltaPercent,
OIDeltaValue: pos.OIDeltaValue,
PriceDeltaPercent: pos.PriceDeltaPercent,
NetLong: pos.NetLong,
NetShort: pos.NetShort,
}
}
}
return nil
}
// calculateMaxCandidates 根据账户状态计算需要分析的候选币种数量
func calculateMaxCandidates(ctx *TradingContext) int {
// 直接返回候选池的全部币种数量
// 因为候选池已经在 auto_trader.go 中筛选过了
// 固定分析前20个评分最高的币种(来自AI500)
return len(ctx.CandidateCoins)
}
// buildFullDecisionPrompt 构建完整的AI决策提示
func buildFullDecisionPrompt(ctx *TradingContext) string {
var sb strings.Builder
sb.WriteString("# 🤖 加密货币交易AI竞赛系统\n\n")
sb.WriteString("你是专业的加密货币交易AI,根据市场数据自主决策,做多做空均可。\n\n")
// 添加BTC市场趋势
sb.WriteString("## 🌍 BTC市场趋势\n")
if btcData, hasBTC := ctx.MarketDataMap["BTCUSDT"]; hasBTC {
sb.WriteString(fmt.Sprintf("- 价格: %.2f | 1h: %+.2f%% | 4h: %+.2f%%\n",
btcData.CurrentPrice, btcData.PriceChange1h, btcData.PriceChange4h))
sb.WriteString(fmt.Sprintf("- MACD: %.4f | RSI: %.2f | 资金费率: %.6f\n\n",
btcData.CurrentMACD, btcData.CurrentRSI7, btcData.FundingRate))
} else {
sb.WriteString("BTC数据暂无\n\n")
}
// 系统状态
sb.WriteString("## 📊 系统状态\n")
sb.WriteString(fmt.Sprintf("- **当前时间**: %s\n", ctx.CurrentTime))
sb.WriteString(fmt.Sprintf("- **运行时长**: %d 分钟\n", ctx.RuntimeMinutes))
sb.WriteString(fmt.Sprintf("- **调用次数**: 第 %d 次\n\n", ctx.CallCount))
// 账户信息
sb.WriteString("## 💰 账户信息\n")
sb.WriteString(fmt.Sprintf("- **账户净值**: %.2f USDT\n", ctx.Account.TotalEquity))
sb.WriteString(fmt.Sprintf("- **可用余额**: %.2f USDT (%.1f%%)\n",
ctx.Account.AvailableBalance,
(ctx.Account.AvailableBalance/ctx.Account.TotalEquity)*100))
sb.WriteString(fmt.Sprintf("- **总盈亏**: %.2f USDT (%+.2f%%)\n",
ctx.Account.TotalPnL, ctx.Account.TotalPnLPct))
sb.WriteString(fmt.Sprintf("- **已用保证金**: %.2f USDT (%.1f%%)\n",
ctx.Account.MarginUsed, ctx.Account.MarginUsedPct))
sb.WriteString(fmt.Sprintf("- **持仓数量**: %d\n\n", ctx.Account.PositionCount))
// 当前持仓详情
if len(ctx.Positions) > 0 {
sb.WriteString("## 📈 当前持仓\n")
for i, pos := range ctx.Positions {
sb.WriteString(fmt.Sprintf("\n### 持仓 #%d: %s %s\n", i+1, pos.Symbol, strings.ToUpper(pos.Side)))
sb.WriteString(fmt.Sprintf("- **入场价**: %.4f USDT\n", pos.EntryPrice))
sb.WriteString(fmt.Sprintf("- **当前价**: %.4f USDT\n", pos.MarkPrice))
sb.WriteString(fmt.Sprintf("- **数量**: %.4f\n", pos.Quantity))
sb.WriteString(fmt.Sprintf("- **杠杆**: %dx\n", pos.Leverage))
sb.WriteString(fmt.Sprintf("- **未实现盈亏**: %.2f USDT (%+.2f%%)\n",
pos.UnrealizedPnL, pos.UnrealizedPnLPct))
sb.WriteString(fmt.Sprintf("- **强平价**: %.4f USDT\n", pos.LiquidationPrice))
sb.WriteString(fmt.Sprintf("- **占用保证金**: %.2f USDT\n", pos.MarginUsed))
// 添加市场数据
if marketData, ok := ctx.MarketDataMap[pos.Symbol]; ok {
sb.WriteString(formatMarketDataBrief(marketData))
}
}
sb.WriteString("\n")
} else {
sb.WriteString("## 📈 当前持仓\n")
sb.WriteString("暂无持仓\n\n")
}
// 候选币种池
sb.WriteString("## 🎯 候选币种池\n")
sb.WriteString(fmt.Sprintf("共 %d 个币种(已过滤持仓价值<15M USD的低流动性币种)\n\n", len(ctx.MarketDataMap)))
displayedCount := 0
for _, coin := range ctx.CandidateCoins {
// 只显示已获取市场数据的币种
marketData, hasData := ctx.MarketDataMap[coin.Symbol]
if !hasData {
continue
}
displayedCount++
// 显示币种来源标签 - 使用圆括号避免与JSON混淆
sourceTags := ""
hasAI500 := false
hasOITop := false
for _, source := range coin.Sources {
if source == "ai500" {
hasAI500 = true
} else if source == "oi_top" {
hasOITop = true
}
}
if hasAI500 && hasOITop {
sourceTags = "(AI500+OI_Top双重信号)"
} else if hasAI500 {
sourceTags = "(AI500高评分)"
} else if hasOITop {
sourceTags = "(OI_Top持仓增长)"
}
sb.WriteString(fmt.Sprintf("\n### 币种 #%d: %s %s\n", displayedCount, coin.Symbol, sourceTags))
sb.WriteString(formatMarketDataBrief(marketData))
// 如果有OI Top数据,也显示出来
if oiTopData, hasOI := ctx.OITopDataMap[coin.Symbol]; hasOI {
sb.WriteString(fmt.Sprintf("**市场热度数据** (OI Top排名 #%d):\n", oiTopData.Rank))
sb.WriteString(fmt.Sprintf(" - 持仓量1h变化: %+.2f%% (价值: $%.0f)\n",
oiTopData.OIDeltaPercent, oiTopData.OIDeltaValue))
sb.WriteString(fmt.Sprintf(" - 价格1h变化: %+.2f%% | 净多仓: %.0f | 净空仓: %.0f\n",
oiTopData.PriceDeltaPercent, oiTopData.NetLong, oiTopData.NetShort))
}
}
// 添加历史表现反馈(如果有)
if ctx.Performance != nil {
sb.WriteString(formatPerformanceFeedback(ctx.Performance))
}
// AI决策要求
sb.WriteString("## 🎯 任务\n\n")
sb.WriteString("分析市场数据,自主决策:\n")
sb.WriteString("1. 评估现有持仓 → 持有或平仓\n")
sb.WriteString(fmt.Sprintf("2. 从%d个候选币种中找交易机会\n", len(ctx.MarketDataMap)))
sb.WriteString("3. 开新仓(如果有机会)\n\n")
sb.WriteString("## 📋 规则\n\n")
sb.WriteString(fmt.Sprintf("1. **单币种仓位上限**: 山寨币≤%.0f USDT | BTC/ETH≤%.0f USDT\n", ctx.Account.TotalEquity*1.5, ctx.Account.TotalEquity*10))
sb.WriteString("2. **杠杆**: 山寨币=20倍 | BTC/ETH=50倍\n")
sb.WriteString("3. **保证金上限**: 总使用率≤90%%\n")
sb.WriteString("4. **风险回报比**: ≥1:2\n\n")
sb.WriteString("### 📤 输出格式\n\n")
sb.WriteString("**思维链分析** (纯文本)\n")
sb.WriteString("- 分析持仓 → 找新机会 → 账户检查\n")
sb.WriteString("- **最后必须列出最终决策摘要**(例如:持有XX,平仓XX,开多XX,开空XX\n\n")
sb.WriteString("---\n\n")
sb.WriteString("**决策JSON** (不要用```标记)\n")
sb.WriteString("[\n")
sb.WriteString(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_long\", \"leverage\": 50, \"position_size_usd\": 15000, \"stop_loss\": 92000, \"take_profit\": 98000, \"reasoning\": \"突破做多\"},\n")
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"hold\", \"reasoning\": \"持续观察\"}\n")
sb.WriteString("]\n\n")
sb.WriteString("**action类型**: open_long | open_short | close_long | close_short | hold | wait\n")
sb.WriteString("**开仓必填**: leverage, position_size_usd, stop_loss, take_profit\n")
sb.WriteString("**position_size_usd**: 仓位价值(非保证金),保证金=position_size_usd/leverage\n\n")
sb.WriteString("### 📝 完整示例\n\n")
// 简化示例仓位(使用新的仓位上限)
btcSize := ctx.Account.TotalEquity * 8 // BTC示例:8倍净值(不超过10倍上限)
altSize := ctx.Account.TotalEquity * 1 // 山寨币示例:1倍净值(不超过1.5倍上限)
sb.WriteString("**思维链**:\n")
sb.WriteString("当前持仓:ETHUSDT多头盈利+2.3%,趋势良好继续持有。\n")
sb.WriteString("新机会:BTC突破上涨,MACD金叉,资金费率低,做多信号强。\n")
sb.WriteString(" SOLUSDT回调至支撑位,出现反弹信号,可小仓位做多。\n")
sb.WriteString("账户:可用余额充足,保证金使用率32%,可分散开仓。\n")
sb.WriteString("**最终决策**:持有ETHUSDT,开多BTCUSDT(8倍净值),开多SOLUSDT(1倍净值)。\n\n")
sb.WriteString("---\n\n")
sb.WriteString("[\n")
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"hold\", \"reasoning\": \"盈利良好,趋势延续\"},\n")
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_long\", \"leverage\": 50, \"position_size_usd\": %.0f, \"stop_loss\": 92000, \"take_profit\": 98000, \"reasoning\": \"突破做多\"},\n", btcSize))
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"SOLUSDT\", \"action\": \"open_long\", \"leverage\": 20, \"position_size_usd\": %.0f, \"stop_loss\": 180, \"take_profit\": 210, \"reasoning\": \"支撑位反弹\"}\n", altSize))
sb.WriteString("]\n\n")
sb.WriteString("现在请开始分析并给出你的决策!\n")
return sb.String()
}
// formatPerformanceFeedback 格式化历史表现反馈
func formatPerformanceFeedback(perfInterface interface{}) string {
// 类型断言(避免循环依赖,使用interface{}
type TradeOutcome struct {
Symbol string
Side string
OpenPrice float64
ClosePrice float64
PnL float64
PnLPct float64
Duration string
}
type SymbolPerformance struct {
Symbol string
TotalTrades int
WinningTrades int
LosingTrades int
WinRate float64
TotalPnL float64
AvgPnL float64
}
type PerformanceAnalysis struct {
TotalTrades int
WinningTrades int
LosingTrades int
WinRate float64
AvgWin float64
AvgLoss float64
ProfitFactor float64
RecentTrades []TradeOutcome
SymbolStats map[string]*SymbolPerformance
BestSymbol string
WorstSymbol string
}
// 使用JSON转换进行类型转换(避免直接类型断言)
jsonData, _ := json.Marshal(perfInterface)
var perf PerformanceAnalysis
if err := json.Unmarshal(jsonData, &perf); err != nil {
return ""
}
var sb strings.Builder
sb.WriteString("## 📊 历史表现反馈\n\n")
if perf.TotalTrades == 0 {
sb.WriteString("暂无历史交易数据\n\n")
return sb.String()
}
// 整体统计
sb.WriteString("### 整体表现\n")
sb.WriteString(fmt.Sprintf("- **总交易数**: %d 笔 (盈利: %d | 亏损: %d)\n",
perf.TotalTrades, perf.WinningTrades, perf.LosingTrades))
sb.WriteString(fmt.Sprintf("- **胜率**: %.1f%%\n", perf.WinRate))
sb.WriteString(fmt.Sprintf("- **平均盈利**: +%.2f%% | 平均亏损: %.2f%%\n",
perf.AvgWin, perf.AvgLoss))
if perf.ProfitFactor > 0 {
sb.WriteString(fmt.Sprintf("- **盈亏比**: %.2f:1\n", perf.ProfitFactor))
}
sb.WriteString("\n")
// 最近交易
if len(perf.RecentTrades) > 0 {
sb.WriteString("### 最近交易\n")
displayCount := len(perf.RecentTrades)
if displayCount > 5 {
displayCount = 5
}
for i := 0; i < displayCount; i++ {
trade := perf.RecentTrades[i]
outcome := "✓"
if trade.PnL < 0 {
outcome = "✗"
}
sb.WriteString(fmt.Sprintf("%d. %s %s: %.4f → %.4f = %+.2f%% %s\n",
i+1, trade.Symbol, strings.ToUpper(trade.Side),
trade.OpenPrice, trade.ClosePrice,
trade.PnLPct, outcome))
}
sb.WriteString("\n")
}
// 币种表现(显示前3个最好和最差)
if len(perf.SymbolStats) > 0 {
sb.WriteString("### 币种表现\n")
if perf.BestSymbol != "" {
if stats, exists := perf.SymbolStats[perf.BestSymbol]; exists {
sb.WriteString(fmt.Sprintf("- **最佳**: %s (胜率%.0f%%, 平均%+.2f%%)\n",
stats.Symbol, stats.WinRate, stats.AvgPnL))
}
}
if perf.WorstSymbol != "" {
if stats, exists := perf.SymbolStats[perf.WorstSymbol]; exists {
sb.WriteString(fmt.Sprintf("- **最差**: %s (胜率%.0f%%, 平均%+.2f%%)\n",
stats.Symbol, stats.WinRate, stats.AvgPnL))
}
}
sb.WriteString("\n")
}
return sb.String()
}
// formatMarketDataBrief 格式化市场数据(简洁版)
func formatMarketDataBrief(data *MarketData) string {
var sb strings.Builder
sb.WriteString("**市场数据** (3分钟线):\n")
sb.WriteString(fmt.Sprintf(" - 价格: %.4f | 1h变化: %+.2f%% | 4h变化: %+.2f%% (%s)\n",
data.CurrentPrice, data.PriceChange1h, data.PriceChange4h, priceTrend(data.PriceChange1h, data.PriceChange4h)))
sb.WriteString(fmt.Sprintf(" - EMA20: %.4f (%s) | MACD: %.4f (%s) | RSI(7): %.2f (%s)\n",
data.CurrentEMA20, pricePosition(data.CurrentPrice, data.CurrentEMA20),
data.CurrentMACD, macdTrend(data.CurrentMACD), data.CurrentRSI7, rsiStatus(data.CurrentRSI7)))
if data.OpenInterest != nil {
oiChange := ((data.OpenInterest.Latest - data.OpenInterest.Average) / data.OpenInterest.Average) * 100
fundingSignal := fundingRateSignal(data.FundingRate)
sb.WriteString(fmt.Sprintf(" - 持仓量: %+.2f%% | 资金费率: %.6f (%s)\n", oiChange, data.FundingRate, fundingSignal))
}
return sb.String()
}
// parseFullDecisionResponse 解析AI的完整决策响应
func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*AIFullDecision, error) {
// 1. 提取 cot_trace(思维链)
cotTrace := extractCoTTrace(aiResponse)
// 2. 提取 JSON 决策列表
decisions, err := extractDecisions(aiResponse)
if err != nil {
// 即使JSON解析失败,也返回思维链
return &AIFullDecision{
CoTTrace: cotTrace,
Decisions: []TradingDecision{},
}, fmt.Errorf("提取决策失败: %w\n\n=== AI思维链分析 ===\n%s", err, cotTrace)
}
// 3. 验证决策(包含仓位价值上限检查)
if err := validateDecisions(decisions, accountEquity); err != nil {
// 验证失败时,也返回思维链和决策,但标记为错误
return &AIFullDecision{
CoTTrace: cotTrace,
Decisions: decisions,
}, fmt.Errorf("决策验证失败: %w\n\n=== AI思维链分析 ===\n%s", err, cotTrace)
}
return &AIFullDecision{
CoTTrace: cotTrace,
Decisions: decisions,
}, nil
}
// extractCoTTrace 提取思维链分析
func extractCoTTrace(response string) string {
// 查找JSON数组的开始位置
jsonStart := strings.Index(response, "[")
if jsonStart > 0 {
// 思维链是JSON数组之前的内容
return strings.TrimSpace(response[:jsonStart])
}
// 如果找不到JSON,整个响应都是思维链
return strings.TrimSpace(response)
}
// extractDecisions 提取JSON决策列表
func extractDecisions(response string) ([]TradingDecision, error) {
// 直接查找JSON数组 - 找第一个完整的JSON数组
arrayStart := strings.Index(response, "[")
if arrayStart == -1 {
return nil, fmt.Errorf("无法找到JSON数组起始")
}
// 从 [ 开始,匹配括号找到对应的 ]
arrayEnd := findMatchingBracket(response, arrayStart)
if arrayEnd == -1 {
return nil, fmt.Errorf("无法找到JSON数组结束")
}
jsonContent := strings.TrimSpace(response[arrayStart : arrayEnd+1])
// 解析JSON
var decisions []TradingDecision
if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil {
return nil, fmt.Errorf("JSON解析失败: %w\nJSON内容: %s", err, jsonContent)
}
return decisions, nil
}
// validateDecisions 验证所有决策(需要账户信息)
func validateDecisions(decisions []TradingDecision, accountEquity float64) error {
for i, decision := range decisions {
if err := validateDecision(&decision, accountEquity); err != nil {
return fmt.Errorf("决策 #%d 验证失败: %w", i+1, err)
}
}
return nil
}
// findMatchingBracket 查找匹配的右括号
func findMatchingBracket(s string, start int) int {
if start >= len(s) || s[start] != '[' {
return -1
}
depth := 0
for i := start; i < len(s); i++ {
switch s[i] {
case '[':
depth++
case ']':
depth--
if depth == 0 {
return i
}
}
}
return -1
}
// validateDecision 验证单个决策的有效性
func validateDecision(d *TradingDecision, accountEquity float64) error {
// 验证action
validActions := map[string]bool{
"open_long": true,
"open_short": true,
"close_long": true,
"close_short": true,
"hold": true,
"wait": true,
}
if !validActions[d.Action] {
return fmt.Errorf("无效的action: %s", d.Action)
}
// 开仓操作必须提供完整参数
if d.Action == "open_long" || d.Action == "open_short" {
// 根据币种判断杠杆上限和仓位价值上限
maxLeverage := 20 // 山寨币固定20倍
maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
maxLeverage = 50 // BTC和ETH固定50倍
maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值
}
if d.Leverage <= 0 || d.Leverage > maxLeverage {
return fmt.Errorf("杠杆必须在1-%d之间(%s: %d", maxLeverage, d.Symbol, d.Leverage)
}
if d.PositionSizeUSD <= 0 {
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
}
// 验证仓位价值上限(加1%容差以避免浮点数精度问题)
tolerance := maxPositionValue * 0.01 // 1%容差
if d.PositionSizeUSD > maxPositionValue+tolerance {
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
return fmt.Errorf("BTC/ETH单币种仓位价值不能超过%.0f USDT(10倍账户净值),实际: %.0f", maxPositionValue, d.PositionSizeUSD)
} else {
return fmt.Errorf("山寨币单币种仓位价值不能超过%.0f USDT(1.5倍账户净值),实际: %.0f", maxPositionValue, d.PositionSizeUSD)
}
}
if d.StopLoss <= 0 || d.TakeProfit <= 0 {
return fmt.Errorf("止损和止盈必须大于0")
}
// 验证止损止盈的合理性
if d.Action == "open_long" {
if d.StopLoss >= d.TakeProfit {
return fmt.Errorf("做多时止损价必须小于止盈价")
}
} else {
if d.StopLoss <= d.TakeProfit {
return fmt.Errorf("做空时止损价必须大于止盈价")
}
}
}
return nil
}
+579
View File
@@ -0,0 +1,579 @@
package market
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// SignalType 交易信号类型
type SignalType string
const (
SignalOpenLong SignalType = "OPEN_LONG" // 开多仓
SignalOpenShort SignalType = "OPEN_SHORT" // 开空仓
SignalCloseLong SignalType = "CLOSE_LONG" // 平多仓
SignalCloseShort SignalType = "CLOSE_SHORT" // 平空仓
SignalHold SignalType = "HOLD" // 持仓不动
SignalWait SignalType = "WAIT" // 观望
)
// TradingSignal AI返回的交易信号
type TradingSignal struct {
Symbol string `json:"symbol"` // 币种符号
Signal SignalType `json:"signal"` // 信号类型
Confidence float64 `json:"confidence"` // 信心度 (0-100)
Reasoning string `json:"reasoning"` // 分析理由
EntryPrice float64 `json:"entry_price"` // 建议入场价格
StopLoss float64 `json:"stop_loss"` // 建议止损价格
TakeProfit float64 `json:"take_profit"` // 建议止盈价格
Timestamp time.Time `json:"timestamp"` // 信号生成时间
}
// AIProvider AI提供商类型
type AIProvider string
const (
ProviderDeepSeek AIProvider = "deepseek"
ProviderQwen AIProvider = "qwen"
)
// AIConfig AI API配置
type AIConfig struct {
Provider AIProvider
APIKey string
SecretKey string // 阿里云需要
BaseURL string
Model string
Timeout time.Duration
}
// 默认配置
var defaultConfig = AIConfig{
Provider: ProviderDeepSeek,
BaseURL: "https://api.deepseek.com/v1",
Model: "deepseek-chat",
Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据
}
// SetDeepSeekAPIKey 设置DeepSeek API密钥
func SetDeepSeekAPIKey(apiKey string) {
defaultConfig.Provider = ProviderDeepSeek
defaultConfig.APIKey = apiKey
defaultConfig.BaseURL = "https://api.deepseek.com/v1"
defaultConfig.Model = "deepseek-chat"
}
// SetQwenAPIKey 设置阿里云Qwen API密钥
func SetQwenAPIKey(apiKey, secretKey string) {
defaultConfig.Provider = ProviderQwen
defaultConfig.APIKey = apiKey
defaultConfig.SecretKey = secretKey
defaultConfig.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max
}
// SetAIConfig 设置完整的AI配置(高级用户)
func SetAIConfig(config AIConfig) {
if config.Timeout == 0 {
config.Timeout = 30 * time.Second
}
defaultConfig = config
}
// DeepSeekConfig 兼容旧代码
type DeepSeekConfig = AIConfig
// SetDeepSeekConfig 兼容旧代码
func SetDeepSeekConfig(config DeepSeekConfig) {
SetAIConfig(config)
}
// GetAITradingSignal 获取AI交易信号
func GetAITradingSignal(symbol string) (*TradingSignal, error) {
// 1. 获取市场数据
marketData, err := GetMarketData(symbol)
if err != nil {
return nil, fmt.Errorf("获取市场数据失败: %w", err)
}
// 2. 格式化为AI提示
prompt := formatMarketDataForAI(marketData)
// 3. 调用DeepSeek API
aiResponse, err := callDeepSeekAPI(prompt)
if err != nil {
return nil, fmt.Errorf("调用DeepSeek API失败: %w", err)
}
// 4. 解析AI响应
signal, err := parseAIResponse(aiResponse, marketData)
if err != nil {
return nil, fmt.Errorf("解析AI响应失败: %w", err)
}
signal.Symbol = marketData.Symbol
signal.Timestamp = time.Now()
return signal, nil
}
// formatMarketDataForAI 将市场数据格式化为AI提示
func formatMarketDataForAI(data *MarketData) string {
var sb strings.Builder
sb.WriteString("你是一位专业的加密货币交易员,请根据以下市场数据分析并给出交易建议。\n\n")
sb.WriteString(fmt.Sprintf("【币种】%s\n\n", data.Symbol))
// 当前指标
sb.WriteString("【当前实时指标】(基于3分钟K线)\n")
sb.WriteString(fmt.Sprintf("• 当前价格: %.4f USDT\n", data.CurrentPrice))
sb.WriteString(fmt.Sprintf("• EMA20: %.4f (价格%s均线)\n", data.CurrentEMA20,
pricePosition(data.CurrentPrice, data.CurrentEMA20)))
sb.WriteString(fmt.Sprintf("• MACD: %.4f (%s)\n", data.CurrentMACD, macdTrend(data.CurrentMACD)))
sb.WriteString(fmt.Sprintf("• RSI(7期): %.2f (%s)\n\n", data.CurrentRSI7, rsiStatus(data.CurrentRSI7)))
// 持仓量和资金费率
if data.OpenInterest != nil {
oiChange := ((data.OpenInterest.Latest - data.OpenInterest.Average) / data.OpenInterest.Average) * 100
sb.WriteString("【持仓量与资金费率】\n")
sb.WriteString(fmt.Sprintf("• 当前持仓量: %.2f (较平均%+.2f%%)\n",
data.OpenInterest.Latest, oiChange))
sb.WriteString(fmt.Sprintf("• 资金费率: %.6f (%s)\n\n",
data.FundingRate, fundingRateStatus(data.FundingRate)))
}
// 日内趋势
if data.IntradaySeries != nil && len(data.IntradaySeries.MACDValues) > 0 {
sb.WriteString("【日内趋势】(3分钟K线最近10个点)\n")
sb.WriteString(fmt.Sprintf("• 价格序列: %s\n", formatFloatArray(data.IntradaySeries.MidPrices)))
sb.WriteString(fmt.Sprintf("• MACD序列: %s (%s)\n",
formatFloatArray(data.IntradaySeries.MACDValues),
seriesTrend(data.IntradaySeries.MACDValues)))
sb.WriteString(fmt.Sprintf("• RSI(7期)序列: %s (%s)\n\n",
formatFloatArray(data.IntradaySeries.RSI7Values),
rsiSeriesTrend(data.IntradaySeries.RSI7Values)))
}
// 长期背景
if data.LongerTermContext != nil {
sb.WriteString("【长期背景】(4小时K线)\n")
sb.WriteString(fmt.Sprintf("• EMA20: %.2f vs EMA50: %.2f (%s)\n",
data.LongerTermContext.EMA20, data.LongerTermContext.EMA50,
emaCross(data.LongerTermContext.EMA20, data.LongerTermContext.EMA50)))
sb.WriteString(fmt.Sprintf("• ATR(3期): %.2f vs ATR(14期): %.2f (波动率%s)\n",
data.LongerTermContext.ATR3, data.LongerTermContext.ATR14,
atrStatus(data.LongerTermContext.ATR3, data.LongerTermContext.ATR14)))
sb.WriteString(fmt.Sprintf("• 当前成交量: %.2f vs 平均成交量: %.2f (%s)\n",
data.LongerTermContext.CurrentVolume, data.LongerTermContext.AverageVolume,
volumeStatus(data.LongerTermContext.CurrentVolume, data.LongerTermContext.AverageVolume)))
if len(data.LongerTermContext.RSI14Values) > 0 {
sb.WriteString(fmt.Sprintf("• 4小时RSI(14期): %.2f (%s)\n\n",
data.LongerTermContext.RSI14Values[len(data.LongerTermContext.RSI14Values)-1],
rsiStatus(data.LongerTermContext.RSI14Values[len(data.LongerTermContext.RSI14Values)-1])))
}
}
// AI指令
sb.WriteString("【交易建议要求】\n")
sb.WriteString("你是一位**激进型交易员**,善于捕捉市场机会。请基于以上数据,给出一个**明确的交易信号**。\n\n")
sb.WriteString("**重要原则:**\n")
sb.WriteString("1. 优先给出 OPEN_LONG 或 OPEN_SHORT 信号,而不是观望\n")
sb.WriteString("2. 即使信号不完美,也要找出最可能的方向\n")
sb.WriteString("3. RSI超买可能是强势延续,RSI超卖可能是抄底机会\n")
sb.WriteString("4. MACD负值转正 = 买入信号,正值转负 = 卖出信号\n")
sb.WriteString("5. 价格突破EMA20 = 趋势确认\n")
sb.WriteString("6. 持仓量增加 + 价格上涨 = 多头强势\n")
sb.WriteString("7. 只有在多空完全平衡、无法判断时才给 WAIT\n\n")
sb.WriteString("请严格按照以下JSON格式返回:\n\n")
sb.WriteString("```json\n")
sb.WriteString("{\n")
sb.WriteString(" \"signal\": \"OPEN_LONG | OPEN_SHORT | CLOSE_LONG | CLOSE_SHORT | HOLD | WAIT\",\n")
sb.WriteString(" \"confidence\": 85.5,\n")
sb.WriteString(" \"reasoning\": \"详细分析理由(200字以内)\",\n")
sb.WriteString(" \"entry_price\": 1.234,\n")
sb.WriteString(" \"stop_loss\": 1.100,\n")
sb.WriteString(" \"take_profit\": 1.450\n")
sb.WriteString("}\n")
sb.WriteString("```\n\n")
sb.WriteString("注意:\n")
sb.WriteString("1. signal必须是以下之一: OPEN_LONG(开多), OPEN_SHORT(开空), CLOSE_LONG(平多), CLOSE_SHORT(平空), HOLD(持有), WAIT(观望)\n")
sb.WriteString("2. confidence是信心度(0-100),即使是中等信号也应该给出\n")
sb.WriteString("3. reasoning要简洁有力,说明最关键的交易依据\n")
sb.WriteString("4. entry_price是建议入场价格(可以略高于或低于当前价)\n")
sb.WriteString("5. stop_loss和take_profit要合理,建议风险回报比至少1:2\n")
return sb.String()
}
// callDeepSeekAPI 调用AI API(支持DeepSeek和Qwen),带重试机制
func callDeepSeekAPI(prompt string) (string, error) {
if defaultConfig.APIKey == "" {
return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()")
}
// 重试配置
maxRetries := 3
var lastErr error
for attempt := 1; attempt <= maxRetries; attempt++ {
if attempt > 1 {
fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries)
}
result, err := callDeepSeekAPIOnce(prompt)
if err == nil {
if attempt > 1 {
fmt.Printf("✓ AI API重试成功\n")
}
return result, nil
}
lastErr = err
// 如果不是网络错误,不重试
if !isRetryableError(err) {
return "", err
}
// 重试前等待
if attempt < maxRetries {
waitTime := time.Duration(attempt) * 2 * time.Second
fmt.Printf("⏳ 等待%v后重试...\n", waitTime)
time.Sleep(waitTime)
}
}
return "", fmt.Errorf("重试%d次后仍然失败: %w", maxRetries, lastErr)
}
// callDeepSeekAPIOnce 单次调用AI API
func callDeepSeekAPIOnce(prompt string) (string, error) {
// 构建请求体
requestBody := map[string]interface{}{
"model": defaultConfig.Model,
"messages": []map[string]string{
{
"role": "user",
"content": prompt,
},
},
"temperature": 0.7,
"max_tokens": 2000,
}
jsonData, err := json.Marshal(requestBody)
if err != nil {
return "", fmt.Errorf("序列化请求失败: %w", err)
}
// 创建HTTP请求
url := fmt.Sprintf("%s/chat/completions", defaultConfig.BaseURL)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return "", fmt.Errorf("创建请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/json")
// 根据不同的Provider设置认证方式
switch defaultConfig.Provider {
case ProviderDeepSeek:
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey))
case ProviderQwen:
// 阿里云Qwen使用API-Key认证
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey))
// 注意:如果使用的不是兼容模式,可能需要不同的认证方式
default:
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey))
}
// 发送请求
client := &http.Client{Timeout: defaultConfig.Timeout}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("发送请求失败: %w", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API返回错误 (status %d): %s", resp.StatusCode, string(body))
}
// 解析响应
var result struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("解析响应失败: %w", err)
}
if len(result.Choices) == 0 {
return "", fmt.Errorf("API返回空响应")
}
return result.Choices[0].Message.Content, nil
}
// isRetryableError 判断错误是否可重试
func isRetryableError(err error) bool {
errStr := err.Error()
// 网络错误、超时、EOF等可以重试
retryableErrors := []string{
"EOF",
"timeout",
"connection reset",
"connection refused",
"temporary failure",
"no such host",
}
for _, retryable := range retryableErrors {
if strings.Contains(errStr, retryable) {
return true
}
}
return false
}
// parseAIResponse 解析AI响应
func parseAIResponse(aiResponse string, marketData *MarketData) (*TradingSignal, error) {
// 尝试从响应中提取JSON
jsonStart := strings.Index(aiResponse, "```json")
jsonEnd := strings.Index(aiResponse, "```\n")
// 如果没找到结束标记,尝试找第二个```
if jsonEnd == -1 || jsonEnd <= jsonStart {
// 从jsonStart之后找第一个```
jsonEnd = strings.Index(aiResponse[jsonStart+7:], "```")
if jsonEnd != -1 {
jsonEnd += jsonStart + 7
}
}
var jsonContent string
if jsonStart != -1 && jsonEnd != -1 && jsonEnd > jsonStart {
jsonContent = aiResponse[jsonStart+7 : jsonEnd]
} else {
// 如果没有markdown代码块,尝试查找第一个完整的JSON对象
jsonStart = strings.Index(aiResponse, "{")
if jsonStart == -1 {
return nil, fmt.Errorf("无法从AI响应中提取JSON: %s", aiResponse)
}
// 找到匹配的右括号
braceCount := 0
jsonEnd = -1
for i := jsonStart; i < len(aiResponse); i++ {
if aiResponse[i] == '{' {
braceCount++
} else if aiResponse[i] == '}' {
braceCount--
if braceCount == 0 {
jsonEnd = i + 1
break
}
}
}
if jsonEnd == -1 {
return nil, fmt.Errorf("无法找到完整的JSON对象")
}
jsonContent = aiResponse[jsonStart:jsonEnd]
}
// 解析JSON
var signal TradingSignal
if err := json.Unmarshal([]byte(jsonContent), &signal); err != nil {
return nil, fmt.Errorf("解析JSON失败: %w, JSON内容: %s", err, jsonContent)
}
// 验证信号类型
validSignals := map[SignalType]bool{
SignalOpenLong: true,
SignalOpenShort: true,
SignalCloseLong: true,
SignalCloseShort: true,
SignalHold: true,
SignalWait: true,
}
if !validSignals[signal.Signal] {
return nil, fmt.Errorf("无效的信号类型: %s", signal.Signal)
}
// 验证信心度范围
if signal.Confidence < 0 || signal.Confidence > 100 {
signal.Confidence = 50 // 默认值
}
return &signal, nil
}
// 辅助函数:价格与均线位置
func pricePosition(price, ema float64) string {
if price > ema {
return "位于上方"
}
return "位于下方"
}
// 辅助函数:MACD趋势
func macdTrend(macd float64) string {
if macd > 0 {
return "多头"
}
return "空头"
}
// 辅助函数:RSI状态
func rsiStatus(rsi float64) string {
if rsi >= 70 {
return "超买"
} else if rsi <= 30 {
return "超卖"
}
return "中性"
}
// 辅助函数:价格趋势(基于1h和4h变化)
func priceTrend(change1h, change4h float64) string {
if change1h > 2 && change4h > 5 {
return "强势上涨"
} else if change1h > 0 && change4h > 0 {
return "温和上涨"
} else if change1h < -2 && change4h < -5 {
return "强势下跌"
} else if change1h < 0 && change4h < 0 {
return "温和下跌"
} else {
return "震荡"
}
}
// 辅助函数:资金费率信号(交易机会解读)
func fundingRateSignal(rate float64) string {
if rate > 0.001 {
return "多头拥挤,考虑做空"
} else if rate > 0.0005 {
return "多头占优"
} else if rate < -0.001 {
return "空头拥挤,考虑做多"
} else if rate < -0.0005 {
return "空头占优"
}
return "中性"
}
// 辅助函数:资金费率状态
func fundingRateStatus(rate float64) string {
if rate > 0.0005 {
return "多头占优,费率偏高"
} else if rate < -0.0005 {
return "空头占优,费率为负"
}
return "费率中性"
}
// 辅助函数:EMA交叉状态
func emaCross(ema20, ema50 float64) string {
if ema20 > ema50 {
return "金叉,多头趋势"
}
return "死叉,空头趋势"
}
// 辅助函数:ATR状态
func atrStatus(atr3, atr14 float64) string {
if atr3 > atr14*1.2 {
return "急剧上升"
} else if atr3 < atr14*0.8 {
return "逐渐下降"
}
return "稳定"
}
// 辅助函数:成交量状态
func volumeStatus(current, average float64) string {
ratio := current / average
if ratio > 1.5 {
return "放量明显"
} else if ratio < 0.5 {
return "缩量明显"
}
return "正常水平"
}
// 辅助函数:序列趋势
func seriesTrend(values []float64) string {
if len(values) < 2 {
return "数据不足"
}
recent := values[len(values)-1]
prev := values[len(values)-2]
if recent > prev*1.1 {
return "强势上升"
} else if recent > prev {
return "小幅上升"
} else if recent < prev*0.9 {
return "快速下降"
} else if recent < prev {
return "小幅下降"
}
return "横盘整理"
}
// 辅助函数:RSI序列趋势
func rsiSeriesTrend(values []float64) string {
if len(values) < 2 {
return "数据不足"
}
recent := values[len(values)-1]
prev := values[len(values)-2]
if recent > 70 && prev > 70 {
return "持续超买"
} else if recent < 30 && prev < 30 {
return "持续超卖"
} else if recent > prev {
return "强度上升"
} else if recent < prev {
return "强度下降"
}
return "稳定"
}
// 辅助函数:格式化浮点数组
func formatFloatArray(values []float64) string {
if len(values) == 0 {
return "[]"
}
var sb strings.Builder
sb.WriteString("[")
for i, v := range values {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(fmt.Sprintf("%.3f", v))
}
sb.WriteString("]")
return sb.String()
}
+552
View File
@@ -0,0 +1,552 @@
package market
import (
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net/http"
"strconv"
"strings"
)
// MarketData 市场数据结构
type MarketData struct {
Symbol string
CurrentPrice float64
PriceChange1h float64 // 1小时价格变化百分比
PriceChange4h float64 // 4小时价格变化百分比
CurrentEMA20 float64
CurrentMACD float64
CurrentRSI7 float64
OpenInterest *OIData
FundingRate float64
IntradaySeries *IntradayData
LongerTermContext *LongerTermData
}
// OIData Open Interest数据
type OIData struct {
Latest float64
Average float64
}
// IntradayData 日内数据(3分钟间隔)
type IntradayData struct {
MidPrices []float64
EMA20Values []float64
MACDValues []float64
RSI7Values []float64
RSI14Values []float64
}
// LongerTermData 长期数据(4小时时间框架)
type LongerTermData struct {
EMA20 float64
EMA50 float64
ATR3 float64
ATR14 float64
CurrentVolume float64
AverageVolume float64
MACDValues []float64
RSI14Values []float64
}
// Kline K线数据
type Kline struct {
OpenTime int64
Open float64
High float64
Low float64
Close float64
Volume float64
CloseTime int64
}
// GetMarketData 获取指定代币的市场数据
func GetMarketData(symbol string) (*MarketData, error) {
// 标准化symbol
symbol = NormalizeSymbol(symbol)
// 获取3分钟K线数据 (最近10个)
klines3m, err := getKlines(symbol, "3m", 40) // 多获取一些用于计算
if err != nil {
return nil, fmt.Errorf("获取3分钟K线失败: %v", err)
}
// 获取4小时K线数据 (最近10个)
klines4h, err := getKlines(symbol, "4h", 60) // 多获取用于计算指标
if err != nil {
return nil, fmt.Errorf("获取4小时K线失败: %v", err)
}
// 计算当前指标 (基于3分钟最新数据)
currentPrice := klines3m[len(klines3m)-1].Close
currentEMA20 := calculateEMA(klines3m, 20)
currentMACD := calculateMACD(klines3m)
currentRSI7 := calculateRSI(klines3m, 7)
// 计算价格变化百分比
// 1小时价格变化 = 20个3分钟K线前的价格
priceChange1h := 0.0
if len(klines3m) >= 21 { // 至少需要21根K线 (当前 + 20根前)
price1hAgo := klines3m[len(klines3m)-21].Close
if price1hAgo > 0 {
priceChange1h = ((currentPrice - price1hAgo) / price1hAgo) * 100
}
}
// 4小时价格变化 = 1个4小时K线前的价格
priceChange4h := 0.0
if len(klines4h) >= 2 {
price4hAgo := klines4h[len(klines4h)-2].Close
if price4hAgo > 0 {
priceChange4h = ((currentPrice - price4hAgo) / price4hAgo) * 100
}
}
// 获取OI数据
oiData, err := getOpenInterestData(symbol)
if err != nil {
// OI失败不影响整体,使用默认值
oiData = &OIData{Latest: 0, Average: 0}
}
// 获取Funding Rate
fundingRate, _ := getFundingRate(symbol)
// 计算日内系列数据
intradayData := calculateIntradaySeries(klines3m)
// 计算长期数据
longerTermData := calculateLongerTermData(klines4h)
return &MarketData{
Symbol: symbol,
CurrentPrice: currentPrice,
PriceChange1h: priceChange1h,
PriceChange4h: priceChange4h,
CurrentEMA20: currentEMA20,
CurrentMACD: currentMACD,
CurrentRSI7: currentRSI7,
OpenInterest: oiData,
FundingRate: fundingRate,
IntradaySeries: intradayData,
LongerTermContext: longerTermData,
}, nil
}
// getKlines 从Binance获取K线数据
func getKlines(symbol, interval string, limit int) ([]Kline, error) {
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=%d",
symbol, interval, limit)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var rawData [][]interface{}
if err := json.Unmarshal(body, &rawData); err != nil {
return nil, err
}
klines := make([]Kline, len(rawData))
for i, item := range rawData {
openTime := int64(item[0].(float64))
open, _ := parseFloat(item[1])
high, _ := parseFloat(item[2])
low, _ := parseFloat(item[3])
close, _ := parseFloat(item[4])
volume, _ := parseFloat(item[5])
closeTime := int64(item[6].(float64))
klines[i] = Kline{
OpenTime: openTime,
Open: open,
High: high,
Low: low,
Close: close,
Volume: volume,
CloseTime: closeTime,
}
}
return klines, nil
}
// calculateEMA 计算EMA
func calculateEMA(klines []Kline, period int) float64 {
if len(klines) < period {
return 0
}
// 计算SMA作为初始EMA
sum := 0.0
for i := 0; i < period; i++ {
sum += klines[i].Close
}
ema := sum / float64(period)
// 计算EMA
multiplier := 2.0 / float64(period+1)
for i := period; i < len(klines); i++ {
ema = (klines[i].Close-ema)*multiplier + ema
}
return ema
}
// calculateMACD 计算MACD
func calculateMACD(klines []Kline) float64 {
if len(klines) < 26 {
return 0
}
// 计算12期和26期EMA
ema12 := calculateEMA(klines, 12)
ema26 := calculateEMA(klines, 26)
// MACD = EMA12 - EMA26
return ema12 - ema26
}
// calculateRSI 计算RSI
func calculateRSI(klines []Kline, period int) float64 {
if len(klines) <= period {
return 0
}
gains := 0.0
losses := 0.0
// 计算初始平均涨跌幅
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)
// 使用Wilder平滑方法计算后续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 计算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))
}
// 计算初始ATR
sum := 0.0
for i := 1; i <= period; i++ {
sum += trs[i]
}
atr := sum / float64(period)
// Wilder平滑
for i := period + 1; i < len(klines); i++ {
atr = (atr*float64(period-1) + trs[i]) / float64(period)
}
return atr
}
// calculateIntradaySeries 计算日内系列数据
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),
}
// 获取最近10个数据点
start := len(klines) - 10
if start < 0 {
start = 0
}
for i := start; i < len(klines); i++ {
data.MidPrices = append(data.MidPrices, klines[i].Close)
// 计算每个点的EMA20
if i >= 19 {
ema20 := calculateEMA(klines[:i+1], 20)
data.EMA20Values = append(data.EMA20Values, ema20)
}
// 计算每个点的MACD
if i >= 25 {
macd := calculateMACD(klines[:i+1])
data.MACDValues = append(data.MACDValues, macd)
}
// 计算每个点的RSI
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)
}
}
return data
}
// calculateLongerTermData 计算长期数据
func calculateLongerTermData(klines []Kline) *LongerTermData {
data := &LongerTermData{
MACDValues: make([]float64, 0, 10),
RSI14Values: make([]float64, 0, 10),
}
// 计算EMA
data.EMA20 = calculateEMA(klines, 20)
data.EMA50 = calculateEMA(klines, 50)
// 计算ATR
data.ATR3 = calculateATR(klines, 3)
data.ATR14 = calculateATR(klines, 14)
// 计算成交量
if len(klines) > 0 {
data.CurrentVolume = klines[len(klines)-1].Volume
// 计算平均成交量
sum := 0.0
for _, k := range klines {
sum += k.Volume
}
data.AverageVolume = sum / float64(len(klines))
}
// 计算MACD和RSI序列
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 获取OI数据
func getOpenInterestData(symbol string) (*OIData, error) {
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/openInterest?symbol=%s", symbol)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result struct {
OpenInterest string `json:"openInterest"`
Symbol string `json:"symbol"`
Time int64 `json:"time"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
oi, _ := strconv.ParseFloat(result.OpenInterest, 64)
return &OIData{
Latest: oi,
Average: oi * 0.999, // 近似平均值
}, nil
}
// getFundingRate 获取资金费率
func getFundingRate(symbol string) (float64, error) {
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/premiumIndex?symbol=%s", symbol)
resp, err := http.Get(url)
if err != nil {
return 0, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return 0, err
}
var result struct {
Symbol string `json:"symbol"`
MarkPrice string `json:"markPrice"`
IndexPrice string `json:"indexPrice"`
LastFundingRate string `json:"lastFundingRate"`
NextFundingTime int64 `json:"nextFundingTime"`
InterestRate string `json:"interestRate"`
Time int64 `json:"time"`
}
if err := json.Unmarshal(body, &result); err != nil {
return 0, err
}
rate, _ := strconv.ParseFloat(result.LastFundingRate, 64)
return rate, nil
}
// FormatMarketData 格式化输出市场数据
func FormatMarketData(data *MarketData) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("current_price = %.2f, current_ema20 = %.3f, current_macd = %.3f, current_rsi (7 period) = %.3f\n\n",
data.CurrentPrice, data.CurrentEMA20, data.CurrentMACD, data.CurrentRSI7))
sb.WriteString(fmt.Sprintf("In addition, here is the latest %s open interest and funding rate for perps:\n\n",
data.Symbol))
if data.OpenInterest != nil {
sb.WriteString(fmt.Sprintf("Open Interest: Latest: %.2f Average: %.2f\n\n",
data.OpenInterest.Latest, data.OpenInterest.Average))
}
sb.WriteString(fmt.Sprintf("Funding Rate: %.2e\n\n", data.FundingRate))
if data.IntradaySeries != nil {
sb.WriteString("Intraday series (3minute intervals, oldest → latest):\n\n")
if len(data.IntradaySeries.MidPrices) > 0 {
sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.IntradaySeries.MidPrices)))
}
if len(data.IntradaySeries.EMA20Values) > 0 {
sb.WriteString(fmt.Sprintf("EMA indicators (20period): %s\n\n", formatFloatSlice(data.IntradaySeries.EMA20Values)))
}
if len(data.IntradaySeries.MACDValues) > 0 {
sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.IntradaySeries.MACDValues)))
}
if len(data.IntradaySeries.RSI7Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (7Period): %s\n\n", formatFloatSlice(data.IntradaySeries.RSI7Values)))
}
if len(data.IntradaySeries.RSI14Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (14Period): %s\n\n", formatFloatSlice(data.IntradaySeries.RSI14Values)))
}
}
if data.LongerTermContext != nil {
sb.WriteString("Longerterm context (4hour timeframe):\n\n")
sb.WriteString(fmt.Sprintf("20Period EMA: %.3f vs. 50Period EMA: %.3f\n\n",
data.LongerTermContext.EMA20, data.LongerTermContext.EMA50))
sb.WriteString(fmt.Sprintf("3Period ATR: %.3f vs. 14Period ATR: %.3f\n\n",
data.LongerTermContext.ATR3, data.LongerTermContext.ATR14))
sb.WriteString(fmt.Sprintf("Current Volume: %.3f vs. Average Volume: %.3f\n\n",
data.LongerTermContext.CurrentVolume, data.LongerTermContext.AverageVolume))
if len(data.LongerTermContext.MACDValues) > 0 {
sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.LongerTermContext.MACDValues)))
}
if len(data.LongerTermContext.RSI14Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (14Period): %s\n\n", formatFloatSlice(data.LongerTermContext.RSI14Values)))
}
}
return sb.String()
}
// formatFloatSlice 格式化float64切片为字符串
func formatFloatSlice(values []float64) string {
strValues := make([]string, len(values))
for i, v := range values {
strValues[i] = fmt.Sprintf("%.3f", v)
}
return "[" + strings.Join(strValues, ", ") + "]"
}
// NormalizeSymbol 标准化symbol,确保是USDT交易对
func NormalizeSymbol(symbol string) string {
symbol = strings.ToUpper(symbol)
if strings.HasSuffix(symbol, "USDT") {
return symbol
}
return symbol + "USDT"
}
// parseFloat 解析float值
func parseFloat(v interface{}) (float64, error) {
switch val := v.(type) {
case string:
return strconv.ParseFloat(val, 64)
case float64:
return val, nil
case int:
return float64(val), nil
case int64:
return float64(val), nil
default:
return 0, fmt.Errorf("unsupported type: %T", v)
}
}
+584
View File
@@ -0,0 +1,584 @@
package pool
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"time"
)
// CoinPoolConfig 币种池配置
type CoinPoolConfig struct {
APIURL string
Timeout time.Duration
CacheDir string
}
var coinPoolConfig = CoinPoolConfig{
APIURL: "http://43.128.34.180:30006/api/ai500/list?auth=admin123sadasd3r323",
Timeout: 30 * time.Second, // 增加到30秒
CacheDir: "coin_pool_cache",
}
// CoinPoolCache 币种池缓存
type CoinPoolCache struct {
Coins []CoinInfo `json:"coins"`
FetchedAt time.Time `json:"fetched_at"`
SourceType string `json:"source_type"` // "api" or "cache"
}
// CoinInfo 币种信息
type CoinInfo struct {
Pair string `json:"pair"` // 交易对符号(例如:BTCUSDT)
Score float64 `json:"score"` // 当前评分
StartTime int64 `json:"start_time"` // 开始时间(Unix时间戳)
StartPrice float64 `json:"start_price"` // 开始价格
LastScore float64 `json:"last_score"` // 最新评分
MaxScore float64 `json:"max_score"` // 最高评分
MaxPrice float64 `json:"max_price"` // 最高价格
IncreasePercent float64 `json:"increase_percent"` // 涨幅百分比
IsAvailable bool `json:"-"` // 是否可交易(内部使用)
}
// CoinPoolAPIResponse API返回的原始数据结构
type CoinPoolAPIResponse struct {
Success bool `json:"success"`
Data struct {
Coins []CoinInfo `json:"coins"`
Count int `json:"count"`
} `json:"data"`
}
// SetCoinPoolAPI 设置币种池API
func SetCoinPoolAPI(apiURL string) {
coinPoolConfig.APIURL = apiURL
}
// SetOITopAPI 设置OI Top API
func SetOITopAPI(apiURL string) {
oiTopConfig.APIURL = apiURL
}
// GetCoinPool 获取币种池列表(带重试和缓存机制)
func GetCoinPool() ([]CoinInfo, error) {
maxRetries := 3
var lastErr error
// 尝试从API获取
for attempt := 1; attempt <= maxRetries; attempt++ {
if attempt > 1 {
log.Printf("⚠️ 第%d次重试获取币种池(共%d次)...", attempt, maxRetries)
time.Sleep(2 * time.Second) // 重试前等待2秒
}
coins, err := fetchCoinPool()
if err == nil {
if attempt > 1 {
log.Printf("✓ 第%d次重试成功", attempt)
}
// 成功获取后保存到缓存
if err := saveCoinPoolCache(coins); err != nil {
log.Printf("⚠️ 保存币种池缓存失败: %v", err)
}
return coins, nil
}
lastErr = err
log.Printf("❌ 第%d次请求失败: %v", attempt, err)
}
// API获取失败,尝试使用缓存
log.Printf("⚠️ API请求全部失败,尝试使用历史缓存数据...")
cachedCoins, err := loadCoinPoolCache()
if err == nil {
log.Printf("✓ 使用历史缓存数据(共%d个币种)", len(cachedCoins))
return cachedCoins, nil
}
log.Printf("❌ 无法加载缓存数据: %v", err)
return nil, fmt.Errorf("获取币种池失败(API重试%d次后,缓存也不可用): %w", maxRetries, lastErr)
}
// fetchCoinPool 实际执行币种池请求
func fetchCoinPool() ([]CoinInfo, error) {
log.Printf("🔄 正在请求AI500币种池...")
client := &http.Client{
Timeout: coinPoolConfig.Timeout,
}
resp, err := client.Get(coinPoolConfig.APIURL)
if err != nil {
return nil, fmt.Errorf("请求币种池API失败: %w", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API返回错误 (status %d): %s", resp.StatusCode, string(body))
}
// 解析API响应
var response CoinPoolAPIResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("JSON解析失败: %w", err)
}
if !response.Success {
return nil, fmt.Errorf("API返回失败状态")
}
if len(response.Data.Coins) == 0 {
return nil, fmt.Errorf("币种列表为空")
}
// 设置IsAvailable标志
coins := response.Data.Coins
for i := range coins {
coins[i].IsAvailable = true
}
log.Printf("✓ 成功获取%d个币种", len(coins))
return coins, nil
}
// saveCoinPoolCache 保存币种池到缓存文件
func saveCoinPoolCache(coins []CoinInfo) error {
// 确保缓存目录存在
if err := os.MkdirAll(coinPoolConfig.CacheDir, 0755); err != nil {
return fmt.Errorf("创建缓存目录失败: %w", err)
}
cache := CoinPoolCache{
Coins: coins,
FetchedAt: time.Now(),
SourceType: "api",
}
data, err := json.MarshalIndent(cache, "", " ")
if err != nil {
return fmt.Errorf("序列化缓存数据失败: %w", err)
}
cachePath := filepath.Join(coinPoolConfig.CacheDir, "latest.json")
if err := ioutil.WriteFile(cachePath, data, 0644); err != nil {
return fmt.Errorf("写入缓存文件失败: %w", err)
}
log.Printf("💾 已保存币种池缓存(%d个币种)", len(coins))
return nil
}
// loadCoinPoolCache 从缓存文件加载币种池
func loadCoinPoolCache() ([]CoinInfo, error) {
cachePath := filepath.Join(coinPoolConfig.CacheDir, "latest.json")
// 检查文件是否存在
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
return nil, fmt.Errorf("缓存文件不存在")
}
data, err := ioutil.ReadFile(cachePath)
if err != nil {
return nil, fmt.Errorf("读取缓存文件失败: %w", err)
}
var cache CoinPoolCache
if err := json.Unmarshal(data, &cache); err != nil {
return nil, fmt.Errorf("解析缓存数据失败: %w", err)
}
// 检查缓存年龄
cacheAge := time.Since(cache.FetchedAt)
if cacheAge > 24*time.Hour {
log.Printf("⚠️ 缓存数据较旧(%.1f小时前),但仍可使用", cacheAge.Hours())
} else {
log.Printf("📂 缓存数据时间: %s%.1f分钟前)",
cache.FetchedAt.Format("2006-01-02 15:04:05"),
cacheAge.Minutes())
}
return cache.Coins, nil
}
// GetAvailableCoins 获取可用的币种列表(过滤不可用的)
func GetAvailableCoins() ([]string, error) {
coins, err := GetCoinPool()
if err != nil {
return nil, err
}
var symbols []string
for _, coin := range coins {
if coin.IsAvailable {
// 确保symbol格式正确(转为大写USDT交易对)
symbol := normalizeSymbol(coin.Pair)
symbols = append(symbols, symbol)
}
}
if len(symbols) == 0 {
return nil, fmt.Errorf("没有可用的币种")
}
return symbols, nil
}
// GetTopRatedCoins 获取评分最高的N个币种(按评分从大到小排序)
func GetTopRatedCoins(limit int) ([]string, error) {
coins, err := GetCoinPool()
if err != nil {
return nil, err
}
// 过滤可用的币种
var availableCoins []CoinInfo
for _, coin := range coins {
if coin.IsAvailable {
availableCoins = append(availableCoins, coin)
}
}
if len(availableCoins) == 0 {
return nil, fmt.Errorf("没有可用的币种")
}
// 按Score降序排序(冒泡排序)
for i := 0; i < len(availableCoins); i++ {
for j := i + 1; j < len(availableCoins); j++ {
if availableCoins[i].Score < availableCoins[j].Score {
availableCoins[i], availableCoins[j] = availableCoins[j], availableCoins[i]
}
}
}
// 取前N个
maxCount := limit
if len(availableCoins) < maxCount {
maxCount = len(availableCoins)
}
var symbols []string
for i := 0; i < maxCount; i++ {
symbol := normalizeSymbol(availableCoins[i].Pair)
symbols = append(symbols, symbol)
}
return symbols, nil
}
// normalizeSymbol 标准化币种符号
func normalizeSymbol(symbol string) string {
// 移除空格
symbol = trimSpaces(symbol)
// 转为大写
symbol = toUpper(symbol)
// 确保以USDT结尾
if !endsWith(symbol, "USDT") {
symbol = symbol + "USDT"
}
return symbol
}
// 辅助函数
func trimSpaces(s string) string {
result := ""
for i := 0; i < len(s); i++ {
if s[i] != ' ' {
result += string(s[i])
}
}
return result
}
func toUpper(s string) string {
result := ""
for i := 0; i < len(s); i++ {
c := s[i]
if c >= 'a' && c <= 'z' {
c = c - 'a' + 'A'
}
result += string(c)
}
return result
}
func endsWith(s, suffix string) bool {
if len(s) < len(suffix) {
return false
}
return s[len(s)-len(suffix):] == suffix
}
// ========== OI Top(持仓量增长Top20)数据 ==========
// OIPosition 持仓量数据
type OIPosition struct {
Symbol string `json:"symbol"`
Rank int `json:"rank"`
CurrentOI float64 `json:"current_oi"` // 当前持仓量
OIDelta float64 `json:"oi_delta"` // 持仓量变化
OIDeltaPercent float64 `json:"oi_delta_percent"` // 持仓量变化百分比
OIDeltaValue float64 `json:"oi_delta_value"` // 持仓量变化价值
PriceDeltaPercent float64 `json:"price_delta_percent"` // 价格变化百分比
NetLong float64 `json:"net_long"` // 净多仓
NetShort float64 `json:"net_short"` // 净空仓
}
// OITopAPIResponse OI Top API返回的数据结构
type OITopAPIResponse struct {
Success bool `json:"success"`
Data struct {
Positions []OIPosition `json:"positions"`
Count int `json:"count"`
Exchange string `json:"exchange"`
TimeRange string `json:"time_range"`
} `json:"data"`
}
// OITopCache OI Top 缓存
type OITopCache struct {
Positions []OIPosition `json:"positions"`
FetchedAt time.Time `json:"fetched_at"`
SourceType string `json:"source_type"`
}
var oiTopConfig = struct {
APIURL string
Timeout time.Duration
CacheDir string
}{
APIURL: "http://43.128.34.180:30006/api/oi/top?auth=admin123sadasd3r323",
Timeout: 30 * time.Second,
CacheDir: "coin_pool_cache",
}
// GetOITopPositions 获取持仓量增长Top20数据(带重试和缓存)
func GetOITopPositions() ([]OIPosition, error) {
maxRetries := 3
var lastErr error
// 尝试从API获取
for attempt := 1; attempt <= maxRetries; attempt++ {
if attempt > 1 {
log.Printf("⚠️ 第%d次重试获取OI Top数据(共%d次)...", attempt, maxRetries)
time.Sleep(2 * time.Second)
}
positions, err := fetchOITop()
if err == nil {
if attempt > 1 {
log.Printf("✓ 第%d次重试成功", attempt)
}
// 成功获取后保存到缓存
if err := saveOITopCache(positions); err != nil {
log.Printf("⚠️ 保存OI Top缓存失败: %v", err)
}
return positions, nil
}
lastErr = err
log.Printf("❌ 第%d次请求OI Top失败: %v", attempt, err)
}
// API获取失败,尝试使用缓存
log.Printf("⚠️ OI Top API请求全部失败,尝试使用历史缓存数据...")
cachedPositions, err := loadOITopCache()
if err == nil {
log.Printf("✓ 使用历史OI Top缓存数据(共%d个币种)", len(cachedPositions))
return cachedPositions, nil
}
log.Printf("❌ 无法加载OI Top缓存数据: %v", err)
return nil, fmt.Errorf("获取OI Top数据失败(API重试%d次后,缓存也不可用): %w", maxRetries, lastErr)
}
// fetchOITop 实际执行OI Top请求
func fetchOITop() ([]OIPosition, error) {
log.Printf("🔄 正在请求OI Top数据...")
client := &http.Client{
Timeout: oiTopConfig.Timeout,
}
resp, err := client.Get(oiTopConfig.APIURL)
if err != nil {
return nil, fmt.Errorf("请求OI Top API失败: %w", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取OI Top响应失败: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("OI Top API返回错误 (status %d): %s", resp.StatusCode, string(body))
}
// 解析API响应
var response OITopAPIResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("OI Top JSON解析失败: %w", err)
}
if !response.Success {
return nil, fmt.Errorf("OI Top API返回失败状态")
}
if len(response.Data.Positions) == 0 {
return nil, fmt.Errorf("OI Top持仓列表为空")
}
log.Printf("✓ 成功获取%d个OI Top币种(时间范围: %s",
len(response.Data.Positions), response.Data.TimeRange)
return response.Data.Positions, nil
}
// saveOITopCache 保存OI Top数据到缓存
func saveOITopCache(positions []OIPosition) error {
if err := os.MkdirAll(oiTopConfig.CacheDir, 0755); err != nil {
return fmt.Errorf("创建缓存目录失败: %w", err)
}
cache := OITopCache{
Positions: positions,
FetchedAt: time.Now(),
SourceType: "api",
}
data, err := json.MarshalIndent(cache, "", " ")
if err != nil {
return fmt.Errorf("序列化OI Top缓存数据失败: %w", err)
}
cachePath := filepath.Join(oiTopConfig.CacheDir, "oi_top_latest.json")
if err := ioutil.WriteFile(cachePath, data, 0644); err != nil {
return fmt.Errorf("写入OI Top缓存文件失败: %w", err)
}
log.Printf("💾 已保存OI Top缓存(%d个币种)", len(positions))
return nil
}
// loadOITopCache 从缓存加载OI Top数据
func loadOITopCache() ([]OIPosition, error) {
cachePath := filepath.Join(oiTopConfig.CacheDir, "oi_top_latest.json")
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
return nil, fmt.Errorf("OI Top缓存文件不存在")
}
data, err := ioutil.ReadFile(cachePath)
if err != nil {
return nil, fmt.Errorf("读取OI Top缓存文件失败: %w", err)
}
var cache OITopCache
if err := json.Unmarshal(data, &cache); err != nil {
return nil, fmt.Errorf("解析OI Top缓存数据失败: %w", err)
}
cacheAge := time.Since(cache.FetchedAt)
if cacheAge > 24*time.Hour {
log.Printf("⚠️ OI Top缓存数据较旧(%.1f小时前),但仍可使用", cacheAge.Hours())
} else {
log.Printf("📂 OI Top缓存数据时间: %s%.1f分钟前)",
cache.FetchedAt.Format("2006-01-02 15:04:05"),
cacheAge.Minutes())
}
return cache.Positions, nil
}
// GetOITopSymbols 获取OI Top的币种符号列表
func GetOITopSymbols() ([]string, error) {
positions, err := 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
}
// MergedCoinPool 合并的币种池(AI500 + OI Top
type MergedCoinPool struct {
AI500Coins []CoinInfo // AI500评分币种
OITopCoins []OIPosition // 持仓量增长Top20
AllSymbols []string // 所有不重复的币种符号
SymbolSources map[string][]string // 每个币种的来源("ai500"/"oi_top"
}
// GetMergedCoinPool 获取合并后的币种池(AI500 + OI Top,去重)
func GetMergedCoinPool(ai500Limit int) (*MergedCoinPool, error) {
// 1. 获取AI500数据
ai500TopSymbols, err := GetTopRatedCoins(ai500Limit)
if err != nil {
log.Printf("⚠️ 获取AI500数据失败: %v", err)
ai500TopSymbols = []string{} // 失败时用空列表
}
// 2. 获取OI Top数据
oiTopSymbols, err := GetOITopSymbols()
if err != nil {
log.Printf("⚠️ 获取OI Top数据失败: %v", err)
oiTopSymbols = []string{} // 失败时用空列表
}
// 3. 合并并去重
symbolSet := make(map[string]bool)
symbolSources := make(map[string][]string)
// 添加AI500币种
for _, symbol := range ai500TopSymbols {
symbolSet[symbol] = true
symbolSources[symbol] = append(symbolSources[symbol], "ai500")
}
// 添加OI Top币种
for _, symbol := range oiTopSymbols {
if !symbolSet[symbol] {
symbolSet[symbol] = true
}
symbolSources[symbol] = append(symbolSources[symbol], "oi_top")
}
// 转换为数组
var allSymbols []string
for symbol := range symbolSet {
allSymbols = append(allSymbols, symbol)
}
// 获取完整数据
ai500Coins, _ := GetCoinPool()
oiTopPositions, _ := GetOITopPositions()
merged := &MergedCoinPool{
AI500Coins: ai500Coins,
OITopCoins: oiTopPositions,
AllSymbols: allSymbols,
SymbolSources: symbolSources,
}
log.Printf("📊 币种池合并完成: AI500=%d, OI_Top=%d, 总计(去重)=%d",
len(ai500TopSymbols), len(oiTopSymbols), len(allSymbols))
return merged, nil
}
+361
View File
@@ -0,0 +1,361 @@
package scanner
import (
"fmt"
"log"
"nofx/market"
"sort"
"sync"
"time"
)
// TradingOpportunity AI识别的交易机会
type TradingOpportunity struct {
Symbol string
Signal market.SignalType
Confidence float64
Reasoning string
EntryPrice float64
StopLoss float64
TakeProfit float64
CurrentPrice float64
Priority int
RiskRewardRatio float64
AnalyzedAt time.Time
}
// ScanConfig 扫描配置
type ScanConfig struct {
MinConfidence float64 // 最小信心度
MaxConcurrent int // 最大并发数
Timeout time.Duration // 超时时间
MinPriority int // 最小优先级
EnableLong bool // 允许做多
EnableShort bool // 允许做空
MinRiskRewardRatio float64 // 最小风险回报比
}
var defaultScanConfig = ScanConfig{
MinConfidence: 65.0,
MaxConcurrent: 10,
Timeout: 60 * time.Second,
MinPriority: 60,
EnableLong: true,
EnableShort: true,
MinRiskRewardRatio: 1.5,
}
// SetScanConfig 设置扫描配置
func SetScanConfig(config ScanConfig) {
defaultScanConfig = config
}
// ScanMarket 扫描市场寻找交易机会
func ScanMarket(symbols []string) ([]*TradingOpportunity, error) {
if len(symbols) == 0 {
return nil, fmt.Errorf("币种列表为空")
}
log.Printf("🔍 开始扫描 %d 个币种...", len(symbols))
startTime := time.Now()
// 结果channel
oppChan := make(chan *TradingOpportunity, len(symbols))
errChan := make(chan error, len(symbols))
// 并发控制
semaphore := make(chan struct{}, defaultScanConfig.MaxConcurrent)
var wg sync.WaitGroup
// 并发扫描
for _, symbol := range symbols {
wg.Add(1)
go func(sym string) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
opp, err := scanSymbol(sym)
if err != nil {
errChan <- fmt.Errorf("%s: %v", sym, err)
return
}
if opp != nil {
oppChan <- opp
}
}(symbol)
}
// 等待完成
go func() {
wg.Wait()
close(oppChan)
close(errChan)
}()
// 收集结果
var opportunities []*TradingOpportunity
var errorCount int
for {
select {
case opp, ok := <-oppChan:
if !ok {
oppChan = nil
} else {
opportunities = append(opportunities, opp)
}
case err, ok := <-errChan:
if !ok {
errChan = nil
} else {
errorCount++
if errorCount <= 3 {
log.Printf("⚠ %v", err)
}
}
}
if oppChan == nil && errChan == nil {
break
}
}
if errorCount > 3 {
log.Printf("⚠ 还有 %d 个错误...", errorCount-3)
}
// 排序
sort.Slice(opportunities, func(i, j int) bool {
return opportunities[i].Priority > opportunities[j].Priority
})
elapsed := time.Since(startTime)
log.Printf("✓ 扫描完成,耗时 %.1fs,找到 %d 个交易机会", elapsed.Seconds(), len(opportunities))
return opportunities, nil
}
// scanSymbol 扫描单个币种
func scanSymbol(symbol string) (*TradingOpportunity, error) {
// 1. 获取市场数据
marketData, err := market.GetMarketData(symbol)
if err != nil {
return nil, err
}
// 2. 获取AI信号
signal, err := market.GetAITradingSignal(symbol)
if err != nil {
return nil, err
}
// 3. 验证信号
if !isValidTradingSignal(signal) {
return nil, nil
}
// 4. 计算指标
priority := calculatePriorityScore(signal, marketData)
rrr := calculateRiskReward(signal)
// 5. 过滤
if priority < defaultScanConfig.MinPriority {
return nil, nil
}
if rrr < defaultScanConfig.MinRiskRewardRatio {
return nil, nil
}
return &TradingOpportunity{
Symbol: symbol,
Signal: signal.Signal,
Confidence: signal.Confidence,
Reasoning: signal.Reasoning,
EntryPrice: signal.EntryPrice,
StopLoss: signal.StopLoss,
TakeProfit: signal.TakeProfit,
CurrentPrice: marketData.CurrentPrice,
Priority: priority,
RiskRewardRatio: rrr,
AnalyzedAt: time.Now(),
}, nil
}
// isValidTradingSignal 验证信号有效性
func isValidTradingSignal(signal *market.TradingSignal) bool {
// 1. 信心度检查
if signal.Confidence < defaultScanConfig.MinConfidence {
return false
}
// 2. 信号类型检查
switch signal.Signal {
case market.SignalOpenLong:
if !defaultScanConfig.EnableLong {
return false
}
// 做多:止损<入场<止盈
if signal.StopLoss >= signal.EntryPrice || signal.TakeProfit <= signal.EntryPrice {
return false
}
case market.SignalOpenShort:
if !defaultScanConfig.EnableShort {
return false
}
// 做空:止盈<入场<止损
if signal.TakeProfit >= signal.EntryPrice || signal.StopLoss <= signal.EntryPrice {
return false
}
default:
// 其他信号类型不用于开仓
return false
}
// 3. 价格合理性
if signal.EntryPrice <= 0 || signal.StopLoss <= 0 || signal.TakeProfit <= 0 {
return false
}
return true
}
// calculateRiskReward 计算风险回报比
func calculateRiskReward(signal *market.TradingSignal) float64 {
var risk, reward float64
if signal.Signal == market.SignalOpenLong {
risk = signal.EntryPrice - signal.StopLoss
reward = signal.TakeProfit - signal.EntryPrice
} else if signal.Signal == market.SignalOpenShort {
risk = signal.StopLoss - signal.EntryPrice
reward = signal.EntryPrice - signal.TakeProfit
}
if risk > 0 {
return reward / risk
}
return 0
}
// calculatePriorityScore 计算优先级评分
func calculatePriorityScore(signal *market.TradingSignal, data *market.MarketData) int {
score := 0
// 1. 信心度 (0-40分)
score += int(signal.Confidence * 0.4)
// 2. 风险回报比 (0-25分)
rrr := calculateRiskReward(signal)
if rrr >= 3.0 {
score += 25
} else if rrr >= 2.5 {
score += 20
} else if rrr >= 2.0 {
score += 15
} else if rrr >= 1.5 {
score += 10
}
// 3. 技术指标确认 (0-25分)
techScore := 0
// RSI
if signal.Signal == market.SignalOpenLong && data.CurrentRSI7 < 35 {
techScore += 7 // 超卖做多
} else if signal.Signal == market.SignalOpenShort && data.CurrentRSI7 > 65 {
techScore += 7 // 超买做空
} else if signal.Signal == market.SignalOpenLong && data.CurrentRSI7 < 45 {
techScore += 3
} else if signal.Signal == market.SignalOpenShort && data.CurrentRSI7 > 55 {
techScore += 3
}
// MACD
if signal.Signal == market.SignalOpenLong && data.CurrentMACD > 0 {
techScore += 6
} else if signal.Signal == market.SignalOpenShort && data.CurrentMACD < 0 {
techScore += 6
}
// EMA趋势
if signal.Signal == market.SignalOpenLong && data.CurrentPrice > data.CurrentEMA20 {
techScore += 6
} else if signal.Signal == market.SignalOpenShort && data.CurrentPrice < data.CurrentEMA20 {
techScore += 6
}
// 资金费率
if data.FundingRate != 0 {
if signal.Signal == market.SignalOpenLong && data.FundingRate < -0.0001 {
techScore += 6
} else if signal.Signal == market.SignalOpenShort && data.FundingRate > 0.0001 {
techScore += 6
}
}
score += techScore
// 4. 成交量 (0-10分)
if data.LongerTermContext != nil && data.LongerTermContext.AverageVolume > 0 {
volumeRatio := data.LongerTermContext.CurrentVolume / data.LongerTermContext.AverageVolume
if volumeRatio > 2.0 {
score += 10
} else if volumeRatio > 1.5 {
score += 7
} else if volumeRatio > 1.2 {
score += 4
}
}
return score
}
// FilterTopN 筛选前N个机会
func FilterTopN(opportunities []*TradingOpportunity, n int) []*TradingOpportunity {
if len(opportunities) <= n {
return opportunities
}
return opportunities[:n]
}
// PrintOpportunity 打印交易机会
func PrintOpportunity(opp *TradingOpportunity, index int) {
fmt.Printf("\n【机会 #%d】%s\n", index+1, opp.Symbol)
fmt.Printf(" 信号: %s\n", GetSignalText(opp.Signal))
fmt.Printf(" 信心度: %.1f%% | 优先级: %d/100\n", opp.Confidence, opp.Priority)
fmt.Printf(" 当前价: %.4f USDT\n", opp.CurrentPrice)
fmt.Printf(" 入场价: %.4f USDT\n", opp.EntryPrice)
fmt.Printf(" 止损价: %.4f USDT (风险: %.2f%%)\n", opp.StopLoss, calculateRiskPercent(opp))
fmt.Printf(" 止盈价: %.4f USDT (收益: %.2f%%)\n", opp.TakeProfit, calculateRewardPercent(opp))
fmt.Printf(" 风险回报比: 1:%.2f\n", opp.RiskRewardRatio)
fmt.Printf(" 分析: %s\n", opp.Reasoning)
}
func GetSignalText(signal market.SignalType) string {
switch signal {
case market.SignalOpenLong:
return "开多 🟢"
case market.SignalOpenShort:
return "开空 🔴"
default:
return string(signal)
}
}
func calculateRiskPercent(opp *TradingOpportunity) float64 {
if opp.Signal == market.SignalOpenLong {
return ((opp.EntryPrice - opp.StopLoss) / opp.EntryPrice) * 100
}
return ((opp.StopLoss - opp.EntryPrice) / opp.EntryPrice) * 100
}
func calculateRewardPercent(opp *TradingOpportunity) float64 {
if opp.Signal == market.SignalOpenLong {
return ((opp.TakeProfit - opp.EntryPrice) / opp.EntryPrice) * 100
}
return ((opp.EntryPrice - opp.TakeProfit) / opp.EntryPrice) * 100
}
+945
View File
@@ -0,0 +1,945 @@
package trader
import (
"encoding/json"
"fmt"
"log"
"nofx/logger"
"nofx/market"
"nofx/pool"
"strings"
"time"
)
// AutoTraderConfig 自动交易配置(简化版 - AI全权决策)
type AutoTraderConfig struct {
// Trader标识
ID string // Trader唯一标识(用于日志目录等)
Name string // Trader显示名称
AIModel string // AI模型: "qwen" 或 "deepseek"
// API配置
BinanceAPIKey string
BinanceSecretKey string
CoinPoolAPIURL string
// AI配置
UseQwen bool
DeepSeekKey string
QwenKey string
// 扫描配置
ScanInterval time.Duration // 扫描间隔(建议3分钟)
// 账户配置
InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置)
// 风险控制(仅作为提示,AI可自主决定)
MaxDailyLoss float64 // 最大日亏损百分比(提示)
MaxDrawdown float64 // 最大回撤百分比(提示)
StopTradingTime time.Duration // 触发风控后暂停时长
}
// AutoTrader 自动交易器
type AutoTrader struct {
id string // Trader唯一标识
name string // Trader显示名称
aiModel string // AI模型名称
config AutoTraderConfig
trader *FuturesTrader
decisionLogger *logger.DecisionLogger // 决策日志记录器
initialBalance float64
dailyPnL float64
lastResetTime time.Time
stopUntil time.Time
isRunning bool
startTime time.Time // 系统启动时间
callCount int // AI调用次数
}
// NewAutoTrader 创建自动交易器
func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
// 设置默认值
if config.ID == "" {
config.ID = "default_trader"
}
if config.Name == "" {
config.Name = "Default Trader"
}
if config.AIModel == "" {
if config.UseQwen {
config.AIModel = "qwen"
} else {
config.AIModel = "deepseek"
}
}
// 初始化AI
if config.UseQwen {
market.SetQwenAPIKey(config.QwenKey, "")
log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name)
} else {
market.SetDeepSeekAPIKey(config.DeepSeekKey)
log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name)
}
// 初始化币种池API
if config.CoinPoolAPIURL != "" {
pool.SetCoinPoolAPI(config.CoinPoolAPIURL)
}
// 初始化币安合约交易器
trader := NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey)
// 验证初始金额配置
if config.InitialBalance <= 0 {
return nil, fmt.Errorf("初始金额必须大于0,请在配置中设置InitialBalance")
}
// 初始化决策日志记录器(使用trader ID创建独立目录)
logDir := fmt.Sprintf("decision_logs/%s", config.ID)
decisionLogger := logger.NewDecisionLogger(logDir)
return &AutoTrader{
id: config.ID,
name: config.Name,
aiModel: config.AIModel,
config: config,
trader: trader,
decisionLogger: decisionLogger,
initialBalance: config.InitialBalance,
lastResetTime: time.Now(),
startTime: time.Now(),
callCount: 0,
isRunning: false,
}, nil
}
// Run 运行自动交易主循环
func (at *AutoTrader) Run() error {
at.isRunning = true
log.Println("🚀 AI驱动自动交易系统启动")
log.Printf("💰 初始余额: %.2f USDT", at.initialBalance)
log.Printf("⚙️ 扫描间隔: %v", at.config.ScanInterval)
log.Println("🤖 AI将全权决定杠杆、仓位大小、止损止盈等参数")
ticker := time.NewTicker(at.config.ScanInterval)
defer ticker.Stop()
// 首次立即执行
if err := at.runCycle(); err != nil {
log.Printf("❌ 执行失败: %v", err)
}
for at.isRunning {
select {
case <-ticker.C:
if err := at.runCycle(); err != nil {
log.Printf("❌ 执行失败: %v", err)
}
}
}
return nil
}
// Stop 停止自动交易
func (at *AutoTrader) Stop() {
at.isRunning = false
log.Println("⏹ 自动交易系统停止")
}
// runCycle 运行一个交易周期(使用AI全权决策)
func (at *AutoTrader) runCycle() error {
at.callCount++
log.Printf("\n" + strings.Repeat("=", 70))
log.Printf("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount)
log.Printf(strings.Repeat("=", 70))
// 创建决策记录
record := &logger.DecisionRecord{
ExecutionLog: []string{},
Success: true,
}
// 1. 检查是否需要停止交易
if time.Now().Before(at.stopUntil) {
remaining := at.stopUntil.Sub(time.Now())
log.Printf("⏸ 风险控制:暂停交易中,剩余 %.0f 分钟", remaining.Minutes())
record.Success = false
record.ErrorMessage = fmt.Sprintf("风险控制暂停中,剩余 %.0f 分钟", remaining.Minutes())
at.decisionLogger.LogDecision(record)
return nil
}
// 2. 重置日盈亏(每天重置)
if time.Since(at.lastResetTime) > 24*time.Hour {
at.dailyPnL = 0
at.lastResetTime = time.Now()
log.Println("📅 日盈亏已重置")
}
// 3. 收集交易上下文
ctx, err := at.buildTradingContext()
if err != nil {
record.Success = false
record.ErrorMessage = fmt.Sprintf("构建交易上下文失败: %v", err)
at.decisionLogger.LogDecision(record)
return fmt.Errorf("构建交易上下文失败: %w", err)
}
// 保存账户状态快照
record.AccountState = logger.AccountSnapshot{
TotalBalance: ctx.Account.TotalEquity,
AvailableBalance: ctx.Account.AvailableBalance,
TotalUnrealizedProfit: ctx.Account.TotalPnL,
PositionCount: ctx.Account.PositionCount,
MarginUsedPct: ctx.Account.MarginUsedPct,
}
// 保存持仓快照
for _, pos := range ctx.Positions {
record.Positions = append(record.Positions, logger.PositionSnapshot{
Symbol: pos.Symbol,
Side: pos.Side,
PositionAmt: pos.Quantity,
EntryPrice: pos.EntryPrice,
MarkPrice: pos.MarkPrice,
UnrealizedProfit: pos.UnrealizedPnL,
Leverage: float64(pos.Leverage),
LiquidationPrice: pos.LiquidationPrice,
})
}
// 保存候选币种列表
for _, coin := range ctx.CandidateCoins {
record.CandidateCoins = append(record.CandidateCoins, coin.Symbol)
}
log.Printf("📊 账户净值: %.2f USDT | 可用: %.2f USDT | 持仓: %d",
ctx.Account.TotalEquity, ctx.Account.AvailableBalance, ctx.Account.PositionCount)
// 4. 调用AI获取完整决策
log.Println("🤖 正在请求AI分析并决策...")
decision, err := market.GetFullTradingDecision(ctx)
// 即使有错误,也保存思维链和决策(用于debug)
if decision != nil {
record.CoTTrace = decision.CoTTrace
if len(decision.Decisions) > 0 {
decisionJSON, _ := json.MarshalIndent(decision.Decisions, "", " ")
record.DecisionJSON = string(decisionJSON)
}
}
if err != nil {
record.Success = false
record.ErrorMessage = fmt.Sprintf("获取AI决策失败: %v", err)
// 打印AI思维链(即使有错误)
if decision != nil && decision.CoTTrace != "" {
log.Printf("\n" + strings.Repeat("-", 70))
log.Println("💭 AI思维链分析(错误情况):")
log.Println(strings.Repeat("-", 70))
log.Println(decision.CoTTrace)
log.Printf(strings.Repeat("-", 70) + "\n")
}
at.decisionLogger.LogDecision(record)
return fmt.Errorf("获取AI决策失败: %w", err)
}
// 5. 打印AI思维链
log.Printf("\n" + strings.Repeat("-", 70))
log.Println("💭 AI思维链分析:")
log.Println(strings.Repeat("-", 70))
log.Println(decision.CoTTrace)
log.Printf(strings.Repeat("-", 70) + "\n")
// 6. 打印AI决策
log.Printf("📋 AI决策列表 (%d 个):\n", len(decision.Decisions))
for i, d := range decision.Decisions {
log.Printf(" [%d] %s: %s - %s", i+1, d.Symbol, d.Action, d.Reasoning)
if d.Action == "open_long" || d.Action == "open_short" {
log.Printf(" 杠杆: %dx | 仓位: %.2f USDT | 止损: %.4f | 止盈: %.4f",
d.Leverage, d.PositionSizeUSD, d.StopLoss, d.TakeProfit)
}
}
log.Println()
// 7. 对决策排序:确保先平仓后开仓(防止仓位叠加超限)
sortedDecisions := sortDecisionsByPriority(decision.Decisions)
log.Println("🔄 执行顺序(已优化): 先平仓→后开仓")
for i, d := range sortedDecisions {
log.Printf(" [%d] %s %s", i+1, d.Symbol, d.Action)
}
log.Println()
// 执行决策并记录结果
for _, d := range sortedDecisions {
actionRecord := logger.DecisionAction{
Action: d.Action,
Symbol: d.Symbol,
Quantity: 0,
Leverage: d.Leverage,
Price: 0,
Timestamp: time.Now(),
Success: false,
}
if err := at.executeDecisionWithRecord(&d, &actionRecord); err != nil {
log.Printf("❌ 执行决策失败 (%s %s): %v", d.Symbol, d.Action, err)
actionRecord.Error = err.Error()
record.ExecutionLog = append(record.ExecutionLog, fmt.Sprintf("❌ %s %s 失败: %v", d.Symbol, d.Action, err))
} else {
actionRecord.Success = true
record.ExecutionLog = append(record.ExecutionLog, fmt.Sprintf("✓ %s %s 成功", d.Symbol, d.Action))
// 成功执行后短暂延迟
time.Sleep(1 * time.Second)
}
record.Decisions = append(record.Decisions, actionRecord)
}
// 8. 保存决策记录
if err := at.decisionLogger.LogDecision(record); err != nil {
log.Printf("⚠ 保存决策记录失败: %v", err)
}
return nil
}
// buildTradingContext 构建交易上下文
func (at *AutoTrader) buildTradingContext() (*market.TradingContext, error) {
// 1. 获取账户信息
balance, err := at.trader.GetBalance()
if err != nil {
return nil, fmt.Errorf("获取账户余额失败: %w", err)
}
// 获取账户字段
totalWalletBalance := 0.0
totalUnrealizedProfit := 0.0
availableBalance := 0.0
if wallet, ok := balance["totalWalletBalance"].(float64); ok {
totalWalletBalance = wallet
}
if unrealized, ok := balance["totalUnrealizedProfit"].(float64); ok {
totalUnrealizedProfit = unrealized
}
if avail, ok := balance["availableBalance"].(float64); ok {
availableBalance = avail
}
// Total Equity = 钱包余额 + 未实现盈亏
totalEquity := totalWalletBalance + totalUnrealizedProfit
// 2. 获取持仓信息
positions, err := at.trader.GetPositions()
if err != nil {
return nil, fmt.Errorf("获取持仓失败: %w", err)
}
var positionInfos []market.PositionInfo
totalMarginUsed := 0.0
for _, pos := range positions {
symbol := pos["symbol"].(string)
side := pos["side"].(string)
entryPrice := pos["entryPrice"].(float64)
markPrice := pos["markPrice"].(float64)
quantity := pos["positionAmt"].(float64)
if quantity < 0 {
quantity = -quantity // 空仓数量为负,转为正数
}
unrealizedPnl := pos["unRealizedProfit"].(float64)
liquidationPrice := pos["liquidationPrice"].(float64)
// 计算盈亏百分比
pnlPct := 0.0
if side == "long" {
pnlPct = ((markPrice - entryPrice) / entryPrice) * 100
} else {
pnlPct = ((entryPrice - markPrice) / entryPrice) * 100
}
// 计算占用保证金(估算)
leverage := 10 // 默认值,实际应该从持仓信息获取
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
marginUsed := (quantity * markPrice) / float64(leverage)
totalMarginUsed += marginUsed
positionInfos = append(positionInfos, market.PositionInfo{
Symbol: symbol,
Side: side,
EntryPrice: entryPrice,
MarkPrice: markPrice,
Quantity: quantity,
Leverage: leverage,
UnrealizedPnL: unrealizedPnl,
UnrealizedPnLPct: pnlPct,
LiquidationPrice: liquidationPrice,
MarginUsed: marginUsed,
})
}
// 3. 获取合并的候选币种池(AI500 + OI Top,去重)
// 无论有没有持仓,都分析相同数量的币种(让AI看到所有好机会)
// AI会根据保证金使用率和现有持仓情况,自己决定是否要换仓
const ai500Limit = 20 // AI500取前20个评分最高的币种
// 获取合并后的币种池(AI500 + OI Top
mergedPool, err := pool.GetMergedCoinPool(ai500Limit)
if err != nil {
return nil, fmt.Errorf("获取合并币种池失败: %w", err)
}
// 构建候选币种列表(包含来源信息)
var candidateCoins []market.CandidateCoin
for _, symbol := range mergedPool.AllSymbols {
sources := mergedPool.SymbolSources[symbol]
candidateCoins = append(candidateCoins, market.CandidateCoin{
Symbol: symbol,
Sources: sources, // "ai500" 和/或 "oi_top"
})
}
log.Printf("📋 合并币种池: AI500前%d + OI_Top20 = 总计%d个候选币种",
ai500Limit, len(candidateCoins))
// 4. 计算总盈亏
totalPnL := totalEquity - at.initialBalance
totalPnLPct := 0.0
if at.initialBalance > 0 {
totalPnLPct = (totalPnL / at.initialBalance) * 100
}
marginUsedPct := 0.0
if totalEquity > 0 {
marginUsedPct = (totalMarginUsed / totalEquity) * 100
}
// 5. 分析历史表现(最近20个周期)
performance, err := at.decisionLogger.AnalyzePerformance(20)
if err != nil {
log.Printf("⚠️ 分析历史表现失败: %v", err)
// 不影响主流程,继续执行
}
// 6. 构建上下文
ctx := &market.TradingContext{
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
CallCount: at.callCount,
Account: market.AccountInfo{
TotalEquity: totalEquity,
AvailableBalance: availableBalance,
TotalPnL: totalPnL,
TotalPnLPct: totalPnLPct,
MarginUsed: totalMarginUsed,
MarginUsedPct: marginUsedPct,
PositionCount: len(positionInfos),
},
Positions: positionInfos,
CandidateCoins: candidateCoins,
Performance: performance, // 添加历史表现分析
}
return ctx, nil
}
// executeDecision 执行AI决策
func (at *AutoTrader) executeDecision(decision *market.TradingDecision) error {
switch decision.Action {
case "open_long":
return at.executeOpenLong(decision)
case "open_short":
return at.executeOpenShort(decision)
case "close_long":
return at.executeCloseLong(decision)
case "close_short":
return at.executeCloseShort(decision)
case "hold", "wait":
// 无需执行,仅记录
return nil
default:
return fmt.Errorf("未知的action: %s", decision.Action)
}
}
// executeDecisionWithRecord 执行AI决策并记录详细信息
func (at *AutoTrader) executeDecisionWithRecord(decision *market.TradingDecision, actionRecord *logger.DecisionAction) error {
switch decision.Action {
case "open_long":
return at.executeOpenLongWithRecord(decision, actionRecord)
case "open_short":
return at.executeOpenShortWithRecord(decision, actionRecord)
case "close_long":
return at.executeCloseLongWithRecord(decision, actionRecord)
case "close_short":
return at.executeCloseShortWithRecord(decision, actionRecord)
case "hold", "wait":
// 无需执行,仅记录
return nil
default:
return fmt.Errorf("未知的action: %s", decision.Action)
}
}
// executeOpenLong 执行开多仓
func (at *AutoTrader) executeOpenLong(decision *market.TradingDecision) error {
log.Printf(" 📈 开多仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
// 计算数量:仓位大小(USD) / 当前价格
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
// 开仓
order, err := at.trader.OpenLong(decision.Symbol, quantity, decision.Leverage)
if err != nil {
return err
}
log.Printf(" ✓ 开仓成功,订单ID: %v, 数量: %.4f", order["orderId"], quantity)
// 设置止损止盈
if err := at.trader.SetStopLoss(decision.Symbol, "LONG", quantity, decision.StopLoss); err != nil {
log.Printf(" ⚠ 设置止损失败: %v", err)
}
if err := at.trader.SetTakeProfit(decision.Symbol, "LONG", quantity, decision.TakeProfit); err != nil {
log.Printf(" ⚠ 设置止盈失败: %v", err)
}
return nil
}
// executeOpenShort 执行开空仓
func (at *AutoTrader) executeOpenShort(decision *market.TradingDecision) error {
log.Printf(" 📉 开空仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
// 计算数量
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
// 开仓
order, err := at.trader.OpenShort(decision.Symbol, quantity, decision.Leverage)
if err != nil {
return err
}
log.Printf(" ✓ 开仓成功,订单ID: %v, 数量: %.4f", order["orderId"], quantity)
// 设置止损止盈
if err := at.trader.SetStopLoss(decision.Symbol, "SHORT", quantity, decision.StopLoss); err != nil {
log.Printf(" ⚠ 设置止损失败: %v", err)
}
if err := at.trader.SetTakeProfit(decision.Symbol, "SHORT", quantity, decision.TakeProfit); err != nil {
log.Printf(" ⚠ 设置止盈失败: %v", err)
}
return nil
}
// executeCloseLong 执行平多仓
func (at *AutoTrader) executeCloseLong(decision *market.TradingDecision) error {
log.Printf(" 🔄 平多仓: %s", decision.Symbol)
_, err := at.trader.CloseLong(decision.Symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
log.Printf(" ✓ 平仓成功")
return nil
}
// executeCloseShort 执行平空仓
func (at *AutoTrader) executeCloseShort(decision *market.TradingDecision) error {
log.Printf(" 🔄 平空仓: %s", decision.Symbol)
_, err := at.trader.CloseShort(decision.Symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
log.Printf(" ✓ 平仓成功")
return nil
}
// executeOpenLongWithRecord 执行开多仓并记录详细信息
func (at *AutoTrader) executeOpenLongWithRecord(decision *market.TradingDecision, actionRecord *logger.DecisionAction) error {
log.Printf(" 📈 开多仓: %s", decision.Symbol)
// ⚠️ 关键:检查是否已有同币种同方向持仓,如果有则拒绝开仓(防止仓位叠加超限)
positions, err := at.trader.GetPositions()
if err == nil {
for _, pos := range positions {
if pos["symbol"] == decision.Symbol && pos["side"] == "long" {
return fmt.Errorf("❌ %s 已有多仓,拒绝开仓以防止仓位叠加超限。如需换仓,请先给出 close_long 决策", decision.Symbol)
}
}
}
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
// 计算数量
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
actionRecord.Quantity = quantity
actionRecord.Price = marketData.CurrentPrice
// 开仓
order, err := at.trader.OpenLong(decision.Symbol, quantity, decision.Leverage)
if err != nil {
return err
}
// 记录订单ID
if orderID, ok := order["orderId"].(int64); ok {
actionRecord.OrderID = orderID
}
log.Printf(" ✓ 开仓成功,订单ID: %v, 数量: %.4f", order["orderId"], quantity)
// 设置止损止盈
if err := at.trader.SetStopLoss(decision.Symbol, "LONG", quantity, decision.StopLoss); err != nil {
log.Printf(" ⚠ 设置止损失败: %v", err)
}
if err := at.trader.SetTakeProfit(decision.Symbol, "LONG", quantity, decision.TakeProfit); err != nil {
log.Printf(" ⚠ 设置止盈失败: %v", err)
}
return nil
}
// executeOpenShortWithRecord 执行开空仓并记录详细信息
func (at *AutoTrader) executeOpenShortWithRecord(decision *market.TradingDecision, actionRecord *logger.DecisionAction) error {
log.Printf(" 📉 开空仓: %s", decision.Symbol)
// ⚠️ 关键:检查是否已有同币种同方向持仓,如果有则拒绝开仓(防止仓位叠加超限)
positions, err := at.trader.GetPositions()
if err == nil {
for _, pos := range positions {
if pos["symbol"] == decision.Symbol && pos["side"] == "short" {
return fmt.Errorf("❌ %s 已有空仓,拒绝开仓以防止仓位叠加超限。如需换仓,请先给出 close_short 决策", decision.Symbol)
}
}
}
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
// 计算数量
quantity := decision.PositionSizeUSD / marketData.CurrentPrice
actionRecord.Quantity = quantity
actionRecord.Price = marketData.CurrentPrice
// 开仓
order, err := at.trader.OpenShort(decision.Symbol, quantity, decision.Leverage)
if err != nil {
return err
}
// 记录订单ID
if orderID, ok := order["orderId"].(int64); ok {
actionRecord.OrderID = orderID
}
log.Printf(" ✓ 开仓成功,订单ID: %v, 数量: %.4f", order["orderId"], quantity)
// 设置止损止盈
if err := at.trader.SetStopLoss(decision.Symbol, "SHORT", quantity, decision.StopLoss); err != nil {
log.Printf(" ⚠ 设置止损失败: %v", err)
}
if err := at.trader.SetTakeProfit(decision.Symbol, "SHORT", quantity, decision.TakeProfit); err != nil {
log.Printf(" ⚠ 设置止盈失败: %v", err)
}
return nil
}
// executeCloseLongWithRecord 执行平多仓并记录详细信息
func (at *AutoTrader) executeCloseLongWithRecord(decision *market.TradingDecision, actionRecord *logger.DecisionAction) error {
log.Printf(" 🔄 平多仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 平仓
order, err := at.trader.CloseLong(decision.Symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
// 记录订单ID
if orderID, ok := order["orderId"].(int64); ok {
actionRecord.OrderID = orderID
}
log.Printf(" ✓ 平仓成功")
return nil
}
// executeCloseShortWithRecord 执行平空仓并记录详细信息
func (at *AutoTrader) executeCloseShortWithRecord(decision *market.TradingDecision, actionRecord *logger.DecisionAction) error {
log.Printf(" 🔄 平空仓: %s", decision.Symbol)
// 获取当前价格
marketData, err := market.GetMarketData(decision.Symbol)
if err != nil {
return err
}
actionRecord.Price = marketData.CurrentPrice
// 平仓
order, err := at.trader.CloseShort(decision.Symbol, 0) // 0 = 全部平仓
if err != nil {
return err
}
// 记录订单ID
if orderID, ok := order["orderId"].(int64); ok {
actionRecord.OrderID = orderID
}
log.Printf(" ✓ 平仓成功")
return nil
}
// GetID 获取trader ID
func (at *AutoTrader) GetID() string {
return at.id
}
// GetName 获取trader名称
func (at *AutoTrader) GetName() string {
return at.name
}
// GetAIModel 获取AI模型
func (at *AutoTrader) GetAIModel() string {
return at.aiModel
}
// GetDecisionLogger 获取决策日志记录器
func (at *AutoTrader) GetDecisionLogger() *logger.DecisionLogger {
return at.decisionLogger
}
// GetStatus 获取系统状态(用于API)
func (at *AutoTrader) GetStatus() map[string]interface{} {
aiProvider := "DeepSeek"
if at.config.UseQwen {
aiProvider = "Qwen"
}
return map[string]interface{}{
"trader_id": at.id,
"trader_name": at.name,
"ai_model": at.aiModel,
"is_running": at.isRunning,
"start_time": at.startTime.Format(time.RFC3339),
"runtime_minutes": int(time.Since(at.startTime).Minutes()),
"call_count": at.callCount,
"initial_balance": at.initialBalance,
"scan_interval": at.config.ScanInterval.String(),
"stop_until": at.stopUntil.Format(time.RFC3339),
"last_reset_time": at.lastResetTime.Format(time.RFC3339),
"ai_provider": aiProvider,
}
}
// GetAccountInfo 获取账户信息(用于API)
func (at *AutoTrader) GetAccountInfo() (map[string]interface{}, error) {
balance, err := at.trader.GetBalance()
if err != nil {
return nil, fmt.Errorf("获取余额失败: %w", err)
}
// 获取账户字段
totalWalletBalance := 0.0
totalUnrealizedProfit := 0.0
availableBalance := 0.0
if wallet, ok := balance["totalWalletBalance"].(float64); ok {
totalWalletBalance = wallet
}
if unrealized, ok := balance["totalUnrealizedProfit"].(float64); ok {
totalUnrealizedProfit = unrealized
}
if avail, ok := balance["availableBalance"].(float64); ok {
availableBalance = avail
}
// Total Equity = 钱包余额 + 未实现盈亏
totalEquity := totalWalletBalance + totalUnrealizedProfit
// 获取持仓计算总保证金
positions, err := at.trader.GetPositions()
if err != nil {
return nil, fmt.Errorf("获取持仓失败: %w", err)
}
totalMarginUsed := 0.0
totalUnrealizedPnL := 0.0
for _, pos := range positions {
markPrice := pos["markPrice"].(float64)
quantity := pos["positionAmt"].(float64)
if quantity < 0 {
quantity = -quantity
}
unrealizedPnl := pos["unRealizedProfit"].(float64)
totalUnrealizedPnL += unrealizedPnl
leverage := 10
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
marginUsed := (quantity * markPrice) / float64(leverage)
totalMarginUsed += marginUsed
}
totalPnL := totalEquity - at.initialBalance
totalPnLPct := 0.0
if at.initialBalance > 0 {
totalPnLPct = (totalPnL / at.initialBalance) * 100
}
marginUsedPct := 0.0
if totalEquity > 0 {
marginUsedPct = (totalMarginUsed / totalEquity) * 100
}
return map[string]interface{}{
// 核心字段
"total_equity": totalEquity, // 账户净值 = wallet + unrealized
"wallet_balance": totalWalletBalance, // 钱包余额(不含未实现盈亏)
"unrealized_profit": totalUnrealizedProfit, // 未实现盈亏(从API
"available_balance": availableBalance, // 可用余额
// 盈亏统计
"total_pnl": totalPnL, // 总盈亏 = equity - initial
"total_pnl_pct": totalPnLPct, // 总盈亏百分比
"total_unrealized_pnl": totalUnrealizedPnL, // 未实现盈亏(从持仓计算)
"initial_balance": at.initialBalance, // 初始余额
"daily_pnl": at.dailyPnL, // 日盈亏
// 持仓信息
"position_count": len(positions), // 持仓数量
"margin_used": totalMarginUsed, // 保证金占用
"margin_used_pct": marginUsedPct, // 保证金使用率
}, nil
}
// GetPositions 获取持仓列表(用于API)
func (at *AutoTrader) GetPositions() ([]map[string]interface{}, error) {
positions, err := at.trader.GetPositions()
if err != nil {
return nil, fmt.Errorf("获取持仓失败: %w", err)
}
var result []map[string]interface{}
for _, pos := range positions {
symbol := pos["symbol"].(string)
side := pos["side"].(string)
entryPrice := pos["entryPrice"].(float64)
markPrice := pos["markPrice"].(float64)
quantity := pos["positionAmt"].(float64)
if quantity < 0 {
quantity = -quantity
}
unrealizedPnl := pos["unRealizedProfit"].(float64)
liquidationPrice := pos["liquidationPrice"].(float64)
leverage := 10
if lev, ok := pos["leverage"].(float64); ok {
leverage = int(lev)
}
pnlPct := 0.0
if side == "long" {
pnlPct = ((markPrice - entryPrice) / entryPrice) * 100
} else {
pnlPct = ((entryPrice - markPrice) / entryPrice) * 100
}
marginUsed := (quantity * markPrice) / float64(leverage)
result = append(result, map[string]interface{}{
"symbol": symbol,
"side": side,
"entry_price": entryPrice,
"mark_price": markPrice,
"quantity": quantity,
"leverage": leverage,
"unrealized_pnl": unrealizedPnl,
"unrealized_pnl_pct": pnlPct,
"liquidation_price": liquidationPrice,
"margin_used": marginUsed,
})
}
return result, nil
}
// sortDecisionsByPriority 对决策排序:先平仓,再开仓,最后hold/wait
// 这样可以避免换仓时仓位叠加超限
func sortDecisionsByPriority(decisions []market.TradingDecision) []market.TradingDecision {
if len(decisions) <= 1 {
return decisions
}
// 定义优先级
getActionPriority := func(action string) int {
switch action {
case "close_long", "close_short":
return 1 // 最高优先级:先平仓
case "open_long", "open_short":
return 2 // 次优先级:后开仓
case "hold", "wait":
return 3 // 最低优先级:观望
default:
return 999 // 未知动作放最后
}
}
// 复制决策列表
sorted := make([]market.TradingDecision, len(decisions))
copy(sorted, decisions)
// 按优先级排序
for i := 0; i < len(sorted)-1; i++ {
for j := i + 1; j < len(sorted); j++ {
if getActionPriority(sorted[i].Action) > getActionPriority(sorted[j].Action) {
sorted[i], sorted[j] = sorted[j], sorted[i]
}
}
}
return sorted
}
+564
View File
@@ -0,0 +1,564 @@
package trader
import (
"context"
"fmt"
"log"
"strconv"
"time"
"github.com/adshao/go-binance/v2/futures"
)
// FuturesTrader 币安合约交易器
type FuturesTrader struct {
client *futures.Client
}
// NewFuturesTrader 创建合约交易器
func NewFuturesTrader(apiKey, secretKey string) *FuturesTrader {
client := futures.NewClient(apiKey, secretKey)
return &FuturesTrader{
client: client,
}
}
// GetBalance 获取账户余额
func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) {
log.Printf("🔄 正在调用币安API获取账户余额...")
account, err := t.client.NewGetAccountService().Do(context.Background())
if err != nil {
log.Printf("❌ 币安API调用失败: %v", err)
return nil, fmt.Errorf("获取账户信息失败: %w", err)
}
result := make(map[string]interface{})
result["totalWalletBalance"], _ = strconv.ParseFloat(account.TotalWalletBalance, 64)
result["availableBalance"], _ = strconv.ParseFloat(account.AvailableBalance, 64)
result["totalUnrealizedProfit"], _ = strconv.ParseFloat(account.TotalUnrealizedProfit, 64)
log.Printf("✓ 币安API返回: 总余额=%s, 可用=%s, 未实现盈亏=%s",
account.TotalWalletBalance,
account.AvailableBalance,
account.TotalUnrealizedProfit)
return result, nil
}
// GetPositions 获取所有持仓
func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) {
positions, err := t.client.NewGetPositionRiskService().Do(context.Background())
if err != nil {
return nil, fmt.Errorf("获取持仓失败: %w", err)
}
var result []map[string]interface{}
for _, pos := range positions {
posAmt, _ := strconv.ParseFloat(pos.PositionAmt, 64)
if posAmt == 0 {
continue // 跳过无持仓的
}
posMap := make(map[string]interface{})
posMap["symbol"] = pos.Symbol
posMap["positionAmt"], _ = strconv.ParseFloat(pos.PositionAmt, 64)
posMap["entryPrice"], _ = strconv.ParseFloat(pos.EntryPrice, 64)
posMap["markPrice"], _ = strconv.ParseFloat(pos.MarkPrice, 64)
posMap["unRealizedProfit"], _ = strconv.ParseFloat(pos.UnRealizedProfit, 64)
posMap["leverage"], _ = strconv.ParseFloat(pos.Leverage, 64)
posMap["liquidationPrice"], _ = strconv.ParseFloat(pos.LiquidationPrice, 64)
// 判断方向
if posAmt > 0 {
posMap["side"] = "long"
} else {
posMap["side"] = "short"
}
result = append(result, posMap)
}
return result, nil
}
// SetLeverage 设置杠杆(智能判断+冷却期)
func (t *FuturesTrader) SetLeverage(symbol string, leverage int) error {
// 先尝试获取当前杠杆(从持仓信息)
currentLeverage := 0
positions, err := t.GetPositions()
if err == nil {
for _, pos := range positions {
if pos["symbol"] == symbol {
if lev, ok := pos["leverage"].(float64); ok {
currentLeverage = int(lev)
break
}
}
}
}
// 如果当前杠杆已经是目标杠杆,跳过
if currentLeverage == leverage && currentLeverage > 0 {
log.Printf(" ✓ %s 杠杆已是 %dx,无需切换", symbol, leverage)
return nil
}
// 切换杠杆
_, err = t.client.NewChangeLeverageService().
Symbol(symbol).
Leverage(leverage).
Do(context.Background())
if err != nil {
// 如果错误信息包含"No need to change",说明杠杆已经是目标值
if contains(err.Error(), "No need to change") {
log.Printf(" ✓ %s 杠杆已是 %dx", symbol, leverage)
return nil
}
return fmt.Errorf("设置杠杆失败: %w", err)
}
log.Printf(" ✓ %s 杠杆已切换为 %dx", symbol, leverage)
// 切换杠杆后等待5秒(避免冷却期错误)
log.Printf(" ⏱ 等待5秒冷却期...")
time.Sleep(5 * time.Second)
return nil
}
// SetMarginType 设置保证金模式
func (t *FuturesTrader) SetMarginType(symbol string, marginType futures.MarginType) error {
err := t.client.NewChangeMarginTypeService().
Symbol(symbol).
MarginType(marginType).
Do(context.Background())
if err != nil {
// 如果已经是该模式,不算错误
if contains(err.Error(), "No need to change") {
log.Printf(" ✓ %s 保证金模式已是 %s", symbol, marginType)
return nil
}
return fmt.Errorf("设置保证金模式失败: %w", err)
}
log.Printf(" ✓ %s 保证金模式已切换为 %s", symbol, marginType)
// 切换保证金模式后等待3秒(避免冷却期错误)
log.Printf(" ⏱ 等待3秒冷却期...")
time.Sleep(3 * time.Second)
return nil
}
// OpenLong 开多仓
func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
// 先取消该币种的所有委托单(清理旧的止损止盈单)
if err := t.CancelAllOrders(symbol); err != nil {
log.Printf(" ⚠ 取消旧委托单失败(可能没有委托单): %v", err)
}
// 设置杠杆
if err := t.SetLeverage(symbol, leverage); err != nil {
return nil, err
}
// 设置逐仓模式
if err := t.SetMarginType(symbol, futures.MarginTypeIsolated); err != nil {
return nil, err
}
// 格式化数量到正确精度
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 创建市价买入订单
order, err := t.client.NewCreateOrderService().
Symbol(symbol).
Side(futures.SideTypeBuy).
PositionSide(futures.PositionSideTypeLong).
Type(futures.OrderTypeMarket).
Quantity(quantityStr).
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("开多仓失败: %w", err)
}
log.Printf("✓ 开多仓成功: %s 数量: %s", symbol, quantityStr)
log.Printf(" 订单ID: %d", order.OrderID)
result := make(map[string]interface{})
result["orderId"] = order.OrderID
result["symbol"] = order.Symbol
result["status"] = order.Status
return result, nil
}
// OpenShort 开空仓
func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
// 先取消该币种的所有委托单(清理旧的止损止盈单)
if err := t.CancelAllOrders(symbol); err != nil {
log.Printf(" ⚠ 取消旧委托单失败(可能没有委托单): %v", err)
}
// 设置杠杆
if err := t.SetLeverage(symbol, leverage); err != nil {
return nil, err
}
// 设置逐仓模式
if err := t.SetMarginType(symbol, futures.MarginTypeIsolated); err != nil {
return nil, err
}
// 格式化数量到正确精度
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 创建市价卖出订单
order, err := t.client.NewCreateOrderService().
Symbol(symbol).
Side(futures.SideTypeSell).
PositionSide(futures.PositionSideTypeShort).
Type(futures.OrderTypeMarket).
Quantity(quantityStr).
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("开空仓失败: %w", err)
}
log.Printf("✓ 开空仓成功: %s 数量: %s", symbol, quantityStr)
log.Printf(" 订单ID: %d", order.OrderID)
result := make(map[string]interface{})
result["orderId"] = order.OrderID
result["symbol"] = order.Symbol
result["status"] = order.Status
return result, nil
}
// CloseLong 平多仓
func (t *FuturesTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) {
// 如果数量为0,获取当前持仓数量
if quantity == 0 {
positions, err := t.GetPositions()
if err != nil {
return nil, err
}
for _, pos := range positions {
if pos["symbol"] == symbol && pos["side"] == "long" {
quantity = pos["positionAmt"].(float64)
break
}
}
if quantity == 0 {
return nil, fmt.Errorf("没有找到 %s 的多仓", symbol)
}
}
// 格式化数量
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 创建市价卖出订单(平多)
order, err := t.client.NewCreateOrderService().
Symbol(symbol).
Side(futures.SideTypeSell).
PositionSide(futures.PositionSideTypeLong).
Type(futures.OrderTypeMarket).
Quantity(quantityStr).
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("平多仓失败: %w", err)
}
log.Printf("✓ 平多仓成功: %s 数量: %s", symbol, quantityStr)
// 平仓后取消该币种的所有挂单(止损止盈单)
if err := t.CancelAllOrders(symbol); err != nil {
log.Printf(" ⚠ 取消挂单失败: %v", err)
}
result := make(map[string]interface{})
result["orderId"] = order.OrderID
result["symbol"] = order.Symbol
result["status"] = order.Status
return result, nil
}
// CloseShort 平空仓
func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) {
// 如果数量为0,获取当前持仓数量
if quantity == 0 {
positions, err := t.GetPositions()
if err != nil {
return nil, err
}
for _, pos := range positions {
if pos["symbol"] == symbol && pos["side"] == "short" {
quantity = -pos["positionAmt"].(float64) // 空仓数量是负的,取绝对值
break
}
}
if quantity == 0 {
return nil, fmt.Errorf("没有找到 %s 的空仓", symbol)
}
}
// 格式化数量
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return nil, err
}
// 创建市价买入订单(平空)
order, err := t.client.NewCreateOrderService().
Symbol(symbol).
Side(futures.SideTypeBuy).
PositionSide(futures.PositionSideTypeShort).
Type(futures.OrderTypeMarket).
Quantity(quantityStr).
Do(context.Background())
if err != nil {
return nil, fmt.Errorf("平空仓失败: %w", err)
}
log.Printf("✓ 平空仓成功: %s 数量: %s", symbol, quantityStr)
// 平仓后取消该币种的所有挂单(止损止盈单)
if err := t.CancelAllOrders(symbol); err != nil {
log.Printf(" ⚠ 取消挂单失败: %v", err)
}
result := make(map[string]interface{})
result["orderId"] = order.OrderID
result["symbol"] = order.Symbol
result["status"] = order.Status
return result, nil
}
// CancelAllOrders 取消该币种的所有挂单
func (t *FuturesTrader) CancelAllOrders(symbol string) error {
err := t.client.NewCancelAllOpenOrdersService().
Symbol(symbol).
Do(context.Background())
if err != nil {
return fmt.Errorf("取消挂单失败: %w", err)
}
log.Printf(" ✓ 已取消 %s 的所有挂单", symbol)
return nil
}
// GetMarketPrice 获取市场价格
func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) {
prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background())
if err != nil {
return 0, fmt.Errorf("获取价格失败: %w", err)
}
if len(prices) == 0 {
return 0, fmt.Errorf("未找到价格")
}
price, err := strconv.ParseFloat(prices[0].Price, 64)
if err != nil {
return 0, err
}
return price, nil
}
// CalculatePositionSize 计算仓位大小
func (t *FuturesTrader) CalculatePositionSize(balance, riskPercent, price float64, leverage int) float64 {
riskAmount := balance * (riskPercent / 100.0)
positionValue := riskAmount * float64(leverage)
quantity := positionValue / price
return quantity
}
// SetStopLoss 设置止损单
func (t *FuturesTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
var side futures.SideType
var posSide futures.PositionSideType
if positionSide == "LONG" {
side = futures.SideTypeSell
posSide = futures.PositionSideTypeLong
} else {
side = futures.SideTypeBuy
posSide = futures.PositionSideTypeShort
}
// 格式化数量
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return err
}
_, err = t.client.NewCreateOrderService().
Symbol(symbol).
Side(side).
PositionSide(posSide).
Type(futures.OrderTypeStopMarket).
StopPrice(fmt.Sprintf("%.8f", stopPrice)).
Quantity(quantityStr).
WorkingType(futures.WorkingTypeContractPrice).
ClosePosition(true).
Do(context.Background())
if err != nil {
return fmt.Errorf("设置止损失败: %w", err)
}
log.Printf(" 止损价设置: %.4f", stopPrice)
return nil
}
// SetTakeProfit 设置止盈单
func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
var side futures.SideType
var posSide futures.PositionSideType
if positionSide == "LONG" {
side = futures.SideTypeSell
posSide = futures.PositionSideTypeLong
} else {
side = futures.SideTypeBuy
posSide = futures.PositionSideTypeShort
}
// 格式化数量
quantityStr, err := t.FormatQuantity(symbol, quantity)
if err != nil {
return err
}
_, err = t.client.NewCreateOrderService().
Symbol(symbol).
Side(side).
PositionSide(posSide).
Type(futures.OrderTypeTakeProfitMarket).
StopPrice(fmt.Sprintf("%.8f", takeProfitPrice)).
Quantity(quantityStr).
WorkingType(futures.WorkingTypeContractPrice).
ClosePosition(true).
Do(context.Background())
if err != nil {
return fmt.Errorf("设置止盈失败: %w", err)
}
log.Printf(" 止盈价设置: %.4f", takeProfitPrice)
return nil
}
// GetSymbolPrecision 获取交易对的数量精度
func (t *FuturesTrader) GetSymbolPrecision(symbol string) (int, error) {
exchangeInfo, err := t.client.NewExchangeInfoService().Do(context.Background())
if err != nil {
return 0, fmt.Errorf("获取交易规则失败: %w", err)
}
for _, s := range exchangeInfo.Symbols {
if s.Symbol == symbol {
// 从LOT_SIZE filter获取精度
for _, filter := range s.Filters {
if filter["filterType"] == "LOT_SIZE" {
stepSize := filter["stepSize"].(string)
precision := calculatePrecision(stepSize)
log.Printf(" %s 数量精度: %d (stepSize: %s)", symbol, precision, stepSize)
return precision, nil
}
}
}
}
log.Printf(" ⚠ %s 未找到精度信息,使用默认精度3", symbol)
return 3, nil // 默认精度为3
}
// calculatePrecision 从stepSize计算精度
func calculatePrecision(stepSize string) int {
// 去除尾部的0
stepSize = trimTrailingZeros(stepSize)
// 查找小数点
dotIndex := -1
for i := 0; i < len(stepSize); i++ {
if stepSize[i] == '.' {
dotIndex = i
break
}
}
// 如果没有小数点或小数点在最后,精度为0
if dotIndex == -1 || dotIndex == len(stepSize)-1 {
return 0
}
// 返回小数点后的位数
return len(stepSize) - dotIndex - 1
}
// trimTrailingZeros 去除尾部的0
func trimTrailingZeros(s string) string {
// 如果没有小数点,直接返回
if !stringContains(s, ".") {
return s
}
// 从后向前遍历,去除尾部的0
for len(s) > 0 && s[len(s)-1] == '0' {
s = s[:len(s)-1]
}
// 如果最后一位是小数点,也去掉
if len(s) > 0 && s[len(s)-1] == '.' {
s = s[:len(s)-1]
}
return s
}
// FormatQuantity 格式化数量到正确的精度
func (t *FuturesTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
precision, err := t.GetSymbolPrecision(symbol)
if err != nil {
// 如果获取失败,使用默认格式
return fmt.Sprintf("%.3f", quantity), nil
}
format := fmt.Sprintf("%%.%df", precision)
return fmt.Sprintf(format, quantity), nil
}
// 辅助函数
func contains(s, substr string) bool {
return len(s) >= len(substr) && stringContains(s, substr)
}
func stringContains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
+117
View File
@@ -0,0 +1,117 @@
# Web Dashboard Changelog
## v2.0 - 重大UI升级(参考nof0设计)
### 🎨 样式改进
#### 配色方案
- ✅ 采用nof0项目的极简黑白灰配色
-**主色调**
- 纯黑背景 `#0a0a0a`
- 白色文字 `#ededed`
- 灰色边框 `rgba(255, 255, 255, 0.1)`
- 品牌色仅用于AI提供商标识(DeepSeek蓝 `#4d6bfe`、Qwen紫 `#8b5cf6`
-**字体**IBM Plex Mono(等宽字体,专业交易界面风格)
-**终端扫描线效果**:灰色扫描线动画,营造专业交易氛围
#### UI组件升级
-**Header**
- 纯白色标题文字
- 玻璃态模糊背景(glass effect
- 粘性定位(sticky top
- 状态指示器带脉冲发光动画
- AI提供商颜色区分(DeepSeek蓝/Qwen紫)
-**统计卡片**
- 半透明背景 `bg-gray-900/50`
- Hover时边框高亮效果
- 等宽字体显示数字
- 大写字母标题,增加专业感
-**通用效果**
- 淡入动画(animate-fade-in
- 平滑过渡效果
- 自定义滚动条样式
- 按钮悬停效果
### 📊 新功能:账户收益率曲线
#### 后端API
- ✅ 新增 `GET /api/equity-history` 端点
- ✅ 返回最近30个周期的账户净值数据
- ✅ 包含时间戳、净值、盈亏、周期号
#### 前端图表组件(EquityChart
-**双显示模式**
- 美元模式:显示绝对金额
- 百分比模式:显示收益率百分比
- 一键切换
-**图表特性**
- 使用Recharts库
- 白灰渐变线条(极简风格)
- 参考线显示初始余额/0%
- 自定义Tooltip(显示详细信息)
- 响应式布局,自适应容器宽度
-**数据展示**
- 实时当前净值和总盈亏
- 初始余额对比
- 数据点数量统计
- 每10秒自动刷新
### 📝 组件结构
```
web/src/
├── components/
│ └── EquityChart.tsx # 收益率曲线图表
├── lib/
│ └── api.ts # 新增getEquityHistory()
├── index.css # 全新样式系统
└── App.tsx # 集成图表组件
```
### 🎯 视觉改进对比
**Before:**
- 基础Tailwind深色主题
- 简单卡片布局
- 灰色单调配色
- 无曲线图表
**After:**
- nof0风格极简黑白灰配色
- 终端扫描线效果
- 纯白文字和灰色主题
- 实时收益率曲线
- IBM Plex Mono字体
- 玻璃态和动画效果
### 🚀 性能优化
- SWR数据缓存和自动刷新
- 组件级代码分割
- 响应式设计优化
- 图表数据自动降采样
### 📱 响应式设计
- 移动端优化布局
- 平板/桌面端多列展示
- 图表自适应容器宽度
- 触摸友好的交互
### 🔧 技术栈
- React 18
- TypeScript
- Vite
- Tailwind CSS
- Recharts
- SWR
- IBM Plex Mono字体
---
**更新日期**: 2025-10-27
+100
View File
@@ -0,0 +1,100 @@
# NOFX Web Dashboard
基于 Vite + React + TypeScript 的AI自动交易监控面板
## 技术栈
- **React 18** - UI框架
- **TypeScript** - 类型安全
- **Vite** - 构建工具
- **Tailwind CSS** - 样式框架
- **SWR** - 数据获取和缓存
- **Zustand** - 状态管理
- **Recharts** - 图表库
## 安装依赖
```bash
npm install
```
## 运行开发服务器
```bash
npm run dev
```
访问 http://localhost:3000
## 构建生产版本
```bash
npm run build
```
## 功能特性
### 实时监控
- **系统状态** - 运行状态、AI提供商、周期数
- **账户信息** - 净值、可用余额、总盈亏、保证金使用率
- **持仓列表** - 实时价格、盈亏、杠杆、强平价
- **决策日志** - 完整的AI思维链(可展开)、决策动作、执行结果
### AI思维链分析
每个决策记录都包含完整的AI思考过程:
- **第一步**:现有持仓分析(技术指标、盈亏评估)
- **第二步**:账户风险评估(保证金使用率、可用余额)
- **第三步**:新机会评估(候选币种筛选、技术形态分析)
- **第四步**:最终决策总结(平仓/开仓/持有决策)
点击 "💭 AI思维链分析" 即可展开查看完整分析过程!
### 自动刷新
- 系统状态、账户、持仓:每5秒刷新
- 决策日志、统计:每10秒刷新
### API集成
前端通过Vite代理访问后端APIhttp://localhost:8080
**API端点:**
- `GET /api/status` - 系统状态
- `GET /api/account` - 账户信息
- `GET /api/positions` - 持仓列表
- `GET /api/decisions` - 决策日志(最近30条)
- `GET /api/decisions/latest` - 最新决策(最近5条)
- `GET /api/statistics` - 统计信息
## 项目结构
```
web/
├── src/
│ ├── components/ # React组件(待扩展)
│ ├── lib/
│ │ └── api.ts # API调用函数
│ ├── store/ # Zustand状态管理(待扩展)
│ ├── types/
│ │ └── index.ts # TypeScript类型定义
│ ├── App.tsx # 主应用组件
│ ├── main.tsx # 入口文件
│ └── index.css # 全局样式
├── index.html # HTML模板
├── vite.config.ts # Vite配置
├── tailwind.config.js # Tailwind配置
├── tsconfig.json # TypeScript配置
└── package.json # 依赖配置
```
## 注意事项
1. **确保后端API服务已启动**(默认端口8080
2. **Node.js版本要求**>= 18.0.0
3. **网络连接**:需要访问Binance API
## 开发计划
- [ ] 添加图表展示(账户净值走势、盈亏曲线)
- [ ] 添加决策详情页面(完整的CoT分析)
- [ ] 添加手动交易控制
- [ ] 添加参数配置页面
- [ ] 添加通知和告警系统
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NOFX - AI Auto Trading Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Generated Vendored Executable
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env node
let mode = process.argv[2]
if (mode === '--info') {
process.stdout.write(require('../')().info() + '\n')
} else if (mode === '--version') {
process.stdout.write(
'autoprefixer ' + require('../package.json').version + '\n'
)
} else {
process.stdout.write(
'autoprefix\n' +
'\n' +
'Options:\n' +
' --info Show target browsers and used prefixes\n' +
' --version Show version number\n' +
' --help Show help\n' +
'\n' +
'Usage:\n' +
' autoprefixer --info\n'
)
}
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import{parseArgs as e}from"node:util";import{exit as a}from"node:process";import{getCompatibleVersions as s}from"./index.js";const n=process.argv.slice(2),{values:o}=e({args:n,options:{"target-year":{type:"string"},"widely-available-on-date":{type:"string"},"include-downstream-browsers":{type:"boolean"},"list-all-compatible-versions":{type:"boolean"},"include-kaios":{type:"boolean"},help:{type:"boolean",short:"h"}},strict:!0});o.help&&(console.log("\nGet Baseline Widely available browser versions or Baseline year browser versions.\n\nUsage: baseline-browser-mapping [options]\n\nOptions:\n --target-year Pass a year between 2015 and the current year to get browser versions compatible \n with all Newly Available features as of the end of the year specified.\n --widely-available-on-date Pass a date in the format 'YYYY-MM-DD' to get versions compatible with Widely \n available on the specified date.\n --include-downstream-browsers Whether to include browsers that use the same engines as a core Baseline browser.\n --include-kaios Whether to include KaiOS in downstream browsers. Requires --include-downstream-browsers.\n --list-all-compatible-versions Whether to include only the minimum compatible browser versions or all compatible versions.\n -h, --help Show help\n\nExamples:\n npx baseline-browser-mapping --target-year 2020\n npx baseline-browser-mapping --widely-available-on-date 2023-04-05\n npx baseline-browser-mapping --include-downstream-browsers\n npx baseline-browser-mapping --list-all-compatible-versions\n".trim()),a(0)),console.log(s({targetYear:o["target-year"]?Number.parseInt(o["target-year"]):void 0,widelyAvailableOnDate:o["widely-available-on-date"],includeDownstreamBrowsers:o["include-downstream-browsers"],listAllCompatibleVersions:o["list-all-compatible-versions"],includeKaiOS:o["include-kaios"]}));
Generated Vendored Executable
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env node
var fs = require('fs')
var updateDb = require('update-browserslist-db')
var browserslist = require('./')
var pkg = require('./package.json')
var args = process.argv.slice(2)
var USAGE =
'Usage:\n' +
' npx browserslist\n' +
' npx browserslist "QUERIES"\n' +
' npx browserslist --json "QUERIES"\n' +
' npx browserslist --config="path/to/browserlist/file"\n' +
' npx browserslist --coverage "QUERIES"\n' +
' npx browserslist --coverage=US "QUERIES"\n' +
' npx browserslist --coverage=US,RU,global "QUERIES"\n' +
' npx browserslist --env="environment name defined in config"\n' +
' npx browserslist --stats="path/to/browserlist/stats/file"\n' +
' npx browserslist --mobile-to-desktop\n' +
' npx browserslist --ignore-unknown-versions\n'
function isArg(arg) {
return args.some(function (str) {
return str === arg || str.indexOf(arg + '=') === 0
})
}
function error(msg) {
process.stderr.write('browserslist: ' + msg + '\n')
process.exit(1)
}
if (isArg('--help') || isArg('-h')) {
process.stdout.write(pkg.description + '.\n\n' + USAGE + '\n')
} else if (isArg('--version') || isArg('-v')) {
process.stdout.write('browserslist ' + pkg.version + '\n')
} else if (isArg('--update-db')) {
/* c8 ignore next 8 */
process.stdout.write(
'The --update-db command is deprecated.\n' +
'Please use npx update-browserslist-db@latest instead.\n'
)
process.stdout.write('Browserslist DB update will still be made.\n')
updateDb(function (str) {
process.stdout.write(str)
})
} else {
var mode = 'browsers'
var opts = {}
var queries
var areas
for (var i = 0; i < args.length; i++) {
if (args[i][0] !== '-') {
queries = args[i].replace(/^["']|["']$/g, '')
continue
}
var arg = args[i].split('=')
var name = arg[0]
var value = arg[1]
if (value) value = value.replace(/^["']|["']$/g, '')
if (name === '--config' || name === '-b') {
opts.config = value
} else if (name === '--env' || name === '-e') {
opts.env = value
} else if (name === '--stats' || name === '-s') {
opts.stats = value
} else if (name === '--coverage' || name === '-c') {
if (mode !== 'json') mode = 'coverage'
if (value) {
areas = value.split(',')
} else {
areas = ['global']
}
} else if (name === '--json') {
mode = 'json'
} else if (name === '--mobile-to-desktop') {
/* c8 ignore next */
opts.mobileToDesktop = true
} else if (name === '--ignore-unknown-versions') {
/* c8 ignore next */
opts.ignoreUnknownVersions = true
} else {
error('Unknown arguments ' + args[i] + '.\n\n' + USAGE)
}
}
var browsers
try {
browsers = browserslist(queries, opts)
} catch (e) {
if (e.name === 'BrowserslistError') {
error(e.message)
} /* c8 ignore start */ else {
throw e
} /* c8 ignore end */
}
var coverage
if (mode === 'browsers') {
browsers.forEach(function (browser) {
process.stdout.write(browser + '\n')
})
} else if (areas) {
coverage = areas.map(function (area) {
var stats
if (area !== 'global') {
stats = area
} else if (opts.stats) {
stats = JSON.parse(fs.readFileSync(opts.stats))
}
var result = browserslist.coverage(browsers, stats)
var round = Math.round(result * 100) / 100.0
return [area, round]
})
if (mode === 'coverage') {
var prefix = 'These browsers account for '
process.stdout.write(prefix)
coverage.forEach(function (data, index) {
var area = data[0]
var round = data[1]
var end = 'globally'
if (area && area !== 'global') {
end = 'in the ' + area.toUpperCase()
} else if (opts.stats) {
end = 'in custom statistics'
}
if (index !== 0) {
process.stdout.write(prefix.replace(/./g, ' '))
}
process.stdout.write(round + '% of all users ' + end + '\n')
})
}
}
if (mode === 'json') {
var data = { browsers: browsers }
if (coverage) {
data.coverage = coverage.reduce(function (object, j) {
object[j[0]] = j[1]
return object
}, {})
}
process.stdout.write(JSON.stringify(data, null, ' ') + '\n')
}
}
Generated Vendored Executable
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env node
const fs = require('fs');
const cssesc = require('../cssesc.js');
const strings = process.argv.splice(2);
const stdin = process.stdin;
const options = {};
const log = console.log;
const main = function() {
const option = strings[0];
if (/^(?:-h|--help|undefined)$/.test(option)) {
log(
'cssesc v%s - https://mths.be/cssesc',
cssesc.version
);
log([
'\nUsage:\n',
'\tcssesc [string]',
'\tcssesc [-i | --identifier] [string]',
'\tcssesc [-s | --single-quotes] [string]',
'\tcssesc [-d | --double-quotes] [string]',
'\tcssesc [-w | --wrap] [string]',
'\tcssesc [-e | --escape-everything] [string]',
'\tcssesc [-v | --version]',
'\tcssesc [-h | --help]',
'\nExamples:\n',
'\tcssesc \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'',
'\tcssesc --identifier \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'',
'\tcssesc --escape-everything \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'',
'\tcssesc --double-quotes --wrap \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'',
'\techo \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\' | cssesc'
].join('\n'));
return process.exit(1);
}
if (/^(?:-v|--version)$/.test(option)) {
log('v%s', cssesc.version);
return process.exit(1);
}
strings.forEach(function(string) {
// Process options
if (/^(?:-i|--identifier)$/.test(string)) {
options.isIdentifier = true;
return;
}
if (/^(?:-s|--single-quotes)$/.test(string)) {
options.quotes = 'single';
return;
}
if (/^(?:-d|--double-quotes)$/.test(string)) {
options.quotes = 'double';
return;
}
if (/^(?:-w|--wrap)$/.test(string)) {
options.wrap = true;
return;
}
if (/^(?:-e|--escape-everything)$/.test(string)) {
options.escapeEverything = true;
return;
}
// Process string(s)
let result;
try {
result = cssesc(string, options);
log(result);
} catch (exception) {
log(exception.message + '\n');
log('Error: failed to escape.');
log('If you think this is a bug in cssesc, please report it:');
log('https://github.com/mathiasbynens/cssesc/issues/new');
log(
'\nStack trace using cssesc@%s:\n',
cssesc.version
);
log(exception.stack);
return process.exit(1);
}
});
// Return with exit status 0 outside of the `forEach` loop, in case
// multiple strings were passed in.
return process.exit(0);
};
if (stdin.isTTY) {
// handle shell arguments
main();
} else {
let timeout;
// Either the script is called from within a non-TTY context, or `stdin`
// content is being piped in.
if (!process.stdout.isTTY) {
// The script was called from a non-TTY context. This is a rather uncommon
// use case we dont actively support. However, we dont want the script
// to wait forever in such cases, so…
timeout = setTimeout(function() {
// …if no piped data arrived after a whole minute, handle shell
// arguments instead.
main();
}, 60000);
}
let data = '';
stdin.on('data', function(chunk) {
clearTimeout(timeout);
data += chunk;
});
stdin.on('end', function() {
strings.push(data.trim());
main();
});
stdin.resume();
}
Generated Vendored Executable
BIN
View File
Binary file not shown.
Generated Vendored Executable
+270
View File
@@ -0,0 +1,270 @@
#!/usr/bin/env node
import { foregroundChild } from 'foreground-child';
import { existsSync } from 'fs';
import { jack } from 'jackspeak';
import { loadPackageJson } from 'package-json-from-dist';
import { join } from 'path';
import { globStream } from './index.js';
const { version } = loadPackageJson(import.meta.url, '../package.json');
const j = jack({
usage: 'glob [options] [<pattern> [<pattern> ...]]',
})
.description(`
Glob v${version}
Expand the positional glob expression arguments into any matching file
system paths found.
`)
.opt({
cmd: {
short: 'c',
hint: 'command',
description: `Run the command provided, passing the glob expression
matches as arguments.`,
},
})
.opt({
default: {
short: 'p',
hint: 'pattern',
description: `If no positional arguments are provided, glob will use
this pattern`,
},
})
.flag({
all: {
short: 'A',
description: `By default, the glob cli command will not expand any
arguments that are an exact match to a file on disk.
This prevents double-expanding, in case the shell expands
an argument whose filename is a glob expression.
For example, if 'app/*.ts' would match 'app/[id].ts', then
on Windows powershell or cmd.exe, 'glob app/*.ts' will
expand to 'app/[id].ts', as expected. However, in posix
shells such as bash or zsh, the shell will first expand
'app/*.ts' to a list of filenames. Then glob will look
for a file matching 'app/[id].ts' (ie, 'app/i.ts' or
'app/d.ts'), which is unexpected.
Setting '--all' prevents this behavior, causing glob
to treat ALL patterns as glob expressions to be expanded,
even if they are an exact match to a file on disk.
When setting this option, be sure to enquote arguments
so that the shell will not expand them prior to passing
them to the glob command process.
`,
},
absolute: {
short: 'a',
description: 'Expand to absolute paths',
},
'dot-relative': {
short: 'd',
description: `Prepend './' on relative matches`,
},
mark: {
short: 'm',
description: `Append a / on any directories matched`,
},
posix: {
short: 'x',
description: `Always resolve to posix style paths, using '/' as the
directory separator, even on Windows. Drive letter
absolute matches on Windows will be expanded to their
full resolved UNC maths, eg instead of 'C:\\foo\\bar',
it will expand to '//?/C:/foo/bar'.
`,
},
follow: {
short: 'f',
description: `Follow symlinked directories when expanding '**'`,
},
realpath: {
short: 'R',
description: `Call 'fs.realpath' on all of the results. In the case
of an entry that cannot be resolved, the entry is
omitted. This incurs a slight performance penalty, of
course, because of the added system calls.`,
},
stat: {
short: 's',
description: `Call 'fs.lstat' on all entries, whether required or not
to determine if it's a valid match.`,
},
'match-base': {
short: 'b',
description: `Perform a basename-only match if the pattern does not
contain any slash characters. That is, '*.js' would be
treated as equivalent to '**/*.js', matching js files
in all directories.
`,
},
dot: {
description: `Allow patterns to match files/directories that start
with '.', even if the pattern does not start with '.'
`,
},
nobrace: {
description: 'Do not expand {...} patterns',
},
nocase: {
description: `Perform a case-insensitive match. This defaults to
'true' on macOS and Windows platforms, and false on
all others.
Note: 'nocase' should only be explicitly set when it is
known that the filesystem's case sensitivity differs
from the platform default. If set 'true' on
case-insensitive file systems, then the walk may return
more or less results than expected.
`,
},
nodir: {
description: `Do not match directories, only files.
Note: to *only* match directories, append a '/' at the
end of the pattern.
`,
},
noext: {
description: `Do not expand extglob patterns, such as '+(a|b)'`,
},
noglobstar: {
description: `Do not expand '**' against multiple path portions.
Ie, treat it as a normal '*' instead.`,
},
'windows-path-no-escape': {
description: `Use '\\' as a path separator *only*, and *never* as an
escape character. If set, all '\\' characters are
replaced with '/' in the pattern.`,
},
})
.num({
'max-depth': {
short: 'D',
description: `Maximum depth to traverse from the current
working directory`,
},
})
.opt({
cwd: {
short: 'C',
description: 'Current working directory to execute/match in',
default: process.cwd(),
},
root: {
short: 'r',
description: `A string path resolved against the 'cwd', which is
used as the starting point for absolute patterns that
start with '/' (but not drive letters or UNC paths
on Windows).
Note that this *doesn't* necessarily limit the walk to
the 'root' directory, and doesn't affect the cwd
starting point for non-absolute patterns. A pattern
containing '..' will still be able to traverse out of
the root directory, if it is not an actual root directory
on the filesystem, and any non-absolute patterns will
still be matched in the 'cwd'.
To start absolute and non-absolute patterns in the same
path, you can use '--root=' to set it to the empty
string. However, be aware that on Windows systems, a
pattern like 'x:/*' or '//host/share/*' will *always*
start in the 'x:/' or '//host/share/' directory,
regardless of the --root setting.
`,
},
platform: {
description: `Defaults to the value of 'process.platform' if
available, or 'linux' if not. Setting --platform=win32
on non-Windows systems may cause strange behavior!`,
validOptions: [
'aix',
'android',
'darwin',
'freebsd',
'haiku',
'linux',
'openbsd',
'sunos',
'win32',
'cygwin',
'netbsd',
],
},
})
.optList({
ignore: {
short: 'i',
description: `Glob patterns to ignore`,
},
})
.flag({
debug: {
short: 'v',
description: `Output a huge amount of noisy debug information about
patterns as they are parsed and used to match files.`,
},
})
.flag({
help: {
short: 'h',
description: 'Show this usage information',
},
});
try {
const { positionals, values } = j.parse();
if (values.help) {
console.log(j.usage());
process.exit(0);
}
if (positionals.length === 0 && !values.default)
throw 'No patterns provided';
if (positionals.length === 0 && values.default)
positionals.push(values.default);
const patterns = values.all ? positionals : positionals.filter(p => !existsSync(p));
const matches = values.all ?
[]
: positionals.filter(p => existsSync(p)).map(p => join(p));
const stream = globStream(patterns, {
absolute: values.absolute,
cwd: values.cwd,
dot: values.dot,
dotRelative: values['dot-relative'],
follow: values.follow,
ignore: values.ignore,
mark: values.mark,
matchBase: values['match-base'],
maxDepth: values['max-depth'],
nobrace: values.nobrace,
nocase: values.nocase,
nodir: values.nodir,
noext: values.noext,
noglobstar: values.noglobstar,
platform: values.platform,
realpath: values.realpath,
root: values.root,
stat: values.stat,
debug: values.debug,
posix: values.posix,
});
const cmd = values.cmd;
if (!cmd) {
matches.forEach(m => console.log(m));
stream.on('data', f => console.log(f));
}
else {
stream.on('data', f => matches.push(f));
stream.on('end', () => foregroundChild(cmd, matches, { shell: true }));
}
}
catch (e) {
console.error(j.usage());
console.error(e instanceof Error ? e.message : String(e));
process.exit(1);
}
//# sourceMappingURL=bin.mjs.map
Generated Vendored Executable
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env node
const { resolve } = require("node:path");
const script = process.argv.splice(2, 1)[0];
if (!script) {
console.error("Usage: jiti <path> [...arguments]");
process.exit(1);
}
const pwd = process.cwd();
const jiti = require("..")(pwd);
const resolved = (process.argv[1] = jiti.resolve(resolve(pwd, script)));
jiti(resolved);
Generated Vendored Executable
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env node
(function() {
var fs = require('fs');
var stringEscape = require('../jsesc.js');
var strings = process.argv.splice(2);
var stdin = process.stdin;
var data;
var timeout;
var isObject = false;
var options = {};
var log = console.log;
var main = function() {
var option = strings[0];
if (/^(?:-h|--help|undefined)$/.test(option)) {
log(
'jsesc v%s - https://mths.be/jsesc',
stringEscape.version
);
log([
'\nUsage:\n',
'\tjsesc [string]',
'\tjsesc [-s | --single-quotes] [string]',
'\tjsesc [-d | --double-quotes] [string]',
'\tjsesc [-w | --wrap] [string]',
'\tjsesc [-e | --escape-everything] [string]',
'\tjsesc [-t | --escape-etago] [string]',
'\tjsesc [-6 | --es6] [string]',
'\tjsesc [-l | --lowercase-hex] [string]',
'\tjsesc [-j | --json] [string]',
'\tjsesc [-o | --object] [stringified_object]', // `JSON.parse()` the argument
'\tjsesc [-p | --pretty] [string]', // `compact: false`
'\tjsesc [-v | --version]',
'\tjsesc [-h | --help]',
'\nExamples:\n',
'\tjsesc \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'',
'\tjsesc --json \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'',
'\tjsesc --json --escape-everything \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'',
'\tjsesc --double-quotes --wrap \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\'',
'\techo \'f\xF6o \u2665 b\xE5r \uD834\uDF06 baz\' | jsesc'
].join('\n'));
return process.exit(1);
}
if (/^(?:-v|--version)$/.test(option)) {
log('v%s', stringEscape.version);
return process.exit(1);
}
strings.forEach(function(string) {
// Process options
if (/^(?:-s|--single-quotes)$/.test(string)) {
options.quotes = 'single';
return;
}
if (/^(?:-d|--double-quotes)$/.test(string)) {
options.quotes = 'double';
return;
}
if (/^(?:-w|--wrap)$/.test(string)) {
options.wrap = true;
return;
}
if (/^(?:-e|--escape-everything)$/.test(string)) {
options.escapeEverything = true;
return;
}
if (/^(?:-t|--escape-etago)$/.test(string)) {
options.escapeEtago = true;
return;
}
if (/^(?:-6|--es6)$/.test(string)) {
options.es6 = true;
return;
}
if (/^(?:-l|--lowercase-hex)$/.test(string)) {
options.lowercaseHex = true;
return;
}
if (/^(?:-j|--json)$/.test(string)) {
options.json = true;
return;
}
if (/^(?:-o|--object)$/.test(string)) {
isObject = true;
return;
}
if (/^(?:-p|--pretty)$/.test(string)) {
isObject = true;
options.compact = false;
return;
}
// Process string(s)
var result;
try {
if (isObject) {
string = JSON.parse(string);
}
result = stringEscape(string, options);
log(result);
} catch(error) {
log(error.message + '\n');
log('Error: failed to escape.');
log('If you think this is a bug in jsesc, please report it:');
log('https://github.com/mathiasbynens/jsesc/issues/new');
log(
'\nStack trace using jsesc@%s:\n',
stringEscape.version
);
log(error.stack);
return process.exit(1);
}
});
// Return with exit status 0 outside of the `forEach` loop, in case
// multiple strings were passed in.
return process.exit(0);
};
if (stdin.isTTY) {
// handle shell arguments
main();
} else {
// Either the script is called from within a non-TTY context,
// or `stdin` content is being piped in.
if (!process.stdout.isTTY) { // called from a non-TTY context
timeout = setTimeout(function() {
// if no piped data arrived after a while, handle shell arguments
main();
}, 250);
}
data = '';
stdin.on('data', function(chunk) {
clearTimeout(timeout);
data += chunk;
});
stdin.on('end', function() {
strings.push(data.trim());
main();
});
stdin.resume();
}
}());
Generated Vendored Executable
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const pkg = require('../package.json')
const JSON5 = require('./')
const argv = parseArgs()
if (argv.version) {
version()
} else if (argv.help) {
usage()
} else {
const inFilename = argv.defaults[0]
let readStream
if (inFilename) {
readStream = fs.createReadStream(inFilename)
} else {
readStream = process.stdin
}
let json5 = ''
readStream.on('data', data => {
json5 += data
})
readStream.on('end', () => {
let space
if (argv.space === 't' || argv.space === 'tab') {
space = '\t'
} else {
space = Number(argv.space)
}
let value
try {
value = JSON5.parse(json5)
if (!argv.validate) {
const json = JSON.stringify(value, null, space)
let writeStream
// --convert is for backward compatibility with v0.5.1. If
// specified with <file> and not --out-file, then a file with
// the same name but with a .json extension will be written.
if (argv.convert && inFilename && !argv.outFile) {
const parsedFilename = path.parse(inFilename)
const outFilename = path.format(
Object.assign(
parsedFilename,
{base: path.basename(parsedFilename.base, parsedFilename.ext) + '.json'}
)
)
writeStream = fs.createWriteStream(outFilename)
} else if (argv.outFile) {
writeStream = fs.createWriteStream(argv.outFile)
} else {
writeStream = process.stdout
}
writeStream.write(json)
}
} catch (err) {
console.error(err.message)
process.exit(1)
}
})
}
function parseArgs () {
let convert
let space
let validate
let outFile
let version
let help
const defaults = []
const args = process.argv.slice(2)
for (let i = 0; i < args.length; i++) {
const arg = args[i]
switch (arg) {
case '--convert':
case '-c':
convert = true
break
case '--space':
case '-s':
space = args[++i]
break
case '--validate':
case '-v':
validate = true
break
case '--out-file':
case '-o':
outFile = args[++i]
break
case '--version':
case '-V':
version = true
break
case '--help':
case '-h':
help = true
break
default:
defaults.push(arg)
break
}
}
return {
convert,
space,
validate,
outFile,
version,
help,
defaults,
}
}
function version () {
console.log(pkg.version)
}
function usage () {
console.log(
`
Usage: json5 [options] <file>
If <file> is not provided, then STDIN is used.
Options:
-s, --space The number of spaces to indent or 't' for tabs
-o, --out-file [file] Output to the specified file, otherwise STDOUT
-v, --validate Validate JSON5 but do not output JSON
-V, --version Output the version number
-h, --help Output usage information`
)
}
Generated Vendored Executable
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env node
'use strict';
var looseEnvify = require('./');
var fs = require('fs');
if (process.argv[2]) {
fs.createReadStream(process.argv[2], {encoding: 'utf8'})
.pipe(looseEnvify(process.argv[2]))
.pipe(process.stdout);
} else {
process.stdin.resume()
process.stdin
.pipe(looseEnvify(__filename))
.pipe(process.stdout);
}
Generated Vendored Executable
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env node
let { nanoid, customAlphabet } = require('..')
function print(msg) {
process.stdout.write(msg + '\n')
}
function error(msg) {
process.stderr.write(msg + '\n')
process.exit(1)
}
if (process.argv.includes('--help') || process.argv.includes('-h')) {
print(`
Usage
$ nanoid [options]
Options
-s, --size Generated ID size
-a, --alphabet Alphabet to use
-h, --help Show this help
Examples
$ nanoid --s 15
S9sBF77U6sDB8Yg
$ nanoid --size 10 --alphabet abc
bcabababca`)
process.exit()
}
let alphabet, size
for (let i = 2; i < process.argv.length; i++) {
let arg = process.argv[i]
if (arg === '--size' || arg === '-s') {
size = Number(process.argv[i + 1])
i += 1
if (Number.isNaN(size) || size <= 0) {
error('Size must be positive integer')
}
} else if (arg === '--alphabet' || arg === '-a') {
alphabet = process.argv[i + 1]
i += 1
} else {
error('Unknown argument ' + arg)
}
}
if (alphabet) {
let customNanoid = customAlphabet(alphabet, size)
print(customNanoid())
} else {
print(nanoid(size))
}
Generated Vendored Executable
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env node
var which = require("../")
if (process.argv.length < 3)
usage()
function usage () {
console.error('usage: which [-as] program ...')
process.exit(1)
}
var all = false
var silent = false
var dashdash = false
var args = process.argv.slice(2).filter(function (arg) {
if (dashdash || !/^-/.test(arg))
return true
if (arg === '--') {
dashdash = true
return false
}
var flags = arg.substr(1).split('')
for (var f = 0; f < flags.length; f++) {
var flag = flags[f]
switch (flag) {
case 's':
silent = true
break
case 'a':
all = true
break
default:
console.error('which: illegal option -- ' + flag)
usage()
}
}
return false
})
process.exit(args.reduce(function (pv, current) {
try {
var f = which.sync(current, { all: all })
if (all)
f = f.join('\n')
if (!silent)
console.log(f)
return pv;
} catch (e) {
return 1;
}
}, 0))
Generated Vendored Executable
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env node
/* eslint-disable no-var, unicorn/prefer-node-protocol */
var parser = require("..");
var fs = require("fs");
var filename = process.argv[2];
if (!filename) {
console.error("no filename specified");
} else {
var file = fs.readFileSync(filename, "utf8");
var ast = parser.parse(file);
console.log(JSON.stringify(ast, null, " "));
}
Generated Vendored Executable
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env node
'use strict';
var path = require('path');
var fs = require('fs');
if (
String(process.env.npm_lifecycle_script).slice(0, 8) !== 'resolve '
&& (
!process.argv
|| process.argv.length < 2
|| (process.argv[1] !== __filename && fs.statSync(process.argv[1]).ino !== fs.statSync(__filename).ino)
|| (process.env.npm_lifecycle_event !== 'npx' && process.env._ && fs.realpathSync(path.resolve(process.env._)) !== __filename)
)
) {
console.error('Error: `resolve` must be run directly as an executable');
process.exit(1);
}
var supportsPreserveSymlinkFlag = require('supports-preserve-symlinks-flag');
var preserveSymlinks = false;
for (var i = 2; i < process.argv.length; i += 1) {
if (process.argv[i].slice(0, 2) === '--') {
if (supportsPreserveSymlinkFlag && process.argv[i] === '--preserve-symlinks') {
preserveSymlinks = true;
} else if (process.argv[i].length > 2) {
console.error('Unknown argument ' + process.argv[i].replace(/[=].*$/, ''));
process.exit(2);
}
process.argv.splice(i, 1);
i -= 1;
if (process.argv[i] === '--') { break; } // eslint-disable-line no-restricted-syntax
}
}
if (process.argv.length < 3) {
console.error('Error: `resolve` expects a specifier');
process.exit(2);
}
var resolve = require('../');
var result = resolve.sync(process.argv[2], {
basedir: process.cwd(),
preserveSymlinks: preserveSymlinks
});
console.log(result);
Generated Vendored Executable
+1912
View File
File diff suppressed because one or more lines are too long
Generated Vendored Executable
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env node
// Standalone semver comparison program.
// Exits successfully and prints matching version(s) if
// any supplied version is valid and passes all tests.
var argv = process.argv.slice(2)
var versions = []
var range = []
var inc = null
var version = require('../package.json').version
var loose = false
var includePrerelease = false
var coerce = false
var rtl = false
var identifier
var semver = require('../semver')
var reverse = false
var options = {}
main()
function main () {
if (!argv.length) return help()
while (argv.length) {
var a = argv.shift()
var indexOfEqualSign = a.indexOf('=')
if (indexOfEqualSign !== -1) {
a = a.slice(0, indexOfEqualSign)
argv.unshift(a.slice(indexOfEqualSign + 1))
}
switch (a) {
case '-rv': case '-rev': case '--rev': case '--reverse':
reverse = true
break
case '-l': case '--loose':
loose = true
break
case '-p': case '--include-prerelease':
includePrerelease = true
break
case '-v': case '--version':
versions.push(argv.shift())
break
case '-i': case '--inc': case '--increment':
switch (argv[0]) {
case 'major': case 'minor': case 'patch': case 'prerelease':
case 'premajor': case 'preminor': case 'prepatch':
inc = argv.shift()
break
default:
inc = 'patch'
break
}
break
case '--preid':
identifier = argv.shift()
break
case '-r': case '--range':
range.push(argv.shift())
break
case '-c': case '--coerce':
coerce = true
break
case '--rtl':
rtl = true
break
case '--ltr':
rtl = false
break
case '-h': case '--help': case '-?':
return help()
default:
versions.push(a)
break
}
}
var options = { loose: loose, includePrerelease: includePrerelease, rtl: rtl }
versions = versions.map(function (v) {
return coerce ? (semver.coerce(v, options) || { version: v }).version : v
}).filter(function (v) {
return semver.valid(v)
})
if (!versions.length) return fail()
if (inc && (versions.length !== 1 || range.length)) { return failInc() }
for (var i = 0, l = range.length; i < l; i++) {
versions = versions.filter(function (v) {
return semver.satisfies(v, range[i], options)
})
if (!versions.length) return fail()
}
return success(versions)
}
function failInc () {
console.error('--inc can only be used on a single version with no range')
fail()
}
function fail () { process.exit(1) }
function success () {
var compare = reverse ? 'rcompare' : 'compare'
versions.sort(function (a, b) {
return semver[compare](a, b, options)
}).map(function (v) {
return semver.clean(v, options)
}).map(function (v) {
return inc ? semver.inc(v, inc, options, identifier) : v
}).forEach(function (v, i, _) { console.log(v) })
}
function help () {
console.log(['SemVer ' + version,
'',
'A JavaScript implementation of the https://semver.org/ specification',
'Copyright Isaac Z. Schlueter',
'',
'Usage: semver [options] <version> [<version> [...]]',
'Prints valid versions sorted by SemVer precedence',
'',
'Options:',
'-r --range <range>',
' Print versions that match the specified range.',
'',
'-i --increment [<level>]',
' Increment a version by the specified level. Level can',
' be one of: major, minor, patch, premajor, preminor,',
" prepatch, or prerelease. Default level is 'patch'.",
' Only one version may be specified.',
'',
'--preid <identifier>',
' Identifier to be used to prefix premajor, preminor,',
' prepatch or prerelease version increments.',
'',
'-l --loose',
' Interpret versions and ranges loosely',
'',
'-p --include-prerelease',
' Always include prerelease versions in range matching',
'',
'-c --coerce',
' Coerce a string into SemVer if possible',
' (does not imply --loose)',
'',
'--rtl',
' Coerce version strings right to left',
'',
'--ltr',
' Coerce version strings left to right (default)',
'',
'Program exits successfully if any valid version satisfies',
'all supplied ranges, and prints all satisfying versions.',
'',
'If no satisfying versions are found, then exits failure.',
'',
'Versions are printed in ascending order, so supplying',
'multiple versions to the utility will just sort them.'
].join('\n'))
}
Generated Vendored Executable
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env node
require("../dist/cli").default();
Generated Vendored Executable
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env node
const Module = require("module");
const {resolve} = require("path");
/*
* Simple wrapper around node that first registers Sucrase with default settings.
*
* This is meant for simple use cases, and doesn't support custom Node/V8 args,
* executing a code snippet, a REPL, or other things that you might find in
* node, babel-node, or ts-node. For more advanced use cases, you can use
* `node -r sucrase/register` or register a require hook programmatically from
* your own code.
*/
require("../register");
process.argv.splice(1, 1);
process.argv[1] = resolve(process.argv[1]);
Module.runMain();
Generated Vendored Executable
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env node
"use strict";
module.exports = require("./cli/index");
Generated Vendored Executable
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env node
"use strict";
module.exports = require("./cli/index");
Generated Vendored Executable
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../lib/tsc.js')
Generated Vendored Executable
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../lib/tsserver.js')
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env node
let { readFileSync } = require('fs')
let { join } = require('path')
require('./check-npm-version')
let updateDb = require('./')
const ROOT = __dirname
function getPackage() {
return JSON.parse(readFileSync(join(ROOT, 'package.json')))
}
let args = process.argv.slice(2)
let USAGE = 'Usage:\n npx update-browserslist-db\n'
function isArg(arg) {
return args.some(i => i === arg)
}
function error(msg) {
process.stderr.write('update-browserslist-db: ' + msg + '\n')
process.exit(1)
}
if (isArg('--help') || isArg('-h')) {
process.stdout.write(getPackage().description + '.\n\n' + USAGE + '\n')
} else if (isArg('--version') || isArg('-v')) {
process.stdout.write('browserslist-lint ' + getPackage().version + '\n')
} else {
try {
updateDb()
} catch (e) {
if (e.name === 'BrowserslistUpdateError') {
error(e.message)
} else {
throw e
}
}
}
Generated Vendored Executable
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env node
import { performance } from 'node:perf_hooks'
import module from 'node:module'
if (!import.meta.url.includes('node_modules')) {
try {
// only available as dev dependency
await import('source-map-support').then((r) => r.default.install())
} catch {}
process.on('unhandledRejection', (err) => {
throw new Error('UNHANDLED PROMISE REJECTION', { cause: err })
})
}
global.__vite_start_time = performance.now()
// check debug mode first before requiring the CLI.
const debugIndex = process.argv.findIndex((arg) => /^(?:-d|--debug)$/.test(arg))
const filterIndex = process.argv.findIndex((arg) =>
/^(?:-f|--filter)$/.test(arg),
)
const profileIndex = process.argv.indexOf('--profile')
if (debugIndex > 0) {
let value = process.argv[debugIndex + 1]
if (!value || value.startsWith('-')) {
value = 'vite:*'
} else {
// support debugging multiple flags with comma-separated list
value = value
.split(',')
.map((v) => `vite:${v}`)
.join(',')
}
process.env.DEBUG = `${
process.env.DEBUG ? process.env.DEBUG + ',' : ''
}${value}`
if (filterIndex > 0) {
const filter = process.argv[filterIndex + 1]
if (filter && !filter.startsWith('-')) {
process.env.VITE_DEBUG_FILTER = filter
}
}
}
function start() {
try {
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- it is supported in Node 22.8.0+ and only called if it exists
module.enableCompileCache?.()
// flush the cache after 10s because the cache is not flushed until process end
// for dev server, the cache is never flushed unless manually flushed because the process.exit is called
// also flushing the cache in SIGINT handler seems to cause the process to hang
setTimeout(() => {
try {
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- it is supported in Node 22.12.0+ and only called if it exists
module.flushCompileCache?.()
} catch {}
}, 10 * 1000).unref()
} catch {}
return import('../dist/node/cli.js')
}
if (profileIndex > 0) {
process.argv.splice(profileIndex, 1)
const next = process.argv[profileIndex]
if (next && !next.startsWith('-')) {
process.argv.splice(profileIndex, 1)
}
const inspector = await import('node:inspector').then((r) => r.default)
const session = (global.__vite_profile_session = new inspector.Session())
session.connect()
session.post('Profiler.enable', () => {
session.post('Profiler.start', start)
})
} else {
start()
}
+2715
View File
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
{
"hash": "53b874d1",
"configHash": "f8bdad83",
"lockfileHash": "1c435015",
"browserHash": "acc2ca4c",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "c6bc2ad7",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "47262748",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "92585479",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "04e40841",
"needsInterop": true
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "bbe7557b",
"needsInterop": true
},
"swr": {
"src": "../../swr/dist/index/index.mjs",
"file": "swr.js",
"fileHash": "98c9a82f",
"needsInterop": false
},
"recharts": {
"src": "../../recharts/es6/index.js",
"file": "recharts.js",
"fileHash": "8d6dc254",
"needsInterop": false
}
},
"chunks": {
"chunk-SVG7M4VJ": {
"file": "chunk-SVG7M4VJ.js"
},
"chunk-2YIMICFJ": {
"file": "chunk-2YIMICFJ.js"
}
}
}
+1935
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+21684
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
+5
View File
@@ -0,0 +1,5 @@
import {
require_react_dom
} from "./chunk-SVG7M4VJ.js";
import "./chunk-2YIMICFJ.js";
export default require_react_dom();
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
+38
View File
@@ -0,0 +1,38 @@
import {
require_react_dom
} from "./chunk-SVG7M4VJ.js";
import {
__commonJS
} from "./chunk-2YIMICFJ.js";
// node_modules/react-dom/client.js
var require_client = __commonJS({
"node_modules/react-dom/client.js"(exports) {
var m = require_react_dom();
if (false) {
exports.createRoot = m.createRoot;
exports.hydrateRoot = m.hydrateRoot;
} else {
i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
exports.createRoot = function(c, o) {
i.usingClientEntryPoint = true;
try {
return m.createRoot(c, o);
} finally {
i.usingClientEntryPoint = false;
}
};
exports.hydrateRoot = function(c, h, o) {
i.usingClientEntryPoint = true;
try {
return m.hydrateRoot(c, h, o);
} finally {
i.usingClientEntryPoint = false;
}
};
}
var i;
}
});
export default require_client();
//# sourceMappingURL=react-dom_client.js.map
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../react-dom/client.js"],
"sourcesContent": ["'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n"],
"mappings": ";;;;;;;;AAAA;AAAA;AAEA,QAAI,IAAI;AACR,QAAI,OAAuC;AACzC,cAAQ,aAAa,EAAE;AACvB,cAAQ,cAAc,EAAE;AAAA,IAC1B,OAAO;AACD,UAAI,EAAE;AACV,cAAQ,aAAa,SAAS,GAAG,GAAG;AAClC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,WAAW,GAAG,CAAC;AAAA,QAC1B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AACA,cAAQ,cAAc,SAAS,GAAG,GAAG,GAAG;AACtC,UAAE,wBAAwB;AAC1B,YAAI;AACF,iBAAO,EAAE,YAAY,GAAG,GAAG,CAAC;AAAA,QAC9B,UAAE;AACA,YAAE,wBAAwB;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAjBM;AAAA;AAAA;",
"names": []
}
+4
View File
@@ -0,0 +1,4 @@
import {
require_react
} from "./chunk-2YIMICFJ.js";
export default require_react();
+7
View File
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
+911
View File
@@ -0,0 +1,911 @@
import {
__commonJS,
require_react
} from "./chunk-2YIMICFJ.js";
// node_modules/react/cjs/react-jsx-dev-runtime.development.js
var require_react_jsx_dev_runtime_development = __commonJS({
"node_modules/react/cjs/react-jsx-dev-runtime.development.js"(exports) {
"use strict";
if (true) {
(function() {
"use strict";
var React = require_react();
var REACT_ELEMENT_TYPE = Symbol.for("react.element");
var REACT_PORTAL_TYPE = Symbol.for("react.portal");
var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
var REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode");
var REACT_PROFILER_TYPE = Symbol.for("react.profiler");
var REACT_PROVIDER_TYPE = Symbol.for("react.provider");
var REACT_CONTEXT_TYPE = Symbol.for("react.context");
var REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
var REACT_SUSPENSE_TYPE = Symbol.for("react.suspense");
var REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list");
var REACT_MEMO_TYPE = Symbol.for("react.memo");
var REACT_LAZY_TYPE = Symbol.for("react.lazy");
var REACT_OFFSCREEN_TYPE = Symbol.for("react.offscreen");
var MAYBE_ITERATOR_SYMBOL = Symbol.iterator;
var FAUX_ITERATOR_SYMBOL = "@@iterator";
function getIteratorFn(maybeIterable) {
if (maybeIterable === null || typeof maybeIterable !== "object") {
return null;
}
var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL];
if (typeof maybeIterator === "function") {
return maybeIterator;
}
return null;
}
var ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
function error(format) {
{
{
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
printWarning("error", format, args);
}
}
}
function printWarning(level, format, args) {
{
var ReactDebugCurrentFrame2 = ReactSharedInternals.ReactDebugCurrentFrame;
var stack = ReactDebugCurrentFrame2.getStackAddendum();
if (stack !== "") {
format += "%s";
args = args.concat([stack]);
}
var argsWithFormat = args.map(function(item) {
return String(item);
});
argsWithFormat.unshift("Warning: " + format);
Function.prototype.apply.call(console[level], console, argsWithFormat);
}
}
var enableScopeAPI = false;
var enableCacheElement = false;
var enableTransitionTracing = false;
var enableLegacyHidden = false;
var enableDebugTracing = false;
var REACT_MODULE_REFERENCE;
{
REACT_MODULE_REFERENCE = Symbol.for("react.module.reference");
}
function isValidElementType(type) {
if (typeof type === "string" || typeof type === "function") {
return true;
}
if (type === REACT_FRAGMENT_TYPE || type === REACT_PROFILER_TYPE || enableDebugTracing || type === REACT_STRICT_MODE_TYPE || type === REACT_SUSPENSE_TYPE || type === REACT_SUSPENSE_LIST_TYPE || enableLegacyHidden || type === REACT_OFFSCREEN_TYPE || enableScopeAPI || enableCacheElement || enableTransitionTracing) {
return true;
}
if (typeof type === "object" && type !== null) {
if (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || // This needs to include all possible module reference object
// types supported by any Flight configuration anywhere since
// we don't know which Flight build this will end up being used
// with.
type.$$typeof === REACT_MODULE_REFERENCE || type.getModuleId !== void 0) {
return true;
}
}
return false;
}
function getWrappedName(outerType, innerType, wrapperName) {
var displayName = outerType.displayName;
if (displayName) {
return displayName;
}
var functionName = innerType.displayName || innerType.name || "";
return functionName !== "" ? wrapperName + "(" + functionName + ")" : wrapperName;
}
function getContextName(type) {
return type.displayName || "Context";
}
function getComponentNameFromType(type) {
if (type == null) {
return null;
}
{
if (typeof type.tag === "number") {
error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue.");
}
}
if (typeof type === "function") {
return type.displayName || type.name || null;
}
if (typeof type === "string") {
return type;
}
switch (type) {
case REACT_FRAGMENT_TYPE:
return "Fragment";
case REACT_PORTAL_TYPE:
return "Portal";
case REACT_PROFILER_TYPE:
return "Profiler";
case REACT_STRICT_MODE_TYPE:
return "StrictMode";
case REACT_SUSPENSE_TYPE:
return "Suspense";
case REACT_SUSPENSE_LIST_TYPE:
return "SuspenseList";
}
if (typeof type === "object") {
switch (type.$$typeof) {
case REACT_CONTEXT_TYPE:
var context = type;
return getContextName(context) + ".Consumer";
case REACT_PROVIDER_TYPE:
var provider = type;
return getContextName(provider._context) + ".Provider";
case REACT_FORWARD_REF_TYPE:
return getWrappedName(type, type.render, "ForwardRef");
case REACT_MEMO_TYPE:
var outerName = type.displayName || null;
if (outerName !== null) {
return outerName;
}
return getComponentNameFromType(type.type) || "Memo";
case REACT_LAZY_TYPE: {
var lazyComponent = type;
var payload = lazyComponent._payload;
var init = lazyComponent._init;
try {
return getComponentNameFromType(init(payload));
} catch (x) {
return null;
}
}
}
}
return null;
}
var assign = Object.assign;
var disabledDepth = 0;
var prevLog;
var prevInfo;
var prevWarn;
var prevError;
var prevGroup;
var prevGroupCollapsed;
var prevGroupEnd;
function disabledLog() {
}
disabledLog.__reactDisabledLog = true;
function disableLogs() {
{
if (disabledDepth === 0) {
prevLog = console.log;
prevInfo = console.info;
prevWarn = console.warn;
prevError = console.error;
prevGroup = console.group;
prevGroupCollapsed = console.groupCollapsed;
prevGroupEnd = console.groupEnd;
var props = {
configurable: true,
enumerable: true,
value: disabledLog,
writable: true
};
Object.defineProperties(console, {
info: props,
log: props,
warn: props,
error: props,
group: props,
groupCollapsed: props,
groupEnd: props
});
}
disabledDepth++;
}
}
function reenableLogs() {
{
disabledDepth--;
if (disabledDepth === 0) {
var props = {
configurable: true,
enumerable: true,
writable: true
};
Object.defineProperties(console, {
log: assign({}, props, {
value: prevLog
}),
info: assign({}, props, {
value: prevInfo
}),
warn: assign({}, props, {
value: prevWarn
}),
error: assign({}, props, {
value: prevError
}),
group: assign({}, props, {
value: prevGroup
}),
groupCollapsed: assign({}, props, {
value: prevGroupCollapsed
}),
groupEnd: assign({}, props, {
value: prevGroupEnd
})
});
}
if (disabledDepth < 0) {
error("disabledDepth fell below zero. This is a bug in React. Please file an issue.");
}
}
}
var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
var prefix;
function describeBuiltInComponentFrame(name, source, ownerFn) {
{
if (prefix === void 0) {
try {
throw Error();
} catch (x) {
var match = x.stack.trim().match(/\n( *(at )?)/);
prefix = match && match[1] || "";
}
}
return "\n" + prefix + name;
}
}
var reentry = false;
var componentFrameCache;
{
var PossiblyWeakMap = typeof WeakMap === "function" ? WeakMap : Map;
componentFrameCache = new PossiblyWeakMap();
}
function describeNativeComponentFrame(fn, construct) {
if (!fn || reentry) {
return "";
}
{
var frame = componentFrameCache.get(fn);
if (frame !== void 0) {
return frame;
}
}
var control;
reentry = true;
var previousPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = void 0;
var previousDispatcher;
{
previousDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = null;
disableLogs();
}
try {
if (construct) {
var Fake = function() {
throw Error();
};
Object.defineProperty(Fake.prototype, "props", {
set: function() {
throw Error();
}
});
if (typeof Reflect === "object" && Reflect.construct) {
try {
Reflect.construct(Fake, []);
} catch (x) {
control = x;
}
Reflect.construct(fn, [], Fake);
} else {
try {
Fake.call();
} catch (x) {
control = x;
}
fn.call(Fake.prototype);
}
} else {
try {
throw Error();
} catch (x) {
control = x;
}
fn();
}
} catch (sample) {
if (sample && control && typeof sample.stack === "string") {
var sampleLines = sample.stack.split("\n");
var controlLines = control.stack.split("\n");
var s = sampleLines.length - 1;
var c = controlLines.length - 1;
while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
c--;
}
for (; s >= 1 && c >= 0; s--, c--) {
if (sampleLines[s] !== controlLines[c]) {
if (s !== 1 || c !== 1) {
do {
s--;
c--;
if (c < 0 || sampleLines[s] !== controlLines[c]) {
var _frame = "\n" + sampleLines[s].replace(" at new ", " at ");
if (fn.displayName && _frame.includes("<anonymous>")) {
_frame = _frame.replace("<anonymous>", fn.displayName);
}
{
if (typeof fn === "function") {
componentFrameCache.set(fn, _frame);
}
}
return _frame;
}
} while (s >= 1 && c >= 0);
}
break;
}
}
}
} finally {
reentry = false;
{
ReactCurrentDispatcher.current = previousDispatcher;
reenableLogs();
}
Error.prepareStackTrace = previousPrepareStackTrace;
}
var name = fn ? fn.displayName || fn.name : "";
var syntheticFrame = name ? describeBuiltInComponentFrame(name) : "";
{
if (typeof fn === "function") {
componentFrameCache.set(fn, syntheticFrame);
}
}
return syntheticFrame;
}
function describeFunctionComponentFrame(fn, source, ownerFn) {
{
return describeNativeComponentFrame(fn, false);
}
}
function shouldConstruct(Component) {
var prototype = Component.prototype;
return !!(prototype && prototype.isReactComponent);
}
function describeUnknownElementTypeFrameInDEV(type, source, ownerFn) {
if (type == null) {
return "";
}
if (typeof type === "function") {
{
return describeNativeComponentFrame(type, shouldConstruct(type));
}
}
if (typeof type === "string") {
return describeBuiltInComponentFrame(type);
}
switch (type) {
case REACT_SUSPENSE_TYPE:
return describeBuiltInComponentFrame("Suspense");
case REACT_SUSPENSE_LIST_TYPE:
return describeBuiltInComponentFrame("SuspenseList");
}
if (typeof type === "object") {
switch (type.$$typeof) {
case REACT_FORWARD_REF_TYPE:
return describeFunctionComponentFrame(type.render);
case REACT_MEMO_TYPE:
return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn);
case REACT_LAZY_TYPE: {
var lazyComponent = type;
var payload = lazyComponent._payload;
var init = lazyComponent._init;
try {
return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn);
} catch (x) {
}
}
}
}
return "";
}
var hasOwnProperty = Object.prototype.hasOwnProperty;
var loggedTypeFailures = {};
var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
function setCurrentlyValidatingElement(element) {
{
if (element) {
var owner = element._owner;
var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
ReactDebugCurrentFrame.setExtraStackFrame(stack);
} else {
ReactDebugCurrentFrame.setExtraStackFrame(null);
}
}
}
function checkPropTypes(typeSpecs, values, location, componentName, element) {
{
var has = Function.call.bind(hasOwnProperty);
for (var typeSpecName in typeSpecs) {
if (has(typeSpecs, typeSpecName)) {
var error$1 = void 0;
try {
if (typeof typeSpecs[typeSpecName] !== "function") {
var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
err.name = "Invariant Violation";
throw err;
}
error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
} catch (ex) {
error$1 = ex;
}
if (error$1 && !(error$1 instanceof Error)) {
setCurrentlyValidatingElement(element);
error("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1);
setCurrentlyValidatingElement(null);
}
if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
loggedTypeFailures[error$1.message] = true;
setCurrentlyValidatingElement(element);
error("Failed %s type: %s", location, error$1.message);
setCurrentlyValidatingElement(null);
}
}
}
}
}
var isArrayImpl = Array.isArray;
function isArray(a) {
return isArrayImpl(a);
}
function typeName(value) {
{
var hasToStringTag = typeof Symbol === "function" && Symbol.toStringTag;
var type = hasToStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
return type;
}
}
function willCoercionThrow(value) {
{
try {
testStringCoercion(value);
return false;
} catch (e) {
return true;
}
}
}
function testStringCoercion(value) {
return "" + value;
}
function checkKeyStringCoercion(value) {
{
if (willCoercionThrow(value)) {
error("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.", typeName(value));
return testStringCoercion(value);
}
}
}
var ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
var RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true
};
var specialPropKeyWarningShown;
var specialPropRefWarningShown;
var didWarnAboutStringRefs;
{
didWarnAboutStringRefs = {};
}
function hasValidRef(config) {
{
if (hasOwnProperty.call(config, "ref")) {
var getter = Object.getOwnPropertyDescriptor(config, "ref").get;
if (getter && getter.isReactWarning) {
return false;
}
}
}
return config.ref !== void 0;
}
function hasValidKey(config) {
{
if (hasOwnProperty.call(config, "key")) {
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
if (getter && getter.isReactWarning) {
return false;
}
}
}
return config.key !== void 0;
}
function warnIfStringRefCannotBeAutoConverted(config, self) {
{
if (typeof config.ref === "string" && ReactCurrentOwner.current && self && ReactCurrentOwner.current.stateNode !== self) {
var componentName = getComponentNameFromType(ReactCurrentOwner.current.type);
if (!didWarnAboutStringRefs[componentName]) {
error('Component "%s" contains the string ref "%s". Support for string refs will be removed in a future major release. This case cannot be automatically converted to an arrow function. We ask you to manually fix this case by using useRef() or createRef() instead. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref', getComponentNameFromType(ReactCurrentOwner.current.type), config.ref);
didWarnAboutStringRefs[componentName] = true;
}
}
}
}
function defineKeyPropWarningGetter(props, displayName) {
{
var warnAboutAccessingKey = function() {
if (!specialPropKeyWarningShown) {
specialPropKeyWarningShown = true;
error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName);
}
};
warnAboutAccessingKey.isReactWarning = true;
Object.defineProperty(props, "key", {
get: warnAboutAccessingKey,
configurable: true
});
}
}
function defineRefPropWarningGetter(props, displayName) {
{
var warnAboutAccessingRef = function() {
if (!specialPropRefWarningShown) {
specialPropRefWarningShown = true;
error("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName);
}
};
warnAboutAccessingRef.isReactWarning = true;
Object.defineProperty(props, "ref", {
get: warnAboutAccessingRef,
configurable: true
});
}
}
var ReactElement = function(type, key, ref, self, source, owner, props) {
var element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type,
key,
ref,
props,
// Record the component responsible for creating this element.
_owner: owner
};
{
element._store = {};
Object.defineProperty(element._store, "validated", {
configurable: false,
enumerable: false,
writable: true,
value: false
});
Object.defineProperty(element, "_self", {
configurable: false,
enumerable: false,
writable: false,
value: self
});
Object.defineProperty(element, "_source", {
configurable: false,
enumerable: false,
writable: false,
value: source
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
function jsxDEV(type, config, maybeKey, source, self) {
{
var propName;
var props = {};
var key = null;
var ref = null;
if (maybeKey !== void 0) {
{
checkKeyStringCoercion(maybeKey);
}
key = "" + maybeKey;
}
if (hasValidKey(config)) {
{
checkKeyStringCoercion(config.key);
}
key = "" + config.key;
}
if (hasValidRef(config)) {
ref = config.ref;
warnIfStringRefCannotBeAutoConverted(config, self);
}
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === void 0) {
props[propName] = defaultProps[propName];
}
}
}
if (key || ref) {
var displayName = typeof type === "function" ? type.displayName || type.name || "Unknown" : type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
}
var ReactCurrentOwner$1 = ReactSharedInternals.ReactCurrentOwner;
var ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame;
function setCurrentlyValidatingElement$1(element) {
{
if (element) {
var owner = element._owner;
var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
ReactDebugCurrentFrame$1.setExtraStackFrame(stack);
} else {
ReactDebugCurrentFrame$1.setExtraStackFrame(null);
}
}
}
var propTypesMisspellWarningShown;
{
propTypesMisspellWarningShown = false;
}
function isValidElement(object) {
{
return typeof object === "object" && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
}
}
function getDeclarationErrorAddendum() {
{
if (ReactCurrentOwner$1.current) {
var name = getComponentNameFromType(ReactCurrentOwner$1.current.type);
if (name) {
return "\n\nCheck the render method of `" + name + "`.";
}
}
return "";
}
}
function getSourceInfoErrorAddendum(source) {
{
if (source !== void 0) {
var fileName = source.fileName.replace(/^.*[\\\/]/, "");
var lineNumber = source.lineNumber;
return "\n\nCheck your code at " + fileName + ":" + lineNumber + ".";
}
return "";
}
}
var ownerHasKeyUseWarning = {};
function getCurrentComponentErrorInfo(parentType) {
{
var info = getDeclarationErrorAddendum();
if (!info) {
var parentName = typeof parentType === "string" ? parentType : parentType.displayName || parentType.name;
if (parentName) {
info = "\n\nCheck the top-level render call using <" + parentName + ">.";
}
}
return info;
}
}
function validateExplicitKey(element, parentType) {
{
if (!element._store || element._store.validated || element.key != null) {
return;
}
element._store.validated = true;
var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType);
if (ownerHasKeyUseWarning[currentComponentErrorInfo]) {
return;
}
ownerHasKeyUseWarning[currentComponentErrorInfo] = true;
var childOwner = "";
if (element && element._owner && element._owner !== ReactCurrentOwner$1.current) {
childOwner = " It was passed a child from " + getComponentNameFromType(element._owner.type) + ".";
}
setCurrentlyValidatingElement$1(element);
error('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.', currentComponentErrorInfo, childOwner);
setCurrentlyValidatingElement$1(null);
}
}
function validateChildKeys(node, parentType) {
{
if (typeof node !== "object") {
return;
}
if (isArray(node)) {
for (var i = 0; i < node.length; i++) {
var child = node[i];
if (isValidElement(child)) {
validateExplicitKey(child, parentType);
}
}
} else if (isValidElement(node)) {
if (node._store) {
node._store.validated = true;
}
} else if (node) {
var iteratorFn = getIteratorFn(node);
if (typeof iteratorFn === "function") {
if (iteratorFn !== node.entries) {
var iterator = iteratorFn.call(node);
var step;
while (!(step = iterator.next()).done) {
if (isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
}
}
}
}
}
}
}
function validatePropTypes(element) {
{
var type = element.type;
if (type === null || type === void 0 || typeof type === "string") {
return;
}
var propTypes;
if (typeof type === "function") {
propTypes = type.propTypes;
} else if (typeof type === "object" && (type.$$typeof === REACT_FORWARD_REF_TYPE || // Note: Memo only checks outer props here.
// Inner props are checked in the reconciler.
type.$$typeof === REACT_MEMO_TYPE)) {
propTypes = type.propTypes;
} else {
return;
}
if (propTypes) {
var name = getComponentNameFromType(type);
checkPropTypes(propTypes, element.props, "prop", name, element);
} else if (type.PropTypes !== void 0 && !propTypesMisspellWarningShown) {
propTypesMisspellWarningShown = true;
var _name = getComponentNameFromType(type);
error("Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?", _name || "Unknown");
}
if (typeof type.getDefaultProps === "function" && !type.getDefaultProps.isReactClassApproved) {
error("getDefaultProps is only used on classic React.createClass definitions. Use a static property named `defaultProps` instead.");
}
}
}
function validateFragmentProps(fragment) {
{
var keys = Object.keys(fragment.props);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key !== "children" && key !== "key") {
setCurrentlyValidatingElement$1(fragment);
error("Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.", key);
setCurrentlyValidatingElement$1(null);
break;
}
}
if (fragment.ref !== null) {
setCurrentlyValidatingElement$1(fragment);
error("Invalid attribute `ref` supplied to `React.Fragment`.");
setCurrentlyValidatingElement$1(null);
}
}
}
var didWarnAboutKeySpread = {};
function jsxWithValidation(type, props, key, isStaticChildren, source, self) {
{
var validType = isValidElementType(type);
if (!validType) {
var info = "";
if (type === void 0 || typeof type === "object" && type !== null && Object.keys(type).length === 0) {
info += " You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.";
}
var sourceInfo = getSourceInfoErrorAddendum(source);
if (sourceInfo) {
info += sourceInfo;
} else {
info += getDeclarationErrorAddendum();
}
var typeString;
if (type === null) {
typeString = "null";
} else if (isArray(type)) {
typeString = "array";
} else if (type !== void 0 && type.$$typeof === REACT_ELEMENT_TYPE) {
typeString = "<" + (getComponentNameFromType(type.type) || "Unknown") + " />";
info = " Did you accidentally export a JSX literal instead of a component?";
} else {
typeString = typeof type;
}
error("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s", typeString, info);
}
var element = jsxDEV(type, props, key, source, self);
if (element == null) {
return element;
}
if (validType) {
var children = props.children;
if (children !== void 0) {
if (isStaticChildren) {
if (isArray(children)) {
for (var i = 0; i < children.length; i++) {
validateChildKeys(children[i], type);
}
if (Object.freeze) {
Object.freeze(children);
}
} else {
error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");
}
} else {
validateChildKeys(children, type);
}
}
}
{
if (hasOwnProperty.call(props, "key")) {
var componentName = getComponentNameFromType(type);
var keys = Object.keys(props).filter(function(k) {
return k !== "key";
});
var beforeExample = keys.length > 0 ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}";
if (!didWarnAboutKeySpread[componentName + beforeExample]) {
var afterExample = keys.length > 0 ? "{" + keys.join(": ..., ") + ": ...}" : "{}";
error('A props object containing a "key" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />', beforeExample, componentName, afterExample, componentName);
didWarnAboutKeySpread[componentName + beforeExample] = true;
}
}
}
if (type === REACT_FRAGMENT_TYPE) {
validateFragmentProps(element);
} else {
validatePropTypes(element);
}
return element;
}
}
var jsxDEV$1 = jsxWithValidation;
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.jsxDEV = jsxDEV$1;
})();
}
}
});
// node_modules/react/jsx-dev-runtime.js
var require_jsx_dev_runtime = __commonJS({
"node_modules/react/jsx-dev-runtime.js"(exports, module) {
if (false) {
module.exports = null;
} else {
module.exports = require_react_jsx_dev_runtime_development();
}
}
});
export default require_jsx_dev_runtime();
/*! Bundled license information:
react/cjs/react-jsx-dev-runtime.development.js:
(**
* @license React
* react-jsx-dev-runtime.development.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
*/
//# sourceMappingURL=react_jsx-dev-runtime.js.map
File diff suppressed because one or more lines are too long
+923
View File
@@ -0,0 +1,923 @@
import {
__commonJS,
require_react
} from "./chunk-2YIMICFJ.js";
// node_modules/react/cjs/react-jsx-runtime.development.js
var require_react_jsx_runtime_development = __commonJS({
"node_modules/react/cjs/react-jsx-runtime.development.js"(exports) {
"use strict";
if (true) {
(function() {
"use strict";
var React = require_react();
var REACT_ELEMENT_TYPE = Symbol.for("react.element");
var REACT_PORTAL_TYPE = Symbol.for("react.portal");
var REACT_FRAGMENT_TYPE = Symbol.for("react.fragment");
var REACT_STRICT_MODE_TYPE = Symbol.for("react.strict_mode");
var REACT_PROFILER_TYPE = Symbol.for("react.profiler");
var REACT_PROVIDER_TYPE = Symbol.for("react.provider");
var REACT_CONTEXT_TYPE = Symbol.for("react.context");
var REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
var REACT_SUSPENSE_TYPE = Symbol.for("react.suspense");
var REACT_SUSPENSE_LIST_TYPE = Symbol.for("react.suspense_list");
var REACT_MEMO_TYPE = Symbol.for("react.memo");
var REACT_LAZY_TYPE = Symbol.for("react.lazy");
var REACT_OFFSCREEN_TYPE = Symbol.for("react.offscreen");
var MAYBE_ITERATOR_SYMBOL = Symbol.iterator;
var FAUX_ITERATOR_SYMBOL = "@@iterator";
function getIteratorFn(maybeIterable) {
if (maybeIterable === null || typeof maybeIterable !== "object") {
return null;
}
var maybeIterator = MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL] || maybeIterable[FAUX_ITERATOR_SYMBOL];
if (typeof maybeIterator === "function") {
return maybeIterator;
}
return null;
}
var ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
function error(format) {
{
{
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
args[_key2 - 1] = arguments[_key2];
}
printWarning("error", format, args);
}
}
}
function printWarning(level, format, args) {
{
var ReactDebugCurrentFrame2 = ReactSharedInternals.ReactDebugCurrentFrame;
var stack = ReactDebugCurrentFrame2.getStackAddendum();
if (stack !== "") {
format += "%s";
args = args.concat([stack]);
}
var argsWithFormat = args.map(function(item) {
return String(item);
});
argsWithFormat.unshift("Warning: " + format);
Function.prototype.apply.call(console[level], console, argsWithFormat);
}
}
var enableScopeAPI = false;
var enableCacheElement = false;
var enableTransitionTracing = false;
var enableLegacyHidden = false;
var enableDebugTracing = false;
var REACT_MODULE_REFERENCE;
{
REACT_MODULE_REFERENCE = Symbol.for("react.module.reference");
}
function isValidElementType(type) {
if (typeof type === "string" || typeof type === "function") {
return true;
}
if (type === REACT_FRAGMENT_TYPE || type === REACT_PROFILER_TYPE || enableDebugTracing || type === REACT_STRICT_MODE_TYPE || type === REACT_SUSPENSE_TYPE || type === REACT_SUSPENSE_LIST_TYPE || enableLegacyHidden || type === REACT_OFFSCREEN_TYPE || enableScopeAPI || enableCacheElement || enableTransitionTracing) {
return true;
}
if (typeof type === "object" && type !== null) {
if (type.$$typeof === REACT_LAZY_TYPE || type.$$typeof === REACT_MEMO_TYPE || type.$$typeof === REACT_PROVIDER_TYPE || type.$$typeof === REACT_CONTEXT_TYPE || type.$$typeof === REACT_FORWARD_REF_TYPE || // This needs to include all possible module reference object
// types supported by any Flight configuration anywhere since
// we don't know which Flight build this will end up being used
// with.
type.$$typeof === REACT_MODULE_REFERENCE || type.getModuleId !== void 0) {
return true;
}
}
return false;
}
function getWrappedName(outerType, innerType, wrapperName) {
var displayName = outerType.displayName;
if (displayName) {
return displayName;
}
var functionName = innerType.displayName || innerType.name || "";
return functionName !== "" ? wrapperName + "(" + functionName + ")" : wrapperName;
}
function getContextName(type) {
return type.displayName || "Context";
}
function getComponentNameFromType(type) {
if (type == null) {
return null;
}
{
if (typeof type.tag === "number") {
error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue.");
}
}
if (typeof type === "function") {
return type.displayName || type.name || null;
}
if (typeof type === "string") {
return type;
}
switch (type) {
case REACT_FRAGMENT_TYPE:
return "Fragment";
case REACT_PORTAL_TYPE:
return "Portal";
case REACT_PROFILER_TYPE:
return "Profiler";
case REACT_STRICT_MODE_TYPE:
return "StrictMode";
case REACT_SUSPENSE_TYPE:
return "Suspense";
case REACT_SUSPENSE_LIST_TYPE:
return "SuspenseList";
}
if (typeof type === "object") {
switch (type.$$typeof) {
case REACT_CONTEXT_TYPE:
var context = type;
return getContextName(context) + ".Consumer";
case REACT_PROVIDER_TYPE:
var provider = type;
return getContextName(provider._context) + ".Provider";
case REACT_FORWARD_REF_TYPE:
return getWrappedName(type, type.render, "ForwardRef");
case REACT_MEMO_TYPE:
var outerName = type.displayName || null;
if (outerName !== null) {
return outerName;
}
return getComponentNameFromType(type.type) || "Memo";
case REACT_LAZY_TYPE: {
var lazyComponent = type;
var payload = lazyComponent._payload;
var init = lazyComponent._init;
try {
return getComponentNameFromType(init(payload));
} catch (x) {
return null;
}
}
}
}
return null;
}
var assign = Object.assign;
var disabledDepth = 0;
var prevLog;
var prevInfo;
var prevWarn;
var prevError;
var prevGroup;
var prevGroupCollapsed;
var prevGroupEnd;
function disabledLog() {
}
disabledLog.__reactDisabledLog = true;
function disableLogs() {
{
if (disabledDepth === 0) {
prevLog = console.log;
prevInfo = console.info;
prevWarn = console.warn;
prevError = console.error;
prevGroup = console.group;
prevGroupCollapsed = console.groupCollapsed;
prevGroupEnd = console.groupEnd;
var props = {
configurable: true,
enumerable: true,
value: disabledLog,
writable: true
};
Object.defineProperties(console, {
info: props,
log: props,
warn: props,
error: props,
group: props,
groupCollapsed: props,
groupEnd: props
});
}
disabledDepth++;
}
}
function reenableLogs() {
{
disabledDepth--;
if (disabledDepth === 0) {
var props = {
configurable: true,
enumerable: true,
writable: true
};
Object.defineProperties(console, {
log: assign({}, props, {
value: prevLog
}),
info: assign({}, props, {
value: prevInfo
}),
warn: assign({}, props, {
value: prevWarn
}),
error: assign({}, props, {
value: prevError
}),
group: assign({}, props, {
value: prevGroup
}),
groupCollapsed: assign({}, props, {
value: prevGroupCollapsed
}),
groupEnd: assign({}, props, {
value: prevGroupEnd
})
});
}
if (disabledDepth < 0) {
error("disabledDepth fell below zero. This is a bug in React. Please file an issue.");
}
}
}
var ReactCurrentDispatcher = ReactSharedInternals.ReactCurrentDispatcher;
var prefix;
function describeBuiltInComponentFrame(name, source, ownerFn) {
{
if (prefix === void 0) {
try {
throw Error();
} catch (x) {
var match = x.stack.trim().match(/\n( *(at )?)/);
prefix = match && match[1] || "";
}
}
return "\n" + prefix + name;
}
}
var reentry = false;
var componentFrameCache;
{
var PossiblyWeakMap = typeof WeakMap === "function" ? WeakMap : Map;
componentFrameCache = new PossiblyWeakMap();
}
function describeNativeComponentFrame(fn, construct) {
if (!fn || reentry) {
return "";
}
{
var frame = componentFrameCache.get(fn);
if (frame !== void 0) {
return frame;
}
}
var control;
reentry = true;
var previousPrepareStackTrace = Error.prepareStackTrace;
Error.prepareStackTrace = void 0;
var previousDispatcher;
{
previousDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = null;
disableLogs();
}
try {
if (construct) {
var Fake = function() {
throw Error();
};
Object.defineProperty(Fake.prototype, "props", {
set: function() {
throw Error();
}
});
if (typeof Reflect === "object" && Reflect.construct) {
try {
Reflect.construct(Fake, []);
} catch (x) {
control = x;
}
Reflect.construct(fn, [], Fake);
} else {
try {
Fake.call();
} catch (x) {
control = x;
}
fn.call(Fake.prototype);
}
} else {
try {
throw Error();
} catch (x) {
control = x;
}
fn();
}
} catch (sample) {
if (sample && control && typeof sample.stack === "string") {
var sampleLines = sample.stack.split("\n");
var controlLines = control.stack.split("\n");
var s = sampleLines.length - 1;
var c = controlLines.length - 1;
while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
c--;
}
for (; s >= 1 && c >= 0; s--, c--) {
if (sampleLines[s] !== controlLines[c]) {
if (s !== 1 || c !== 1) {
do {
s--;
c--;
if (c < 0 || sampleLines[s] !== controlLines[c]) {
var _frame = "\n" + sampleLines[s].replace(" at new ", " at ");
if (fn.displayName && _frame.includes("<anonymous>")) {
_frame = _frame.replace("<anonymous>", fn.displayName);
}
{
if (typeof fn === "function") {
componentFrameCache.set(fn, _frame);
}
}
return _frame;
}
} while (s >= 1 && c >= 0);
}
break;
}
}
}
} finally {
reentry = false;
{
ReactCurrentDispatcher.current = previousDispatcher;
reenableLogs();
}
Error.prepareStackTrace = previousPrepareStackTrace;
}
var name = fn ? fn.displayName || fn.name : "";
var syntheticFrame = name ? describeBuiltInComponentFrame(name) : "";
{
if (typeof fn === "function") {
componentFrameCache.set(fn, syntheticFrame);
}
}
return syntheticFrame;
}
function describeFunctionComponentFrame(fn, source, ownerFn) {
{
return describeNativeComponentFrame(fn, false);
}
}
function shouldConstruct(Component) {
var prototype = Component.prototype;
return !!(prototype && prototype.isReactComponent);
}
function describeUnknownElementTypeFrameInDEV(type, source, ownerFn) {
if (type == null) {
return "";
}
if (typeof type === "function") {
{
return describeNativeComponentFrame(type, shouldConstruct(type));
}
}
if (typeof type === "string") {
return describeBuiltInComponentFrame(type);
}
switch (type) {
case REACT_SUSPENSE_TYPE:
return describeBuiltInComponentFrame("Suspense");
case REACT_SUSPENSE_LIST_TYPE:
return describeBuiltInComponentFrame("SuspenseList");
}
if (typeof type === "object") {
switch (type.$$typeof) {
case REACT_FORWARD_REF_TYPE:
return describeFunctionComponentFrame(type.render);
case REACT_MEMO_TYPE:
return describeUnknownElementTypeFrameInDEV(type.type, source, ownerFn);
case REACT_LAZY_TYPE: {
var lazyComponent = type;
var payload = lazyComponent._payload;
var init = lazyComponent._init;
try {
return describeUnknownElementTypeFrameInDEV(init(payload), source, ownerFn);
} catch (x) {
}
}
}
}
return "";
}
var hasOwnProperty = Object.prototype.hasOwnProperty;
var loggedTypeFailures = {};
var ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
function setCurrentlyValidatingElement(element) {
{
if (element) {
var owner = element._owner;
var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
ReactDebugCurrentFrame.setExtraStackFrame(stack);
} else {
ReactDebugCurrentFrame.setExtraStackFrame(null);
}
}
}
function checkPropTypes(typeSpecs, values, location, componentName, element) {
{
var has = Function.call.bind(hasOwnProperty);
for (var typeSpecName in typeSpecs) {
if (has(typeSpecs, typeSpecName)) {
var error$1 = void 0;
try {
if (typeof typeSpecs[typeSpecName] !== "function") {
var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
err.name = "Invariant Violation";
throw err;
}
error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
} catch (ex) {
error$1 = ex;
}
if (error$1 && !(error$1 instanceof Error)) {
setCurrentlyValidatingElement(element);
error("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1);
setCurrentlyValidatingElement(null);
}
if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
loggedTypeFailures[error$1.message] = true;
setCurrentlyValidatingElement(element);
error("Failed %s type: %s", location, error$1.message);
setCurrentlyValidatingElement(null);
}
}
}
}
}
var isArrayImpl = Array.isArray;
function isArray(a) {
return isArrayImpl(a);
}
function typeName(value) {
{
var hasToStringTag = typeof Symbol === "function" && Symbol.toStringTag;
var type = hasToStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
return type;
}
}
function willCoercionThrow(value) {
{
try {
testStringCoercion(value);
return false;
} catch (e) {
return true;
}
}
}
function testStringCoercion(value) {
return "" + value;
}
function checkKeyStringCoercion(value) {
{
if (willCoercionThrow(value)) {
error("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.", typeName(value));
return testStringCoercion(value);
}
}
}
var ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
var RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true
};
var specialPropKeyWarningShown;
var specialPropRefWarningShown;
var didWarnAboutStringRefs;
{
didWarnAboutStringRefs = {};
}
function hasValidRef(config) {
{
if (hasOwnProperty.call(config, "ref")) {
var getter = Object.getOwnPropertyDescriptor(config, "ref").get;
if (getter && getter.isReactWarning) {
return false;
}
}
}
return config.ref !== void 0;
}
function hasValidKey(config) {
{
if (hasOwnProperty.call(config, "key")) {
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
if (getter && getter.isReactWarning) {
return false;
}
}
}
return config.key !== void 0;
}
function warnIfStringRefCannotBeAutoConverted(config, self) {
{
if (typeof config.ref === "string" && ReactCurrentOwner.current && self && ReactCurrentOwner.current.stateNode !== self) {
var componentName = getComponentNameFromType(ReactCurrentOwner.current.type);
if (!didWarnAboutStringRefs[componentName]) {
error('Component "%s" contains the string ref "%s". Support for string refs will be removed in a future major release. This case cannot be automatically converted to an arrow function. We ask you to manually fix this case by using useRef() or createRef() instead. Learn more about using refs safely here: https://reactjs.org/link/strict-mode-string-ref', getComponentNameFromType(ReactCurrentOwner.current.type), config.ref);
didWarnAboutStringRefs[componentName] = true;
}
}
}
}
function defineKeyPropWarningGetter(props, displayName) {
{
var warnAboutAccessingKey = function() {
if (!specialPropKeyWarningShown) {
specialPropKeyWarningShown = true;
error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName);
}
};
warnAboutAccessingKey.isReactWarning = true;
Object.defineProperty(props, "key", {
get: warnAboutAccessingKey,
configurable: true
});
}
}
function defineRefPropWarningGetter(props, displayName) {
{
var warnAboutAccessingRef = function() {
if (!specialPropRefWarningShown) {
specialPropRefWarningShown = true;
error("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", displayName);
}
};
warnAboutAccessingRef.isReactWarning = true;
Object.defineProperty(props, "ref", {
get: warnAboutAccessingRef,
configurable: true
});
}
}
var ReactElement = function(type, key, ref, self, source, owner, props) {
var element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type,
key,
ref,
props,
// Record the component responsible for creating this element.
_owner: owner
};
{
element._store = {};
Object.defineProperty(element._store, "validated", {
configurable: false,
enumerable: false,
writable: true,
value: false
});
Object.defineProperty(element, "_self", {
configurable: false,
enumerable: false,
writable: false,
value: self
});
Object.defineProperty(element, "_source", {
configurable: false,
enumerable: false,
writable: false,
value: source
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
function jsxDEV(type, config, maybeKey, source, self) {
{
var propName;
var props = {};
var key = null;
var ref = null;
if (maybeKey !== void 0) {
{
checkKeyStringCoercion(maybeKey);
}
key = "" + maybeKey;
}
if (hasValidKey(config)) {
{
checkKeyStringCoercion(config.key);
}
key = "" + config.key;
}
if (hasValidRef(config)) {
ref = config.ref;
warnIfStringRefCannotBeAutoConverted(config, self);
}
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === void 0) {
props[propName] = defaultProps[propName];
}
}
}
if (key || ref) {
var displayName = typeof type === "function" ? type.displayName || type.name || "Unknown" : type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
}
var ReactCurrentOwner$1 = ReactSharedInternals.ReactCurrentOwner;
var ReactDebugCurrentFrame$1 = ReactSharedInternals.ReactDebugCurrentFrame;
function setCurrentlyValidatingElement$1(element) {
{
if (element) {
var owner = element._owner;
var stack = describeUnknownElementTypeFrameInDEV(element.type, element._source, owner ? owner.type : null);
ReactDebugCurrentFrame$1.setExtraStackFrame(stack);
} else {
ReactDebugCurrentFrame$1.setExtraStackFrame(null);
}
}
}
var propTypesMisspellWarningShown;
{
propTypesMisspellWarningShown = false;
}
function isValidElement(object) {
{
return typeof object === "object" && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
}
}
function getDeclarationErrorAddendum() {
{
if (ReactCurrentOwner$1.current) {
var name = getComponentNameFromType(ReactCurrentOwner$1.current.type);
if (name) {
return "\n\nCheck the render method of `" + name + "`.";
}
}
return "";
}
}
function getSourceInfoErrorAddendum(source) {
{
if (source !== void 0) {
var fileName = source.fileName.replace(/^.*[\\\/]/, "");
var lineNumber = source.lineNumber;
return "\n\nCheck your code at " + fileName + ":" + lineNumber + ".";
}
return "";
}
}
var ownerHasKeyUseWarning = {};
function getCurrentComponentErrorInfo(parentType) {
{
var info = getDeclarationErrorAddendum();
if (!info) {
var parentName = typeof parentType === "string" ? parentType : parentType.displayName || parentType.name;
if (parentName) {
info = "\n\nCheck the top-level render call using <" + parentName + ">.";
}
}
return info;
}
}
function validateExplicitKey(element, parentType) {
{
if (!element._store || element._store.validated || element.key != null) {
return;
}
element._store.validated = true;
var currentComponentErrorInfo = getCurrentComponentErrorInfo(parentType);
if (ownerHasKeyUseWarning[currentComponentErrorInfo]) {
return;
}
ownerHasKeyUseWarning[currentComponentErrorInfo] = true;
var childOwner = "";
if (element && element._owner && element._owner !== ReactCurrentOwner$1.current) {
childOwner = " It was passed a child from " + getComponentNameFromType(element._owner.type) + ".";
}
setCurrentlyValidatingElement$1(element);
error('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.', currentComponentErrorInfo, childOwner);
setCurrentlyValidatingElement$1(null);
}
}
function validateChildKeys(node, parentType) {
{
if (typeof node !== "object") {
return;
}
if (isArray(node)) {
for (var i = 0; i < node.length; i++) {
var child = node[i];
if (isValidElement(child)) {
validateExplicitKey(child, parentType);
}
}
} else if (isValidElement(node)) {
if (node._store) {
node._store.validated = true;
}
} else if (node) {
var iteratorFn = getIteratorFn(node);
if (typeof iteratorFn === "function") {
if (iteratorFn !== node.entries) {
var iterator = iteratorFn.call(node);
var step;
while (!(step = iterator.next()).done) {
if (isValidElement(step.value)) {
validateExplicitKey(step.value, parentType);
}
}
}
}
}
}
}
function validatePropTypes(element) {
{
var type = element.type;
if (type === null || type === void 0 || typeof type === "string") {
return;
}
var propTypes;
if (typeof type === "function") {
propTypes = type.propTypes;
} else if (typeof type === "object" && (type.$$typeof === REACT_FORWARD_REF_TYPE || // Note: Memo only checks outer props here.
// Inner props are checked in the reconciler.
type.$$typeof === REACT_MEMO_TYPE)) {
propTypes = type.propTypes;
} else {
return;
}
if (propTypes) {
var name = getComponentNameFromType(type);
checkPropTypes(propTypes, element.props, "prop", name, element);
} else if (type.PropTypes !== void 0 && !propTypesMisspellWarningShown) {
propTypesMisspellWarningShown = true;
var _name = getComponentNameFromType(type);
error("Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?", _name || "Unknown");
}
if (typeof type.getDefaultProps === "function" && !type.getDefaultProps.isReactClassApproved) {
error("getDefaultProps is only used on classic React.createClass definitions. Use a static property named `defaultProps` instead.");
}
}
}
function validateFragmentProps(fragment) {
{
var keys = Object.keys(fragment.props);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key !== "children" && key !== "key") {
setCurrentlyValidatingElement$1(fragment);
error("Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.", key);
setCurrentlyValidatingElement$1(null);
break;
}
}
if (fragment.ref !== null) {
setCurrentlyValidatingElement$1(fragment);
error("Invalid attribute `ref` supplied to `React.Fragment`.");
setCurrentlyValidatingElement$1(null);
}
}
}
var didWarnAboutKeySpread = {};
function jsxWithValidation(type, props, key, isStaticChildren, source, self) {
{
var validType = isValidElementType(type);
if (!validType) {
var info = "";
if (type === void 0 || typeof type === "object" && type !== null && Object.keys(type).length === 0) {
info += " You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.";
}
var sourceInfo = getSourceInfoErrorAddendum(source);
if (sourceInfo) {
info += sourceInfo;
} else {
info += getDeclarationErrorAddendum();
}
var typeString;
if (type === null) {
typeString = "null";
} else if (isArray(type)) {
typeString = "array";
} else if (type !== void 0 && type.$$typeof === REACT_ELEMENT_TYPE) {
typeString = "<" + (getComponentNameFromType(type.type) || "Unknown") + " />";
info = " Did you accidentally export a JSX literal instead of a component?";
} else {
typeString = typeof type;
}
error("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s", typeString, info);
}
var element = jsxDEV(type, props, key, source, self);
if (element == null) {
return element;
}
if (validType) {
var children = props.children;
if (children !== void 0) {
if (isStaticChildren) {
if (isArray(children)) {
for (var i = 0; i < children.length; i++) {
validateChildKeys(children[i], type);
}
if (Object.freeze) {
Object.freeze(children);
}
} else {
error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");
}
} else {
validateChildKeys(children, type);
}
}
}
{
if (hasOwnProperty.call(props, "key")) {
var componentName = getComponentNameFromType(type);
var keys = Object.keys(props).filter(function(k) {
return k !== "key";
});
var beforeExample = keys.length > 0 ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}";
if (!didWarnAboutKeySpread[componentName + beforeExample]) {
var afterExample = keys.length > 0 ? "{" + keys.join(": ..., ") + ": ...}" : "{}";
error('A props object containing a "key" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />', beforeExample, componentName, afterExample, componentName);
didWarnAboutKeySpread[componentName + beforeExample] = true;
}
}
}
if (type === REACT_FRAGMENT_TYPE) {
validateFragmentProps(element);
} else {
validatePropTypes(element);
}
return element;
}
}
function jsxWithValidationStatic(type, props, key) {
{
return jsxWithValidation(type, props, key, true);
}
}
function jsxWithValidationDynamic(type, props, key) {
{
return jsxWithValidation(type, props, key, false);
}
}
var jsx = jsxWithValidationDynamic;
var jsxs = jsxWithValidationStatic;
exports.Fragment = REACT_FRAGMENT_TYPE;
exports.jsx = jsx;
exports.jsxs = jsxs;
})();
}
}
});
// node_modules/react/jsx-runtime.js
var require_jsx_runtime = __commonJS({
"node_modules/react/jsx-runtime.js"(exports, module) {
if (false) {
module.exports = null;
} else {
module.exports = require_react_jsx_runtime_development();
}
}
});
export default require_jsx_runtime();
/*! Bundled license information:
react/cjs/react-jsx-runtime.development.js:
(**
* @license React
* react-jsx-runtime.development.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
*/
//# sourceMappingURL=react_jsx-runtime.js.map
File diff suppressed because one or more lines are too long
+34375
View File
File diff suppressed because it is too large Load Diff
+7
View File
File diff suppressed because one or more lines are too long
+1141
View File
File diff suppressed because it is too large Load Diff
+7
View File
File diff suppressed because one or more lines are too long
+128
View File
@@ -0,0 +1,128 @@
declare namespace QuickLRU {
interface Options<KeyType, ValueType> {
/**
The maximum number of milliseconds an item should remain in the cache.
@default Infinity
By default, `maxAge` will be `Infinity`, which means that items will never expire.
Lazy expiration upon the next write or read call.
Individual expiration of an item can be specified by the `set(key, value, maxAge)` method.
*/
readonly maxAge?: number;
/**
The maximum number of items before evicting the least recently used items.
*/
readonly maxSize: number;
/**
Called right before an item is evicted from the cache.
Useful for side effects or for items like object URLs that need explicit cleanup (`revokeObjectURL`).
*/
onEviction?: (key: KeyType, value: ValueType) => void;
}
}
declare class QuickLRU<KeyType, ValueType>
implements Iterable<[KeyType, ValueType]> {
/**
The stored item count.
*/
readonly size: number;
/**
Simple ["Least Recently Used" (LRU) cache](https://en.m.wikipedia.org/wiki/Cache_replacement_policies#Least_Recently_Used_.28LRU.29).
The instance is [`iterable`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Iteration_protocols) so you can use it directly in a [`for…of`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/for...of) loop.
@example
```
import QuickLRU = require('quick-lru');
const lru = new QuickLRU({maxSize: 1000});
lru.set('🦄', '🌈');
lru.has('🦄');
//=> true
lru.get('🦄');
//=> '🌈'
```
*/
constructor(options: QuickLRU.Options<KeyType, ValueType>);
[Symbol.iterator](): IterableIterator<[KeyType, ValueType]>;
/**
Set an item. Returns the instance.
Individual expiration of an item can be specified with the `maxAge` option. If not specified, the global `maxAge` value will be used in case it is specified in the constructor, otherwise the item will never expire.
@returns The list instance.
*/
set(key: KeyType, value: ValueType, options?: {maxAge?: number}): this;
/**
Get an item.
@returns The stored item or `undefined`.
*/
get(key: KeyType): ValueType | undefined;
/**
Check if an item exists.
*/
has(key: KeyType): boolean;
/**
Get an item without marking it as recently used.
@returns The stored item or `undefined`.
*/
peek(key: KeyType): ValueType | undefined;
/**
Delete an item.
@returns `true` if the item is removed or `false` if the item doesn't exist.
*/
delete(key: KeyType): boolean;
/**
Delete all items.
*/
clear(): void;
/**
Update the `maxSize` in-place, discarding items as necessary. Insertion order is mostly preserved, though this is not a strong guarantee.
Useful for on-the-fly tuning of cache sizes in live systems.
*/
resize(maxSize: number): void;
/**
Iterable for all the keys.
*/
keys(): IterableIterator<KeyType>;
/**
Iterable for all the values.
*/
values(): IterableIterator<ValueType>;
/**
Iterable for all entries, starting with the oldest (ascending in recency).
*/
entriesAscending(): IterableIterator<[KeyType, ValueType]>;
/**
Iterable for all entries, starting with the newest (descending in recency).
*/
entriesDescending(): IterableIterator<[KeyType, ValueType]>;
}
export = QuickLRU;
+263
View File
@@ -0,0 +1,263 @@
'use strict';
class QuickLRU {
constructor(options = {}) {
if (!(options.maxSize && options.maxSize > 0)) {
throw new TypeError('`maxSize` must be a number greater than 0');
}
if (typeof options.maxAge === 'number' && options.maxAge === 0) {
throw new TypeError('`maxAge` must be a number greater than 0');
}
this.maxSize = options.maxSize;
this.maxAge = options.maxAge || Infinity;
this.onEviction = options.onEviction;
this.cache = new Map();
this.oldCache = new Map();
this._size = 0;
}
_emitEvictions(cache) {
if (typeof this.onEviction !== 'function') {
return;
}
for (const [key, item] of cache) {
this.onEviction(key, item.value);
}
}
_deleteIfExpired(key, item) {
if (typeof item.expiry === 'number' && item.expiry <= Date.now()) {
if (typeof this.onEviction === 'function') {
this.onEviction(key, item.value);
}
return this.delete(key);
}
return false;
}
_getOrDeleteIfExpired(key, item) {
const deleted = this._deleteIfExpired(key, item);
if (deleted === false) {
return item.value;
}
}
_getItemValue(key, item) {
return item.expiry ? this._getOrDeleteIfExpired(key, item) : item.value;
}
_peek(key, cache) {
const item = cache.get(key);
return this._getItemValue(key, item);
}
_set(key, value) {
this.cache.set(key, value);
this._size++;
if (this._size >= this.maxSize) {
this._size = 0;
this._emitEvictions(this.oldCache);
this.oldCache = this.cache;
this.cache = new Map();
}
}
_moveToRecent(key, item) {
this.oldCache.delete(key);
this._set(key, item);
}
* _entriesAscending() {
for (const item of this.oldCache) {
const [key, value] = item;
if (!this.cache.has(key)) {
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield item;
}
}
}
for (const item of this.cache) {
const [key, value] = item;
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield item;
}
}
}
get(key) {
if (this.cache.has(key)) {
const item = this.cache.get(key);
return this._getItemValue(key, item);
}
if (this.oldCache.has(key)) {
const item = this.oldCache.get(key);
if (this._deleteIfExpired(key, item) === false) {
this._moveToRecent(key, item);
return item.value;
}
}
}
set(key, value, {maxAge = this.maxAge === Infinity ? undefined : Date.now() + this.maxAge} = {}) {
if (this.cache.has(key)) {
this.cache.set(key, {
value,
maxAge
});
} else {
this._set(key, {value, expiry: maxAge});
}
}
has(key) {
if (this.cache.has(key)) {
return !this._deleteIfExpired(key, this.cache.get(key));
}
if (this.oldCache.has(key)) {
return !this._deleteIfExpired(key, this.oldCache.get(key));
}
return false;
}
peek(key) {
if (this.cache.has(key)) {
return this._peek(key, this.cache);
}
if (this.oldCache.has(key)) {
return this._peek(key, this.oldCache);
}
}
delete(key) {
const deleted = this.cache.delete(key);
if (deleted) {
this._size--;
}
return this.oldCache.delete(key) || deleted;
}
clear() {
this.cache.clear();
this.oldCache.clear();
this._size = 0;
}
resize(newSize) {
if (!(newSize && newSize > 0)) {
throw new TypeError('`maxSize` must be a number greater than 0');
}
const items = [...this._entriesAscending()];
const removeCount = items.length - newSize;
if (removeCount < 0) {
this.cache = new Map(items);
this.oldCache = new Map();
this._size = items.length;
} else {
if (removeCount > 0) {
this._emitEvictions(items.slice(0, removeCount));
}
this.oldCache = new Map(items.slice(removeCount));
this.cache = new Map();
this._size = 0;
}
this.maxSize = newSize;
}
* keys() {
for (const [key] of this) {
yield key;
}
}
* values() {
for (const [, value] of this) {
yield value;
}
}
* [Symbol.iterator]() {
for (const item of this.cache) {
const [key, value] = item;
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield [key, value.value];
}
}
for (const item of this.oldCache) {
const [key, value] = item;
if (!this.cache.has(key)) {
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield [key, value.value];
}
}
}
}
* entriesDescending() {
let items = [...this.cache];
for (let i = items.length - 1; i >= 0; --i) {
const item = items[i];
const [key, value] = item;
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield [key, value.value];
}
}
items = [...this.oldCache];
for (let i = items.length - 1; i >= 0; --i) {
const item = items[i];
const [key, value] = item;
if (!this.cache.has(key)) {
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield [key, value.value];
}
}
}
}
* entriesAscending() {
for (const [key, value] of this._entriesAscending()) {
yield [key, value.value];
}
}
get size() {
if (!this._size) {
return this.oldCache.size;
}
let oldCacheSize = 0;
for (const key of this.oldCache.keys()) {
if (!this.cache.has(key)) {
oldCacheSize++;
}
}
return Math.min(this._size + oldCacheSize, this.maxSize);
}
}
module.exports = QuickLRU;
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
{
"name": "@alloc/quick-lru",
"version": "5.2.0",
"description": "Simple “Least Recently Used” (LRU) cache",
"license": "MIT",
"repository": "sindresorhus/quick-lru",
"funding": "https://github.com/sponsors/sindresorhus",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"engines": {
"node": ">=10"
},
"scripts": {
"test": "xo && nyc ava && tsd"
},
"files": [
"index.js",
"index.d.ts"
],
"keywords": [
"lru",
"quick",
"cache",
"caching",
"least",
"recently",
"used",
"fast",
"map",
"hash",
"buffer"
],
"devDependencies": {
"ava": "^2.0.0",
"coveralls": "^3.0.3",
"nyc": "^15.0.0",
"tsd": "^0.11.0",
"xo": "^0.26.0"
}
}
+139
View File
@@ -0,0 +1,139 @@
# quick-lru [![Build Status](https://travis-ci.org/sindresorhus/quick-lru.svg?branch=master)](https://travis-ci.org/sindresorhus/quick-lru) [![Coverage Status](https://coveralls.io/repos/github/sindresorhus/quick-lru/badge.svg?branch=master)](https://coveralls.io/github/sindresorhus/quick-lru?branch=master)
> Simple [“Least Recently Used” (LRU) cache](https://en.m.wikipedia.org/wiki/Cache_replacement_policies#Least_Recently_Used_.28LRU.29)
Useful when you need to cache something and limit memory usage.
Inspired by the [`hashlru` algorithm](https://github.com/dominictarr/hashlru#algorithm), but instead uses [`Map`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map) to support keys of any type, not just strings, and values can be `undefined`.
## Install
```
$ npm install quick-lru
```
## Usage
```js
const QuickLRU = require('quick-lru');
const lru = new QuickLRU({maxSize: 1000});
lru.set('🦄', '🌈');
lru.has('🦄');
//=> true
lru.get('🦄');
//=> '🌈'
```
## API
### new QuickLRU(options?)
Returns a new instance.
### options
Type: `object`
#### maxSize
*Required*\
Type: `number`
The maximum number of items before evicting the least recently used items.
#### maxAge
Type: `number`\
Default: `Infinity`
The maximum number of milliseconds an item should remain in cache.
By default maxAge will be Infinity, which means that items will never expire.
Lazy expiration happens upon the next `write` or `read` call.
Individual expiration of an item can be specified by the `set(key, value, options)` method.
#### onEviction
*Optional*\
Type: `(key, value) => void`
Called right before an item is evicted from the cache.
Useful for side effects or for items like object URLs that need explicit cleanup (`revokeObjectURL`).
### Instance
The instance is [`iterable`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Iteration_protocols) so you can use it directly in a [`for…of`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/for...of) loop.
Both `key` and `value` can be of any type.
#### .set(key, value, options?)
Set an item. Returns the instance.
Individual expiration of an item can be specified with the `maxAge` option. If not specified, the global `maxAge` value will be used in case it is specified on the constructor, otherwise the item will never expire.
#### .get(key)
Get an item.
#### .has(key)
Check if an item exists.
#### .peek(key)
Get an item without marking it as recently used.
#### .delete(key)
Delete an item.
Returns `true` if the item is removed or `false` if the item doesn't exist.
#### .clear()
Delete all items.
#### .resize(maxSize)
Update the `maxSize`, discarding items as necessary. Insertion order is mostly preserved, though this is not a strong guarantee.
Useful for on-the-fly tuning of cache sizes in live systems.
#### .keys()
Iterable for all the keys.
#### .values()
Iterable for all the values.
#### .entriesAscending()
Iterable for all entries, starting with the oldest (ascending in recency).
#### .entriesDescending()
Iterable for all entries, starting with the newest (descending in recency).
#### .size
The stored item count.
---
<div align="center">
<b>
<a href="https://tidelift.com/subscription/pkg/npm-quick-lru?utm_source=npm-quick-lru&utm_medium=referral&utm_campaign=readme">Get professional support for this package with a Tidelift subscription</a>
</b>
<br>
<sub>
Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
</sub>
</div>
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+19
View File
@@ -0,0 +1,19 @@
# @babel/code-frame
> Generate errors that contain a code frame that point to source locations.
See our website [@babel/code-frame](https://babeljs.io/docs/babel-code-frame) for more information.
## Install
Using npm:
```sh
npm install --save-dev @babel/code-frame
```
or using yarn:
```sh
yarn add @babel/code-frame --dev
```
+216
View File
@@ -0,0 +1,216 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var picocolors = require('picocolors');
var jsTokens = require('js-tokens');
var helperValidatorIdentifier = require('@babel/helper-validator-identifier');
function isColorSupported() {
return (typeof process === "object" && (process.env.FORCE_COLOR === "0" || process.env.FORCE_COLOR === "false") ? false : picocolors.isColorSupported
);
}
const compose = (f, g) => v => f(g(v));
function buildDefs(colors) {
return {
keyword: colors.cyan,
capitalized: colors.yellow,
jsxIdentifier: colors.yellow,
punctuator: colors.yellow,
number: colors.magenta,
string: colors.green,
regex: colors.magenta,
comment: colors.gray,
invalid: compose(compose(colors.white, colors.bgRed), colors.bold),
gutter: colors.gray,
marker: compose(colors.red, colors.bold),
message: compose(colors.red, colors.bold),
reset: colors.reset
};
}
const defsOn = buildDefs(picocolors.createColors(true));
const defsOff = buildDefs(picocolors.createColors(false));
function getDefs(enabled) {
return enabled ? defsOn : defsOff;
}
const sometimesKeywords = new Set(["as", "async", "from", "get", "of", "set"]);
const NEWLINE$1 = /\r\n|[\n\r\u2028\u2029]/;
const BRACKET = /^[()[\]{}]$/;
let tokenize;
{
const JSX_TAG = /^[a-z][\w-]*$/i;
const getTokenType = function (token, offset, text) {
if (token.type === "name") {
if (helperValidatorIdentifier.isKeyword(token.value) || helperValidatorIdentifier.isStrictReservedWord(token.value, true) || sometimesKeywords.has(token.value)) {
return "keyword";
}
if (JSX_TAG.test(token.value) && (text[offset - 1] === "<" || text.slice(offset - 2, offset) === "</")) {
return "jsxIdentifier";
}
if (token.value[0] !== token.value[0].toLowerCase()) {
return "capitalized";
}
}
if (token.type === "punctuator" && BRACKET.test(token.value)) {
return "bracket";
}
if (token.type === "invalid" && (token.value === "@" || token.value === "#")) {
return "punctuator";
}
return token.type;
};
tokenize = function* (text) {
let match;
while (match = jsTokens.default.exec(text)) {
const token = jsTokens.matchToToken(match);
yield {
type: getTokenType(token, match.index, text),
value: token.value
};
}
};
}
function highlight(text) {
if (text === "") return "";
const defs = getDefs(true);
let highlighted = "";
for (const {
type,
value
} of tokenize(text)) {
if (type in defs) {
highlighted += value.split(NEWLINE$1).map(str => defs[type](str)).join("\n");
} else {
highlighted += value;
}
}
return highlighted;
}
let deprecationWarningShown = false;
const NEWLINE = /\r\n|[\n\r\u2028\u2029]/;
function getMarkerLines(loc, source, opts) {
const startLoc = Object.assign({
column: 0,
line: -1
}, loc.start);
const endLoc = Object.assign({}, startLoc, loc.end);
const {
linesAbove = 2,
linesBelow = 3
} = opts || {};
const startLine = startLoc.line;
const startColumn = startLoc.column;
const endLine = endLoc.line;
const endColumn = endLoc.column;
let start = Math.max(startLine - (linesAbove + 1), 0);
let end = Math.min(source.length, endLine + linesBelow);
if (startLine === -1) {
start = 0;
}
if (endLine === -1) {
end = source.length;
}
const lineDiff = endLine - startLine;
const markerLines = {};
if (lineDiff) {
for (let i = 0; i <= lineDiff; i++) {
const lineNumber = i + startLine;
if (!startColumn) {
markerLines[lineNumber] = true;
} else if (i === 0) {
const sourceLength = source[lineNumber - 1].length;
markerLines[lineNumber] = [startColumn, sourceLength - startColumn + 1];
} else if (i === lineDiff) {
markerLines[lineNumber] = [0, endColumn];
} else {
const sourceLength = source[lineNumber - i].length;
markerLines[lineNumber] = [0, sourceLength];
}
}
} else {
if (startColumn === endColumn) {
if (startColumn) {
markerLines[startLine] = [startColumn, 0];
} else {
markerLines[startLine] = true;
}
} else {
markerLines[startLine] = [startColumn, endColumn - startColumn];
}
}
return {
start,
end,
markerLines
};
}
function codeFrameColumns(rawLines, loc, opts = {}) {
const shouldHighlight = opts.forceColor || isColorSupported() && opts.highlightCode;
const defs = getDefs(shouldHighlight);
const lines = rawLines.split(NEWLINE);
const {
start,
end,
markerLines
} = getMarkerLines(loc, lines, opts);
const hasColumns = loc.start && typeof loc.start.column === "number";
const numberMaxWidth = String(end).length;
const highlightedLines = shouldHighlight ? highlight(rawLines) : rawLines;
let frame = highlightedLines.split(NEWLINE, end).slice(start, end).map((line, index) => {
const number = start + 1 + index;
const paddedNumber = ` ${number}`.slice(-numberMaxWidth);
const gutter = ` ${paddedNumber} |`;
const hasMarker = markerLines[number];
const lastMarkerLine = !markerLines[number + 1];
if (hasMarker) {
let markerLine = "";
if (Array.isArray(hasMarker)) {
const markerSpacing = line.slice(0, Math.max(hasMarker[0] - 1, 0)).replace(/[^\t]/g, " ");
const numberOfMarkers = hasMarker[1] || 1;
markerLine = ["\n ", defs.gutter(gutter.replace(/\d/g, " ")), " ", markerSpacing, defs.marker("^").repeat(numberOfMarkers)].join("");
if (lastMarkerLine && opts.message) {
markerLine += " " + defs.message(opts.message);
}
}
return [defs.marker(">"), defs.gutter(gutter), line.length > 0 ? ` ${line}` : "", markerLine].join("");
} else {
return ` ${defs.gutter(gutter)}${line.length > 0 ? ` ${line}` : ""}`;
}
}).join("\n");
if (opts.message && !hasColumns) {
frame = `${" ".repeat(numberMaxWidth + 1)}${opts.message}\n${frame}`;
}
if (shouldHighlight) {
return defs.reset(frame);
} else {
return frame;
}
}
function index (rawLines, lineNumber, colNumber, opts = {}) {
if (!deprecationWarningShown) {
deprecationWarningShown = true;
const message = "Passing lineNumber and colNumber is deprecated to @babel/code-frame. Please use `codeFrameColumns`.";
if (process.emitWarning) {
process.emitWarning(message, "DeprecationWarning");
} else {
const deprecationError = new Error(message);
deprecationError.name = "DeprecationWarning";
console.warn(new Error(message));
}
}
colNumber = Math.max(colNumber, 0);
const location = {
start: {
column: colNumber,
line: lineNumber
}
};
return codeFrameColumns(rawLines, location, opts);
}
exports.codeFrameColumns = codeFrameColumns;
exports.default = index;
exports.highlight = highlight;
//# sourceMappingURL=index.js.map
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@babel/code-frame",
"version": "7.27.1",
"description": "Generate errors that contain a code frame that point to source locations.",
"author": "The Babel Team (https://babel.dev/team)",
"homepage": "https://babel.dev/docs/en/next/babel-code-frame",
"bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-code-frame"
},
"main": "./lib/index.js",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"devDependencies": {
"import-meta-resolve": "^4.1.0",
"strip-ansi": "^4.0.0"
},
"engines": {
"node": ">=6.9.0"
},
"type": "commonjs"
}
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+19
View File
@@ -0,0 +1,19 @@
# @babel/compat-data
> The compat-data to determine required Babel plugins
See our website [@babel/compat-data](https://babeljs.io/docs/babel-compat-data) for more information.
## Install
Using npm:
```sh
npm install --save @babel/compat-data
```
or using yarn:
```sh
yarn add @babel/compat-data
```
+2
View File
@@ -0,0 +1,2 @@
// Todo (Babel 8): remove this file as Babel 8 drop support of core-js 2
module.exports = require("./data/corejs2-built-ins.json");
+2
View File
@@ -0,0 +1,2 @@
// Todo (Babel 8): remove this file now that it is included in babel-plugin-polyfill-corejs3
module.exports = require("./data/corejs3-shipped-proposals.json");
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
[
"esnext.promise.all-settled",
"esnext.string.match-all",
"esnext.global-this"
]
+18
View File
@@ -0,0 +1,18 @@
{
"es6.module": {
"chrome": "61",
"and_chr": "61",
"edge": "16",
"firefox": "60",
"and_ff": "60",
"node": "13.2.0",
"opera": "48",
"op_mob": "45",
"safari": "10.1",
"ios": "10.3",
"samsung": "8.2",
"android": "61",
"electron": "2.0",
"ios_saf": "10.3"
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"transform-async-to-generator": [
"bugfix/transform-async-arrows-in-class"
],
"transform-parameters": [
"bugfix/transform-edge-default-parameters",
"bugfix/transform-safari-id-destructuring-collision-in-function-expression"
],
"transform-function-name": [
"bugfix/transform-edge-function-name"
],
"transform-block-scoping": [
"bugfix/transform-safari-block-shadowing",
"bugfix/transform-safari-for-shadowing"
],
"transform-template-literals": [
"bugfix/transform-tagged-template-caching"
],
"transform-optional-chaining": [
"bugfix/transform-v8-spread-parameters-in-optional-chaining"
],
"proposal-optional-chaining": [
"bugfix/transform-v8-spread-parameters-in-optional-chaining"
],
"transform-class-properties": [
"bugfix/transform-v8-static-class-fields-redefine-readonly",
"bugfix/transform-firefox-class-in-computed-class-key",
"bugfix/transform-safari-class-field-initializer-scope"
],
"proposal-class-properties": [
"bugfix/transform-v8-static-class-fields-redefine-readonly",
"bugfix/transform-firefox-class-in-computed-class-key",
"bugfix/transform-safari-class-field-initializer-scope"
]
}
+203
View File
@@ -0,0 +1,203 @@
{
"bugfix/transform-async-arrows-in-class": {
"chrome": "55",
"opera": "42",
"edge": "15",
"firefox": "52",
"safari": "11",
"node": "7.6",
"deno": "1",
"ios": "11",
"samsung": "6",
"opera_mobile": "42",
"electron": "1.6"
},
"bugfix/transform-edge-default-parameters": {
"chrome": "49",
"opera": "36",
"edge": "18",
"firefox": "52",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "36",
"electron": "0.37"
},
"bugfix/transform-edge-function-name": {
"chrome": "51",
"opera": "38",
"edge": "79",
"firefox": "53",
"safari": "10",
"node": "6.5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "41",
"electron": "1.2"
},
"bugfix/transform-safari-block-shadowing": {
"chrome": "49",
"opera": "36",
"edge": "12",
"firefox": "44",
"safari": "11",
"node": "6",
"deno": "1",
"ie": "11",
"ios": "11",
"samsung": "5",
"opera_mobile": "36",
"electron": "0.37"
},
"bugfix/transform-safari-for-shadowing": {
"chrome": "49",
"opera": "36",
"edge": "12",
"firefox": "4",
"safari": "11",
"node": "6",
"deno": "1",
"ie": "11",
"ios": "11",
"samsung": "5",
"rhino": "1.7.13",
"opera_mobile": "36",
"electron": "0.37"
},
"bugfix/transform-safari-id-destructuring-collision-in-function-expression": {
"chrome": "49",
"opera": "36",
"edge": "14",
"firefox": "2",
"safari": "16.3",
"node": "6",
"deno": "1",
"ios": "16.3",
"samsung": "5",
"opera_mobile": "36",
"electron": "0.37"
},
"bugfix/transform-tagged-template-caching": {
"chrome": "41",
"opera": "28",
"edge": "12",
"firefox": "34",
"safari": "13",
"node": "4",
"deno": "1",
"ios": "13",
"samsung": "3.4",
"rhino": "1.7.14",
"opera_mobile": "28",
"electron": "0.21"
},
"bugfix/transform-v8-spread-parameters-in-optional-chaining": {
"chrome": "91",
"opera": "77",
"edge": "91",
"firefox": "74",
"safari": "13.1",
"node": "16.9",
"deno": "1.9",
"ios": "13.4",
"samsung": "16",
"opera_mobile": "64",
"electron": "13.0"
},
"transform-optional-chaining": {
"chrome": "80",
"opera": "67",
"edge": "80",
"firefox": "74",
"safari": "13.1",
"node": "14",
"deno": "1",
"ios": "13.4",
"samsung": "13",
"rhino": "1.8",
"opera_mobile": "57",
"electron": "8.0"
},
"proposal-optional-chaining": {
"chrome": "80",
"opera": "67",
"edge": "80",
"firefox": "74",
"safari": "13.1",
"node": "14",
"deno": "1",
"ios": "13.4",
"samsung": "13",
"rhino": "1.8",
"opera_mobile": "57",
"electron": "8.0"
},
"transform-parameters": {
"chrome": "49",
"opera": "36",
"edge": "15",
"firefox": "52",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "36",
"electron": "0.37"
},
"transform-async-to-generator": {
"chrome": "55",
"opera": "42",
"edge": "15",
"firefox": "52",
"safari": "10.1",
"node": "7.6",
"deno": "1",
"ios": "10.3",
"samsung": "6",
"opera_mobile": "42",
"electron": "1.6"
},
"transform-template-literals": {
"chrome": "41",
"opera": "28",
"edge": "13",
"firefox": "34",
"safari": "9",
"node": "4",
"deno": "1",
"ios": "9",
"samsung": "3.4",
"opera_mobile": "28",
"electron": "0.21"
},
"transform-function-name": {
"chrome": "51",
"opera": "38",
"edge": "14",
"firefox": "53",
"safari": "10",
"node": "6.5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "41",
"electron": "1.2"
},
"transform-block-scoping": {
"chrome": "50",
"opera": "37",
"edge": "14",
"firefox": "53",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "37",
"electron": "1.1"
}
}
+838
View File
@@ -0,0 +1,838 @@
{
"transform-explicit-resource-management": {
"chrome": "134",
"edge": "134",
"firefox": "141",
"node": "24",
"electron": "35.0"
},
"transform-duplicate-named-capturing-groups-regex": {
"chrome": "126",
"opera": "112",
"edge": "126",
"firefox": "129",
"safari": "17.4",
"node": "23",
"ios": "17.4",
"electron": "31.0"
},
"transform-regexp-modifiers": {
"chrome": "125",
"opera": "111",
"edge": "125",
"firefox": "132",
"node": "23",
"samsung": "27",
"electron": "31.0"
},
"transform-unicode-sets-regex": {
"chrome": "112",
"opera": "98",
"edge": "112",
"firefox": "116",
"safari": "17",
"node": "20",
"deno": "1.32",
"ios": "17",
"samsung": "23",
"opera_mobile": "75",
"electron": "24.0"
},
"bugfix/transform-v8-static-class-fields-redefine-readonly": {
"chrome": "98",
"opera": "84",
"edge": "98",
"firefox": "75",
"safari": "15",
"node": "12",
"deno": "1.18",
"ios": "15",
"samsung": "11",
"opera_mobile": "52",
"electron": "17.0"
},
"bugfix/transform-firefox-class-in-computed-class-key": {
"chrome": "74",
"opera": "62",
"edge": "79",
"firefox": "126",
"safari": "16",
"node": "12",
"deno": "1",
"ios": "16",
"samsung": "11",
"opera_mobile": "53",
"electron": "6.0"
},
"bugfix/transform-safari-class-field-initializer-scope": {
"chrome": "74",
"opera": "62",
"edge": "79",
"firefox": "69",
"safari": "16",
"node": "12",
"deno": "1",
"ios": "16",
"samsung": "11",
"opera_mobile": "53",
"electron": "6.0"
},
"transform-class-static-block": {
"chrome": "94",
"opera": "80",
"edge": "94",
"firefox": "93",
"safari": "16.4",
"node": "16.11",
"deno": "1.14",
"ios": "16.4",
"samsung": "17",
"opera_mobile": "66",
"electron": "15.0"
},
"proposal-class-static-block": {
"chrome": "94",
"opera": "80",
"edge": "94",
"firefox": "93",
"safari": "16.4",
"node": "16.11",
"deno": "1.14",
"ios": "16.4",
"samsung": "17",
"opera_mobile": "66",
"electron": "15.0"
},
"transform-private-property-in-object": {
"chrome": "91",
"opera": "77",
"edge": "91",
"firefox": "90",
"safari": "15",
"node": "16.9",
"deno": "1.9",
"ios": "15",
"samsung": "16",
"opera_mobile": "64",
"electron": "13.0"
},
"proposal-private-property-in-object": {
"chrome": "91",
"opera": "77",
"edge": "91",
"firefox": "90",
"safari": "15",
"node": "16.9",
"deno": "1.9",
"ios": "15",
"samsung": "16",
"opera_mobile": "64",
"electron": "13.0"
},
"transform-class-properties": {
"chrome": "74",
"opera": "62",
"edge": "79",
"firefox": "90",
"safari": "14.1",
"node": "12",
"deno": "1",
"ios": "14.5",
"samsung": "11",
"opera_mobile": "53",
"electron": "6.0"
},
"proposal-class-properties": {
"chrome": "74",
"opera": "62",
"edge": "79",
"firefox": "90",
"safari": "14.1",
"node": "12",
"deno": "1",
"ios": "14.5",
"samsung": "11",
"opera_mobile": "53",
"electron": "6.0"
},
"transform-private-methods": {
"chrome": "84",
"opera": "70",
"edge": "84",
"firefox": "90",
"safari": "15",
"node": "14.6",
"deno": "1",
"ios": "15",
"samsung": "14",
"opera_mobile": "60",
"electron": "10.0"
},
"proposal-private-methods": {
"chrome": "84",
"opera": "70",
"edge": "84",
"firefox": "90",
"safari": "15",
"node": "14.6",
"deno": "1",
"ios": "15",
"samsung": "14",
"opera_mobile": "60",
"electron": "10.0"
},
"transform-numeric-separator": {
"chrome": "75",
"opera": "62",
"edge": "79",
"firefox": "70",
"safari": "13",
"node": "12.5",
"deno": "1",
"ios": "13",
"samsung": "11",
"rhino": "1.7.14",
"opera_mobile": "54",
"electron": "6.0"
},
"proposal-numeric-separator": {
"chrome": "75",
"opera": "62",
"edge": "79",
"firefox": "70",
"safari": "13",
"node": "12.5",
"deno": "1",
"ios": "13",
"samsung": "11",
"rhino": "1.7.14",
"opera_mobile": "54",
"electron": "6.0"
},
"transform-logical-assignment-operators": {
"chrome": "85",
"opera": "71",
"edge": "85",
"firefox": "79",
"safari": "14",
"node": "15",
"deno": "1.2",
"ios": "14",
"samsung": "14",
"opera_mobile": "60",
"electron": "10.0"
},
"proposal-logical-assignment-operators": {
"chrome": "85",
"opera": "71",
"edge": "85",
"firefox": "79",
"safari": "14",
"node": "15",
"deno": "1.2",
"ios": "14",
"samsung": "14",
"opera_mobile": "60",
"electron": "10.0"
},
"transform-nullish-coalescing-operator": {
"chrome": "80",
"opera": "67",
"edge": "80",
"firefox": "72",
"safari": "13.1",
"node": "14",
"deno": "1",
"ios": "13.4",
"samsung": "13",
"rhino": "1.8",
"opera_mobile": "57",
"electron": "8.0"
},
"proposal-nullish-coalescing-operator": {
"chrome": "80",
"opera": "67",
"edge": "80",
"firefox": "72",
"safari": "13.1",
"node": "14",
"deno": "1",
"ios": "13.4",
"samsung": "13",
"rhino": "1.8",
"opera_mobile": "57",
"electron": "8.0"
},
"transform-optional-chaining": {
"chrome": "91",
"opera": "77",
"edge": "91",
"firefox": "74",
"safari": "13.1",
"node": "16.9",
"deno": "1.9",
"ios": "13.4",
"samsung": "16",
"opera_mobile": "64",
"electron": "13.0"
},
"proposal-optional-chaining": {
"chrome": "91",
"opera": "77",
"edge": "91",
"firefox": "74",
"safari": "13.1",
"node": "16.9",
"deno": "1.9",
"ios": "13.4",
"samsung": "16",
"opera_mobile": "64",
"electron": "13.0"
},
"transform-json-strings": {
"chrome": "66",
"opera": "53",
"edge": "79",
"firefox": "62",
"safari": "12",
"node": "10",
"deno": "1",
"ios": "12",
"samsung": "9",
"rhino": "1.7.14",
"opera_mobile": "47",
"electron": "3.0"
},
"proposal-json-strings": {
"chrome": "66",
"opera": "53",
"edge": "79",
"firefox": "62",
"safari": "12",
"node": "10",
"deno": "1",
"ios": "12",
"samsung": "9",
"rhino": "1.7.14",
"opera_mobile": "47",
"electron": "3.0"
},
"transform-optional-catch-binding": {
"chrome": "66",
"opera": "53",
"edge": "79",
"firefox": "58",
"safari": "11.1",
"node": "10",
"deno": "1",
"ios": "11.3",
"samsung": "9",
"opera_mobile": "47",
"electron": "3.0"
},
"proposal-optional-catch-binding": {
"chrome": "66",
"opera": "53",
"edge": "79",
"firefox": "58",
"safari": "11.1",
"node": "10",
"deno": "1",
"ios": "11.3",
"samsung": "9",
"opera_mobile": "47",
"electron": "3.0"
},
"transform-parameters": {
"chrome": "49",
"opera": "36",
"edge": "18",
"firefox": "52",
"safari": "16.3",
"node": "6",
"deno": "1",
"ios": "16.3",
"samsung": "5",
"opera_mobile": "36",
"electron": "0.37"
},
"transform-async-generator-functions": {
"chrome": "63",
"opera": "50",
"edge": "79",
"firefox": "57",
"safari": "12",
"node": "10",
"deno": "1",
"ios": "12",
"samsung": "8",
"opera_mobile": "46",
"electron": "3.0"
},
"proposal-async-generator-functions": {
"chrome": "63",
"opera": "50",
"edge": "79",
"firefox": "57",
"safari": "12",
"node": "10",
"deno": "1",
"ios": "12",
"samsung": "8",
"opera_mobile": "46",
"electron": "3.0"
},
"transform-object-rest-spread": {
"chrome": "60",
"opera": "47",
"edge": "79",
"firefox": "55",
"safari": "11.1",
"node": "8.3",
"deno": "1",
"ios": "11.3",
"samsung": "8",
"opera_mobile": "44",
"electron": "2.0"
},
"proposal-object-rest-spread": {
"chrome": "60",
"opera": "47",
"edge": "79",
"firefox": "55",
"safari": "11.1",
"node": "8.3",
"deno": "1",
"ios": "11.3",
"samsung": "8",
"opera_mobile": "44",
"electron": "2.0"
},
"transform-dotall-regex": {
"chrome": "62",
"opera": "49",
"edge": "79",
"firefox": "78",
"safari": "11.1",
"node": "8.10",
"deno": "1",
"ios": "11.3",
"samsung": "8",
"rhino": "1.7.15",
"opera_mobile": "46",
"electron": "3.0"
},
"transform-unicode-property-regex": {
"chrome": "64",
"opera": "51",
"edge": "79",
"firefox": "78",
"safari": "11.1",
"node": "10",
"deno": "1",
"ios": "11.3",
"samsung": "9",
"opera_mobile": "47",
"electron": "3.0"
},
"proposal-unicode-property-regex": {
"chrome": "64",
"opera": "51",
"edge": "79",
"firefox": "78",
"safari": "11.1",
"node": "10",
"deno": "1",
"ios": "11.3",
"samsung": "9",
"opera_mobile": "47",
"electron": "3.0"
},
"transform-named-capturing-groups-regex": {
"chrome": "64",
"opera": "51",
"edge": "79",
"firefox": "78",
"safari": "11.1",
"node": "10",
"deno": "1",
"ios": "11.3",
"samsung": "9",
"opera_mobile": "47",
"electron": "3.0"
},
"transform-async-to-generator": {
"chrome": "55",
"opera": "42",
"edge": "15",
"firefox": "52",
"safari": "11",
"node": "7.6",
"deno": "1",
"ios": "11",
"samsung": "6",
"opera_mobile": "42",
"electron": "1.6"
},
"transform-exponentiation-operator": {
"chrome": "52",
"opera": "39",
"edge": "14",
"firefox": "52",
"safari": "10.1",
"node": "7",
"deno": "1",
"ios": "10.3",
"samsung": "6",
"rhino": "1.7.14",
"opera_mobile": "41",
"electron": "1.3"
},
"transform-template-literals": {
"chrome": "41",
"opera": "28",
"edge": "13",
"firefox": "34",
"safari": "13",
"node": "4",
"deno": "1",
"ios": "13",
"samsung": "3.4",
"opera_mobile": "28",
"electron": "0.21"
},
"transform-literals": {
"chrome": "44",
"opera": "31",
"edge": "12",
"firefox": "53",
"safari": "9",
"node": "4",
"deno": "1",
"ios": "9",
"samsung": "4",
"rhino": "1.7.15",
"opera_mobile": "32",
"electron": "0.30"
},
"transform-function-name": {
"chrome": "51",
"opera": "38",
"edge": "79",
"firefox": "53",
"safari": "10",
"node": "6.5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "41",
"electron": "1.2"
},
"transform-arrow-functions": {
"chrome": "47",
"opera": "34",
"edge": "13",
"firefox": "43",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"rhino": "1.7.13",
"opera_mobile": "34",
"electron": "0.36"
},
"transform-block-scoped-functions": {
"chrome": "41",
"opera": "28",
"edge": "12",
"firefox": "46",
"safari": "10",
"node": "4",
"deno": "1",
"ie": "11",
"ios": "10",
"samsung": "3.4",
"opera_mobile": "28",
"electron": "0.21"
},
"transform-classes": {
"chrome": "46",
"opera": "33",
"edge": "13",
"firefox": "45",
"safari": "10",
"node": "5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "33",
"electron": "0.36"
},
"transform-object-super": {
"chrome": "46",
"opera": "33",
"edge": "13",
"firefox": "45",
"safari": "10",
"node": "5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "33",
"electron": "0.36"
},
"transform-shorthand-properties": {
"chrome": "43",
"opera": "30",
"edge": "12",
"firefox": "33",
"safari": "9",
"node": "4",
"deno": "1",
"ios": "9",
"samsung": "4",
"rhino": "1.7.14",
"opera_mobile": "30",
"electron": "0.27"
},
"transform-duplicate-keys": {
"chrome": "42",
"opera": "29",
"edge": "12",
"firefox": "34",
"safari": "9",
"node": "4",
"deno": "1",
"ios": "9",
"samsung": "3.4",
"opera_mobile": "29",
"electron": "0.25"
},
"transform-computed-properties": {
"chrome": "44",
"opera": "31",
"edge": "12",
"firefox": "34",
"safari": "7.1",
"node": "4",
"deno": "1",
"ios": "8",
"samsung": "4",
"rhino": "1.8",
"opera_mobile": "32",
"electron": "0.30"
},
"transform-for-of": {
"chrome": "51",
"opera": "38",
"edge": "15",
"firefox": "53",
"safari": "10",
"node": "6.5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "41",
"electron": "1.2"
},
"transform-sticky-regex": {
"chrome": "49",
"opera": "36",
"edge": "13",
"firefox": "3",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"rhino": "1.7.15",
"opera_mobile": "36",
"electron": "0.37"
},
"transform-unicode-escapes": {
"chrome": "44",
"opera": "31",
"edge": "12",
"firefox": "53",
"safari": "9",
"node": "4",
"deno": "1",
"ios": "9",
"samsung": "4",
"rhino": "1.7.15",
"opera_mobile": "32",
"electron": "0.30"
},
"transform-unicode-regex": {
"chrome": "50",
"opera": "37",
"edge": "13",
"firefox": "46",
"safari": "12",
"node": "6",
"deno": "1",
"ios": "12",
"samsung": "5",
"opera_mobile": "37",
"electron": "1.1"
},
"transform-spread": {
"chrome": "46",
"opera": "33",
"edge": "13",
"firefox": "45",
"safari": "10",
"node": "5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "33",
"electron": "0.36"
},
"transform-destructuring": {
"chrome": "51",
"opera": "38",
"edge": "15",
"firefox": "53",
"safari": "10",
"node": "6.5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "41",
"electron": "1.2"
},
"transform-block-scoping": {
"chrome": "50",
"opera": "37",
"edge": "14",
"firefox": "53",
"safari": "11",
"node": "6",
"deno": "1",
"ios": "11",
"samsung": "5",
"opera_mobile": "37",
"electron": "1.1"
},
"transform-typeof-symbol": {
"chrome": "48",
"opera": "35",
"edge": "12",
"firefox": "36",
"safari": "9",
"node": "6",
"deno": "1",
"ios": "9",
"samsung": "5",
"rhino": "1.8",
"opera_mobile": "35",
"electron": "0.37"
},
"transform-new-target": {
"chrome": "46",
"opera": "33",
"edge": "14",
"firefox": "41",
"safari": "10",
"node": "5",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "33",
"electron": "0.36"
},
"transform-regenerator": {
"chrome": "50",
"opera": "37",
"edge": "13",
"firefox": "53",
"safari": "10",
"node": "6",
"deno": "1",
"ios": "10",
"samsung": "5",
"opera_mobile": "37",
"electron": "1.1"
},
"transform-member-expression-literals": {
"chrome": "7",
"opera": "12",
"edge": "12",
"firefox": "2",
"safari": "5.1",
"node": "0.4",
"deno": "1",
"ie": "9",
"android": "4",
"ios": "6",
"phantom": "1.9",
"samsung": "1",
"rhino": "1.7.13",
"opera_mobile": "12",
"electron": "0.20"
},
"transform-property-literals": {
"chrome": "7",
"opera": "12",
"edge": "12",
"firefox": "2",
"safari": "5.1",
"node": "0.4",
"deno": "1",
"ie": "9",
"android": "4",
"ios": "6",
"phantom": "1.9",
"samsung": "1",
"rhino": "1.7.13",
"opera_mobile": "12",
"electron": "0.20"
},
"transform-reserved-words": {
"chrome": "13",
"opera": "10.50",
"edge": "12",
"firefox": "2",
"safari": "3.1",
"node": "0.6",
"deno": "1",
"ie": "9",
"android": "4.4",
"ios": "6",
"phantom": "1.9",
"samsung": "1",
"rhino": "1.7.13",
"opera_mobile": "10.1",
"electron": "0.20"
},
"transform-export-namespace-from": {
"chrome": "72",
"deno": "1.0",
"edge": "79",
"firefox": "80",
"node": "13.2.0",
"opera": "60",
"opera_mobile": "51",
"safari": "14.1",
"ios": "14.5",
"samsung": "11.0",
"android": "72",
"electron": "5.0"
},
"proposal-export-namespace-from": {
"chrome": "72",
"deno": "1.0",
"edge": "79",
"firefox": "80",
"node": "13.2.0",
"opera": "60",
"opera_mobile": "51",
"safari": "14.1",
"ios": "14.5",
"samsung": "11.0",
"android": "72",
"electron": "5.0"
}
}
+2
View File
@@ -0,0 +1,2 @@
// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
module.exports = require("./data/native-modules.json");
+2
View File
@@ -0,0 +1,2 @@
// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
module.exports = require("./data/overlapping-plugins.json");
+40
View File
@@ -0,0 +1,40 @@
{
"name": "@babel/compat-data",
"version": "7.28.5",
"author": "The Babel Team (https://babel.dev/team)",
"license": "MIT",
"description": "The compat-data to determine required Babel plugins",
"repository": {
"type": "git",
"url": "https://github.com/babel/babel.git",
"directory": "packages/babel-compat-data"
},
"publishConfig": {
"access": "public"
},
"exports": {
"./plugins": "./plugins.js",
"./native-modules": "./native-modules.js",
"./corejs2-built-ins": "./corejs2-built-ins.js",
"./corejs3-shipped-proposals": "./corejs3-shipped-proposals.js",
"./overlapping-plugins": "./overlapping-plugins.js",
"./plugin-bugfixes": "./plugin-bugfixes.js"
},
"scripts": {
"build-data": "./scripts/download-compat-table.sh && node ./scripts/build-data.mjs && node ./scripts/build-modules-support.mjs && node ./scripts/build-bugfixes-targets.mjs"
},
"keywords": [
"babel",
"compat-table",
"compat-data"
],
"devDependencies": {
"@mdn/browser-compat-data": "^6.0.8",
"core-js-compat": "^3.43.0",
"electron-to-chromium": "^1.5.140"
},
"engines": {
"node": ">=6.9.0"
},
"type": "commonjs"
}
+2
View File
@@ -0,0 +1,2 @@
// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
module.exports = require("./data/plugin-bugfixes.json");
+2
View File
@@ -0,0 +1,2 @@
// Todo (Babel 8): remove this file, in Babel 8 users import the .json directly
module.exports = require("./data/plugins.json");
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+19
View File
@@ -0,0 +1,19 @@
# @babel/core
> Babel compiler core.
See our website [@babel/core](https://babeljs.io/docs/babel-core) for more information or the [issues](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A%20core%22+is%3Aopen) associated with this package.
## Install
Using npm:
```sh
npm install --save-dev @babel/core
```
or using yarn:
```sh
yarn add @babel/core --dev
```
+5
View File
@@ -0,0 +1,5 @@
"use strict";
0 && 0;
//# sourceMappingURL=cache-contexts.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"names":[],"sources":["../../src/config/cache-contexts.ts"],"sourcesContent":["import type { ConfigContext } from \"./config-chain.ts\";\nimport type {\n CallerMetadata,\n TargetsListOrObject,\n} from \"./validation/options.ts\";\n\nexport type { ConfigContext as FullConfig };\n\nexport type FullPreset = {\n targets: TargetsListOrObject;\n} & ConfigContext;\nexport type FullPlugin = {\n assumptions: { [name: string]: boolean };\n} & FullPreset;\n\n// Context not including filename since it is used in places that cannot\n// process 'ignore'/'only' and other filename-based logic.\nexport type SimpleConfig = {\n envName: string;\n caller: CallerMetadata | undefined;\n};\nexport type SimplePreset = {\n targets: TargetsListOrObject;\n} & SimpleConfig;\nexport type SimplePlugin = {\n assumptions: {\n [name: string]: boolean;\n };\n} & SimplePreset;\n"],"mappings":"","ignoreList":[]}
+261
View File
@@ -0,0 +1,261 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.assertSimpleType = assertSimpleType;
exports.makeStrongCache = makeStrongCache;
exports.makeStrongCacheSync = makeStrongCacheSync;
exports.makeWeakCache = makeWeakCache;
exports.makeWeakCacheSync = makeWeakCacheSync;
function _gensync() {
const data = require("gensync");
_gensync = function () {
return data;
};
return data;
}
var _async = require("../gensync-utils/async.js");
var _util = require("./util.js");
const synchronize = gen => {
return _gensync()(gen).sync;
};
function* genTrue() {
return true;
}
function makeWeakCache(handler) {
return makeCachedFunction(WeakMap, handler);
}
function makeWeakCacheSync(handler) {
return synchronize(makeWeakCache(handler));
}
function makeStrongCache(handler) {
return makeCachedFunction(Map, handler);
}
function makeStrongCacheSync(handler) {
return synchronize(makeStrongCache(handler));
}
function makeCachedFunction(CallCache, handler) {
const callCacheSync = new CallCache();
const callCacheAsync = new CallCache();
const futureCache = new CallCache();
return function* cachedFunction(arg, data) {
const asyncContext = yield* (0, _async.isAsync)();
const callCache = asyncContext ? callCacheAsync : callCacheSync;
const cached = yield* getCachedValueOrWait(asyncContext, callCache, futureCache, arg, data);
if (cached.valid) return cached.value;
const cache = new CacheConfigurator(data);
const handlerResult = handler(arg, cache);
let finishLock;
let value;
if ((0, _util.isIterableIterator)(handlerResult)) {
value = yield* (0, _async.onFirstPause)(handlerResult, () => {
finishLock = setupAsyncLocks(cache, futureCache, arg);
});
} else {
value = handlerResult;
}
updateFunctionCache(callCache, cache, arg, value);
if (finishLock) {
futureCache.delete(arg);
finishLock.release(value);
}
return value;
};
}
function* getCachedValue(cache, arg, data) {
const cachedValue = cache.get(arg);
if (cachedValue) {
for (const {
value,
valid
} of cachedValue) {
if (yield* valid(data)) return {
valid: true,
value
};
}
}
return {
valid: false,
value: null
};
}
function* getCachedValueOrWait(asyncContext, callCache, futureCache, arg, data) {
const cached = yield* getCachedValue(callCache, arg, data);
if (cached.valid) {
return cached;
}
if (asyncContext) {
const cached = yield* getCachedValue(futureCache, arg, data);
if (cached.valid) {
const value = yield* (0, _async.waitFor)(cached.value.promise);
return {
valid: true,
value
};
}
}
return {
valid: false,
value: null
};
}
function setupAsyncLocks(config, futureCache, arg) {
const finishLock = new Lock();
updateFunctionCache(futureCache, config, arg, finishLock);
return finishLock;
}
function updateFunctionCache(cache, config, arg, value) {
if (!config.configured()) config.forever();
let cachedValue = cache.get(arg);
config.deactivate();
switch (config.mode()) {
case "forever":
cachedValue = [{
value,
valid: genTrue
}];
cache.set(arg, cachedValue);
break;
case "invalidate":
cachedValue = [{
value,
valid: config.validator()
}];
cache.set(arg, cachedValue);
break;
case "valid":
if (cachedValue) {
cachedValue.push({
value,
valid: config.validator()
});
} else {
cachedValue = [{
value,
valid: config.validator()
}];
cache.set(arg, cachedValue);
}
}
}
class CacheConfigurator {
constructor(data) {
this._active = true;
this._never = false;
this._forever = false;
this._invalidate = false;
this._configured = false;
this._pairs = [];
this._data = void 0;
this._data = data;
}
simple() {
return makeSimpleConfigurator(this);
}
mode() {
if (this._never) return "never";
if (this._forever) return "forever";
if (this._invalidate) return "invalidate";
return "valid";
}
forever() {
if (!this._active) {
throw new Error("Cannot change caching after evaluation has completed.");
}
if (this._never) {
throw new Error("Caching has already been configured with .never()");
}
this._forever = true;
this._configured = true;
}
never() {
if (!this._active) {
throw new Error("Cannot change caching after evaluation has completed.");
}
if (this._forever) {
throw new Error("Caching has already been configured with .forever()");
}
this._never = true;
this._configured = true;
}
using(handler) {
if (!this._active) {
throw new Error("Cannot change caching after evaluation has completed.");
}
if (this._never || this._forever) {
throw new Error("Caching has already been configured with .never or .forever()");
}
this._configured = true;
const key = handler(this._data);
const fn = (0, _async.maybeAsync)(handler, `You appear to be using an async cache handler, but Babel has been called synchronously`);
if ((0, _async.isThenable)(key)) {
return key.then(key => {
this._pairs.push([key, fn]);
return key;
});
}
this._pairs.push([key, fn]);
return key;
}
invalidate(handler) {
this._invalidate = true;
return this.using(handler);
}
validator() {
const pairs = this._pairs;
return function* (data) {
for (const [key, fn] of pairs) {
if (key !== (yield* fn(data))) return false;
}
return true;
};
}
deactivate() {
this._active = false;
}
configured() {
return this._configured;
}
}
function makeSimpleConfigurator(cache) {
function cacheFn(val) {
if (typeof val === "boolean") {
if (val) cache.forever();else cache.never();
return;
}
return cache.using(() => assertSimpleType(val()));
}
cacheFn.forever = () => cache.forever();
cacheFn.never = () => cache.never();
cacheFn.using = cb => cache.using(() => assertSimpleType(cb()));
cacheFn.invalidate = cb => cache.invalidate(() => assertSimpleType(cb()));
return cacheFn;
}
function assertSimpleType(value) {
if ((0, _async.isThenable)(value)) {
throw new Error(`You appear to be using an async cache handler, ` + `which your current version of Babel does not support. ` + `We may add support for this in the future, ` + `but if you're on the most recent version of @babel/core and still ` + `seeing this error, then you'll need to synchronously handle your caching logic.`);
}
if (value != null && typeof value !== "string" && typeof value !== "boolean" && typeof value !== "number") {
throw new Error("Cache keys must be either string, boolean, number, null, or undefined.");
}
return value;
}
class Lock {
constructor() {
this.released = false;
this.promise = void 0;
this._resolve = void 0;
this.promise = new Promise(resolve => {
this._resolve = resolve;
});
}
release(value) {
this.released = true;
this._resolve(value);
}
}
0 && 0;
//# sourceMappingURL=caching.js.map
File diff suppressed because one or more lines are too long
+469
View File
@@ -0,0 +1,469 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.buildPresetChain = buildPresetChain;
exports.buildPresetChainWalker = void 0;
exports.buildRootChain = buildRootChain;
function _path() {
const data = require("path");
_path = function () {
return data;
};
return data;
}
function _debug() {
const data = require("debug");
_debug = function () {
return data;
};
return data;
}
var _options = require("./validation/options.js");
var _patternToRegex = require("./pattern-to-regex.js");
var _printer = require("./printer.js");
var _rewriteStackTrace = require("../errors/rewrite-stack-trace.js");
var _configError = require("../errors/config-error.js");
var _index = require("./files/index.js");
var _caching = require("./caching.js");
var _configDescriptors = require("./config-descriptors.js");
const debug = _debug()("babel:config:config-chain");
function* buildPresetChain(arg, context) {
const chain = yield* buildPresetChainWalker(arg, context);
if (!chain) return null;
return {
plugins: dedupDescriptors(chain.plugins),
presets: dedupDescriptors(chain.presets),
options: chain.options.map(o => createConfigChainOptions(o)),
files: new Set()
};
}
const buildPresetChainWalker = exports.buildPresetChainWalker = makeChainWalker({
root: preset => loadPresetDescriptors(preset),
env: (preset, envName) => loadPresetEnvDescriptors(preset)(envName),
overrides: (preset, index) => loadPresetOverridesDescriptors(preset)(index),
overridesEnv: (preset, index, envName) => loadPresetOverridesEnvDescriptors(preset)(index)(envName),
createLogger: () => () => {}
});
const loadPresetDescriptors = (0, _caching.makeWeakCacheSync)(preset => buildRootDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors));
const loadPresetEnvDescriptors = (0, _caching.makeWeakCacheSync)(preset => (0, _caching.makeStrongCacheSync)(envName => buildEnvDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors, envName)));
const loadPresetOverridesDescriptors = (0, _caching.makeWeakCacheSync)(preset => (0, _caching.makeStrongCacheSync)(index => buildOverrideDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors, index)));
const loadPresetOverridesEnvDescriptors = (0, _caching.makeWeakCacheSync)(preset => (0, _caching.makeStrongCacheSync)(index => (0, _caching.makeStrongCacheSync)(envName => buildOverrideEnvDescriptors(preset, preset.alias, _configDescriptors.createUncachedDescriptors, index, envName))));
function* buildRootChain(opts, context) {
let configReport, babelRcReport;
const programmaticLogger = new _printer.ConfigPrinter();
const programmaticChain = yield* loadProgrammaticChain({
options: opts,
dirname: context.cwd
}, context, undefined, programmaticLogger);
if (!programmaticChain) return null;
const programmaticReport = yield* programmaticLogger.output();
let configFile;
if (typeof opts.configFile === "string") {
configFile = yield* (0, _index.loadConfig)(opts.configFile, context.cwd, context.envName, context.caller);
} else if (opts.configFile !== false) {
configFile = yield* (0, _index.findRootConfig)(context.root, context.envName, context.caller);
}
let {
babelrc,
babelrcRoots
} = opts;
let babelrcRootsDirectory = context.cwd;
const configFileChain = emptyChain();
const configFileLogger = new _printer.ConfigPrinter();
if (configFile) {
const validatedFile = validateConfigFile(configFile);
const result = yield* loadFileChain(validatedFile, context, undefined, configFileLogger);
if (!result) return null;
configReport = yield* configFileLogger.output();
if (babelrc === undefined) {
babelrc = validatedFile.options.babelrc;
}
if (babelrcRoots === undefined) {
babelrcRootsDirectory = validatedFile.dirname;
babelrcRoots = validatedFile.options.babelrcRoots;
}
mergeChain(configFileChain, result);
}
let ignoreFile, babelrcFile;
let isIgnored = false;
const fileChain = emptyChain();
if ((babelrc === true || babelrc === undefined) && typeof context.filename === "string") {
const pkgData = yield* (0, _index.findPackageData)(context.filename);
if (pkgData && babelrcLoadEnabled(context, pkgData, babelrcRoots, babelrcRootsDirectory)) {
({
ignore: ignoreFile,
config: babelrcFile
} = yield* (0, _index.findRelativeConfig)(pkgData, context.envName, context.caller));
if (ignoreFile) {
fileChain.files.add(ignoreFile.filepath);
}
if (ignoreFile && shouldIgnore(context, ignoreFile.ignore, null, ignoreFile.dirname)) {
isIgnored = true;
}
if (babelrcFile && !isIgnored) {
const validatedFile = validateBabelrcFile(babelrcFile);
const babelrcLogger = new _printer.ConfigPrinter();
const result = yield* loadFileChain(validatedFile, context, undefined, babelrcLogger);
if (!result) {
isIgnored = true;
} else {
babelRcReport = yield* babelrcLogger.output();
mergeChain(fileChain, result);
}
}
if (babelrcFile && isIgnored) {
fileChain.files.add(babelrcFile.filepath);
}
}
}
if (context.showConfig) {
console.log(`Babel configs on "${context.filename}" (ascending priority):\n` + [configReport, babelRcReport, programmaticReport].filter(x => !!x).join("\n\n") + "\n-----End Babel configs-----");
}
const chain = mergeChain(mergeChain(mergeChain(emptyChain(), configFileChain), fileChain), programmaticChain);
return {
plugins: isIgnored ? [] : dedupDescriptors(chain.plugins),
presets: isIgnored ? [] : dedupDescriptors(chain.presets),
options: isIgnored ? [] : chain.options.map(o => createConfigChainOptions(o)),
fileHandling: isIgnored ? "ignored" : "transpile",
ignore: ignoreFile || undefined,
babelrc: babelrcFile || undefined,
config: configFile || undefined,
files: chain.files
};
}
function babelrcLoadEnabled(context, pkgData, babelrcRoots, babelrcRootsDirectory) {
if (typeof babelrcRoots === "boolean") return babelrcRoots;
const absoluteRoot = context.root;
if (babelrcRoots === undefined) {
return pkgData.directories.includes(absoluteRoot);
}
let babelrcPatterns = babelrcRoots;
if (!Array.isArray(babelrcPatterns)) {
babelrcPatterns = [babelrcPatterns];
}
babelrcPatterns = babelrcPatterns.map(pat => {
return typeof pat === "string" ? _path().resolve(babelrcRootsDirectory, pat) : pat;
});
if (babelrcPatterns.length === 1 && babelrcPatterns[0] === absoluteRoot) {
return pkgData.directories.includes(absoluteRoot);
}
return babelrcPatterns.some(pat => {
if (typeof pat === "string") {
pat = (0, _patternToRegex.default)(pat, babelrcRootsDirectory);
}
return pkgData.directories.some(directory => {
return matchPattern(pat, babelrcRootsDirectory, directory, context);
});
});
}
const validateConfigFile = (0, _caching.makeWeakCacheSync)(file => ({
filepath: file.filepath,
dirname: file.dirname,
options: (0, _options.validate)("configfile", file.options, file.filepath)
}));
const validateBabelrcFile = (0, _caching.makeWeakCacheSync)(file => ({
filepath: file.filepath,
dirname: file.dirname,
options: (0, _options.validate)("babelrcfile", file.options, file.filepath)
}));
const validateExtendFile = (0, _caching.makeWeakCacheSync)(file => ({
filepath: file.filepath,
dirname: file.dirname,
options: (0, _options.validate)("extendsfile", file.options, file.filepath)
}));
const loadProgrammaticChain = makeChainWalker({
root: input => buildRootDescriptors(input, "base", _configDescriptors.createCachedDescriptors),
env: (input, envName) => buildEnvDescriptors(input, "base", _configDescriptors.createCachedDescriptors, envName),
overrides: (input, index) => buildOverrideDescriptors(input, "base", _configDescriptors.createCachedDescriptors, index),
overridesEnv: (input, index, envName) => buildOverrideEnvDescriptors(input, "base", _configDescriptors.createCachedDescriptors, index, envName),
createLogger: (input, context, baseLogger) => buildProgrammaticLogger(input, context, baseLogger)
});
const loadFileChainWalker = makeChainWalker({
root: file => loadFileDescriptors(file),
env: (file, envName) => loadFileEnvDescriptors(file)(envName),
overrides: (file, index) => loadFileOverridesDescriptors(file)(index),
overridesEnv: (file, index, envName) => loadFileOverridesEnvDescriptors(file)(index)(envName),
createLogger: (file, context, baseLogger) => buildFileLogger(file.filepath, context, baseLogger)
});
function* loadFileChain(input, context, files, baseLogger) {
const chain = yield* loadFileChainWalker(input, context, files, baseLogger);
chain == null || chain.files.add(input.filepath);
return chain;
}
const loadFileDescriptors = (0, _caching.makeWeakCacheSync)(file => buildRootDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors));
const loadFileEnvDescriptors = (0, _caching.makeWeakCacheSync)(file => (0, _caching.makeStrongCacheSync)(envName => buildEnvDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors, envName)));
const loadFileOverridesDescriptors = (0, _caching.makeWeakCacheSync)(file => (0, _caching.makeStrongCacheSync)(index => buildOverrideDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors, index)));
const loadFileOverridesEnvDescriptors = (0, _caching.makeWeakCacheSync)(file => (0, _caching.makeStrongCacheSync)(index => (0, _caching.makeStrongCacheSync)(envName => buildOverrideEnvDescriptors(file, file.filepath, _configDescriptors.createUncachedDescriptors, index, envName))));
function buildFileLogger(filepath, context, baseLogger) {
if (!baseLogger) {
return () => {};
}
return baseLogger.configure(context.showConfig, _printer.ChainFormatter.Config, {
filepath
});
}
function buildRootDescriptors({
dirname,
options
}, alias, descriptors) {
return descriptors(dirname, options, alias);
}
function buildProgrammaticLogger(_, context, baseLogger) {
var _context$caller;
if (!baseLogger) {
return () => {};
}
return baseLogger.configure(context.showConfig, _printer.ChainFormatter.Programmatic, {
callerName: (_context$caller = context.caller) == null ? void 0 : _context$caller.name
});
}
function buildEnvDescriptors({
dirname,
options
}, alias, descriptors, envName) {
var _options$env;
const opts = (_options$env = options.env) == null ? void 0 : _options$env[envName];
return opts ? descriptors(dirname, opts, `${alias}.env["${envName}"]`) : null;
}
function buildOverrideDescriptors({
dirname,
options
}, alias, descriptors, index) {
var _options$overrides;
const opts = (_options$overrides = options.overrides) == null ? void 0 : _options$overrides[index];
if (!opts) throw new Error("Assertion failure - missing override");
return descriptors(dirname, opts, `${alias}.overrides[${index}]`);
}
function buildOverrideEnvDescriptors({
dirname,
options
}, alias, descriptors, index, envName) {
var _options$overrides2, _override$env;
const override = (_options$overrides2 = options.overrides) == null ? void 0 : _options$overrides2[index];
if (!override) throw new Error("Assertion failure - missing override");
const opts = (_override$env = override.env) == null ? void 0 : _override$env[envName];
return opts ? descriptors(dirname, opts, `${alias}.overrides[${index}].env["${envName}"]`) : null;
}
function makeChainWalker({
root,
env,
overrides,
overridesEnv,
createLogger
}) {
return function* chainWalker(input, context, files = new Set(), baseLogger) {
const {
dirname
} = input;
const flattenedConfigs = [];
const rootOpts = root(input);
if (configIsApplicable(rootOpts, dirname, context, input.filepath)) {
flattenedConfigs.push({
config: rootOpts,
envName: undefined,
index: undefined
});
const envOpts = env(input, context.envName);
if (envOpts && configIsApplicable(envOpts, dirname, context, input.filepath)) {
flattenedConfigs.push({
config: envOpts,
envName: context.envName,
index: undefined
});
}
(rootOpts.options.overrides || []).forEach((_, index) => {
const overrideOps = overrides(input, index);
if (configIsApplicable(overrideOps, dirname, context, input.filepath)) {
flattenedConfigs.push({
config: overrideOps,
index,
envName: undefined
});
const overrideEnvOpts = overridesEnv(input, index, context.envName);
if (overrideEnvOpts && configIsApplicable(overrideEnvOpts, dirname, context, input.filepath)) {
flattenedConfigs.push({
config: overrideEnvOpts,
index,
envName: context.envName
});
}
}
});
}
if (flattenedConfigs.some(({
config: {
options: {
ignore,
only
}
}
}) => shouldIgnore(context, ignore, only, dirname))) {
return null;
}
const chain = emptyChain();
const logger = createLogger(input, context, baseLogger);
for (const {
config,
index,
envName
} of flattenedConfigs) {
if (!(yield* mergeExtendsChain(chain, config.options, dirname, context, files, baseLogger))) {
return null;
}
logger(config, index, envName);
yield* mergeChainOpts(chain, config);
}
return chain;
};
}
function* mergeExtendsChain(chain, opts, dirname, context, files, baseLogger) {
if (opts.extends === undefined) return true;
const file = yield* (0, _index.loadConfig)(opts.extends, dirname, context.envName, context.caller);
if (files.has(file)) {
throw new Error(`Configuration cycle detected loading ${file.filepath}.\n` + `File already loaded following the config chain:\n` + Array.from(files, file => ` - ${file.filepath}`).join("\n"));
}
files.add(file);
const fileChain = yield* loadFileChain(validateExtendFile(file), context, files, baseLogger);
files.delete(file);
if (!fileChain) return false;
mergeChain(chain, fileChain);
return true;
}
function mergeChain(target, source) {
target.options.push(...source.options);
target.plugins.push(...source.plugins);
target.presets.push(...source.presets);
for (const file of source.files) {
target.files.add(file);
}
return target;
}
function* mergeChainOpts(target, {
options,
plugins,
presets
}) {
target.options.push(options);
target.plugins.push(...(yield* plugins()));
target.presets.push(...(yield* presets()));
return target;
}
function emptyChain() {
return {
options: [],
presets: [],
plugins: [],
files: new Set()
};
}
function createConfigChainOptions(opts) {
const options = Object.assign({}, opts);
delete options.extends;
delete options.env;
delete options.overrides;
delete options.plugins;
delete options.presets;
delete options.passPerPreset;
delete options.ignore;
delete options.only;
delete options.test;
delete options.include;
delete options.exclude;
if (hasOwnProperty.call(options, "sourceMap")) {
options.sourceMaps = options.sourceMap;
delete options.sourceMap;
}
return options;
}
function dedupDescriptors(items) {
const map = new Map();
const descriptors = [];
for (const item of items) {
if (typeof item.value === "function") {
const fnKey = item.value;
let nameMap = map.get(fnKey);
if (!nameMap) {
nameMap = new Map();
map.set(fnKey, nameMap);
}
let desc = nameMap.get(item.name);
if (!desc) {
desc = {
value: item
};
descriptors.push(desc);
if (!item.ownPass) nameMap.set(item.name, desc);
} else {
desc.value = item;
}
} else {
descriptors.push({
value: item
});
}
}
return descriptors.reduce((acc, desc) => {
acc.push(desc.value);
return acc;
}, []);
}
function configIsApplicable({
options
}, dirname, context, configName) {
return (options.test === undefined || configFieldIsApplicable(context, options.test, dirname, configName)) && (options.include === undefined || configFieldIsApplicable(context, options.include, dirname, configName)) && (options.exclude === undefined || !configFieldIsApplicable(context, options.exclude, dirname, configName));
}
function configFieldIsApplicable(context, test, dirname, configName) {
const patterns = Array.isArray(test) ? test : [test];
return matchesPatterns(context, patterns, dirname, configName);
}
function ignoreListReplacer(_key, value) {
if (value instanceof RegExp) {
return String(value);
}
return value;
}
function shouldIgnore(context, ignore, only, dirname) {
if (ignore && matchesPatterns(context, ignore, dirname)) {
var _context$filename;
const message = `No config is applied to "${(_context$filename = context.filename) != null ? _context$filename : "(unknown)"}" because it matches one of \`ignore: ${JSON.stringify(ignore, ignoreListReplacer)}\` from "${dirname}"`;
debug(message);
if (context.showConfig) {
console.log(message);
}
return true;
}
if (only && !matchesPatterns(context, only, dirname)) {
var _context$filename2;
const message = `No config is applied to "${(_context$filename2 = context.filename) != null ? _context$filename2 : "(unknown)"}" because it fails to match one of \`only: ${JSON.stringify(only, ignoreListReplacer)}\` from "${dirname}"`;
debug(message);
if (context.showConfig) {
console.log(message);
}
return true;
}
return false;
}
function matchesPatterns(context, patterns, dirname, configName) {
return patterns.some(pattern => matchPattern(pattern, dirname, context.filename, context, configName));
}
function matchPattern(pattern, dirname, pathToTest, context, configName) {
if (typeof pattern === "function") {
return !!(0, _rewriteStackTrace.endHiddenCallStack)(pattern)(pathToTest, {
dirname,
envName: context.envName,
caller: context.caller
});
}
if (typeof pathToTest !== "string") {
throw new _configError.default(`Configuration contains string/RegExp pattern, but no filename was passed to Babel`, configName);
}
if (typeof pattern === "string") {
pattern = (0, _patternToRegex.default)(pattern, dirname);
}
return pattern.test(pathToTest);
}
0 && 0;
//# sourceMappingURL=config-chain.js.map
File diff suppressed because one or more lines are too long
+190
View File
@@ -0,0 +1,190 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createCachedDescriptors = createCachedDescriptors;
exports.createDescriptor = createDescriptor;
exports.createUncachedDescriptors = createUncachedDescriptors;
function _gensync() {
const data = require("gensync");
_gensync = function () {
return data;
};
return data;
}
var _functional = require("../gensync-utils/functional.js");
var _index = require("./files/index.js");
var _item = require("./item.js");
var _caching = require("./caching.js");
var _resolveTargets = require("./resolve-targets.js");
function isEqualDescriptor(a, b) {
var _a$file, _b$file, _a$file2, _b$file2;
return a.name === b.name && a.value === b.value && a.options === b.options && a.dirname === b.dirname && a.alias === b.alias && a.ownPass === b.ownPass && ((_a$file = a.file) == null ? void 0 : _a$file.request) === ((_b$file = b.file) == null ? void 0 : _b$file.request) && ((_a$file2 = a.file) == null ? void 0 : _a$file2.resolved) === ((_b$file2 = b.file) == null ? void 0 : _b$file2.resolved);
}
function* handlerOf(value) {
return value;
}
function optionsWithResolvedBrowserslistConfigFile(options, dirname) {
if (typeof options.browserslistConfigFile === "string") {
options.browserslistConfigFile = (0, _resolveTargets.resolveBrowserslistConfigFile)(options.browserslistConfigFile, dirname);
}
return options;
}
function createCachedDescriptors(dirname, options, alias) {
const {
plugins,
presets,
passPerPreset
} = options;
return {
options: optionsWithResolvedBrowserslistConfigFile(options, dirname),
plugins: plugins ? () => createCachedPluginDescriptors(plugins, dirname)(alias) : () => handlerOf([]),
presets: presets ? () => createCachedPresetDescriptors(presets, dirname)(alias)(!!passPerPreset) : () => handlerOf([])
};
}
function createUncachedDescriptors(dirname, options, alias) {
return {
options: optionsWithResolvedBrowserslistConfigFile(options, dirname),
plugins: (0, _functional.once)(() => createPluginDescriptors(options.plugins || [], dirname, alias)),
presets: (0, _functional.once)(() => createPresetDescriptors(options.presets || [], dirname, alias, !!options.passPerPreset))
};
}
const PRESET_DESCRIPTOR_CACHE = new WeakMap();
const createCachedPresetDescriptors = (0, _caching.makeWeakCacheSync)((items, cache) => {
const dirname = cache.using(dir => dir);
return (0, _caching.makeStrongCacheSync)(alias => (0, _caching.makeStrongCache)(function* (passPerPreset) {
const descriptors = yield* createPresetDescriptors(items, dirname, alias, passPerPreset);
return descriptors.map(desc => loadCachedDescriptor(PRESET_DESCRIPTOR_CACHE, desc));
}));
});
const PLUGIN_DESCRIPTOR_CACHE = new WeakMap();
const createCachedPluginDescriptors = (0, _caching.makeWeakCacheSync)((items, cache) => {
const dirname = cache.using(dir => dir);
return (0, _caching.makeStrongCache)(function* (alias) {
const descriptors = yield* createPluginDescriptors(items, dirname, alias);
return descriptors.map(desc => loadCachedDescriptor(PLUGIN_DESCRIPTOR_CACHE, desc));
});
});
const DEFAULT_OPTIONS = {};
function loadCachedDescriptor(cache, desc) {
const {
value,
options = DEFAULT_OPTIONS
} = desc;
if (options === false) return desc;
let cacheByOptions = cache.get(value);
if (!cacheByOptions) {
cacheByOptions = new WeakMap();
cache.set(value, cacheByOptions);
}
let possibilities = cacheByOptions.get(options);
if (!possibilities) {
possibilities = [];
cacheByOptions.set(options, possibilities);
}
if (!possibilities.includes(desc)) {
const matches = possibilities.filter(possibility => isEqualDescriptor(possibility, desc));
if (matches.length > 0) {
return matches[0];
}
possibilities.push(desc);
}
return desc;
}
function* createPresetDescriptors(items, dirname, alias, passPerPreset) {
return yield* createDescriptors("preset", items, dirname, alias, passPerPreset);
}
function* createPluginDescriptors(items, dirname, alias) {
return yield* createDescriptors("plugin", items, dirname, alias);
}
function* createDescriptors(type, items, dirname, alias, ownPass) {
const descriptors = yield* _gensync().all(items.map((item, index) => createDescriptor(item, dirname, {
type,
alias: `${alias}$${index}`,
ownPass: !!ownPass
})));
assertNoDuplicates(descriptors);
return descriptors;
}
function* createDescriptor(pair, dirname, {
type,
alias,
ownPass
}) {
const desc = (0, _item.getItemDescriptor)(pair);
if (desc) {
return desc;
}
let name;
let options;
let value = pair;
if (Array.isArray(value)) {
if (value.length === 3) {
[value, options, name] = value;
} else {
[value, options] = value;
}
}
let file = undefined;
let filepath = null;
if (typeof value === "string") {
if (typeof type !== "string") {
throw new Error("To resolve a string-based item, the type of item must be given");
}
const resolver = type === "plugin" ? _index.loadPlugin : _index.loadPreset;
const request = value;
({
filepath,
value
} = yield* resolver(value, dirname));
file = {
request,
resolved: filepath
};
}
if (!value) {
throw new Error(`Unexpected falsy value: ${String(value)}`);
}
if (typeof value === "object" && value.__esModule) {
if (value.default) {
value = value.default;
} else {
throw new Error("Must export a default export when using ES6 modules.");
}
}
if (typeof value !== "object" && typeof value !== "function") {
throw new Error(`Unsupported format: ${typeof value}. Expected an object or a function.`);
}
if (filepath !== null && typeof value === "object" && value) {
throw new Error(`Plugin/Preset files are not allowed to export objects, only functions. In ${filepath}`);
}
return {
name,
alias: filepath || alias,
value,
options,
dirname,
ownPass,
file
};
}
function assertNoDuplicates(items) {
const map = new Map();
for (const item of items) {
if (typeof item.value !== "function") continue;
let nameMap = map.get(item.value);
if (!nameMap) {
nameMap = new Set();
map.set(item.value, nameMap);
}
if (nameMap.has(item.name)) {
const conflicts = items.filter(i => i.value === item.value);
throw new Error([`Duplicate plugin/preset detected.`, `If you'd like to use two separate instances of a plugin,`, `they need separate names, e.g.`, ``, ` plugins: [`, ` ['some-plugin', {}],`, ` ['some-plugin', {}, 'some unique name'],`, ` ]`, ``, `Duplicates detected are:`, `${JSON.stringify(conflicts, null, 2)}`].join("\n"));
}
nameMap.add(item.name);
}
}
0 && 0;
//# sourceMappingURL=config-descriptors.js.map
File diff suppressed because one or more lines are too long
+290
View File
@@ -0,0 +1,290 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ROOT_CONFIG_FILENAMES = void 0;
exports.findConfigUpwards = findConfigUpwards;
exports.findRelativeConfig = findRelativeConfig;
exports.findRootConfig = findRootConfig;
exports.loadConfig = loadConfig;
exports.resolveShowConfigPath = resolveShowConfigPath;
function _debug() {
const data = require("debug");
_debug = function () {
return data;
};
return data;
}
function _fs() {
const data = require("fs");
_fs = function () {
return data;
};
return data;
}
function _path() {
const data = require("path");
_path = function () {
return data;
};
return data;
}
function _json() {
const data = require("json5");
_json = function () {
return data;
};
return data;
}
function _gensync() {
const data = require("gensync");
_gensync = function () {
return data;
};
return data;
}
var _caching = require("../caching.js");
var _configApi = require("../helpers/config-api.js");
var _utils = require("./utils.js");
var _moduleTypes = require("./module-types.js");
var _patternToRegex = require("../pattern-to-regex.js");
var _configError = require("../../errors/config-error.js");
var fs = require("../../gensync-utils/fs.js");
require("module");
var _rewriteStackTrace = require("../../errors/rewrite-stack-trace.js");
var _async = require("../../gensync-utils/async.js");
const debug = _debug()("babel:config:loading:files:configuration");
const ROOT_CONFIG_FILENAMES = exports.ROOT_CONFIG_FILENAMES = ["babel.config.js", "babel.config.cjs", "babel.config.mjs", "babel.config.json", "babel.config.cts", "babel.config.ts", "babel.config.mts"];
const RELATIVE_CONFIG_FILENAMES = [".babelrc", ".babelrc.js", ".babelrc.cjs", ".babelrc.mjs", ".babelrc.json", ".babelrc.cts"];
const BABELIGNORE_FILENAME = ".babelignore";
const runConfig = (0, _caching.makeWeakCache)(function* runConfig(options, cache) {
yield* [];
return {
options: (0, _rewriteStackTrace.endHiddenCallStack)(options)((0, _configApi.makeConfigAPI)(cache)),
cacheNeedsConfiguration: !cache.configured()
};
});
function* readConfigCode(filepath, data) {
if (!_fs().existsSync(filepath)) return null;
let options = yield* (0, _moduleTypes.default)(filepath, (yield* (0, _async.isAsync)()) ? "auto" : "require", "You appear to be using a native ECMAScript module configuration " + "file, which is only supported when running Babel asynchronously " + "or when using the Node.js `--experimental-require-module` flag.", "You appear to be using a configuration file that contains top-level " + "await, which is only supported when running Babel asynchronously.");
let cacheNeedsConfiguration = false;
if (typeof options === "function") {
({
options,
cacheNeedsConfiguration
} = yield* runConfig(options, data));
}
if (!options || typeof options !== "object" || Array.isArray(options)) {
throw new _configError.default(`Configuration should be an exported JavaScript object.`, filepath);
}
if (typeof options.then === "function") {
options.catch == null || options.catch(() => {});
throw new _configError.default(`You appear to be using an async configuration, ` + `which your current version of Babel does not support. ` + `We may add support for this in the future, ` + `but if you're on the most recent version of @babel/core and still ` + `seeing this error, then you'll need to synchronously return your config.`, filepath);
}
if (cacheNeedsConfiguration) throwConfigError(filepath);
return buildConfigFileObject(options, filepath);
}
const cfboaf = new WeakMap();
function buildConfigFileObject(options, filepath) {
let configFilesByFilepath = cfboaf.get(options);
if (!configFilesByFilepath) {
cfboaf.set(options, configFilesByFilepath = new Map());
}
let configFile = configFilesByFilepath.get(filepath);
if (!configFile) {
configFile = {
filepath,
dirname: _path().dirname(filepath),
options
};
configFilesByFilepath.set(filepath, configFile);
}
return configFile;
}
const packageToBabelConfig = (0, _caching.makeWeakCacheSync)(file => {
const babel = file.options.babel;
if (babel === undefined) return null;
if (typeof babel !== "object" || Array.isArray(babel) || babel === null) {
throw new _configError.default(`.babel property must be an object`, file.filepath);
}
return {
filepath: file.filepath,
dirname: file.dirname,
options: babel
};
});
const readConfigJSON5 = (0, _utils.makeStaticFileCache)((filepath, content) => {
let options;
try {
options = _json().parse(content);
} catch (err) {
throw new _configError.default(`Error while parsing config - ${err.message}`, filepath);
}
if (!options) throw new _configError.default(`No config detected`, filepath);
if (typeof options !== "object") {
throw new _configError.default(`Config returned typeof ${typeof options}`, filepath);
}
if (Array.isArray(options)) {
throw new _configError.default(`Expected config object but found array`, filepath);
}
delete options.$schema;
return {
filepath,
dirname: _path().dirname(filepath),
options
};
});
const readIgnoreConfig = (0, _utils.makeStaticFileCache)((filepath, content) => {
const ignoreDir = _path().dirname(filepath);
const ignorePatterns = content.split("\n").map(line => line.replace(/#.*$/, "").trim()).filter(Boolean);
for (const pattern of ignorePatterns) {
if (pattern[0] === "!") {
throw new _configError.default(`Negation of file paths is not supported.`, filepath);
}
}
return {
filepath,
dirname: _path().dirname(filepath),
ignore: ignorePatterns.map(pattern => (0, _patternToRegex.default)(pattern, ignoreDir))
};
});
function findConfigUpwards(rootDir) {
let dirname = rootDir;
for (;;) {
for (const filename of ROOT_CONFIG_FILENAMES) {
if (_fs().existsSync(_path().join(dirname, filename))) {
return dirname;
}
}
const nextDir = _path().dirname(dirname);
if (dirname === nextDir) break;
dirname = nextDir;
}
return null;
}
function* findRelativeConfig(packageData, envName, caller) {
let config = null;
let ignore = null;
const dirname = _path().dirname(packageData.filepath);
for (const loc of packageData.directories) {
if (!config) {
var _packageData$pkg;
config = yield* loadOneConfig(RELATIVE_CONFIG_FILENAMES, loc, envName, caller, ((_packageData$pkg = packageData.pkg) == null ? void 0 : _packageData$pkg.dirname) === loc ? packageToBabelConfig(packageData.pkg) : null);
}
if (!ignore) {
const ignoreLoc = _path().join(loc, BABELIGNORE_FILENAME);
ignore = yield* readIgnoreConfig(ignoreLoc);
if (ignore) {
debug("Found ignore %o from %o.", ignore.filepath, dirname);
}
}
}
return {
config,
ignore
};
}
function findRootConfig(dirname, envName, caller) {
return loadOneConfig(ROOT_CONFIG_FILENAMES, dirname, envName, caller);
}
function* loadOneConfig(names, dirname, envName, caller, previousConfig = null) {
const configs = yield* _gensync().all(names.map(filename => readConfig(_path().join(dirname, filename), envName, caller)));
const config = configs.reduce((previousConfig, config) => {
if (config && previousConfig) {
throw new _configError.default(`Multiple configuration files found. Please remove one:\n` + ` - ${_path().basename(previousConfig.filepath)}\n` + ` - ${config.filepath}\n` + `from ${dirname}`);
}
return config || previousConfig;
}, previousConfig);
if (config) {
debug("Found configuration %o from %o.", config.filepath, dirname);
}
return config;
}
function* loadConfig(name, dirname, envName, caller) {
const filepath = (((v, w) => (v = v.split("."), w = w.split("."), +v[0] > +w[0] || v[0] == w[0] && +v[1] >= +w[1]))(process.versions.node, "8.9") ? require.resolve : (r, {
paths: [b]
}, M = require("module")) => {
let f = M._findPath(r, M._nodeModulePaths(b).concat(b));
if (f) return f;
f = new Error(`Cannot resolve module '${r}'`);
f.code = "MODULE_NOT_FOUND";
throw f;
})(name, {
paths: [dirname]
});
const conf = yield* readConfig(filepath, envName, caller);
if (!conf) {
throw new _configError.default(`Config file contains no configuration data`, filepath);
}
debug("Loaded config %o from %o.", name, dirname);
return conf;
}
function readConfig(filepath, envName, caller) {
const ext = _path().extname(filepath);
switch (ext) {
case ".js":
case ".cjs":
case ".mjs":
case ".ts":
case ".cts":
case ".mts":
return readConfigCode(filepath, {
envName,
caller
});
default:
return readConfigJSON5(filepath);
}
}
function* resolveShowConfigPath(dirname) {
const targetPath = process.env.BABEL_SHOW_CONFIG_FOR;
if (targetPath != null) {
const absolutePath = _path().resolve(dirname, targetPath);
const stats = yield* fs.stat(absolutePath);
if (!stats.isFile()) {
throw new Error(`${absolutePath}: BABEL_SHOW_CONFIG_FOR must refer to a regular file, directories are not supported.`);
}
return absolutePath;
}
return null;
}
function throwConfigError(filepath) {
throw new _configError.default(`\
Caching was left unconfigured. Babel's plugins, presets, and .babelrc.js files can be configured
for various types of caching, using the first param of their handler functions:
module.exports = function(api) {
// The API exposes the following:
// Cache the returned value forever and don't call this function again.
api.cache(true);
// Don't cache at all. Not recommended because it will be very slow.
api.cache(false);
// Cached based on the value of some function. If this function returns a value different from
// a previously-encountered value, the plugins will re-evaluate.
var env = api.cache(() => process.env.NODE_ENV);
// If testing for a specific env, we recommend specifics to avoid instantiating a plugin for
// any possible NODE_ENV value that might come up during plugin execution.
var isProd = api.cache(() => process.env.NODE_ENV === "production");
// .cache(fn) will perform a linear search though instances to find the matching plugin based
// based on previous instantiated plugins. If you want to recreate the plugin and discard the
// previous instance whenever something changes, you may use:
var isProd = api.cache.invalidate(() => process.env.NODE_ENV === "production");
// Note, we also expose the following more-verbose versions of the above examples:
api.cache.forever(); // api.cache(true)
api.cache.never(); // api.cache(false)
api.cache.using(fn); // api.cache(fn)
// Return the value that will be cached.
return { };
};`, filepath);
}
0 && 0;
//# sourceMappingURL=configuration.js.map

Some files were not shown because too many files have changed in this diff Show More