mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
refactor: rename pool to provider (Data Provider)
This commit is contained in:
@@ -34,7 +34,6 @@ docs/
|
||||
|
||||
# Runtime data
|
||||
decision_logs/
|
||||
coin_pool_cache/
|
||||
*.log
|
||||
|
||||
# Config files (should be mounted)
|
||||
|
||||
@@ -38,7 +38,6 @@ data/
|
||||
|
||||
# 决策日志
|
||||
decision_logs/
|
||||
coin_pool_cache/
|
||||
nofx_test
|
||||
|
||||
# Node.js
|
||||
|
||||
+2
-2
@@ -205,8 +205,8 @@ nofx/
|
||||
├── market/ # マーケットデータ取得
|
||||
│ └── data.go # マーケットデータ&テクニカル指標(K線、RSI、MACD)
|
||||
│
|
||||
├── pool/ # コインプール管理
|
||||
│ └── coin_pool.go # AI500 + OI Topマージプール
|
||||
├── provider/ # データプロバイダー管理
|
||||
│ └── data_provider.go # AI500 + OI Top データプロバイダー
|
||||
│
|
||||
├── logger/ # ロギングシステム
|
||||
│ └── decision_logger.go # 判断記録 + パフォーマンス分析
|
||||
|
||||
+9
-9
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"nofx/debate"
|
||||
"nofx/logger"
|
||||
"nofx/pool"
|
||||
"nofx/provider"
|
||||
"nofx/store"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -161,32 +161,32 @@ func (h *DebateHandler) HandleCreateDebate(c *gin.Context) {
|
||||
case "coinpool":
|
||||
// Fetch from coin pool API
|
||||
if coinSource.CoinPoolAPIURL != "" {
|
||||
pool.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
provider.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
}
|
||||
if coins, err := pool.GetTopRatedCoins(1); err == nil && len(coins) > 0 {
|
||||
if coins, err := provider.GetTopRatedCoins(1); err == nil && len(coins) > 0 {
|
||||
req.Symbol = coins[0]
|
||||
logger.Infof("Fetched coin from pool API: %s", req.Symbol)
|
||||
}
|
||||
case "oi_top":
|
||||
// Fetch from OI top API
|
||||
if coinSource.OITopAPIURL != "" {
|
||||
pool.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
provider.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
}
|
||||
if coins, err := pool.GetOITopSymbols(); err == nil && len(coins) > 0 {
|
||||
if coins, err := provider.GetOITopSymbols(); err == nil && len(coins) > 0 {
|
||||
req.Symbol = coins[0]
|
||||
logger.Infof("Fetched coin from OI Top API: %s", req.Symbol)
|
||||
}
|
||||
case "mixed":
|
||||
// Try coin pool first, then OI top
|
||||
if coinSource.UseCoinPool && coinSource.CoinPoolAPIURL != "" {
|
||||
pool.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
if coins, err := pool.GetTopRatedCoins(1); err == nil && len(coins) > 0 {
|
||||
provider.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
if coins, err := provider.GetTopRatedCoins(1); err == nil && len(coins) > 0 {
|
||||
req.Symbol = coins[0]
|
||||
logger.Infof("Fetched coin from pool API (mixed): %s", req.Symbol)
|
||||
}
|
||||
} else if coinSource.UseOITop && coinSource.OITopAPIURL != "" {
|
||||
pool.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
if coins, err := pool.GetOITopSymbols(); err == nil && len(coins) > 0 {
|
||||
provider.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
if coins, err := provider.GetOITopSymbols(); err == nil && len(coins) > 0 {
|
||||
req.Symbol = coins[0]
|
||||
logger.Infof("Fetched coin from OI Top API (mixed): %s", req.Symbol)
|
||||
}
|
||||
|
||||
+10
-16
@@ -8,7 +8,7 @@ import (
|
||||
"nofx/logger"
|
||||
"nofx/market"
|
||||
"nofx/mcp"
|
||||
"nofx/pool"
|
||||
"nofx/provider"
|
||||
"nofx/store"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -76,8 +76,6 @@ type OITopData struct {
|
||||
OIDeltaPercent float64 // Open interest change percentage (1 hour)
|
||||
OIDeltaValue float64 // Open interest change value
|
||||
PriceDeltaPercent float64 // Price change percentage
|
||||
NetLong float64 // Net long positions
|
||||
NetShort float64 // Net short positions
|
||||
}
|
||||
|
||||
// TradingStats trading statistics (for AI input)
|
||||
@@ -120,7 +118,7 @@ type Context struct {
|
||||
MultiTFMarket map[string]map[string]*market.Data `json:"-"`
|
||||
OITopDataMap map[string]*OITopData `json:"-"`
|
||||
QuantDataMap map[string]*QuantData `json:"-"`
|
||||
OIRankingData *pool.OIRankingData `json:"-"` // Market-wide OI ranking data
|
||||
OIRankingData *provider.OIRankingData `json:"-"` // Market-wide OI ranking data
|
||||
BTCETHLeverage int `json:"-"`
|
||||
AltcoinLeverage int `json:"-"`
|
||||
Timeframes []string `json:"-"`
|
||||
@@ -175,8 +173,6 @@ type FlowTypeData struct {
|
||||
|
||||
type OIData struct {
|
||||
CurrentOI float64 `json:"current_oi"`
|
||||
NetLong float64 `json:"net_long"`
|
||||
NetShort float64 `json:"net_short"`
|
||||
Delta map[string]*OIDeltaData `json:"delta,omitempty"`
|
||||
}
|
||||
|
||||
@@ -242,7 +238,7 @@ func GetFullDecisionWithStrategy(ctx *Context, mcpClient mcp.AIClient, engine *S
|
||||
// Ensure OITopDataMap is initialized
|
||||
if ctx.OITopDataMap == nil {
|
||||
ctx.OITopDataMap = make(map[string]*OITopData)
|
||||
oiPositions, err := pool.GetOITopPositions()
|
||||
oiPositions, err := provider.GetOITopPositions()
|
||||
if err == nil {
|
||||
for _, pos := range oiPositions {
|
||||
ctx.OITopDataMap[pos.Symbol] = &OITopData{
|
||||
@@ -250,8 +246,6 @@ func GetFullDecisionWithStrategy(ctx *Context, mcpClient mcp.AIClient, engine *S
|
||||
OIDeltaPercent: pos.OIDeltaPercent,
|
||||
OIDeltaValue: pos.OIDeltaValue,
|
||||
PriceDeltaPercent: pos.PriceDeltaPercent,
|
||||
NetLong: pos.NetLong,
|
||||
NetShort: pos.NetShort,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,10 +384,10 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
coinSource := e.config.CoinSource
|
||||
|
||||
if coinSource.CoinPoolAPIURL != "" {
|
||||
pool.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
provider.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
}
|
||||
if coinSource.OITopAPIURL != "" {
|
||||
pool.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
provider.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
}
|
||||
|
||||
switch coinSource.SourceType {
|
||||
@@ -463,7 +457,7 @@ func (e *StrategyEngine) getCoinPoolCoins(limit int) ([]CandidateCoin, error) {
|
||||
limit = 30
|
||||
}
|
||||
|
||||
symbols, err := pool.GetTopRatedCoins(limit)
|
||||
symbols, err := provider.GetTopRatedCoins(limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -483,7 +477,7 @@ func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
positions, err := pool.GetOITopPositions()
|
||||
positions, err := provider.GetOITopPositions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -646,7 +640,7 @@ func (e *StrategyEngine) FetchQuantDataBatch(symbols []string) map[string]*Quant
|
||||
}
|
||||
|
||||
// FetchOIRankingData fetches market-wide OI ranking data
|
||||
func (e *StrategyEngine) FetchOIRankingData() *pool.OIRankingData {
|
||||
func (e *StrategyEngine) FetchOIRankingData() *provider.OIRankingData {
|
||||
indicators := e.config.Indicators
|
||||
if !indicators.EnableOIRanking {
|
||||
return nil
|
||||
@@ -680,7 +674,7 @@ func (e *StrategyEngine) FetchOIRankingData() *pool.OIRankingData {
|
||||
|
||||
logger.Infof("📊 Fetching OI ranking data (duration: %s, limit: %d)", duration, limit)
|
||||
|
||||
data, err := pool.GetOIRankingData(baseURL, authKey, duration, limit)
|
||||
data, err := provider.GetOIRankingData(baseURL, authKey, duration, limit)
|
||||
if err != nil {
|
||||
logger.Warnf("⚠️ Failed to fetch OI ranking data: %v", err)
|
||||
return nil
|
||||
@@ -956,7 +950,7 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
|
||||
|
||||
// OI Ranking data (market-wide open interest changes)
|
||||
if ctx.OIRankingData != nil {
|
||||
sb.WriteString(pool.FormatOIRankingForAI(ctx.OIRankingData))
|
||||
sb.WriteString(provider.FormatOIRankingForAI(ctx.OIRankingData))
|
||||
}
|
||||
|
||||
sb.WriteString("---\n\n")
|
||||
|
||||
@@ -57,8 +57,8 @@ nofx/
|
||||
│ └── monitor.go # Market data cache
|
||||
│ └── types.go # market structure
|
||||
|
||||
├── pool/ # Coin pool management
|
||||
│ └── coin_pool.go # AI500 + OI Top merged pool
|
||||
├── provider/ # Data provider management
|
||||
│ └── data_provider.go # AI500 + OI Top data provider
|
||||
│
|
||||
├── logger/ # Logging system
|
||||
│ └── decision_logger.go # Decision recording + performance analysis
|
||||
|
||||
@@ -57,8 +57,8 @@ nofx/
|
||||
│ └── monitor.go # 行情数据缓存
|
||||
│ └── types.go # market结构体
|
||||
│
|
||||
├── pool/ # 币种池管理
|
||||
│ └── coin_pool.go # AI500 + OI Top 合并池
|
||||
├── provider/ # 数据源管理
|
||||
│ └── data_provider.go # AI500 + OI Top 数据源
|
||||
│
|
||||
├── logger/ # 日志系统
|
||||
│ └── decision_logger.go # 决策记录 + 性能分析
|
||||
|
||||
@@ -1,473 +0,0 @@
|
||||
# 🐳 Docker One-Click Deployment Guide
|
||||
|
||||
This guide will help you quickly deploy the NOFX AI Trading Competition System using Docker.
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
Before you begin, ensure your system has:
|
||||
|
||||
- **Docker**: Version 20.10 or higher
|
||||
- **Docker Compose**: Version 2.0 or higher
|
||||
|
||||
### Installing Docker
|
||||
|
||||
#### macOS / Windows
|
||||
Download and install [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||
|
||||
#### Linux (Ubuntu/Debian)
|
||||
|
||||
> #### Docker Compose Version Notes
|
||||
>
|
||||
> **New User Recommendation:**
|
||||
> - **Use Docker Desktop**: Automatically includes latest Docker Compose, no separate installation needed
|
||||
> - Simple installation, one-click setup, provides GUI management
|
||||
> - Supports macOS, Windows, and some Linux distributions
|
||||
>
|
||||
> **Upgrading User Note:**
|
||||
> - **Deprecating standalone docker-compose**: No longer recommended to download the independent Docker Compose binary
|
||||
> - **Use built-in version**: Docker 20.10+ includes `docker compose` command (with space)
|
||||
> - If still using old `docker-compose`, please upgrade to new syntax
|
||||
|
||||
*Recommended: Use Docker Desktop (if available) or Docker CE with built-in Compose*
|
||||
|
||||
```bash
|
||||
# Install Docker (includes compose)
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
|
||||
# Add user to docker group
|
||||
sudo usermod -aG docker $USER
|
||||
newgrp docker
|
||||
|
||||
# Verify installation (new command)
|
||||
docker --version
|
||||
docker compose --version # Docker 24+ includes this, no separate installation needed
|
||||
```
|
||||
|
||||
## 🚀 Quick Start (3 Steps)
|
||||
|
||||
### Step 1: Prepare Configuration File
|
||||
|
||||
```bash
|
||||
# Copy configuration template
|
||||
cp config.json.example config.json
|
||||
|
||||
# Edit configuration file with your API keys
|
||||
nano config.json # or use any other editor
|
||||
|
||||
⚠️ **Note**: Basic config.json is still needed for some settings, but ~~trader configurations~~ are now done through the web interface.
|
||||
```
|
||||
|
||||
**Required fields:**
|
||||
```json
|
||||
{
|
||||
"traders": [
|
||||
{
|
||||
"id": "my_trader",
|
||||
"name": "My AI Trader",
|
||||
"ai_model": "deepseek",
|
||||
"binance_api_key": "YOUR_BINANCE_API_KEY", // ← Your Binance API Key
|
||||
"binance_secret_key": "YOUR_BINANCE_SECRET_KEY", // ← Your Binance Secret Key
|
||||
"deepseek_key": "YOUR_DEEPSEEK_API_KEY", // ← Your DeepSeek API Key
|
||||
"initial_balance": 1000.0,
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"use_default_coins": true,
|
||||
"api_server_port": 8080
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: One-Click Start
|
||||
|
||||
```bash
|
||||
# Build and start all services (first run)
|
||||
docker compose up -d --build
|
||||
|
||||
# Subsequent starts (without rebuilding)
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Startup options:**
|
||||
- `--build`: Build Docker images (use on first run or after code updates)
|
||||
- `-d`: Run in detached mode (background)
|
||||
|
||||
### Step 3: Access the System
|
||||
|
||||
Once deployed, open your browser and visit:
|
||||
|
||||
- **Web Interface**: http://localhost:3000
|
||||
- **API Health Check**: http://localhost:8080/api/health
|
||||
|
||||
## 📊 Service Management
|
||||
|
||||
### View Running Status
|
||||
```bash
|
||||
# View all container status
|
||||
docker compose ps
|
||||
|
||||
# View service health status
|
||||
docker compose ps --format json | jq
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
# View all service logs
|
||||
docker compose logs -f
|
||||
|
||||
# View backend logs only
|
||||
docker compose logs -f backend
|
||||
|
||||
# View frontend logs only
|
||||
docker compose logs -f frontend
|
||||
|
||||
# View last 100 lines
|
||||
docker compose logs --tail=100
|
||||
```
|
||||
|
||||
### Stop Services
|
||||
```bash
|
||||
# Stop all services (keep data)
|
||||
docker compose stop
|
||||
|
||||
# Stop and remove containers (keep data)
|
||||
docker compose down
|
||||
|
||||
# Stop and remove containers and volumes (clear all data)
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### Restart Services
|
||||
```bash
|
||||
# Restart all services
|
||||
docker compose restart
|
||||
|
||||
# Restart backend only
|
||||
docker compose restart backend
|
||||
|
||||
# Restart frontend only
|
||||
docker compose restart frontend
|
||||
```
|
||||
|
||||
### Update Services
|
||||
```bash
|
||||
# Pull latest code
|
||||
git pull
|
||||
|
||||
# Rebuild and restart
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## 🔧 Advanced Configuration
|
||||
|
||||
### Change Ports
|
||||
|
||||
Edit `docker-compose.yml` to modify port mappings:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "8080:8080" # Change to "your_port:8080"
|
||||
|
||||
frontend:
|
||||
ports:
|
||||
- "3000:80" # Change to "your_port:80"
|
||||
```
|
||||
|
||||
### Resource Limits
|
||||
|
||||
Add resource limits in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 2G
|
||||
reservations:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create `.env` file to manage environment variables:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
TZ=Asia/Shanghai
|
||||
BACKEND_PORT=8080
|
||||
FRONTEND_PORT=3000
|
||||
```
|
||||
|
||||
Then use in `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "${BACKEND_PORT}:8080"
|
||||
```
|
||||
|
||||
## 📁 Data Persistence
|
||||
|
||||
The system automatically persists data to local directories:
|
||||
|
||||
- `./decision_logs/`: AI decision logs
|
||||
- `./coin_pool_cache/`: Coin pool cache
|
||||
- ~~`./config.json`: Configuration file (mounted)~~ (Deprecated)
|
||||
|
||||
**Data locations:**
|
||||
```bash
|
||||
# View data directories
|
||||
ls -la decision_logs/
|
||||
ls -la coin_pool_cache/
|
||||
|
||||
# Backup data
|
||||
tar -czf backup_$(date +%Y%m%d).tar.gz decision_logs/ coin_pool_cache/ ~~config.json~~ trading.db
|
||||
|
||||
# Restore data
|
||||
tar -xzf backup_20241029.tar.gz
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Container Won't Start
|
||||
|
||||
```bash
|
||||
# View detailed error messages
|
||||
docker compose logs backend
|
||||
docker compose logs frontend
|
||||
|
||||
# Check container status
|
||||
docker compose ps -a
|
||||
|
||||
# Rebuild (clear cache)
|
||||
docker compose build --no-cache
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
```bash
|
||||
# Find process using the port
|
||||
lsof -i :8080 # backend port
|
||||
lsof -i :3000 # frontend port
|
||||
|
||||
# Kill the process
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
### Configuration File Not Found
|
||||
|
||||
```bash
|
||||
# ~~Ensure config.json exists~~
|
||||
# ~~ls -la config.json~~
|
||||
|
||||
# ~~If not exists, copy template~~
|
||||
# ~~cp config.json.example config.json~~
|
||||
|
||||
*Note: Now using SQLite database for configuration storage, no longer need config.json*
|
||||
```
|
||||
|
||||
### Health Check Failing
|
||||
|
||||
```bash
|
||||
# Check health status
|
||||
docker inspect nofx-backend | jq '.[0].State.Health'
|
||||
docker inspect nofx-frontend | jq '.[0].State.Health'
|
||||
|
||||
# Manually test health endpoints
|
||||
curl http://localhost:8080/api/health
|
||||
curl http://localhost:3000/health
|
||||
```
|
||||
|
||||
### Frontend Can't Connect to Backend
|
||||
|
||||
```bash
|
||||
# Check network connectivity
|
||||
docker compose exec frontend ping backend
|
||||
|
||||
# Check if backend service is running
|
||||
docker compose exec frontend wget -O- http://backend:8080/health
|
||||
```
|
||||
|
||||
### Clean Docker Resources
|
||||
|
||||
```bash
|
||||
# Clean unused images
|
||||
docker image prune -a
|
||||
|
||||
# Clean unused volumes
|
||||
docker volume prune
|
||||
|
||||
# Clean all unused resources (use with caution)
|
||||
docker system prune -a --volumes
|
||||
```
|
||||
|
||||
## 🔐 Security Recommendations
|
||||
|
||||
1. ~~**Don't commit config.json to Git**~~
|
||||
```bash
|
||||
# ~~Ensure config.json is in .gitignore~~
|
||||
# ~~echo "config.json" >> .gitignore~~
|
||||
```
|
||||
|
||||
*Note: Now using trading.db database, ensure not to commit sensitive data*
|
||||
|
||||
2. **Use environment variables for sensitive data**
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
backend:
|
||||
environment:
|
||||
- BINANCE_API_KEY=${BINANCE_API_KEY}
|
||||
- BINANCE_SECRET_KEY=${BINANCE_SECRET_KEY}
|
||||
```
|
||||
|
||||
3. **Restrict API access**
|
||||
```yaml
|
||||
# Only allow local access
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
```
|
||||
|
||||
4. **Regularly update images**
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 🌐 Production Deployment
|
||||
|
||||
### Using Nginx Reverse Proxy
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/nofx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8080/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configure HTTPS (Let's Encrypt)
|
||||
|
||||
```bash
|
||||
# Install Certbot
|
||||
sudo apt-get install certbot python3-certbot-nginx
|
||||
|
||||
# Get SSL certificate
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
|
||||
# Auto-renewal
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Using Docker Swarm (Cluster Deployment)
|
||||
|
||||
```bash
|
||||
# Initialize Swarm
|
||||
docker swarm init
|
||||
|
||||
# Deploy stack
|
||||
docker stack deploy -c docker-compose.yml nofx
|
||||
|
||||
# View service status
|
||||
docker stack services nofx
|
||||
|
||||
# Scale services
|
||||
docker service scale nofx_backend=3
|
||||
```
|
||||
|
||||
## 📈 Monitoring & Logging
|
||||
|
||||
### Log Management
|
||||
|
||||
```bash
|
||||
# Configure log rotation (already configured in docker-compose.yml)
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# View log statistics
|
||||
docker compose logs --timestamps | wc -l
|
||||
```
|
||||
|
||||
### Monitoring Tool Integration
|
||||
|
||||
Integrate Prometheus + Grafana for monitoring:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml (add monitoring services)
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
ports:
|
||||
- "3001:3000"
|
||||
```
|
||||
|
||||
## 🆘 Get Help
|
||||
|
||||
- **GitHub Issues**: [Submit an issue](https://github.com/yourusername/open-nofx/issues)
|
||||
- **Documentation**: Check [README.md](README.md)
|
||||
- **Community**: Join our Discord/Telegram group
|
||||
|
||||
## 📝 Command Cheat Sheet
|
||||
|
||||
```bash
|
||||
# Start
|
||||
docker compose up -d --build # Build and start
|
||||
docker compose up -d # Start (without rebuilding)
|
||||
|
||||
# Stop
|
||||
docker compose stop # Stop services
|
||||
docker compose down # Stop and remove containers
|
||||
docker compose down -v # Stop and remove containers and data
|
||||
|
||||
# View
|
||||
docker compose ps # View status
|
||||
docker compose logs -f # View logs
|
||||
docker compose top # View processes
|
||||
|
||||
# Restart
|
||||
docker compose restart # Restart all services
|
||||
docker compose restart backend # Restart backend
|
||||
|
||||
# Update
|
||||
git pull && docker compose up -d --build
|
||||
|
||||
# Clean
|
||||
docker compose down -v # Clear all data
|
||||
docker system prune -a # Clean Docker resources
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
🎉 Congratulations! You've successfully deployed the NOFX AI Trading Competition System!
|
||||
|
||||
If you encounter any issues, please check the [Troubleshooting](#-troubleshooting) section or submit an issue.
|
||||
@@ -1,489 +0,0 @@
|
||||
# 🐳 Docker 一键部署教程
|
||||
|
||||
本教程将指导你使用 Docker 快速部署 NOFX AI 交易竞赛系统。
|
||||
|
||||
## 📋 前置要求
|
||||
|
||||
在开始之前,请确保你的系统已安装:
|
||||
|
||||
- **Docker**: 版本 20.10 或更高
|
||||
- **Docker Compose**: 版本 2.0 或更高
|
||||
|
||||
### 安装 Docker
|
||||
|
||||
> #### 提示:Docker Compose 版本说明
|
||||
>
|
||||
> **新用户建议**:
|
||||
> - **推荐使用 Docker Desktop**:自动包含最新 Docker Compose,无需单独安装
|
||||
> - 安装简单,一键搞定,提供图形界面管理
|
||||
> - 支持 macOS、Windows、部分 Linux 发行版
|
||||
>
|
||||
> **旧用户提醒**:
|
||||
> - **弃用独立 docker-compose**:不再推荐下载独立的 Docker Compose 二进制文件
|
||||
> - **使用内置版**:Docker 20.10+ 自带 `docker compose` 命令(注意是空格)
|
||||
> - 如果还在使用旧的 `docker-compose`,请升级到新语法
|
||||
|
||||
#### macOS / Windows
|
||||
下载并安装 [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||
|
||||
**安装后验证:**
|
||||
```bash
|
||||
docker --version
|
||||
docker compose --version # 注意:使用空格,不再是连字符
|
||||
```
|
||||
|
||||
#### Linux (Ubuntu/Debian)
|
||||
**推荐方式:使用 Docker Desktop(如果可用)或 Docker CE**
|
||||
|
||||
```bash
|
||||
# 安装 Docker (自动包含 compose)
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
|
||||
# 将当前用户加入 docker 组
|
||||
sudo usermod -aG docker $USER
|
||||
newgrp docker
|
||||
|
||||
# 验证安装(新命令)
|
||||
docker --version
|
||||
docker compose --version # Docker 24+ 自带,无需单独安装
|
||||
```
|
||||
|
||||
## 🚀 快速开始(3步完成部署)
|
||||
|
||||
### 第 1 步:准备配置文件
|
||||
|
||||
```bash
|
||||
# 复制配置文件模板
|
||||
cp config.json.example config.json
|
||||
|
||||
# 编辑配置文件,填入你的 API 密钥
|
||||
nano config.json # 或使用其他编辑器
|
||||
```
|
||||
|
||||
**必须配置的字段:**
|
||||
```json
|
||||
{
|
||||
"use_default_coins": true,
|
||||
"api_server_port": 8081,
|
||||
"jwt_secret": "YOUR_JWT_SECRET_CHANGE_IN_PRODUCTION" // ← 填入一个长随机字符串作为JWT密钥
|
||||
}
|
||||
```
|
||||
|
||||
> **⚠️ 重要安全提醒**:
|
||||
> - `jwt_secret` 字段是用户认证系统的关键安全配置
|
||||
> - **必须设置一个长度至少32位的随机字符串**
|
||||
> - 在生产环境中,建议使用64位以上的随机字符串
|
||||
> - 可以使用命令生成:`openssl rand -base64 64`
|
||||
|
||||
**配置说明:**
|
||||
- 🔐 **用户认证**:系统现在支持用户注册登录,每个用户都有独立的AI模型和交易所配置
|
||||
- 🚫 **移除traders配置**:不再需要在config.json中预配置交易员,用户可以通过Web界面创建
|
||||
- 🔑 **JWT密钥**:用于保护用户会话安全,强烈建议在生产环境中设置复杂密钥
|
||||
|
||||
### 第 2 步:一键启动
|
||||
|
||||
```bash
|
||||
# 构建并启动所有服务(首次运行)
|
||||
docker compose up -d --build
|
||||
|
||||
# 后续启动(不重新构建)
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**启动过程说明:**
|
||||
- `--build`: 构建 Docker 镜像(首次运行或代码更新后使用)
|
||||
- `-d`: 后台运行(detached mode)
|
||||
|
||||
### 第 3 步:访问系统
|
||||
|
||||
部署成功后,打开浏览器访问:
|
||||
|
||||
- **Web 界面**: http://localhost:3000
|
||||
- **API 文档**: http://localhost:8080/api/health
|
||||
|
||||
## 📊 服务管理
|
||||
|
||||
### 查看运行状态
|
||||
```bash
|
||||
# 查看所有容器状态
|
||||
docker compose ps
|
||||
|
||||
# 查看服务健康状态
|
||||
docker compose ps --format json | jq
|
||||
```
|
||||
|
||||
### 查看日志
|
||||
```bash
|
||||
# 查看所有服务日志
|
||||
docker compose logs -f
|
||||
|
||||
# 只查看后端日志
|
||||
docker compose logs -f backend
|
||||
|
||||
# 只查看前端日志
|
||||
docker compose logs -f frontend
|
||||
|
||||
# 查看最近 100 行日志
|
||||
docker compose logs --tail=100
|
||||
```
|
||||
|
||||
### 停止服务
|
||||
```bash
|
||||
# 停止所有服务(保留数据)
|
||||
docker compose stop
|
||||
|
||||
# 停止并删除容器(保留数据)
|
||||
docker compose down
|
||||
|
||||
# 停止并删除容器和卷(清除所有数据)
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### 重启服务
|
||||
```bash
|
||||
# 重启所有服务
|
||||
docker compose restart
|
||||
|
||||
# 只重启后端
|
||||
docker compose restart backend
|
||||
|
||||
# 只重启前端
|
||||
docker compose restart frontend
|
||||
```
|
||||
|
||||
### 更新服务
|
||||
```bash
|
||||
# 拉取最新代码
|
||||
git pull
|
||||
|
||||
# 重新构建并重启
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## 🔧 高级配置
|
||||
|
||||
### 修改端口
|
||||
|
||||
编辑 `docker-compose.yml`,修改端口映射:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "8080:8080" # 改为 "你的端口:8080"
|
||||
|
||||
frontend:
|
||||
ports:
|
||||
- "3000:80" # 改为 "你的端口:80"
|
||||
```
|
||||
|
||||
### 资源限制
|
||||
|
||||
在 `docker-compose.yml` 中添加资源限制:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 2G
|
||||
reservations:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
创建 `.env` 文件来管理环境变量:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
TZ=Asia/Shanghai
|
||||
BACKEND_PORT=8080
|
||||
FRONTEND_PORT=3000
|
||||
```
|
||||
|
||||
然后在 `docker-compose.yml` 中使用:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "${BACKEND_PORT}:8080"
|
||||
```
|
||||
|
||||
## 📁 数据持久化
|
||||
|
||||
系统会自动持久化以下数据到本地目录:
|
||||
|
||||
- `./decision_logs/`: AI 决策日志
|
||||
- `./coin_pool_cache/`: 币种池缓存
|
||||
- `./config.json`: 配置文件(挂载)
|
||||
|
||||
**数据位置:**
|
||||
```bash
|
||||
# 查看数据目录
|
||||
ls -la decision_logs/
|
||||
ls -la coin_pool_cache/
|
||||
|
||||
# 备份数据
|
||||
tar -czf backup_$(date +%Y%m%d).tar.gz decision_logs/ coin_pool_cache/ config.json
|
||||
|
||||
# 恢复数据
|
||||
tar -xzf backup_20241029.tar.gz
|
||||
```
|
||||
|
||||
## 🐛 故障排查
|
||||
|
||||
### 容器无法启动
|
||||
|
||||
```bash
|
||||
# 查看详细错误信息
|
||||
docker compose logs backend
|
||||
docker compose logs frontend
|
||||
|
||||
# 检查容器状态
|
||||
docker compose ps -a
|
||||
|
||||
# 重新构建(清除缓存)
|
||||
docker compose build --no-cache
|
||||
```
|
||||
|
||||
### 端口被占用
|
||||
|
||||
```bash
|
||||
# 查找占用端口的进程
|
||||
lsof -i :8080 # 后端端口
|
||||
lsof -i :3000 # 前端端口
|
||||
|
||||
# 杀死占用端口的进程
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
### 配置文件未找到
|
||||
|
||||
```bash
|
||||
# 确保 config.json 存在
|
||||
ls -la config.json
|
||||
|
||||
# 如果不存在,复制模板
|
||||
cp config.json.example config.json
|
||||
```
|
||||
|
||||
### 健康检查失败
|
||||
|
||||
```bash
|
||||
# 检查健康状态
|
||||
docker inspect nofx-backend | jq '.[0].State.Health'
|
||||
docker inspect nofx-frontend | jq '.[0].State.Health'
|
||||
|
||||
# 手动测试健康端点
|
||||
curl http://localhost:8080/api/health
|
||||
curl http://localhost:3000/health
|
||||
```
|
||||
|
||||
### 前端无法连接后端
|
||||
|
||||
```bash
|
||||
# 检查网络连接
|
||||
docker compose exec frontend ping backend
|
||||
|
||||
# 检查后端服务是否正常
|
||||
docker compose exec frontend wget -O- http://backend:8080/health
|
||||
```
|
||||
|
||||
### 清理 Docker 资源
|
||||
|
||||
```bash
|
||||
# 清理未使用的镜像
|
||||
docker image prune -a
|
||||
|
||||
# 清理未使用的卷
|
||||
docker volume prune
|
||||
|
||||
# 清理所有未使用的资源(慎用)
|
||||
docker system prune -a --volumes
|
||||
```
|
||||
|
||||
## 🔐 安全建议
|
||||
|
||||
1. **JWT密钥安全配置**
|
||||
```bash
|
||||
# 生成强随机JWT密钥
|
||||
openssl rand -base64 64
|
||||
|
||||
# 或者使用其他工具生成
|
||||
head -c 64 /dev/urandom | base64
|
||||
```
|
||||
|
||||
**JWT密钥要求:**
|
||||
- 长度至少32位,推荐64位以上
|
||||
- 使用随机生成的字符串
|
||||
- 在生产环境中绝不使用默认值
|
||||
- 定期更换(会使现有用户需要重新登录)
|
||||
|
||||
2. ~~**不要将 config.json 提交到 Git**~~
|
||||
```bash
|
||||
# ~~确保 config.json 在 .gitignore 中~~
|
||||
# ~~echo "config.json" >> .gitignore~~
|
||||
```
|
||||
|
||||
*注意:现在使用trading.db数据库,请确保不提交敏感数据*
|
||||
|
||||
3. **使用环境变量存储敏感信息**
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
backend:
|
||||
environment:
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
# 用户的API密钥现在通过Web界面配置,不再需要环境变量
|
||||
```
|
||||
|
||||
4. **限制 API 访问**
|
||||
```yaml
|
||||
# 只允许本地访问
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
```
|
||||
|
||||
4. **定期更新镜像**
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 🌐 生产环境部署
|
||||
|
||||
### 使用 Nginx 反向代理
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/nofx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8080/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配置 HTTPS (Let's Encrypt)
|
||||
|
||||
```bash
|
||||
# 安装 Certbot
|
||||
sudo apt-get install certbot python3-certbot-nginx
|
||||
|
||||
# 获取 SSL 证书
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
|
||||
# 自动续期
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
### 使用 Docker Swarm (集群部署)
|
||||
|
||||
```bash
|
||||
# 初始化 Swarm
|
||||
docker swarm init
|
||||
|
||||
# 部署堆栈
|
||||
docker stack deploy -c docker-compose.yml nofx
|
||||
|
||||
# 查看服务状态
|
||||
docker stack services nofx
|
||||
|
||||
# 扩展服务
|
||||
docker service scale nofx_backend=3
|
||||
```
|
||||
|
||||
## 📈 监控与日志
|
||||
|
||||
### 日志管理
|
||||
|
||||
```bash
|
||||
# 配置日志轮转(已在 docker-compose.yml 中配置)
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# 查看日志统计
|
||||
docker compose logs --timestamps | wc -l
|
||||
```
|
||||
|
||||
### 监控工具集成
|
||||
|
||||
可以集成 Prometheus + Grafana 进行监控:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml (添加监控服务)
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
ports:
|
||||
- "3001:3000"
|
||||
```
|
||||
|
||||
## 🆘 获取帮助
|
||||
|
||||
- **GitHub Issues**: [提交问题](https://github.com/yourusername/open-nofx/issues)
|
||||
- **文档**: 查看 [README.md](README.md)
|
||||
- **社区**: 加入我们的 Discord/Telegram 群组
|
||||
|
||||
## 📝 常用命令速查表
|
||||
|
||||
```bash
|
||||
# 启动
|
||||
docker compose up -d --build # 构建并启动
|
||||
docker compose up -d # 启动(不重新构建)
|
||||
|
||||
# 停止
|
||||
docker compose stop # 停止服务
|
||||
docker compose down # 停止并删除容器
|
||||
docker compose down -v # 停止并删除容器和数据
|
||||
|
||||
# 查看
|
||||
docker compose ps # 查看状态
|
||||
docker compose logs -f # 查看日志
|
||||
docker compose top # 查看进程
|
||||
|
||||
# 重启
|
||||
docker compose restart # 重启所有服务
|
||||
docker compose restart backend # 重启后端
|
||||
|
||||
# 更新
|
||||
git pull && docker compose up -d --build
|
||||
|
||||
# 清理
|
||||
docker compose down -v # 清除所有数据
|
||||
docker system prune -a # 清理 Docker 资源
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
🎉 恭喜!你已经成功部署了 NOFX AI 交易竞赛系统!
|
||||
|
||||
如有问题,请查看[故障排查](#-故障排查)部分或提交 Issue。
|
||||
@@ -1,472 +0,0 @@
|
||||
# 🐳 Dockerワンクリックデプロイガイド
|
||||
|
||||
このガイドは、Dockerを使用してNOFX AIトレーディング競争システムを迅速にデプロイする方法を説明します。
|
||||
|
||||
## 📋 前提条件
|
||||
|
||||
開始する前に、システムに以下が必要です:
|
||||
|
||||
- **Docker**: バージョン20.10以上
|
||||
- **Docker Compose**: バージョン2.0以上
|
||||
|
||||
### Dockerのインストール
|
||||
|
||||
#### macOS / Windows
|
||||
[Docker Desktop](https://www.docker.com/products/docker-desktop/)をダウンロードしてインストール
|
||||
|
||||
#### Linux (Ubuntu/Debian)
|
||||
|
||||
> #### Docker Composeバージョンに関する注意
|
||||
>
|
||||
> **新規ユーザー推奨:**
|
||||
> - **Docker Desktopを使用**: 最新のDocker Composeが自動的に含まれ、別途インストールは不要
|
||||
> - シンプルなインストール、ワンクリックセットアップ、GUI管理を提供
|
||||
> - macOS、Windows、一部のLinuxディストリビューションをサポート
|
||||
>
|
||||
> **既存ユーザー向け注意:**
|
||||
> - **スタンドアロンdocker-composeの非推奨**: 独立したDocker Composeバイナリのダウンロードは推奨されません
|
||||
> - **組み込みバージョンを使用**: Docker 20.10+には`docker compose`コマンド(スペース付き)が含まれています
|
||||
> - 古い`docker-compose`をまだ使用している場合は、新しい構文にアップグレードしてください
|
||||
|
||||
*推奨:Docker Desktop(利用可能な場合)またはCompose組み込みのDocker CEを使用*
|
||||
|
||||
```bash
|
||||
# Dockerをインストール(composeを含む)
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
|
||||
# dockerグループにユーザーを追加
|
||||
sudo usermod -aG docker $USER
|
||||
newgrp docker
|
||||
|
||||
# インストールを確認(新しいコマンド)
|
||||
docker --version
|
||||
docker compose --version # Docker 24+にはこれが含まれており、別途インストール不要
|
||||
```
|
||||
|
||||
## 🚀 クイックスタート(3ステップ)
|
||||
|
||||
### ステップ1:設定ファイルを準備
|
||||
|
||||
```bash
|
||||
# 設定テンプレートをコピー
|
||||
cp config.json.example config.json
|
||||
|
||||
# APIキーで設定ファイルを編集
|
||||
nano config.json # または他のエディタを使用
|
||||
```
|
||||
|
||||
**必須フィールド:**
|
||||
```json
|
||||
{
|
||||
"traders": [
|
||||
{
|
||||
"id": "my_trader",
|
||||
"name": "My AI Trader",
|
||||
"ai_model": "deepseek",
|
||||
"binance_api_key": "YOUR_BINANCE_API_KEY", // ← BinanceのAPIキー
|
||||
"binance_secret_key": "YOUR_BINANCE_SECRET_KEY", // ← Binanceのシークレットキー
|
||||
"deepseek_key": "YOUR_DEEPSEEK_API_KEY", // ← DeepSeekのAPIキー
|
||||
"initial_balance": 1000.0,
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"use_default_coins": true,
|
||||
"api_server_port": 8080
|
||||
}
|
||||
```
|
||||
|
||||
### ステップ2:ワンクリック起動
|
||||
|
||||
```bash
|
||||
# すべてのサービスをビルドして起動(初回実行)
|
||||
docker compose up -d --build
|
||||
|
||||
# 以降の起動(リビルドなし)
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**起動オプション:**
|
||||
- `--build`: Dockerイメージをビルド(初回実行またはコード更新後に使用)
|
||||
- `-d`: デタッチモードで実行(バックグラウンド)
|
||||
|
||||
### ステップ3:システムにアクセス
|
||||
|
||||
デプロイが完了したら、ブラウザを開いて以下にアクセス:
|
||||
|
||||
- **Webインターフェース**: http://localhost:3000
|
||||
- **APIヘルスチェック**: http://localhost:8080/health
|
||||
|
||||
## 📊 サービス管理
|
||||
|
||||
### 実行状態を表示
|
||||
|
||||
```bash
|
||||
# すべてのコンテナステータスを表示
|
||||
docker compose ps
|
||||
|
||||
# サービスヘルスステータスを表示
|
||||
docker compose ps --format json | jq
|
||||
```
|
||||
|
||||
### ログを表示
|
||||
|
||||
```bash
|
||||
# すべてのサービスログを表示
|
||||
docker compose logs -f
|
||||
|
||||
# バックエンドログのみを表示
|
||||
docker compose logs -f backend
|
||||
|
||||
# フロントエンドログのみを表示
|
||||
docker compose logs -f frontend
|
||||
|
||||
# 最後の100行を表示
|
||||
docker compose logs --tail=100
|
||||
```
|
||||
|
||||
### サービスを停止
|
||||
|
||||
```bash
|
||||
# すべてのサービスを停止(データを保持)
|
||||
docker compose stop
|
||||
|
||||
# コンテナを停止して削除(データを保持)
|
||||
docker compose down
|
||||
|
||||
# コンテナとボリュームを停止して削除(すべてのデータをクリア)
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### サービスを再起動
|
||||
|
||||
```bash
|
||||
# すべてのサービスを再起動
|
||||
docker compose restart
|
||||
|
||||
# バックエンドのみを再起動
|
||||
docker compose restart backend
|
||||
|
||||
# フロントエンドのみを再起動
|
||||
docker compose restart frontend
|
||||
```
|
||||
|
||||
### サービスを更新
|
||||
|
||||
```bash
|
||||
# 最新のコードをプル
|
||||
git pull
|
||||
|
||||
# リビルドして再起動
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## 🔧 高度な設定
|
||||
|
||||
### ポートを変更
|
||||
|
||||
`docker-compose.yml`を編集してポートマッピングを変更:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "8080:8080" # "your_port:8080"に変更
|
||||
|
||||
frontend:
|
||||
ports:
|
||||
- "3000:80" # "your_port:80"に変更
|
||||
```
|
||||
|
||||
### リソース制限
|
||||
|
||||
`docker-compose.yml`にリソース制限を追加:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 2G
|
||||
reservations:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
```
|
||||
|
||||
### 環境変数
|
||||
|
||||
`.env`ファイルを作成して環境変数を管理:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
TZ=Asia/Tokyo
|
||||
BACKEND_PORT=8080
|
||||
FRONTEND_PORT=3000
|
||||
```
|
||||
|
||||
次に`docker-compose.yml`で使用:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "${BACKEND_PORT}:8080"
|
||||
```
|
||||
|
||||
## 📁 データの永続化
|
||||
|
||||
システムは自動的にデータをローカルディレクトリに永続化します:
|
||||
|
||||
- `./decision_logs/`: AI判断ログ
|
||||
- `./coin_pool_cache/`: コインプールキャッシュ
|
||||
- `./config.json`: 設定ファイル(マウント済み)
|
||||
|
||||
**データの場所:**
|
||||
```bash
|
||||
# データディレクトリを表示
|
||||
ls -la decision_logs/
|
||||
ls -la coin_pool_cache/
|
||||
|
||||
# データをバックアップ
|
||||
tar -czf backup_$(date +%Y%m%d).tar.gz decision_logs/ coin_pool_cache/ config.json
|
||||
|
||||
# データを復元
|
||||
tar -xzf backup_20241029.tar.gz
|
||||
```
|
||||
|
||||
## 🐛 トラブルシューティング
|
||||
|
||||
### コンテナが起動しない
|
||||
|
||||
```bash
|
||||
# 詳細なエラーメッセージを表示
|
||||
docker compose logs backend
|
||||
docker compose logs frontend
|
||||
|
||||
# コンテナステータスを確認
|
||||
docker compose ps -a
|
||||
|
||||
# リビルド(キャッシュをクリア)
|
||||
docker compose build --no-cache
|
||||
```
|
||||
|
||||
### ポートが既に使用中
|
||||
|
||||
```bash
|
||||
# ポートを使用しているプロセスを検索
|
||||
lsof -i :8080 # バックエンドポート
|
||||
lsof -i :3000 # フロントエンドポート
|
||||
|
||||
# プロセスを強制終了
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
### 設定ファイルが見つからない
|
||||
|
||||
```bash
|
||||
# config.jsonが存在することを確認
|
||||
ls -la config.json
|
||||
|
||||
# 存在しない場合、テンプレートをコピー
|
||||
cp config.json.example config.json
|
||||
```
|
||||
|
||||
### ヘルスチェックが失敗
|
||||
|
||||
```bash
|
||||
# ヘルスステータスを確認
|
||||
docker inspect nofx-backend | jq '.[0].State.Health'
|
||||
docker inspect nofx-frontend | jq '.[0].State.Health'
|
||||
|
||||
# ヘルスエンドポイントを手動でテスト
|
||||
curl http://localhost:8080/health
|
||||
curl http://localhost:3000/health
|
||||
```
|
||||
|
||||
### フロントエンドがバックエンドに接続できない
|
||||
|
||||
```bash
|
||||
# ネットワーク接続を確認
|
||||
docker compose exec frontend ping backend
|
||||
|
||||
# バックエンドサービスが実行中か確認
|
||||
docker compose exec frontend wget -O- http://backend:8080/health
|
||||
```
|
||||
|
||||
### Dockerリソースをクリーン
|
||||
|
||||
```bash
|
||||
# 未使用のイメージをクリーン
|
||||
docker image prune -a
|
||||
|
||||
# 未使用のボリュームをクリーン
|
||||
docker volume prune
|
||||
|
||||
# すべての未使用リソースをクリーン(注意して使用)
|
||||
docker system prune -a --volumes
|
||||
```
|
||||
|
||||
## 🔐 セキュリティ推奨事項
|
||||
|
||||
1. **config.jsonをGitにコミットしない**
|
||||
```bash
|
||||
# config.jsonが.gitignoreに含まれていることを確認
|
||||
echo "config.json" >> .gitignore
|
||||
```
|
||||
|
||||
2. **機密データには環境変数を使用**
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
backend:
|
||||
environment:
|
||||
- BINANCE_API_KEY=${BINANCE_API_KEY}
|
||||
- BINANCE_SECRET_KEY=${BINANCE_SECRET_KEY}
|
||||
```
|
||||
|
||||
3. **APIアクセスを制限**
|
||||
```yaml
|
||||
# ローカルアクセスのみを許可
|
||||
services:
|
||||
backend:
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
```
|
||||
|
||||
4. **イメージを定期的に更新**
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 🌐 本番環境デプロイ
|
||||
|
||||
### Nginxリバースプロキシの使用
|
||||
|
||||
```nginx
|
||||
# /etc/nginx/sites-available/nofx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8080/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### HTTPSの設定(Let's Encrypt)
|
||||
|
||||
```bash
|
||||
# Certbotをインストール
|
||||
sudo apt-get install certbot python3-certbot-nginx
|
||||
|
||||
# SSL証明書を取得
|
||||
sudo certbot --nginx -d your-domain.com
|
||||
|
||||
# 自動更新
|
||||
sudo certbot renew --dry-run
|
||||
```
|
||||
|
||||
### Docker Swarmの使用(クラスタデプロイ)
|
||||
|
||||
```bash
|
||||
# Swarmを初期化
|
||||
docker swarm init
|
||||
|
||||
# スタックをデプロイ
|
||||
docker stack deploy -c docker-compose.yml nofx
|
||||
|
||||
# サービスステータスを表示
|
||||
docker stack services nofx
|
||||
|
||||
# サービスをスケール
|
||||
docker service scale nofx_backend=3
|
||||
```
|
||||
|
||||
## 📈 監視&ロギング
|
||||
|
||||
### ログ管理
|
||||
|
||||
```bash
|
||||
# ログローテーションを設定(docker-compose.ymlで既に設定済み)
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ログ統計を表示
|
||||
docker compose logs --timestamps | wc -l
|
||||
```
|
||||
|
||||
### 監視ツール統合
|
||||
|
||||
Prometheus + Grafanaで監視を統合:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml(監視サービスを追加)
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
ports:
|
||||
- "3001:3000"
|
||||
```
|
||||
|
||||
## 🆘 ヘルプを取得
|
||||
|
||||
- **GitHub Issues**: [Issueを提出](https://github.com/yourusername/open-nofx/issues)
|
||||
- **ドキュメント**: [README.md](README.md)を確認
|
||||
- **コミュニティ**: Discord/Telegramグループに参加
|
||||
|
||||
## 📝 コマンドチートシート
|
||||
|
||||
```bash
|
||||
# 起動
|
||||
docker compose up -d --build # ビルドして起動
|
||||
docker compose up -d # 起動(リビルドなし)
|
||||
|
||||
# 停止
|
||||
docker compose stop # サービスを停止
|
||||
docker compose down # コンテナを停止して削除
|
||||
docker compose down -v # コンテナとデータを停止して削除
|
||||
|
||||
# 表示
|
||||
docker compose ps # ステータスを表示
|
||||
docker compose logs -f # ログを表示
|
||||
docker compose top # プロセスを表示
|
||||
|
||||
# 再起動
|
||||
docker compose restart # すべてのサービスを再起動
|
||||
docker compose restart backend # バックエンドを再起動
|
||||
|
||||
# 更新
|
||||
git pull && docker compose up -d --build
|
||||
|
||||
# クリーン
|
||||
docker compose down -v # すべてのデータをクリア
|
||||
docker system prune -a # Dockerリソースをクリーン
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
🎉 おめでとうございます!NOFX AIトレーディング競争システムのデプロイに成功しました!
|
||||
|
||||
問題が発生した場合は、[トラブルシューティング](#-トラブルシューティング)セクションを確認するか、Issueを提出してください。
|
||||
@@ -1,4 +1,4 @@
|
||||
package pool
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -6,48 +6,23 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// defaultMainstreamCoins default mainstream coin pool (read from config file)
|
||||
var defaultMainstreamCoins = []string{
|
||||
"BTCUSDT",
|
||||
"ETHUSDT",
|
||||
"SOLUSDT",
|
||||
"BNBUSDT",
|
||||
"XRPUSDT",
|
||||
"DOGEUSDT",
|
||||
"ADAUSDT",
|
||||
"HYPEUSDT",
|
||||
// AI500Config AI500 data provider configuration
|
||||
type AI500Config struct {
|
||||
APIURL string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// CoinPoolConfig coin pool configuration
|
||||
type CoinPoolConfig struct {
|
||||
APIURL string
|
||||
Timeout time.Duration
|
||||
CacheDir string
|
||||
UseDefaultCoins bool // Whether to use default mainstream coins
|
||||
var ai500Config = AI500Config{
|
||||
APIURL: "",
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
var coinPoolConfig = CoinPoolConfig{
|
||||
APIURL: "",
|
||||
Timeout: 30 * time.Second, // Increased to 30 seconds
|
||||
CacheDir: "coin_pool_cache",
|
||||
UseDefaultCoins: false, // Default is not to use
|
||||
}
|
||||
|
||||
// CoinPoolCache coin pool cache
|
||||
type CoinPoolCache struct {
|
||||
Coins []CoinInfo `json:"coins"`
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
SourceType string `json:"source_type"` // "api" or "cache"
|
||||
}
|
||||
|
||||
// CoinInfo coin information
|
||||
type CoinInfo struct {
|
||||
// CoinData coin information
|
||||
type CoinData struct {
|
||||
Pair string `json:"pair"` // Trading pair symbol (e.g.: BTCUSDT)
|
||||
Score float64 `json:"score"` // Current score
|
||||
StartTime int64 `json:"start_time"` // Start time (Unix timestamp)
|
||||
@@ -59,18 +34,18 @@ type CoinInfo struct {
|
||||
IsAvailable bool `json:"-"` // Whether tradable (internal use)
|
||||
}
|
||||
|
||||
// CoinPoolAPIResponse raw data structure returned by API
|
||||
type CoinPoolAPIResponse struct {
|
||||
// AI500APIResponse raw data structure returned by AI500 API
|
||||
type AI500APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
Coins []CoinInfo `json:"coins"`
|
||||
Coins []CoinData `json:"coins"`
|
||||
Count int `json:"count"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// SetCoinPoolAPI sets coin pool API
|
||||
func SetCoinPoolAPI(apiURL string) {
|
||||
coinPoolConfig.APIURL = apiURL
|
||||
// SetAI500API sets AI500 data provider API
|
||||
func SetAI500API(apiURL string) {
|
||||
ai500Config.APIURL = apiURL
|
||||
}
|
||||
|
||||
// SetOITopAPI sets OI Top API
|
||||
@@ -78,31 +53,12 @@ func SetOITopAPI(apiURL string) {
|
||||
oiTopConfig.APIURL = apiURL
|
||||
}
|
||||
|
||||
// SetUseDefaultCoins sets whether to use default mainstream coins
|
||||
func SetUseDefaultCoins(useDefault bool) {
|
||||
coinPoolConfig.UseDefaultCoins = useDefault
|
||||
}
|
||||
|
||||
// SetDefaultCoins sets default mainstream coin list
|
||||
func SetDefaultCoins(coins []string) {
|
||||
if len(coins) > 0 {
|
||||
defaultMainstreamCoins = coins
|
||||
log.Printf("✓ Default coin pool set (%d coins): %v", len(coins), coins)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCoinPool retrieves coin pool list (with retry and cache mechanism)
|
||||
func GetCoinPool() ([]CoinInfo, error) {
|
||||
// First check if default coin list is enabled
|
||||
if coinPoolConfig.UseDefaultCoins {
|
||||
log.Printf("✓ Default mainstream coin list enabled")
|
||||
return convertSymbolsToCoins(defaultMainstreamCoins), nil
|
||||
}
|
||||
|
||||
// GetAI500Data retrieves AI500 coin list (with retry mechanism)
|
||||
func GetAI500Data() ([]CoinData, error) {
|
||||
// Check if API URL is configured
|
||||
if strings.TrimSpace(coinPoolConfig.APIURL) == "" {
|
||||
log.Printf("⚠️ Coin pool API URL not configured, using default mainstream coin list")
|
||||
return convertSymbolsToCoins(defaultMainstreamCoins), nil
|
||||
if strings.TrimSpace(ai500Config.APIURL) == "" {
|
||||
return nil, fmt.Errorf("AI500 API URL not configured")
|
||||
}
|
||||
|
||||
maxRetries := 3
|
||||
@@ -111,19 +67,15 @@ func GetCoinPool() ([]CoinInfo, error) {
|
||||
// Try to fetch from API
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
log.Printf("⚠️ Retry attempt %d of %d to fetch coin pool...", attempt, maxRetries)
|
||||
time.Sleep(2 * time.Second) // Wait 2 seconds before retry
|
||||
log.Printf("⚠️ Retry attempt %d of %d to fetch AI500 data...", attempt, maxRetries)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
coins, err := fetchCoinPool()
|
||||
coins, err := fetchAI500()
|
||||
if err == nil {
|
||||
if attempt > 1 {
|
||||
log.Printf("✓ Retry attempt %d succeeded", attempt)
|
||||
}
|
||||
// Save to cache after successful fetch
|
||||
if err := saveCoinPoolCache(coins); err != nil {
|
||||
log.Printf("⚠️ Failed to save coin pool cache: %v", err)
|
||||
}
|
||||
return coins, nil
|
||||
}
|
||||
|
||||
@@ -131,30 +83,20 @@ func GetCoinPool() ([]CoinInfo, error) {
|
||||
log.Printf("❌ Request attempt %d failed: %v", attempt, err)
|
||||
}
|
||||
|
||||
// API fetch failed, try to use cache
|
||||
log.Printf("⚠️ All API requests failed, trying to use historical cache data...")
|
||||
cachedCoins, err := loadCoinPoolCache()
|
||||
if err == nil {
|
||||
log.Printf("✓ Using historical cache data (%d coins)", len(cachedCoins))
|
||||
return cachedCoins, nil
|
||||
}
|
||||
|
||||
// Cache also failed, use default mainstream coins
|
||||
log.Printf("⚠️ Unable to load cache data (last error: %v), using default mainstream coin list", lastErr)
|
||||
return convertSymbolsToCoins(defaultMainstreamCoins), nil
|
||||
return nil, fmt.Errorf("all API requests failed: %w", lastErr)
|
||||
}
|
||||
|
||||
// fetchCoinPool actually executes coin pool request
|
||||
func fetchCoinPool() ([]CoinInfo, error) {
|
||||
log.Printf("🔄 Requesting AI500 coin pool...")
|
||||
// fetchAI500 actually executes AI500 request
|
||||
func fetchAI500() ([]CoinData, error) {
|
||||
log.Printf("🔄 Requesting AI500 data...")
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: coinPoolConfig.Timeout,
|
||||
Timeout: ai500Config.Timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Get(coinPoolConfig.APIURL)
|
||||
resp, err := client.Get(ai500Config.APIURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to request coin pool API: %w", err)
|
||||
return nil, fmt.Errorf("failed to request AI500 API: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -168,7 +110,7 @@ func fetchCoinPool() ([]CoinInfo, error) {
|
||||
}
|
||||
|
||||
// Parse API response
|
||||
var response CoinPoolAPIResponse
|
||||
var response AI500APIResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("JSON parsing failed: %w", err)
|
||||
}
|
||||
@@ -191,68 +133,9 @@ func fetchCoinPool() ([]CoinInfo, error) {
|
||||
return coins, nil
|
||||
}
|
||||
|
||||
// saveCoinPoolCache saves coin pool to cache file
|
||||
func saveCoinPoolCache(coins []CoinInfo) error {
|
||||
// Ensure cache directory exists
|
||||
if err := os.MkdirAll(coinPoolConfig.CacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
cache := CoinPoolCache{
|
||||
Coins: coins,
|
||||
FetchedAt: time.Now(),
|
||||
SourceType: "api",
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(cache, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize cache data: %w", err)
|
||||
}
|
||||
|
||||
cachePath := filepath.Join(coinPoolConfig.CacheDir, "latest.json")
|
||||
if err := ioutil.WriteFile(cachePath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write cache file: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("💾 Coin pool cache saved (%d coins)", len(coins))
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadCoinPoolCache loads coin pool from cache file
|
||||
func loadCoinPoolCache() ([]CoinInfo, error) {
|
||||
cachePath := filepath.Join(coinPoolConfig.CacheDir, "latest.json")
|
||||
|
||||
// Check if file exists
|
||||
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("cache file does not exist")
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(cachePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read cache file: %w", err)
|
||||
}
|
||||
|
||||
var cache CoinPoolCache
|
||||
if err := json.Unmarshal(data, &cache); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse cache data: %w", err)
|
||||
}
|
||||
|
||||
// Check cache age
|
||||
cacheAge := time.Since(cache.FetchedAt)
|
||||
if cacheAge > 24*time.Hour {
|
||||
log.Printf("⚠️ Cache data is old (%.1f hours ago), but still usable", cacheAge.Hours())
|
||||
} else {
|
||||
log.Printf("📂 Cache data timestamp: %s (%.1f minutes ago)",
|
||||
cache.FetchedAt.Format("2006-01-02 15:04:05"),
|
||||
cacheAge.Minutes())
|
||||
}
|
||||
|
||||
return cache.Coins, nil
|
||||
}
|
||||
|
||||
// GetAvailableCoins retrieves available coin list (filters out unavailable ones)
|
||||
func GetAvailableCoins() ([]string, error) {
|
||||
coins, err := GetCoinPool()
|
||||
coins, err := GetAI500Data()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -260,7 +143,6 @@ func GetAvailableCoins() ([]string, error) {
|
||||
var symbols []string
|
||||
for _, coin := range coins {
|
||||
if coin.IsAvailable {
|
||||
// Ensure symbol format is correct (convert to uppercase USDT pair)
|
||||
symbol := normalizeSymbol(coin.Pair)
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
@@ -275,13 +157,13 @@ func GetAvailableCoins() ([]string, error) {
|
||||
|
||||
// GetTopRatedCoins retrieves top N coins by score (sorted by score descending)
|
||||
func GetTopRatedCoins(limit int) ([]string, error) {
|
||||
coins, err := GetCoinPool()
|
||||
coins, err := GetAI500Data()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter available coins
|
||||
var availableCoins []CoinInfo
|
||||
var availableCoins []CoinData
|
||||
for _, coin := range coins {
|
||||
if coin.IsAvailable {
|
||||
availableCoins = append(availableCoins, coin)
|
||||
@@ -318,17 +200,11 @@ func GetTopRatedCoins(limit int) ([]string, error) {
|
||||
|
||||
// normalizeSymbol normalizes coin symbol
|
||||
func normalizeSymbol(symbol string) string {
|
||||
// Remove spaces
|
||||
symbol = trimSpaces(symbol)
|
||||
|
||||
// Convert to uppercase
|
||||
symbol = toUpper(symbol)
|
||||
|
||||
// Ensure ends with USDT
|
||||
if !endsWith(symbol, "USDT") {
|
||||
symbol = symbol + "USDT"
|
||||
}
|
||||
|
||||
return symbol
|
||||
}
|
||||
|
||||
@@ -362,18 +238,6 @@ func endsWith(s, suffix string) bool {
|
||||
return s[len(s)-len(suffix):] == suffix
|
||||
}
|
||||
|
||||
// convertSymbolsToCoins converts symbol list to CoinInfo list
|
||||
func convertSymbolsToCoins(symbols []string) []CoinInfo {
|
||||
coins := make([]CoinInfo, 0, len(symbols))
|
||||
for _, symbol := range symbols {
|
||||
coins = append(coins, CoinInfo{
|
||||
Pair: symbol,
|
||||
Score: 0,
|
||||
IsAvailable: true,
|
||||
})
|
||||
}
|
||||
return coins
|
||||
}
|
||||
|
||||
// ========== OI Top (Open Interest Growth Top 20) Data ==========
|
||||
|
||||
@@ -381,18 +245,18 @@ func convertSymbolsToCoins(symbols []string) []CoinInfo {
|
||||
type OIPosition struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Rank int `json:"rank"`
|
||||
CurrentOI float64 `json:"current_oi"` // Current open interest
|
||||
OIDelta float64 `json:"oi_delta"` // Open interest change
|
||||
OIDeltaPercent float64 `json:"oi_delta_percent"` // Open interest change percentage
|
||||
OIDeltaValue float64 `json:"oi_delta_value"` // Open interest change value
|
||||
PriceDeltaPercent float64 `json:"price_delta_percent"` // Price change percentage
|
||||
NetLong float64 `json:"net_long"` // Net long position
|
||||
NetShort float64 `json:"net_short"` // Net short position
|
||||
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 data structure returned by OI Top API
|
||||
type OITopAPIResponse struct {
|
||||
Code int `json:"code"` // 0 = success
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
Positions []OIPosition `json:"positions"`
|
||||
Count int `json:"count"`
|
||||
@@ -404,35 +268,24 @@ type OITopAPIResponse struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// OITopCache OI Top cache
|
||||
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 string
|
||||
Timeout time.Duration
|
||||
}{
|
||||
APIURL: "",
|
||||
Timeout: 30 * time.Second,
|
||||
CacheDir: "coin_pool_cache",
|
||||
APIURL: "",
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// GetOITopPositions retrieves OI Top 20 data (with retry and cache)
|
||||
// GetOITopPositions retrieves OI Top 20 data (with retry)
|
||||
func GetOITopPositions() ([]OIPosition, error) {
|
||||
// Check if API URL is configured
|
||||
if strings.TrimSpace(oiTopConfig.APIURL) == "" {
|
||||
log.Printf("⚠️ OI Top API URL not configured, skipping OI Top data fetch")
|
||||
return []OIPosition{}, nil // Return empty list, not an error
|
||||
return []OIPosition{}, nil
|
||||
}
|
||||
|
||||
maxRetries := 3
|
||||
var lastErr error
|
||||
|
||||
// Try to fetch from API
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
log.Printf("⚠️ Retry attempt %d of %d to fetch OI Top data...", attempt, maxRetries)
|
||||
@@ -444,10 +297,6 @@ func GetOITopPositions() ([]OIPosition, error) {
|
||||
if attempt > 1 {
|
||||
log.Printf("✓ Retry attempt %d succeeded", attempt)
|
||||
}
|
||||
// Save to cache after successful fetch
|
||||
if err := saveOITopCache(positions); err != nil {
|
||||
log.Printf("⚠️ Failed to save OI Top cache: %v", err)
|
||||
}
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
@@ -455,16 +304,7 @@ func GetOITopPositions() ([]OIPosition, error) {
|
||||
log.Printf("❌ OI Top request attempt %d failed: %v", attempt, err)
|
||||
}
|
||||
|
||||
// API fetch failed, try to use cache
|
||||
log.Printf("⚠️ All OI Top API requests failed, trying to use historical cache data...")
|
||||
cachedPositions, err := loadOITopCache()
|
||||
if err == nil {
|
||||
log.Printf("✓ Using historical OI Top cache data (%d coins)", len(cachedPositions))
|
||||
return cachedPositions, nil
|
||||
}
|
||||
|
||||
// Cache also failed, return empty list (OI Top is optional)
|
||||
log.Printf("⚠️ Unable to load OI Top cache data (last error: %v), skipping OI Top data", lastErr)
|
||||
log.Printf("⚠️ All OI Top API requests failed (last error: %v), skipping OI Top data", lastErr)
|
||||
return []OIPosition{}, nil
|
||||
}
|
||||
|
||||
@@ -491,7 +331,6 @@ func fetchOITop() ([]OIPosition, error) {
|
||||
return nil, fmt.Errorf("OI Top API returned error (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Parse API response
|
||||
var response OITopAPIResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("OI Top JSON parsing failed: %w", err)
|
||||
@@ -510,62 +349,6 @@ func fetchOITop() ([]OIPosition, error) {
|
||||
return response.Data.Positions, nil
|
||||
}
|
||||
|
||||
// saveOITopCache saves OI Top data to cache
|
||||
func saveOITopCache(positions []OIPosition) error {
|
||||
if err := os.MkdirAll(oiTopConfig.CacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
cache := OITopCache{
|
||||
Positions: positions,
|
||||
FetchedAt: time.Now(),
|
||||
SourceType: "api",
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(cache, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to serialize OI Top cache data: %w", err)
|
||||
}
|
||||
|
||||
cachePath := filepath.Join(oiTopConfig.CacheDir, "oi_top_latest.json")
|
||||
if err := ioutil.WriteFile(cachePath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write OI Top cache file: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("💾 OI Top cache saved (%d coins)", len(positions))
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadOITopCache loads OI Top data from cache
|
||||
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 cache file does not exist")
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(cachePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read OI Top cache file: %w", err)
|
||||
}
|
||||
|
||||
var cache OITopCache
|
||||
if err := json.Unmarshal(data, &cache); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse OI Top cache data: %w", err)
|
||||
}
|
||||
|
||||
cacheAge := time.Since(cache.FetchedAt)
|
||||
if cacheAge > 24*time.Hour {
|
||||
log.Printf("⚠️ OI Top cache data is old (%.1f hours ago), but still usable", cacheAge.Hours())
|
||||
} else {
|
||||
log.Printf("📂 OI Top cache data timestamp: %s (%.1f minutes ago)",
|
||||
cache.FetchedAt.Format("2006-01-02 15:04:05"),
|
||||
cacheAge.Minutes())
|
||||
}
|
||||
|
||||
return cache.Positions, nil
|
||||
}
|
||||
|
||||
// GetOITopSymbols retrieves OI Top coin symbol list
|
||||
func GetOITopSymbols() ([]string, error) {
|
||||
positions, err := GetOITopPositions()
|
||||
@@ -582,25 +365,24 @@ func GetOITopSymbols() ([]string, error) {
|
||||
return symbols, nil
|
||||
}
|
||||
|
||||
// MergedCoinPool merged coin pool (AI500 + OI Top)
|
||||
type MergedCoinPool struct {
|
||||
AI500Coins []CoinInfo // AI500 score coins
|
||||
OITopCoins []OIPosition // Open interest growth Top 20
|
||||
AllSymbols []string // All unique coin symbols
|
||||
SymbolSources map[string][]string // Source of each coin ("ai500"/"oi_top")
|
||||
// MergedData merged data (AI500 + OI Top)
|
||||
type MergedData struct {
|
||||
AI500Coins []CoinData
|
||||
OITopCoins []OIPosition
|
||||
AllSymbols []string
|
||||
SymbolSources map[string][]string
|
||||
}
|
||||
|
||||
// OIRankingData OI ranking data for debate (includes both top and low)
|
||||
type OIRankingData struct {
|
||||
TimeRange string `json:"time_range"` // e.g., "1小时"
|
||||
Duration string `json:"duration"` // e.g., "1h"
|
||||
TopPositions []OIPosition `json:"top_positions"` // 持仓增加排行
|
||||
LowPositions []OIPosition `json:"low_positions"` // 持仓减少排行
|
||||
TimeRange string `json:"time_range"`
|
||||
Duration string `json:"duration"`
|
||||
TopPositions []OIPosition `json:"top_positions"`
|
||||
LowPositions []OIPosition `json:"low_positions"`
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
}
|
||||
|
||||
// GetOIRankingData retrieves OI ranking data (both top increase and low decrease)
|
||||
// duration: "1h", "4h", "24h" etc. limit: number of results
|
||||
func GetOIRankingData(baseURL, authKey string, duration string, limit int) (*OIRankingData, error) {
|
||||
if baseURL == "" || authKey == "" {
|
||||
return nil, fmt.Errorf("OI API URL or auth key not configured")
|
||||
@@ -618,7 +400,7 @@ func GetOIRankingData(baseURL, authKey string, duration string, limit int) (*OIR
|
||||
FetchedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Fetch top ranking (持仓增加)
|
||||
// Fetch top ranking
|
||||
topURL := fmt.Sprintf("%s/api/oi/top-ranking?limit=%d&duration=%s&auth=%s", baseURL, limit, duration, authKey)
|
||||
topPositions, timeRange, err := fetchOIRanking(topURL)
|
||||
if err != nil {
|
||||
@@ -628,7 +410,7 @@ func GetOIRankingData(baseURL, authKey string, duration string, limit int) (*OIR
|
||||
result.TimeRange = timeRange
|
||||
}
|
||||
|
||||
// Fetch low ranking (持仓减少)
|
||||
// Fetch low ranking
|
||||
lowURL := fmt.Sprintf("%s/api/oi/low-ranking?limit=%d&duration=%s&auth=%s", baseURL, limit, duration, authKey)
|
||||
lowPositions, _, err := fetchOIRanking(lowURL)
|
||||
if err != nil {
|
||||
@@ -684,49 +466,39 @@ func FormatOIRankingForAI(data *OIRankingData) string {
|
||||
|
||||
sb.WriteString(fmt.Sprintf("## 📊 市场持仓量变化数据 (Open Interest Changes in %s / %s)\n\n", data.TimeRange, data.Duration))
|
||||
|
||||
// Top rankings (持仓增加)
|
||||
if len(data.TopPositions) > 0 {
|
||||
sb.WriteString("### 🔺 持仓量增加排行 (OI Increase Ranking)\n")
|
||||
sb.WriteString("市场资金正在流入以下币种,可能表示趋势延续或新仓位建立:\n\n")
|
||||
sb.WriteString("| 排名 | 币种 | 持仓变化值(USDT) | 变化幅度 | 价格变化 | 多头 | 空头 |\n")
|
||||
sb.WriteString("|------|------|------------------|----------|----------|------|------|\n")
|
||||
sb.WriteString("| 排名 | 币种 | 持仓变化值(USDT) | 变化幅度 | 价格变化 |\n")
|
||||
sb.WriteString("|------|------|------------------|----------|----------|\n")
|
||||
for _, pos := range data.TopPositions {
|
||||
sb.WriteString(fmt.Sprintf("| #%d | %s | %s | %+.2f%% | %+.2f%% | %.0f | %.0f |\n",
|
||||
sb.WriteString(fmt.Sprintf("| #%d | %s | %s | %+.2f%% | %+.2f%% |\n",
|
||||
pos.Rank,
|
||||
pos.Symbol,
|
||||
formatOIValue(pos.OIDeltaValue),
|
||||
pos.OIDeltaPercent,
|
||||
pos.PriceDeltaPercent,
|
||||
pos.NetLong,
|
||||
pos.NetShort,
|
||||
))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Market interpretation
|
||||
sb.WriteString("**解读**: 持仓增加 + 价格上涨 = 多头主导; 持仓增加 + 价格下跌 = 空头主导\n\n")
|
||||
}
|
||||
|
||||
// Low rankings (持仓减少)
|
||||
if len(data.LowPositions) > 0 {
|
||||
sb.WriteString("### 🔻 持仓量减少排行 (OI Decrease Ranking)\n")
|
||||
sb.WriteString("市场资金正在流出以下币种,可能表示趋势反转或仓位平仓:\n\n")
|
||||
sb.WriteString("| 排名 | 币种 | 持仓变化值(USDT) | 变化幅度 | 价格变化 | 多头 | 空头 |\n")
|
||||
sb.WriteString("|------|------|------------------|----------|----------|------|------|\n")
|
||||
sb.WriteString("| 排名 | 币种 | 持仓变化值(USDT) | 变化幅度 | 价格变化 |\n")
|
||||
sb.WriteString("|------|------|------------------|----------|----------|\n")
|
||||
for _, pos := range data.LowPositions {
|
||||
sb.WriteString(fmt.Sprintf("| #%d | %s | %s | %+.2f%% | %+.2f%% | %.0f | %.0f |\n",
|
||||
sb.WriteString(fmt.Sprintf("| #%d | %s | %s | %+.2f%% | %+.2f%% |\n",
|
||||
pos.Rank,
|
||||
pos.Symbol,
|
||||
formatOIValue(pos.OIDeltaValue),
|
||||
pos.OIDeltaPercent,
|
||||
pos.PriceDeltaPercent,
|
||||
pos.NetLong,
|
||||
pos.NetShort,
|
||||
))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Market interpretation
|
||||
sb.WriteString("**解读**: 持仓减少 + 价格上涨 = 空头平仓(反弹); 持仓减少 + 价格下跌 = 多头平仓(回调)\n\n")
|
||||
}
|
||||
|
||||
@@ -753,33 +525,28 @@ func formatOIValue(v float64) string {
|
||||
return fmt.Sprintf("%s%.2f", sign, v)
|
||||
}
|
||||
|
||||
// GetMergedCoinPool retrieves merged coin pool (AI500 + OI Top, deduplicated)
|
||||
func GetMergedCoinPool(ai500Limit int) (*MergedCoinPool, error) {
|
||||
// 1. Get AI500 data
|
||||
// GetMergedData retrieves merged data (AI500 + OI Top, deduplicated)
|
||||
func GetMergedData(ai500Limit int) (*MergedData, error) {
|
||||
ai500TopSymbols, err := GetTopRatedCoins(ai500Limit)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to get AI500 data: %v", err)
|
||||
ai500TopSymbols = []string{} // Use empty list on failure
|
||||
ai500TopSymbols = []string{}
|
||||
}
|
||||
|
||||
// 2. Get OI Top data
|
||||
oiTopSymbols, err := GetOITopSymbols()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to get OI Top data: %v", err)
|
||||
oiTopSymbols = []string{} // Use empty list on failure
|
||||
oiTopSymbols = []string{}
|
||||
}
|
||||
|
||||
// 3. Merge and deduplicate
|
||||
symbolSet := make(map[string]bool)
|
||||
symbolSources := make(map[string][]string)
|
||||
|
||||
// Add AI500 coins
|
||||
for _, symbol := range ai500TopSymbols {
|
||||
symbolSet[symbol] = true
|
||||
symbolSources[symbol] = append(symbolSources[symbol], "ai500")
|
||||
}
|
||||
|
||||
// Add OI Top coins
|
||||
for _, symbol := range oiTopSymbols {
|
||||
if !symbolSet[symbol] {
|
||||
symbolSet[symbol] = true
|
||||
@@ -787,25 +554,46 @@ func GetMergedCoinPool(ai500Limit int) (*MergedCoinPool, error) {
|
||||
symbolSources[symbol] = append(symbolSources[symbol], "oi_top")
|
||||
}
|
||||
|
||||
// Convert to array
|
||||
var allSymbols []string
|
||||
for symbol := range symbolSet {
|
||||
allSymbols = append(allSymbols, symbol)
|
||||
}
|
||||
|
||||
// Get complete data
|
||||
ai500Coins, _ := GetCoinPool()
|
||||
ai500Coins, _ := GetAI500Data()
|
||||
oiTopPositions, _ := GetOITopPositions()
|
||||
|
||||
merged := &MergedCoinPool{
|
||||
merged := &MergedData{
|
||||
AI500Coins: ai500Coins,
|
||||
OITopCoins: oiTopPositions,
|
||||
AllSymbols: allSymbols,
|
||||
SymbolSources: symbolSources,
|
||||
}
|
||||
|
||||
log.Printf("📊 Coin pool merge complete: AI500=%d, OI_Top=%d, Total(deduplicated)=%d",
|
||||
log.Printf("📊 Data merge complete: AI500=%d, OI_Top=%d, Total(deduplicated)=%d",
|
||||
len(ai500TopSymbols), len(oiTopSymbols), len(allSymbols))
|
||||
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
// ========== Backward Compatibility Aliases ==========
|
||||
|
||||
// Deprecated: Use SetAI500API instead
|
||||
func SetCoinPoolAPI(apiURL string) {
|
||||
SetAI500API(apiURL)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetAI500Data instead
|
||||
func GetCoinPool() ([]CoinData, error) {
|
||||
return GetAI500Data()
|
||||
}
|
||||
|
||||
// Deprecated: Use MergedData instead
|
||||
type MergedCoinPool = MergedData
|
||||
|
||||
// Deprecated: Use GetMergedData instead
|
||||
func GetMergedCoinPool(ai500Limit int) (*MergedData, error) {
|
||||
return GetMergedData(ai500Limit)
|
||||
}
|
||||
|
||||
// Deprecated: Use CoinData instead
|
||||
type CoinInfo = CoinData
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"nofx/decision"
|
||||
"nofx/market"
|
||||
"nofx/pool"
|
||||
"nofx/provider"
|
||||
"nofx/store"
|
||||
|
||||
"github.com/agiledragon/gomonkey/v2"
|
||||
@@ -354,9 +354,9 @@ func (s *AutoTraderTestSuite) TestGetCandidateCoins() {
|
||||
s.autoTrader.defaultCoins = []string{} // Empty default coins
|
||||
s.autoTrader.tradingCoins = []string{} // Empty custom coins
|
||||
|
||||
// Mock pool.GetMergedCoinPool
|
||||
s.patches.ApplyFunc(pool.GetMergedCoinPool, func(ai500Limit int) (*pool.MergedCoinPool, error) {
|
||||
return &pool.MergedCoinPool{
|
||||
// Mock provider.GetMergedCoinPool
|
||||
s.patches.ApplyFunc(provider.GetMergedCoinPool, func(ai500Limit int) (*provider.MergedCoinPool, error) {
|
||||
return &provider.MergedCoinPool{
|
||||
AllSymbols: []string{"BTCUSDT", "ETHUSDT"},
|
||||
SymbolSources: map[string][]string{
|
||||
"BTCUSDT": {"ai500", "oi_top"},
|
||||
|
||||
@@ -25,15 +25,15 @@ export function CoinSourceEditor({
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
sourceType: { zh: '数据来源类型', en: 'Source Type' },
|
||||
static: { zh: '静态列表', en: 'Static List' },
|
||||
coinpool: { zh: 'AI500 币种池', en: 'AI500 Coin Pool' },
|
||||
coinpool: { zh: 'AI500 数据源', en: 'AI500 Data Provider' },
|
||||
oi_top: { zh: 'OI Top 持仓增长', en: 'OI Top' },
|
||||
mixed: { zh: '混合模式', en: 'Mixed Mode' },
|
||||
staticCoins: { zh: '自定义币种', en: 'Custom Coins' },
|
||||
addCoin: { zh: '添加币种', en: 'Add Coin' },
|
||||
useCoinPool: { zh: '启用 AI500 币种池', en: 'Enable AI500 Coin Pool' },
|
||||
coinPoolLimit: { zh: '币种池数量上限', en: 'Coin Pool Limit' },
|
||||
useCoinPool: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' },
|
||||
coinPoolLimit: { zh: '数据源数量上限', en: 'Data Provider Limit' },
|
||||
coinPoolApiUrl: { zh: 'AI500 API URL', en: 'AI500 API URL' },
|
||||
coinPoolApiUrlPlaceholder: { zh: '输入 AI500 币种池 API 地址...', en: 'Enter AI500 coin pool API URL...' },
|
||||
coinPoolApiUrlPlaceholder: { zh: '输入 AI500 数据源 API 地址...', en: 'Enter AI500 data provider API URL...' },
|
||||
useOITop: { zh: '启用 OI Top 数据', en: 'Enable OI Top' },
|
||||
oiTopLimit: { zh: 'OI Top 数量上限', en: 'OI Top Limit' },
|
||||
oiTopApiUrl: { zh: 'OI Top API URL', en: 'OI Top API URL' },
|
||||
|
||||
@@ -493,7 +493,7 @@ export const translations = {
|
||||
signalSource: 'Signal Source',
|
||||
signalSourceConfig: 'Signal Source Configuration',
|
||||
coinPoolDescription:
|
||||
'API endpoint for coin pool data, leave blank to disable this signal source',
|
||||
'API endpoint for AI500 data provider, leave blank to disable this signal source',
|
||||
oiTopDescription:
|
||||
'API endpoint for open interest rankings, leave blank to disable this signal source',
|
||||
information: 'Information',
|
||||
@@ -773,18 +773,18 @@ export const translations = {
|
||||
candidateCoinsZeroWarning: 'Candidate Coins Count is 0',
|
||||
possibleReasons: 'Possible Reasons:',
|
||||
coinPoolApiNotConfigured:
|
||||
'Coin pool API not configured or inaccessible (check signal source settings)',
|
||||
'AI500 data provider API not configured or inaccessible (check signal source settings)',
|
||||
apiConnectionTimeout: 'API connection timeout or returned empty data',
|
||||
noCustomCoinsAndApiFailed:
|
||||
'No custom coins configured and API fetch failed',
|
||||
solutions: 'Solutions:',
|
||||
setCustomCoinsInConfig: 'Set custom coin list in trader configuration',
|
||||
orConfigureCorrectApiUrl: 'Or configure correct coin pool API address',
|
||||
orConfigureCorrectApiUrl: 'Or configure correct data provider API address',
|
||||
orDisableCoinPoolOptions:
|
||||
'Or disable "Use Coin Pool" and "Use OI Top" options',
|
||||
'Or disable "Use AI500 Data Provider" and "Use OI Top" options',
|
||||
signalSourceNotConfigured: 'Signal Source Not Configured',
|
||||
signalSourceWarningMessage:
|
||||
'You have traders that enabled "Use Coin Pool" or "Use OI Top", but signal source API address is not configured yet. This will cause candidate coins count to be 0, and traders cannot work properly.',
|
||||
'You have traders that enabled "Use AI500 Data Provider" or "Use OI Top", but signal source API address is not configured yet. This will cause candidate coins count to be 0, and traders cannot work properly.',
|
||||
configureSignalSourceNow: 'Configure Signal Source Now',
|
||||
|
||||
// FAQ Page
|
||||
@@ -1565,7 +1565,7 @@ export const translations = {
|
||||
noExchangesConfigured: '暂无已配置的交易所',
|
||||
signalSource: '信号源',
|
||||
signalSourceConfig: '信号源配置',
|
||||
coinPoolDescription: '用于获取币种池数据的API地址,留空则不使用此信号源',
|
||||
coinPoolDescription: '用于获取 AI500 数据源的 API 地址,留空则不使用此数据源',
|
||||
oiTopDescription: '用于获取持仓量排行数据的API地址,留空则不使用此信号源',
|
||||
information: '说明',
|
||||
signalSourceInfo1:
|
||||
@@ -1809,16 +1809,16 @@ export const translations = {
|
||||
candidateCoins: '候选币种',
|
||||
candidateCoinsZeroWarning: '候选币种数量为 0',
|
||||
possibleReasons: '可能原因:',
|
||||
coinPoolApiNotConfigured: '币种池API未配置或无法访问(请检查信号源设置)',
|
||||
coinPoolApiNotConfigured: 'AI500 数据源 API 未配置或无法访问(请检查信号源设置)',
|
||||
apiConnectionTimeout: 'API连接超时或返回数据为空',
|
||||
noCustomCoinsAndApiFailed: '未配置自定义币种且API获取失败',
|
||||
solutions: '解决方案:',
|
||||
setCustomCoinsInConfig: '在交易员配置中设置自定义币种列表',
|
||||
orConfigureCorrectApiUrl: '或者配置正确的币种池API地址',
|
||||
orDisableCoinPoolOptions: '或者禁用"使用币种池"和"使用OI Top"选项',
|
||||
orConfigureCorrectApiUrl: '或者配置正确的数据源 API 地址',
|
||||
orDisableCoinPoolOptions: '或者禁用"使用 AI500 数据源"和"使用 OI Top"选项',
|
||||
signalSourceNotConfigured: '信号源未配置',
|
||||
signalSourceWarningMessage:
|
||||
'您有交易员启用了"使用币种池"或"使用OI Top",但尚未配置信号源API地址。这将导致候选币种数量为0,交易员无法正常工作。',
|
||||
'您有交易员启用了"使用 AI500 数据源"或"使用 OI Top",但尚未配置信号源 API 地址。这将导致候选币种数量为 0,交易员无法正常工作。',
|
||||
configureSignalSourceNow: '立即配置信号源',
|
||||
|
||||
// FAQ Page
|
||||
|
||||
Reference in New Issue
Block a user