From ffdd2ffbae99cd77f90c9c9facd6f290d033dd2b Mon Sep 17 00:00:00 2001 From: TIANHE Date: Fri, 27 Feb 2026 01:57:04 +0800 Subject: [PATCH] V2.2.1: Membership & Billing, USDT TRC20 payment, VIP free indicators, AI Trading Radar, simplified strategy creation, system settings simplification, bug fixes and UI improvements --- README.md | 575 ++++-------- README_CN.md | 674 -------------- README_JA.md | 630 ------------- README_KO.md | 643 ------------- README_TW.md | 644 ------------- backend_api_python/Dockerfile | 21 +- backend_api_python/README.md | 2 +- .../app/data/market_symbols_seed.py | 10 +- .../app/data_sources/__init__.py | 17 - .../app/data_sources/cache_manager.py | 10 +- .../app/data_sources/circuit_breaker.py | 12 - .../app/data_sources/cn_stock.py | 877 ------------------ .../app/data_sources/data_manager.py | 629 ------------- .../app/data_sources/factory.py | 8 +- backend_api_python/app/routes/__init__.py | 4 +- backend_api_python/app/routes/billing.py | 105 +++ .../app/routes/fast_analysis.py | 2 +- .../app/routes/global_market.py | 367 +++++--- backend_api_python/app/routes/ibkr.py | 4 +- backend_api_python/app/routes/indicator.py | 28 +- backend_api_python/app/routes/kline.py | 2 +- backend_api_python/app/routes/market.py | 17 +- backend_api_python/app/routes/settings.py | 637 ++++--------- backend_api_python/app/routes/user.py | 72 +- .../app/services/billing_service.py | 312 ++++++- .../app/services/community_service.py | 205 ++-- .../app/services/ibkr_trading/README.md | 7 +- .../app/services/ibkr_trading/__init__.py | 2 +- .../app/services/ibkr_trading/client.py | 4 +- .../app/services/ibkr_trading/symbols.py | 31 +- backend_api_python/app/services/kline.py | 4 +- .../app/services/live_trading/execution.py | 4 +- .../app/services/live_trading/factory.py | 6 +- .../app/services/market_data_collector.py | 198 +--- .../app/services/pending_order_worker.py | 22 +- backend_api_python/app/services/search.py | 12 +- .../app/services/symbol_name.py | 120 --- .../app/services/trading_executor.py | 8 +- .../app/services/usdt_payment_service.py | 366 ++++++++ backend_api_python/env.example | 50 +- backend_api_python/migrations/init.sql | 67 +- backend_api_python/requirements.txt | 4 +- docs/CHANGELOG.md | 183 +++- docs/IBKR_TRADING_GUIDE_CN.md | 173 ---- docs/IBKR_TRADING_GUIDE_EN.md | 12 +- quantdinger_vue/src/api/billing.js | 39 + quantdinger_vue/src/api/global-market.js | 5 +- quantdinger_vue/src/config/router.config.js | 16 + quantdinger_vue/src/layouts/BasicLayout.vue | 2 +- quantdinger_vue/src/locales/lang/ar-SA.js | 4 - quantdinger_vue/src/locales/lang/de-DE.js | 5 - quantdinger_vue/src/locales/lang/en-US.js | 184 +++- quantdinger_vue/src/locales/lang/fr-FR.js | 4 - quantdinger_vue/src/locales/lang/ja-JP.js | 4 - quantdinger_vue/src/locales/lang/ko-KR.js | 4 - quantdinger_vue/src/locales/lang/th-TH.js | 4 - quantdinger_vue/src/locales/lang/vi-VN.js | 8 - quantdinger_vue/src/locales/lang/zh-CN.js | 178 +++- quantdinger_vue/src/locales/lang/zh-TW.js | 4 - .../src/views/ai-analysis/index.vue | 59 +- .../src/views/ai-asset-analysis/index.vue | 491 ++++++++++ quantdinger_vue/src/views/billing/index.vue | 768 +++++++++++++++ .../src/views/indicator-analysis/index.vue | 33 +- .../components/IndicatorCard.vue | 15 + .../components/IndicatorDetail.vue | 3 + quantdinger_vue/src/views/portfolio/index.vue | 30 +- quantdinger_vue/src/views/profile/index.vue | 6 +- quantdinger_vue/src/views/settings/index.vue | 10 +- .../components/PositionRecords.vue | 19 +- .../src/views/trading-assistant/index.vue | 438 ++++++--- .../src/views/user-manage/index.vue | 27 + 71 files changed, 4067 insertions(+), 6073 deletions(-) delete mode 100644 README_CN.md delete mode 100644 README_JA.md delete mode 100644 README_KO.md delete mode 100644 README_TW.md delete mode 100644 backend_api_python/app/data_sources/cn_stock.py delete mode 100644 backend_api_python/app/data_sources/data_manager.py create mode 100644 backend_api_python/app/routes/billing.py create mode 100644 backend_api_python/app/services/usdt_payment_service.py delete mode 100644 docs/IBKR_TRADING_GUIDE_CN.md create mode 100644 quantdinger_vue/src/api/billing.js create mode 100644 quantdinger_vue/src/views/ai-asset-analysis/index.vue create mode 100644 quantdinger_vue/src/views/billing/index.vue diff --git a/README.md b/README.md index 233d486..18ec9e0 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,6 @@

QuantDinger

-
- 🇺🇸 English | - 🇨🇳 简体中文 | - 繁體中文 | - 🇯🇵 日本語 | - 🇰🇷 한국어 -

@@ -42,7 +35,7 @@

Telegram Group - Discord + Discord X

@@ -57,7 +50,7 @@ QuantDinger is a **local-first, privacy-first, self-hosted quantitative trading ### Why Local-First? -Unlike SaaS platforms that lock your data and strategies in the cloud, QuantDinger runs locally. Your strategies, trading logs, API keys, and analysis results stay on your machine. No vendor lock-in, no subscription fees, no data exfiltration. +Unlike SaaS platforms that lock your data and strategies in the cloud, QuantDinger runs locally. Your strategies, trading logs, API keys, and analysis results stay on your machine. No vendor lock-in, no data exfiltration. ### Who is this for? @@ -67,17 +60,14 @@ QuantDinger is built for traders, researchers, and engineers who: - Prefer engineering over marketing - Need a complete workflow: data, analysis, backtesting, and execution -### Core Features - -QuantDinger includes a built-in **LLM-based multi-agent research system** that gathers financial intelligence from the web, combines it with local market data, and generates analysis reports. This integrates with strategy development, backtesting, and live trading workflows. - ### Core Value -- **🔓 Apache 2.0 Open Source (Code)**: Permissive and commercial-friendly. You can fork and modify the codebase under Apache 2.0, while preserving required notices. -- **🐍 Python-Native & Visual**: Write indicators in standard Python (easier than PineScript) with AI assistance. Visualize signals directly on charts—a "Local TradingView" experience. -- **🤖 AI-Loop Optimization**: It doesn't just run strategies; AI analyzes backtest results to suggest parameter tuning (Stop-Loss/TP/MACD settings), forming a closed optimization loop. -- **🌍 Universal Market Access**: One unified system for Crypto (Live), US/CN Stocks, Forex, and Futures (Data/Notify). -- **⚡ Docker & Clean Arch**: 4-line command deployment. Modern Tech Stack (Vue + Python) with a clean, separation-of-concerns architecture. +- **🔓 Apache 2.0 Open Source (Code)**: Permissive and commercial-friendly +- **🐍 Python-Native & Visual**: Write indicators in Python with AI assistance, visualize on built-in K-line charts +- **🤖 AI-Loop Optimization**: AI analyzes backtest results to suggest parameter tuning, forming a closed optimization loop +- **🌍 Universal Market Access**: Crypto (Live), US Stocks (IBKR), Forex (MT5), Futures (Data/Notify) +- **💳 Built-in Monetization**: Membership subscription, credit system, USDT on-chain payment +- **⚡ Docker & Clean Arch**: 4-line command deployment with modern tech stack --- @@ -171,38 +161,54 @@ QuantDinger includes a built-in **LLM-based multi-agent research system** that g *From Indicator to Execution, Seamlessly.* 1. **Indicator**: Define your market entry/exit signals. -2. **Strategy Config**: Attach risk management rules (Position sizing, Stop-Loss, Take-Profit). -3. **Backtest & AI Optimization**: Run backtests, view rich performance metrics, and **let AI analyze the result to suggest improvements** (e.g., "Adjust MACD threshold to X"). +2. **Strategy Config**: Simplified creation with smart defaults (15min K-line, 5x leverage, market order). Advanced mode available for full customization. +3. **Backtest & AI Optimization**: Run backtests, view rich performance metrics, and **let AI analyze the result to suggest improvements**. 4. **Execution Mode**: - **Live Trading**: - **Cryptocurrency**: Direct API execution for 10+ exchanges (Binance, OKX, Bitget, Bybit, etc.) - - **US/HK Stocks**: Via Interactive Brokers (IBKR) 🆕 - - **Forex**: Via MetaTrader 5 (MT5) 🆕 - - **Signal Notification**: For markets without live trading support (A-shares/Futures), send signals via Telegram, Discord, Email, SMS, or Webhook. + - **US Stocks**: Via Interactive Brokers (IBKR) + - **Forex**: Via MetaTrader 5 (MT5) + - **Signal Notification**: Send signals via Telegram, Discord, Email, SMS, or Webhook. +5. **Risk Control**: Mandatory disclaimer acknowledgment before live trading. Market order mode by default for reliable execution. -### 3. AI-Powered Analysis -*Fast, Accurate, Professional Reports.* +### 3. AI-Powered Analysis & Trading Radar +*Fast, Accurate, Multi-Market Intelligence.* -QuantDinger features a streamlined AI analysis system: - -- **Fast Analysis Mode**: Single LLM call architecture for quick, accurate analysis (replaces complex multi-agent system) -- **Global Market Integration**: Real-time market data, heatmaps, and economic calendar integrated into the analysis page -- **ATR-Based Trading Levels**: Stop-loss and take-profit recommendations based on technical analysis (ATR, Support/Resistance) +- **Fast Analysis Mode**: Single LLM call architecture for quick, accurate analysis +- **AI Trading Opportunities Radar** 🆕: Auto-scans Crypto, US Stocks, and Forex markets every hour, displaying opportunities in a rolling carousel +- **ATR-Based Trading Levels**: Stop-loss and take-profit recommendations based on technical analysis - **Analysis Memory**: Store analysis results for history review and continuous learning - **Strategic Integration**: AI analysis can serve as a "Market Filter" for your strategies -### 4. Universal Data Engine +### 4. Membership & Billing System 🆕 +*Built-in Monetization for SaaS Deployment.* + +- **Subscription Plans**: Monthly / Yearly / Lifetime tiers with configurable pricing +- **Credit System**: Each plan includes credits; lifetime members receive monthly credit bonuses +- **USDT On-Chain Payment** 💰: TRC20 scan-to-pay with HD Wallet (xpub) address derivation per order, automatic on-chain reconciliation via TronGrid API +- **Admin Configuration**: All plan prices, credits, and payment settings configurable via System Settings + +### 5. Indicator Community & VIP System +*Share, Discover, and Trade Indicators.* + +- **Publish & Share**: Share your Python indicators with the community +- **Credit-Based Purchase**: Buy premium indicators from other users with credits +- **VIP Free Indicators** 🆕: Mark indicators as "VIP Free" — VIP members can use them without spending credits +- **Rating & Reviews**: Rate and review purchased indicators +- **Live Performance Tracking**: Real-time performance stats aggregated from backtests and live trades + +### 6. Universal Data Engine QuantDinger provides a unified data interface across multiple markets: - **Cryptocurrency**: Direct API connections for trading (10+ exchanges) and CCXT integration for market data (100+ sources) -- **Stocks**: Yahoo Finance, Finnhub, Tiingo (US stocks), and AkShare (CN/HK stocks) +- **Stocks**: Yahoo Finance, Finnhub, Tiingo (US stocks) - **Futures/Forex**: OANDA and major futures data sources - **Proxy Support**: Built-in proxy configuration for restricted network environments -### 5. Memory-Augmented Agents (Local RAG + Reflection Loop) +### 7. Memory-Augmented Agents (Local RAG + Reflection Loop) -QuantDinger’s agents don’t start from scratch every time. The backend includes a **local memory store** and an optional **reflection/verification loop**: +QuantDinger's agents don't start from scratch every time. The backend includes a **local memory store** and an optional **reflection/verification loop**: - **What it is**: RAG-style experience retrieval injected into agent prompts (NOT model fine-tuning). - **Where it lives**: PostgreSQL database (shared with main data) or local files under `backend_api_python/data/memory/` (privacy-first). @@ -284,23 +290,7 @@ flowchart TB A2 -.->|"manual review"| M8 ``` -**Retrieval ranking (simplified)**: - -\[ -score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_score -\] - -Config lives in `.env` (see `backend_api_python/env.example`): `ENABLE_AGENT_MEMORY`, `AGENT_MEMORY_TOP_K`, `AGENT_MEMORY_ENABLE_VECTOR`, `AGENT_MEMORY_HALF_LIFE_DAYS`, and `ENABLE_REFLECTION_WORKER`. - -### 6. Strategy Runtime - -- **Thread-Based Executor**: Independent thread pool for strategy execution -- **Auto-Restore**: Resumes running strategies after system restarts -- **Order Queue**: Background worker for order execution - -### 7. Multi-LLM Provider Support - -QuantDinger supports multiple AI providers with auto-detection: +### 8. Multi-LLM Provider Support | Provider | Features | |----------|----------| @@ -312,14 +302,6 @@ QuantDinger supports multiple AI providers with auto-detection: Simply configure your preferred provider's API key in `.env`. The system auto-detects available providers. -### 8. Indicator Community -*Share, Discover, and Trade Indicators.* - -- **Publish & Share**: Share your Python indicators with the community -- **Purchase System**: Buy premium indicators from other users -- **Rating & Reviews**: Rate and review purchased indicators -- **Admin Review**: Moderation system for quality control - ### 9. User Management & Security - **Multi-User Support**: PostgreSQL-backed user accounts with role-based permissions @@ -328,19 +310,26 @@ Simply configure your preferred provider's API key in `.env`. The system auto-de - **Security Features**: Cloudflare Turnstile captcha, IP/account rate limiting - **Demo Mode**: Read-only mode for public demonstrations -### 10. Tech Stack +### 10. Strategy Runtime + +- **Thread-Based Executor**: Independent thread pool for strategy execution +- **Auto-Restore**: Resumes running strategies after system restarts +- **Market Order Default**: Prioritizes execution reliability over price optimization +- **Order Queue**: Background worker for order execution + +### 11. Tech Stack - **Backend**: Python (Flask) + PostgreSQL + Redis (optional) - **Frontend**: Vue 2 + Ant Design Vue + KlineCharts/ECharts +- **Payment**: USDT TRC20 on-chain (HD Wallet xpub derivation + TronGrid API) +- **Mobile**: Vue 3 + Capacitor (Android / iOS) — see `QuantDinger-Mobile/` - **Deployment**: Docker Compose (with PostgreSQL) -- **Current Version**: V2.1.1 ([Changelog](docs/CHANGELOG.md)) +- **Current Version**: V2.2.1 ([Changelog](docs/CHANGELOG.md)) --- ## 🔌 Supported Exchanges & Brokers -QuantDinger supports multiple execution methods for different market types: - ### Cryptocurrency Exchanges (Direct API) | Exchange | Markets | @@ -359,19 +348,22 @@ QuantDinger supports multiple execution methods for different market types: | Broker | Markets | Platform | |:------:|:--------|:---------| -| **Interactive Brokers (IBKR)** | US Stocks, HK Stocks | TWS / IB Gateway 🆕 | -| **MetaTrader 5 (MT5)** | Forex | MT5 Terminal 🆕 | +| **Interactive Brokers (IBKR)** | US Stocks | TWS / IB Gateway | +| **MetaTrader 5 (MT5)** | Forex | MT5 Terminal | -### Market Data (via CCXT) +### Supported Markets -Bybit, Gate.io, Kraken, KuCoin, HTX, and 100+ other exchanges for market data. +| Market Type | Data Sources | Trading | +|-------------|--------------|---------| +| **Cryptocurrency** | Binance, OKX, Bitget, + 100 exchanges | ✅ Full support | +| **US Stocks** | Yahoo Finance, Finnhub, Tiingo | ✅ Via IBKR | +| **Forex** | Finnhub, OANDA | ✅ Via MT5 | +| **Futures** | Exchange APIs | ⚡ Data only | --- ### Multi-Language Support -QuantDinger is built for a global audience with comprehensive internationalization: -

English Simplified Chinese @@ -389,20 +381,88 @@ All UI elements, error messages, and documentation are fully translated. Languag --- -### Supported Markets +## 🚀 Quick Start -| Market Type | Data Sources | Trading | -|-------------|--------------|---------| -| **Cryptocurrency** | Binance, OKX, Bitget, + 100 exchanges | ✅ Full support | -| **US Stocks** | Yahoo Finance, Finnhub, Tiingo | ✅ Via IBKR 🆕 | -| **HK Stocks** | AkShare, East Money | ✅ Via IBKR 🆕 | -| **CN Stocks (A-shares)** | AkShare, East Money | ⚡ Data only | -| **Forex** | Finnhub, OANDA | ✅ Via MT5 🆕 | -| **Futures** | Exchange APIs, AkShare | ⚡ Data only | +### Option 1: Docker (Recommended) + +```bash +# 1. Clone & configure +git clone https://github.com/brokermr810/QuantDinger.git +cd QuantDinger +cp backend_api_python/env.example backend_api_python/.env + +# 2. Edit .env — set your admin password & AI API key +# ADMIN_USER=quantdinger +# ADMIN_PASSWORD=your_password +# OPENROUTER_API_KEY=your_key (optional, for AI features) + +# 3. Start all services +docker-compose up -d --build +``` + +> **Windows PowerShell**: use `Copy-Item backend_api_python\env.example -Destination backend_api_python\.env` instead of `cp`. + +**That's it!** Services will be available at: + +| Service | URL | +|---------|-----| +| Frontend UI | http://localhost:8888 | +| Backend API | http://localhost:5000 | + +Default login: `quantdinger` / `123456` (change in `.env` for production). + +#### Common Docker Commands + +```bash +docker-compose ps # View status +docker-compose logs -f backend # View backend logs +docker-compose restart # Restart services +docker-compose up -d --build # Rebuild & restart +docker-compose down # Stop services +``` + +#### Update to Latest Version + +```bash +git pull && docker-compose up -d --build +``` + +#### Backup & Restore + +```bash +# Backup database +docker exec quantdinger-db pg_dump -U quantdinger quantdinger > backup.sql + +# Restore database +cat backup.sql | docker exec -i quantdinger-db psql -U quantdinger quantdinger +``` --- -### Architecture (Current Repo) +### Option 2: Local Development + +**Prerequisites**: Python 3.10+, Node.js 16+, PostgreSQL 14+ + +```bash +# 1. Setup database +sudo -u postgres psql -c "CREATE DATABASE quantdinger; CREATE USER quantdinger WITH ENCRYPTED PASSWORD 'your_password'; GRANT ALL PRIVILEGES ON DATABASE quantdinger TO quantdinger;" +psql -U quantdinger -d quantdinger -f backend_api_python/migrations/init.sql + +# 2. Start backend +cd backend_api_python +pip install -r requirements.txt +cp env.example .env # Edit .env with your DATABASE_URL +python run.py # → http://localhost:5000 + +# 3. Start frontend (in another terminal) +cd quantdinger_vue +npm install +npm run serve # → http://localhost:8000 +``` + +--- + +### Architecture ```text ┌─────────────────────────────┐ @@ -416,355 +476,55 @@ All UI elements, error messages, and documentation are fully translated. Languag │ (Flask + strategy runtime) │ └──────────────┬──────────────┘ │ - ├─ PostgreSQL (multi-user support) + ├─ PostgreSQL (multi-user, orders, membership) ├─ Redis (optional cache) + ├─ TronGrid API (USDT payment verification) └─ Data providers / LLMs / Exchanges ``` ---- - ### Repository Layout ```text . -├─ backend_api_python/ # Flask API + AI + backtest + strategy runtime +├─ backend_api_python/ # Flask API + AI + backtest + strategy + billing │ ├─ app/ -│ ├─ env.example # Copy to .env for local config -│ ├─ requirements.txt +│ │ ├─ routes/ # API endpoints (user, billing, indicator, etc.) +│ │ └─ services/ # Business logic (trading, payment, community) +│ ├─ migrations/init.sql # Database schema +│ ├─ env.example # Copy to .env for configuration │ └─ run.py # Entrypoint -└─ quantdinger_vue/ # Vue 2 UI (dev server proxies /api -> backend) +├─ quantdinger_vue/ # Vue 2 UI (Ant Design Vue) +└─ QuantDinger-Mobile/ # Vue 3 + Capacitor mobile app (optional) ``` --- -## Quick Start - -### Option 1: Docker Deployment (Recommended) - -The fastest way to get QuantDinger running with PostgreSQL database and multi-user support. - -#### 1. Configure Environment - -Create a `.env` file in project root: - -```bash -# Database Configuration -POSTGRES_USER=quantdinger -POSTGRES_PASSWORD=your_secure_password -POSTGRES_DB=quantdinger - -# Admin Account (created on first startup) -ADMIN_USER=quantdinger -ADMIN_PASSWORD=123456 - -# Optional: AI Features -OPENROUTER_API_KEY=your_api_key -``` - -#### 2. Start Services - -**Linux / macOS** -```bash -git clone https://github.com/brokermr810/QuantDinger.git && \ -cd QuantDinger && \ -cp backend_api_python/env.example backend_api_python/.env && \ -docker-compose up -d --build -``` - -**Windows (PowerShell)** -```powershell -git clone https://github.com/brokermr810/QuantDinger.git -cd QuantDinger -Copy-Item backend_api_python\env.example -Destination backend_api_python\.env -docker-compose up -d --build -``` - -This will automatically: -- Start PostgreSQL database (port 5432) -- Initialize database schema -- Start backend API (port 5000) -- Start frontend (port 8888) -- Create admin user from `ADMIN_USER`/`ADMIN_PASSWORD` in `.env` - -#### 3. Access the Application - -- **Frontend UI**: http://localhost:8888 -- **Backend API**: http://localhost:5000 -- **Default Account**: Uses `ADMIN_USER` / `ADMIN_PASSWORD` from `.env` (default: `quantdinger` / `123456`, please change for production) - -> **Note**: For production, edit `backend_api_python/.env` to set strong passwords, add `OPENROUTER_API_KEY` for AI features, then restart with `docker-compose restart backend`. - -#### Docker Commands Reference - -```bash -# View running status -docker-compose ps - -# View logs -docker-compose logs -f - -# View backend logs only -docker-compose logs -f backend - -# View frontend logs only -docker-compose logs -f frontend - -# Stop services -docker-compose down - -# Stop and remove volumes (WARNING: deletes database!) -docker-compose down -v - -# Restart services -docker-compose restart - -# Rebuild and restart -docker-compose up -d --build - -# Enter backend container -docker exec -it quantdinger-backend /bin/bash - -# Enter frontend container -docker exec -it quantdinger-frontend /bin/sh -``` - -#### Docker Architecture - -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Frontend │ │ Backend │ │ PostgreSQL │ -│ (Nginx) │────▶│ (Python) │────▶│ Database │ -│ Port: 8888 │ │ Port: 5000 │ │ Port: 5432 │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - └───────────────────────┴───────────────────────┘ - Docker Network -``` - -- **Frontend**: Vue.js app served by Nginx, proxies API requests to backend -- **Backend**: Python Flask API service with multi-user authentication -- **PostgreSQL**: Database for user data, strategies, and trading records - -#### Data Persistence - -The following data is persisted across container restarts: - -```yaml -volumes: - postgres_data: # PostgreSQL database - - ./backend_api_python/logs:/app/logs # Logs - - ./backend_api_python/data:/app/data # Data directory - - ./backend_api_python/.env:/app/.env # Configuration -``` - -#### Customization - -**Change ports** - Edit `docker-compose.yml`: - -```yaml -services: - frontend: - ports: - - "8080:80" # Change to port 8080 - - backend: - ports: - - "5001:5000" # Change to port 5001 -``` - -**Configure HTTPS** - Use a reverse proxy (like Caddy/Nginx): - -```bash -# Using Caddy (automatic HTTPS) -caddy reverse-proxy --from yourdomain.com --to localhost:80 -``` - -#### Production Recommendations - -**Security:** - -```bash -# Generate strong SECRET_KEY -openssl rand -hex 32 - -# Set secure admin password -ADMIN_PASSWORD=your-very-secure-password -``` - -**Resource limits** - Add to `docker-compose.yml`: - -```yaml -services: - backend: - deploy: - resources: - limits: - cpus: '2' - memory: 2G - reservations: - cpus: '0.5' - memory: 512M -``` - -**Log management:** - -```yaml -services: - backend: - logging: - driver: "json-file" - options: - max-size: "100m" - max-file: "3" -``` - -#### Docker Troubleshooting - -**Frontend can't connect to backend:** - -```bash -docker-compose logs backend -curl http://localhost:5000/api/health -``` - -**Database connection issues:** - -```bash -# Check PostgreSQL container status -docker-compose logs postgres - -# Verify PostgreSQL is ready -docker exec quantdinger-db pg_isready -U quantdinger - -# Connect to database manually -docker exec -it quantdinger-db psql -U quantdinger -d quantdinger -``` - -**Build failures:** - -```bash -# Clear Docker cache and rebuild -docker-compose build --no-cache -``` - -**Out of memory:** - -```bash -# Check memory usage -docker stats - -# Add swap space (Linux) -sudo fallocate -l 2G /swapfile -sudo chmod 600 /swapfile -sudo mkswap /swapfile -sudo swapon /swapfile -``` - -#### Updating - -```bash -# Pull latest code -git pull - -# Rebuild and restart -docker-compose up -d --build -``` - -#### Backup - -```bash -# Backup PostgreSQL database -docker exec quantdinger-db pg_dump -U quantdinger quantdinger > backup/quantdinger_$(date +%Y%m%d).sql - -# Backup configuration -cp backend_api_python/.env backup/.env_$(date +%Y%m%d) - -# Restore database (if needed) -cat backup/quantdinger_YYYYMMDD.sql | docker exec -i quantdinger-db psql -U quantdinger quantdinger -``` - ---- - -### Option 2: Local Development - -**Prerequisites** - -- Python 3.10+ recommended -- Node.js 16+ recommended -- PostgreSQL 14+ installed and running - -#### 1. Setup PostgreSQL - -```bash -# Create database and user -sudo -u postgres psql -CREATE DATABASE quantdinger; -CREATE USER quantdinger WITH ENCRYPTED PASSWORD 'your_password'; -GRANT ALL PRIVILEGES ON DATABASE quantdinger TO quantdinger; -\q - -# Initialize schema -psql -U quantdinger -d quantdinger -f backend_api_python/migrations/init.sql -``` - -#### 2. Start the backend (Flask API) - -```bash -cd backend_api_python -pip install -r requirements.txt -cp env.example .env # Windows: copy env.example .env -``` - -Edit `.env` and set: -```bash -DATABASE_URL=postgresql://quantdinger:your_password@localhost:5432/quantdinger -SECRET_KEY=your-secret-key -ADMIN_USER=quantdinger -ADMIN_PASSWORD=123456 -``` - -Then start: -```bash -python run.py -``` - -Backend will be available at `http://localhost:5000`. - -#### 2. Start the frontend (Vue UI) - -```bash -cd quantdinger_vue -npm install -npm run serve -``` - -Frontend dev server runs at `http://localhost:8000` and proxies `/api/*` to `http://localhost:5000` (see `quantdinger_vue/vue.config.js`). - ---- - ### Configuration (.env) -Use `backend_api_python/env.example` as a template. Common settings include: +Use `backend_api_python/env.example` as a template. Key settings: -- **Auth**: `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD` -- **Server**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG` -- **Database**: `DATABASE_URL` (PostgreSQL connection string) -- **AI / LLM**: `LLM_PROVIDER` (openrouter/openai/google/deepseek/grok), provider-specific API keys -- **OAuth**: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` -- **Security**: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `ENABLE_REGISTRATION` -- **Web search**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY` -- **Order Execution**: `ORDER_MODE` (maker/market), `MAKER_WAIT_SEC`, `MAKER_OFFSET_BPS` -- **Proxy (optional)**: `PROXY_PORT` or `PROXY_URL` -- **Workers**: `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR` +| Category | Variables | +|----------|-----------| +| **Auth** | `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD` | +| **Database** | `DATABASE_URL` (PostgreSQL connection string) | +| **AI / LLM** | `LLM_PROVIDER`, `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, etc. | +| **OAuth** | `GOOGLE_CLIENT_ID`, `GITHUB_CLIENT_ID`, etc. | +| **Security** | `TURNSTILE_SITE_KEY`, `ENABLE_REGISTRATION` | +| **Order Execution** | `ORDER_MODE` (market/maker), `MAKER_WAIT_SEC` | +| **Membership** 🆕 | `MEMBERSHIP_MONTHLY_PRICE_USD`, `MEMBERSHIP_MONTHLY_CREDITS`, `MEMBERSHIP_YEARLY_PRICE_USD`, etc. | +| **USDT Payment** 🆕 | `USDT_PAY_ENABLED`, `USDT_TRC20_XPUB`, `TRONGRID_API_KEY`, `USDT_ORDER_EXPIRE_MINUTES` | +| **Proxy** | `PROXY_PORT` or `PROXY_URL` | +| **Workers** | `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR` | --- ### API -The backend provides REST endpoints for login, market data, indicators, backtesting, strategies, and AI analysis. +The backend provides REST endpoints for login, market data, indicators, backtesting, strategies, AI analysis, and billing. -- Health: `GET /health` (also supports `GET /api/health` for deployment probes) -- Auth (frontend-compatible): `POST /api/user/login`, `POST /api/user/logout`, `GET /api/user/info` +- Health: `GET /api/health` +- Auth: `POST /api/user/login`, `GET /api/user/info` +- Billing: `GET /api/billing/plans`, `POST /api/billing/usdt/create-order` 🆕 For the full route list, see `backend_api_python/app/routes/`. @@ -780,7 +540,7 @@ Licensed under the **Apache License 2.0**. See `LICENSE`. - **Contributing**: [Contributing Guide](CONTRIBUTING.md) · [Contributors](CONTRIBUTORS.md) - **Telegram**: [QuantDinger Group](https://t.me/quantdinger) -- **Discord**: [Join Server](https://discord.gg/vwJ8zxFh9Q) +- **Discord**: [Join Server](https://discord.gg/tyx5B6TChr) - **📺 Video Demo**: [Project Introduction](https://youtu.be/HPTVpqL7knM) - **YouTube**: [@quantdinger](https://youtube.com/@quantdinger) - **Email**: [brokermr810@gmail.com](mailto:brokermr810@gmail.com) @@ -867,11 +627,12 @@ QuantDinger stands on the shoulders of great open-source projects: | **CCXT** | Cryptocurrency exchange trading library | [github.com/ccxt/ccxt](https://github.com/ccxt/ccxt) | | **yfinance** | Yahoo Finance market data downloader | [github.com/ranaroussi/yfinance](https://github.com/ranaroussi/yfinance) | | **akshare** | China financial data interface | [github.com/akfamily/akshare](https://github.com/akfamily/akshare) | -| **requests** | HTTP library for Python | [requests.readthedocs.io](https://requests.readthedocs.io/) | +| **bip-utils** | HD wallet key derivation (BIP-32/44) | [GitHub](https://github.com/ebellocchia/bip_utils) | | **Vue.js** | Progressive JavaScript framework | [vuejs.org](https://vuejs.org/) | | **Ant Design Vue** | Enterprise-class UI components | [antdv.com](https://antdv.com/) | | **KlineCharts** | Lightweight financial charting library | [github.com/klinecharts/KLineChart](https://github.com/klinecharts/KLineChart) | | **Lightweight Charts** | TradingView charting library | [github.com/nicepkg/lightweight-charts](https://github.com/nicepkg/lightweight-charts) | | **ECharts** | Apache data visualization library | [echarts.apache.org](https://echarts.apache.org/) | +| **Capacitor** | Cross-platform native runtime | [capacitorjs.com](https://capacitorjs.com/) | Thanks to all maintainers and contributors across these ecosystems! ❤️ diff --git a/README_CN.md b/README_CN.md deleted file mode 100644 index 6bef74e..0000000 --- a/README_CN.md +++ /dev/null @@ -1,674 +0,0 @@ -

- 🇺🇸 English | - 🇨🇳 简体中文 | - 🇹🇼 繁體中文 | - 🇯🇵 日本語 | - 🇰🇷 한국어 -
-
- -
- - QuantDinger Logo - - -

QuantDinger

- -

- 下一代 AI 量化交易平台 -

- -

- 🤖 AI 原生 · 🐍 可视化 Python · 🌍 全球多市场 · 🔒 隐私优先 -

-

- 拥有 AI 副驾驶的构建、回测与交易平台。比 PineScript 更强,比 SaaS 更智能。 -

- -

- 官方社区 · - 在线演示 · - 📺 视频演示 · - 🌟 加入我们 -

- -

- License - Python - Vue - Docker - Stars -

- -

- Telegram Group - Discord - X -

-
- ---- - -## 📖 简介 - -### QuantDinger 是什么? - -QuantDinger 是一个**本地优先、隐私优先、自托管的量化交易基础设施**。它运行在你的机器/服务器上,提供 **PostgreSQL 支持的多用户账号体系**,同时让你完全控制自己的策略、交易数据和 API 密钥。 - -### 为什么选择本地优先? - -与将你的数据和策略锁定在云端的 SaaS 平台不同,QuantDinger 在本地运行。你的策略、交易日志、API 密钥和分析结果都保留在你的机器上。没有供应商锁定,没有订阅费用,没有数据泄露风险。 - -### 适合谁使用? - -QuantDinger 为以下用户而构建: -- 重视数据主权和隐私的交易员、研究员和工程师 -- 需要透明、可审计的交易基础设施 -- 更偏好工程而非营销 -- 需要完整的工作流:数据、分析、回测和执行 - -### 核心功能 - -QuantDinger 包含一个内置的**基于 LLM 的多智能体研究系统**,能够从网络收集金融情报,结合本地市场数据,生成分析报告。这与策略开发、回测和实盘交易工作流无缝集成。 - -### 核心价值 - -- **🔓 Apache 2.0 开源(代码)**:宽松且商业友好。你可以在 Apache 2.0 下 fork/修改代码,但需保留许可与署名等必要声明。 -- **🐍 Python 原生 & 可视化**:使用标准 Python 编写指标(比 PineScript 更简单),并由 AI 辅助。直接在图表上可视化信号——打造“本地版 TradingView”体验。 -- **🤖 AI 闭环优化**:不仅运行策略,AI 还会分析回测结果并建议参数调整(止损/止盈/MACD 设置),形成闭环优化。 -- **🌍 全球市场接入**:统一系统支持加密货币(实盘)、美股/A股、外汇和期货(数据/通知)。 -- **⚡ Docker & 清晰架构**:4 行命令极速部署。现代技术栈(Vue + Python),架构清晰,关注点分离。 - ---- - -## 📺 视频演示 - -
- - QuantDinger 项目介绍视频 - -

点击上方视频观看 QuantDinger 项目介绍

-
- ---- - -## 📚 文档 - -### 开发指南 -- [Python 策略开发指南](docs/STRATEGY_DEV_GUIDE_CN.md) -- [盈透证券 (IBKR) 实盘交易指南](docs/IBKR_TRADING_GUIDE_CN.md) 🆕 -- [MetaTrader 5 (MT5) 外汇实盘交易指南](docs/MT5_TRADING_GUIDE_CN.md) 🆕 - -### 通知配置 -- [Telegram 通知配置](docs/NOTIFICATION_TELEGRAM_CONFIG_CH.md) -- [邮件 (SMTP) 通知配置](docs/NOTIFICATION_EMAIL_CONFIG_CH.md) -- [短信 (Twilio) 通知配置](docs/NOTIFICATION_SMS_CONFIG_CH.md) - -## 📸 功能预览 - -
-

🗺️ 系统架构总览

-

QuantDinger AI 驱动的研究、回测和自动化交易功能全景图。

- QuantDinger 系统拓扑图 -
- -
- -
-

📊 专业量化仪表盘

-

实时监控市场动态、资产状况和策略状态。

- QuantDinger Dashboard -
- -
- - - - - - - - - - - - - -
-

🤖 AI 深度投研

-

多智能体协作进行市场情绪与技术分析。

- AI Market Analysis -
-

💬 智能交易助手

-

通过自然语言接口获取即时市场洞察。

- Trading Assistant -
-

📈 交互式指标分析

-

丰富的技术指标库,支持拖拽式分析。

- Indicator Analysis -
-

🐍 Python 策略生成

-

内置编辑器,支持 AI 辅助策略代码编写。

- Code Generation -
-

📊 资产监测

-

追踪持仓、设置预警,并通过邮件/Telegram 接收 AI 分析报告。

- Portfolio Monitor -
- ---- - -## ✨ 关键特性 - -### 1. 可视化 Python 策略工作台 -*比 PineScript 更强,比 SaaS 更智能。* - -- **Python 原生**:用 Python 编写指标和策略。利用完整的 Python 生态(Pandas, Numpy, TA-Lib),而不是 PineScript 这种专用语言。 -- **"Mini-TradingView" 体验**:直接在内置的 K 线图上运行你的 Python 指标。在历史数据上可视化调试买卖信号。 -- **AI 辅助编程**:让内置 AI 为你编写复杂的逻辑。从创意到代码只需几秒钟。 - -### 2. 完整的交易生命周期 -*从指标到执行,无缝衔接。* - -1. **指标**:定义你的市场入场/出场信号。 -2. **策略配置**:附加风险管理规则(仓位管理、止损、止盈)。 -3. **回测 & AI 优化**:运行回测,查看丰富的性能指标,并**让 AI 分析结果以建议改进**(例如:“调整 MACD 阈值为 X”)。 -4. **执行模式**: - - **实盘交易**: - - **加密货币**:直接 API 执行,支持 10+ 交易所(Binance, OKX, Bitget, Bybit 等) - - **美股/港股**:通过盈透证券 (IBKR) 🆕 - - **外汇**:通过 MetaTrader 5 (MT5) 🆕 - - **信号通知**:针对不支持实盘交易的市场(A股/期货),通过 Telegram, Discord, Email, SMS 或 Webhook 发送信号。 - -### 3. AI 智能分析 -*快速、准确、专业的分析报告。* - -QuantDinger 具备精简高效的 AI 分析系统: - -- **快速分析模式**:单次 LLM 调用架构,快速准确地生成分析(替代复杂的多智能体系统) -- **全球市场集成**:实时市场数据、热力图和财经日历集成到分析页面 -- **基于 ATR 的交易建议**:止损和止盈建议基于技术分析(ATR、支撑/阻力位) -- **分析记忆系统**:存储分析结果用于历史回顾和持续学习 -- **策略集成**:AI 分析可作为策略的"市场过滤器" - -### 4. 通用数据引擎 - -QuantDinger 提供跨多个市场的统一数据接口: - -- **加密货币**:直接 API 连接进行交易(10+ 交易所)和 CCXT 集成获取行情数据(100+ 数据源) -- **股票**:Yahoo Finance、Finnhub、Tiingo(美股)和 AkShare(A股/港股) -- **期货/外汇**:OANDA 和主要期货数据源 -- **代理支持**:内置代理配置,适应受限网络环境 - -### 5. 🧠 AI 记忆增强系统(Memory-Augmented Agents) -QuantDinger 的多智能体不是“每次从零开始”。它内置了一个**本地记忆库 + 反思闭环**,让每个智能体在生成提示词(prompt)时能检索过往经验,并在事后验证/复盘后把结果写回记忆库。 - -- **本质**:RAG 风格的“经验检索增强”,**不是**训练/微调模型权重(零外部向量库依赖)。 -- **隐私**:所有记忆与反思记录默认落盘在本地 SQLite:`backend_api_python/data/memory/`。 - -#### 逻辑图(从请求到记忆闭环) - -```mermaid -flowchart TB - %% ===== 🌐 入口层 ===== - subgraph Entry["🌐 API 入口"] - A["📡 POST /api/analysis/multi"] - A2["🔄 POST /api/analysis/reflect"] - end - - %% ===== ⚙️ 服务编排层 ===== - subgraph Service["⚙️ 服务编排"] - B[AnalysisService] - C[AgentCoordinator] - D["📊 构建上下文
price · kline · news · indicators"] - end - - %% ===== 🤖 多智能体工作流 ===== - subgraph Agents["🤖 多智能体工作流"] - - subgraph P1["📈 Phase 1 · 多维分析(并行)"] - E1["🔍 MarketAnalyst
技术面分析"] - E2["📑 FundamentalAnalyst
基本面分析"] - E3["📰 NewsAnalyst
新闻舆情"] - E4["💭 SentimentAnalyst
市场情绪"] - E5["⚠️ RiskAnalyst
风险评估"] - end - - subgraph P2["🎯 Phase 2 · 多空博弈(并行)"] - F1["🐂 BullResearcher
看多论据"] - F2["🐻 BearResearcher
看空论据"] - end - - subgraph P3["💹 Phase 3 · 交易决策"] - G["🎰 TraderAgent
综合研判 → BUY / SELL / HOLD"] - end - - end - - %% ===== 🧠 记忆层 ===== - subgraph Memory["🧠 本地记忆库 SQLite(data/memory/)"] - M1[("market_analyst")] - M2[("fundamental")] - M3[("news_analyst")] - M4[("sentiment")] - M5[("risk_analyst")] - M6[("bull_researcher")] - M7[("bear_researcher")] - M8[("trader_agent")] - end - - %% ===== 🔄 反思闭环 ===== - subgraph Reflect["🔄 反思闭环(可选)"] - R[ReflectionService] - RR[("reflection_records.db")] - W["⏰ ReflectionWorker"] - end - - %% ===== 主流程 ===== - A --> B --> C --> D - D --> P1 --> P2 --> P3 - - %% ===== 记忆读写 ===== - E1 <-.-> M1 - E2 <-.-> M2 - E3 <-.-> M3 - E4 <-.-> M4 - E5 <-.-> M5 - F1 <-.-> M6 - F2 <-.-> M7 - G <-.-> M8 - - %% ===== 反思流程 ===== - C --> R --> RR - W --> RR - W -.->|"验证 + 学习"| M8 - A2 -.->|"手动复盘"| M8 -``` - -#### 1) 记忆是如何“注入提示词”的? -每个 agent 在 `analyze()` 时会: -- **构造 situation**:例如 `"{market}:{symbol} fundamental analysis"`、`"{market}:{symbol} trading decision"` 等 -- **携带结构化 metadata**:`market/symbol/timeframe` + `memory_features`(价格、涨跌幅、技术指标等) -- **检索 Top-K 历史经验**:转成一段可读的 `memory_prompt` -- **拼进 system_prompt**:模型在做本次分析前先“读历史经验” - -你可以在这些文件里看到同样的模式: -- `backend_api_python/app/services/agents/base_agent.py`:`get_memories()` + `format_memories_for_prompt()` -- `backend_api_python/app/services/agents/*_agents.py`、`trader_agent.py`:把 `memory_prompt` 拼进 system prompt - -#### 2) 记忆检索算法(为什么“像”RAG?) -每个角色的记忆表保存(简化): -- **situation / recommendation / result / returns** -- **market / symbol / timeframe / features_json** -- **embedding(可选 BLOB)**:本地“哈希向量”嵌入(无外部依赖) - -检索时会从最近 `AGENT_MEMORY_CANDIDATE_LIMIT` 条候选中打分排序: -\[ -score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_score -\] - -- **sim**:默认用 embedding cosine,相同维度的本地哈希向量;没有 embedding 时退化为 difflib 文本相似度 -- **recency**:半衰期衰减(`AGENT_MEMORY_HALF_LIFE_DAYS`) -- **returns_score**:对收益做 `tanh` 压缩,避免极值支配排序 -- **timeframe 惩罚**:如果查询 timeframe 与记忆记录 timeframe 不一致,会额外扣分 - -#### 3) “学习”从哪里来?(两条写入通道) -- **自动反思(可选)**: - - 分析结束后,系统会把 BUY/SELL/HOLD 记录到 `reflection_records.db` - - 开启 `ENABLE_REFLECTION_WORKER=true` 后,后台线程会按 `REFLECTION_WORKER_INTERVAL_SEC` 轮询到期记录,拉取最新价格做验证,并把验证结果写回 `trader_agent_memory.db` -- **手动复盘(推荐)**: - - 调用 `POST /api/analysis/reflect`,把你的真实交易结果(returns/result)写回记忆库,用于后续决策增强 - -#### 4) 关键环境变量(`.env`) -- **ENABLE_AGENT_MEMORY**:是否启用记忆增强(默认 true) -- **AGENT_MEMORY_TOP_K**:每次注入的经验条数(默认 5) -- **AGENT_MEMORY_CANDIDATE_LIMIT**:候选池大小(默认 500) -- **AGENT_MEMORY_ENABLE_VECTOR**:是否启用 embedding cosine(默认 true;否则退化为文本相似) -- **AGENT_MEMORY_EMBEDDING_DIM**:哈希 embedding 维度(默认 256) -- **AGENT_MEMORY_HALF_LIFE_DAYS**:时间衰减半衰期(默认 30) -- **AGENT_MEMORY_W_SIM / W_RECENCY / W_RETURNS**:三项权重(默认 0.75 / 0.20 / 0.05) -- **ENABLE_REFLECTION_WORKER**:是否启用自动验证闭环(默认 false) -- **REFLECTION_WORKER_INTERVAL_SEC**:自动验证周期(默认 86400 秒) - -### 6. 策略运行时 - -- **基于线程的执行器**:独立的线程池用于策略执行 -- **自动恢复**:系统重启后恢复运行中的策略 -- **订单队列**:后台工作线程用于订单执行 - -### 7. 多LLM提供商支持 - -QuantDinger 支持多个 AI 提供商,具备自动检测功能: - -| 提供商 | 特点 | -|--------|------| -| **OpenRouter** | 多模型网关(默认),100+ 模型 | -| **OpenAI** | GPT-4o, GPT-4o-mini | -| **Google Gemini** | Gemini 1.5 Flash/Pro | -| **DeepSeek** | DeepSeek Chat(性价比高) | -| **xAI Grok** | Grok Beta | - -只需在 `.env` 中配置您首选提供商的 API 密钥,系统会自动检测可用提供商。 - -### 8. 指标社区 -*分享、发现、交易指标。* - -- **发布与分享**:与社区分享你的 Python 指标 -- **购买系统**:从其他用户购买优质付费指标 -- **评分与评论**:对购买的指标进行评分和评论 -- **管理员审核**:质量控制的审核系统 - -### 9. 用户管理与安全 - -- **多用户支持**:基于 PostgreSQL 的用户账户,支持基于角色的权限管理 -- **OAuth 登录**:Google 和 GitHub OAuth 集成 -- **邮箱验证**:通过邮箱验证码进行注册和密码重置 -- **安全功能**:Cloudflare Turnstile 人机验证、IP/账户速率限制 -- **演示模式**:用于公开演示的只读模式 - -### 10. 技术栈 - -- **后端**:Python (Flask) + PostgreSQL + Redis(可选) -- **前端**:Vue 2 + Ant Design Vue + KlineCharts/ECharts -- **部署**:Docker Compose -- **当前版本**:V2.1.1 ([更新日志](docs/CHANGELOG.md)) - ---- - -## 🔌 支持的交易所和券商 - -QuantDinger 支持多种市场类型的执行方式: - -### 加密货币交易所(直接 API) - -| 交易所 | 市场 | -|:--------:|:---------| -| Binance | 现货, 合约, 杠杆 | -| OKX | 现货, 永续, 期权 | -| Bitget | 现货, 合约, 跟单交易 | -| Bybit | 现货, 线性合约 | -| Coinbase Exchange | 现货 | -| Kraken | 现货, 合约 | -| KuCoin | 现货, 合约 | -| Gate.io | 现货, 合约 | -| Bitfinex | 现货, 衍生品 | -| Deepcoin | 现货, 永续 | - -### 传统券商 - -| 券商 | 市场 | 平台 | -|:------:|:--------|:---------| -| **盈透证券 (IBKR)** | 美股, 港股 | TWS / IB Gateway 🆕 | -| **MetaTrader 5 (MT5)** | 外汇 | MT5 终端 🆕 | - -### 行情数据(通过 CCXT) - -Bybit、Gate.io、Kraken、KuCoin、HTX 以及 100+ 其他交易所用于行情数据。 - - ---- - -### 多语言支持 - -QuantDinger 为全球用户构建,提供全面的国际化支持: - -

- English - Simplified Chinese - Traditional Chinese - Japanese - Korean - German - French - Thai - Vietnamese - Arabic -

- -所有 UI 元素、错误信息和文档均已完全翻译。语言会根据浏览器设置自动检测,也可以在应用中手动切换。 - ---- - -### 支持的市场 - -| 市场类型 | 数据源 | 交易 | -|-------------|--------------|---------| -| **加密货币** | Binance, OKX, Bitget, + 100 交易所 | ✅ 全面支持 | -| **美股** | Yahoo Finance, Finnhub, Tiingo | ✅ 通过盈透证券 🆕 | -| **港股** | AkShare, 东方财富 | ✅ 通过盈透证券 🆕 | -| **A股** | AkShare, 东方财富 | ⚡ 仅数据 | -| **外汇** | Finnhub, OANDA | ✅ 通过 MT5 🆕 | -| **期货** | 交易所 API, AkShare | ⚡ 仅数据 | - ---- - -### 架构 (当前仓库) - -```text -┌─────────────────────────────┐ -│ quantdinger_vue │ -│ (Vue 2 + Ant Design Vue) │ -└──────────────┬──────────────┘ - │ HTTP (/api/*) - ▼ -┌─────────────────────────────┐ -│ backend_api_python │ -│ (Flask + 策略运行时) │ -└──────────────┬──────────────┘ - │ - ├─ PostgreSQL(多用户支持) - ├─ Redis (可选缓存) - └─ 数据提供商 / LLMs / 交易所 -``` - ---- - -### 仓库目录结构 - -```text -. -├─ backend_api_python/ # Flask API + AI + 回测 + 策略运行时 -│ ├─ app/ -│ ├─ env.example # 复制为 .env 进行本地配置 -│ ├─ requirements.txt -│ └─ run.py # 入口点 -└─ quantdinger_vue/ # Vue 2 UI (开发服务器代理 /api -> 后端) -``` - ---- - -## 快速开始 - -### 选项 1: Docker 部署 (推荐) - -运行 QuantDinger 最快的方式。 - -#### 1. 一键启动 - -**Linux / macOS** -```bash -git clone https://github.com/brokermr810/QuantDinger.git && \ -cd QuantDinger && \ -cp backend_api_python/env.example backend_api_python/.env && \ -docker-compose up -d --build -``` - -**Windows (PowerShell)** -```powershell -git clone https://github.com/brokermr810/QuantDinger.git -cd QuantDinger -Copy-Item backend_api_python\env.example -Destination backend_api_python\.env -docker-compose up -d --build -``` - -#### 2. 访问与配置 - -- **前端 UI**: http://localhost:8888 -- **默认账号**: `quantdinger` / `123456` - -> **注意**:为了使用 AI 功能或生产环境安全,请编辑 `backend_api_python/.env`(添加 `OPENROUTER_API_KEY`,修改密码),然后执行 `docker-compose restart backend` 重启服务。 - -#### 3. 访问应用 - -- **前端 UI**: http://localhost -- **后端 API**: http://localhost:5000 - -#### Docker 命令参考 - -```bash -# 查看运行状态 -docker-compose ps - -# 查看日志 -docker-compose logs -f - -# 停止服务 -docker-compose down - -# 停止并删除卷 (警告:会删除数据库!) -docker-compose down -v -``` - -#### 数据持久化 - -以下数据挂载到主机,重启容器后依然保留: - -```yaml -volumes: - - ./backend_api_python/logs:/app/logs # 日志 - - ./backend_api_python/data:/app/data # 数据目录(包含 quantdinger.db) - - ./backend_api_python/.env:/app/.env # 配置文件 -``` - ---- - -### 选项 2: 本地开发 - -**先决条件** -- 推荐 Python 3.10+ -- 推荐 Node.js 16+ - -#### 1. 启动后端 (Flask API) - -```bash -cd backend_api_python -pip install -r requirements.txt -cp env.example .env # Windows: copy env.example .env -python run.py -``` - -后端将在 `http://localhost:5000` 上可用。 - -#### 2. 启动前端 (Vue UI) - -```bash -cd quantdinger_vue -npm install -npm run serve -``` - -前端开发服务器运行在 `http://localhost:8000` 并将 `/api/*` 代理到 `http://localhost:5000`。 - ---- - -### 配置 (.env) - -使用 `backend_api_python/env.example` 作为模板。常用设置包括: - -- **认证**: `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD` -- **服务器**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG` -- **数据库**: `DATABASE_URL` (PostgreSQL 连接字符串) -- **AI / LLM**: `LLM_PROVIDER` (openrouter/openai/google/deepseek/grok), 各提供商 API 密钥 -- **OAuth**: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` -- **安全**: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `ENABLE_REGISTRATION` -- **网络搜索**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY` -- **订单执行**: `ORDER_MODE` (maker/market), `MAKER_WAIT_SEC`, `MAKER_OFFSET_BPS` -- **代理 (可选)**: `PROXY_PORT` 或 `PROXY_URL` -- **后台服务**: `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR` - ---- - -## 🤝 社区与支持 - -- **贡献**: [贡献指南](CONTRIBUTING.md) · [贡献者](CONTRIBUTORS.md) -- **Telegram**: [QuantDinger 群组](https://t.me/quantdinger) -- **Discord**: [加入服务器](https://discord.gg/vwJ8zxFh9Q) -- **📺 视频演示**: [项目介绍](https://youtu.be/HPTVpqL7knM) -- **YouTube**: [@quantdinger](https://youtube.com/@quantdinger) -- **Email**: [brokermr810@gmail.com](mailto:brokermr810@gmail.com) -- **GitHub Issues**: [报告 Bug / 功能请求](https://github.com/brokermr810/QuantDinger/issues) - ---- - -## 💼 商业授权与赞助(Commercial License & Sponsorship) - -QuantDinger 的代码使用 **Apache License 2.0** 授权。但需要注意:**Apache 2.0 不授予商标权**。QuantDinger 的名称/Logo/品牌标识受商标与品牌政策约束(与代码许可分离): - -- **版权/署名**:你必须保留必要的版权与许可声明(例如仓库中的 LICENSE/NOTICE 等,以及代码中的署名信息)。 -- **商标(名称/Logo/品牌)**:你不得使用 QuantDinger 的名称/Logo/品牌来暗示背书或误导来源;若再发布修改版,一般需要移除/替换 QuantDinger 品牌标识,除非获得书面许可。 - -如果你希望在再发布版本中**保留/修改 QuantDinger 品牌展示**(包括 UI 品牌、Logo 使用等),请联系我们获取 **商业授权**。 - -另见:`TRADEMARKS.md` - -### 商业授权可获得 - -- **品牌/版权展示的商用授权**(以双方约定为准) -- **运维支持**:部署、升级、故障处理与维护建议 -- **咨询服务**:架构评审、性能调优、策略工作流咨询 -- **赞助商权益**:成为项目赞助商,可按约定展示你的 Logo/广告(README/官网/应用内等) - -### 联系方式 - -- **Telegram**: [QuantDinger Group](https://t.me/worldinbroker) -- **Email**: [brokermr810@gmail.com](mailto:brokermr810@gmail.com) - ---- - -### 💝 直接支持(捐赠) - -你的贡献帮助我们维护和改进 QuantDinger。 - -**加密货币捐赠 (ERC-20 / BEP-20 / Polygon / Arbitrum)** - -``` -0x96fa4962181bea077f8c7240efe46afbe73641a7 -``` - -

- USDT - ETH -

- ---- - -### 🎓 支持伙伴 - -我们很荣幸获得推动量化金融教育和研究的学术机构和组织的支持。 - -
- - - - -
- - 印第安纳大学量化金融学会 - -

- 量化金融学会 (QFS)
- 印第安纳大学布卢明顿分校
- 培养下一代量化金融专业人才 -
-
- -> 💡 **有兴趣成为支持伙伴吗?** 我们欢迎与大学、研究机构和组织合作。请通过 [brokermr810@gmail.com](mailto:brokermr810@gmail.com) 或 [Telegram](https://t.me/worldinbroker) 联系我们。 - ---- - -### 致谢 - -QuantDinger 站在这些伟大的开源项目肩膀之上:Flask, Pandas, CCXT, Vue.js, Ant Design Vue, KlineCharts 等。 - -感谢所有维护者和贡献者! ❤️ - diff --git a/README_JA.md b/README_JA.md deleted file mode 100644 index 8826aa6..0000000 --- a/README_JA.md +++ /dev/null @@ -1,630 +0,0 @@ -
- 🇺🇸 English | - 🇨🇳 简体中文 | - 🇹🇼 繁體中文 | - 🇯🇵 日本語 | - 🇰🇷 한국어 -
-
- -
- - QuantDinger Logo - - -

QuantDinger

- -

- 次世代 AI 定量的取引プラットフォーム -

- -

- 🤖 AIネイティブ · 🐍 ビジュアルPython · 🌍 グローバルマルチマーケット · 🔒 プライバシーファースト -

-

- AIコパイロットを搭載した構築、バックテスト、取引プラットフォーム。PineScriptより強力、SaaSよりスマート。 -

- -

- 公式コミュニティ · - ライブデモ · - 📺 動画デモ · - 🌟 参加する -

- -

- License - Python - Vue - Docker - Stars -

- -

- Telegram Group - Discord - X -

-
- ---- - -## 📖 はじめに - -### QuantDingerとは? - -QuantDingerは **ローカルファースト、プライバシー重視のセルフホスト型定量取引インフラ** です。あなたのマシン/サーバー上で実行され、**PostgreSQL によるマルチユーザーアカウント** を提供しつつ、戦略・取引データ・APIキーを完全にコントロールできます。 - -### なぜローカルファーストなのか? - -データと戦略をクラウドにロックするSaaSプラットフォームとは異なり、QuantDingerはローカルで実行されます。あなたの戦略、取引ログ、APIキー、分析結果はすべてあなたのマシンに保存されます。ベンダーロックインなし、サブスクリプション費用なし、データ流出なし。 - -### 誰のためのものか? - -QuantDingerは以下のユーザー向けに構築されています: -- データ主権とプライバシーを重視するトレーダー、研究者、エンジニア -- 透明で監査可能な取引インフラストラクチャを必要とする人 -- マーケティングよりもエンジニアリングを好む人 -- 完全なワークフローが必要な人:データ、分析、バックテスト、実行 - -### コア機能 - -QuantDingerには、ウェブから金融情報を収集し、ローカル市場データと組み合わせて分析レポートを生成する、内蔵の **LLMベースのマルチエージェント研究システム** が含まれています。これは戦略開発、バックテスト、実取引ワークフローと統合されています。 - -### コアバリュー - -- **🔓 Apache 2.0 オープンソース(コード)**: 寛容で商用利用に適しています。Apache 2.0 の範囲で fork/改変が可能ですが、ライセンス/著作権表示など必要な告知は保持してください。 -- **🐍 Pythonネイティブ & ビジュアル**: 標準のPythonでインジケーターを作成(PineScriptより簡単)、AIが支援します。チャート上でシグナルを直接可視化 ——「ローカル版TradingView」体験を構築。 -- **🤖 AIループ最適化**: 戦略を実行するだけでなく、AIがバックテスト結果を分析してパラメータ調整(ストップロス/利益確定/MACD設定)を提案し、閉ループ最適化を形成します。 -- **🌍 グローバルマーケットアクセス**: 暗号資産(実取引)、米国/中国株、FX、先物(データ/通知)をサポートする統一システム。 -- **⚡ Docker & クリーンアーキテクチャ**: 4行のコマンドで高速デプロイ。モダンな技術スタック(Vue + Python)、クリーンなアーキテクチャ、関心の分離。 - ---- - -## 📺 動画デモ - -
- - QuantDinger プロジェクト紹介動画 - -

上の動画をクリックしてQuantDingerプロジェクト紹介をご覧ください

-
- ---- - -## 📚 ドキュメント - -### 開発ガイド -- [Python 戦略開発ガイド](docs/STRATEGY_DEV_GUIDE_JA.md) -- [Interactive Brokers (IBKR) トレーディングガイド](docs/IBKR_TRADING_GUIDE_EN.md) 🆕 -- [MetaTrader 5 (MT5) トレーディングガイド](docs/MT5_TRADING_GUIDE_EN.md) 🆕 - -### 通知設定 -- [Telegram 通知設定](docs/NOTIFICATION_TELEGRAM_CONFIG_EN.md) -- [メール (SMTP) 通知設定](docs/NOTIFICATION_EMAIL_CONFIG_EN.md) -- [SMS (Twilio) 通知設定](docs/NOTIFICATION_SMS_CONFIG_EN.md) - -## 📸 ビジュアルツアー - -
-

🗺️ システムアーキテクチャ概要

-

QuantDinger の AI 駆動型リサーチ、バックテスト、自動取引機能の全体像。

- QuantDinger システムトポロジー -
- -
- -
-

📊 プロフェッショナル・クオンツダッシュボード

-

市場の動向、資産、戦略ステータスをリアルタイムで監視。

- QuantDinger Dashboard -
- -
- - - - - - - - - - - - - -
-

🤖 AI ディープリサーチ

-

市場センチメントとテクニカル分析のためのマルチエージェント・コラボレーション。

- AI Market Analysis -
-

💬 スマートトレーディングアシスタント

-

即座に市場の洞察を得るための自然言語インターフェース。

- Trading Assistant -
-

📈 インタラクティブなインジケーター分析

-

ドラッグ&ドロップ分析を備えた豊富なテクニカルインジケーターライブラリ。

- Indicator Analysis -
-

🐍 Python 戦略生成

-

AI支援による戦略コーディングが可能な内蔵エディタ。

- Code Generation -
-

📊 ポートフォリオモニター

-

ポジション追跡、アラート設定、メール/Telegram経由でAI分析レポートを受信。

- Portfolio Monitor -
- ---- - -## ✨ 主な機能 - -### 1. ビジュアルPython戦略ワークベンチ -*PineScriptより強力、SaaSよりスマート。* - -- **Pythonネイティブ**: Pythonでインジケーターと戦略を記述。PineScriptのような専用言語ではなく、完全なPythonエコシステム(Pandas, Numpy, TA-Lib)を活用。 -- **"Mini-TradingView" 体験**: 内蔵のKラインチャート上でPythonインジケーターを直接実行。過去データ上で売買シグナルを視覚的にデバッグ。 -- **AI支援コーディング**: 内蔵AIに複雑なロジックを書いてもらいましょう。アイデアからコードまで数秒で完了。 - -### 2. 完全な取引ライフサイクル -*インジケーターから実行まで、シームレスに。* - -1. **インジケーター**: 市場のエントリー/エグジットシグナルを定義。 -2. **戦略設定**: リスク管理ルール(ポジションサイジング、ストップロス、利益確定)を追加。 -3. **バックテスト & AI最適化**: バックテストを実行し、豊富なパフォーマンス指標を表示し、**AIに結果を分析させて改善を提案**させます(例:「MACD閾値をXに調整」)。 -4. **実行モード**: - - **実取引**: - - **暗号資産**: 直接API実行、10以上の取引所(Binance, OKX, Bitget, Bybitなど)をサポート - - **米国/香港株式**: Interactive Brokers (IBKR)経由 🆕 - - **外国為替**: MetaTrader 5 (MT5)経由 🆕 - - **シグナル通知**: 実取引をサポートしていない市場(A株/先物)向けに、Telegram, Discord, Email, SMS, Webhook経由でシグナルを送信。 - -### 3. AI分析システム -*高速・正確・プロフェッショナルなレポート。* - -QuantDingerは効率的なAI分析システムを搭載: - -- **高速分析モード**: 単一LLM呼び出しアーキテクチャによる迅速で正確な分析(複雑なマルチエージェントシステムに代わる) -- **グローバル市場統合**: リアルタイム市場データ、ヒートマップ、経済カレンダーを分析ページに統合 -- **ATRベースの取引レベル**: テクニカル分析(ATR、サポート/レジスタンス)に基づくストップロス・利確の推奨 -- **分析メモリ**: 履歴確認と継続学習のための分析結果の保存 -- **戦略統合**: AI分析を戦略の「市場フィルター」として使用可能 - -### 4. ユニバーサルデータエンジン - -QuantDingerは複数の市場にわたる統一されたデータインターフェースを提供します: - -- **暗号資産**: 取引のための直接API接続(10以上の取引所)と、市場データのためのCCXT統合(100以上のソース) -- **株式**: Yahoo Finance、Finnhub、Tiingo(米国株)、AkShare(中国/香港株) -- **先物/FX**: OANDAおよび主要な先物データソース -- **プロキシサポート**: 制限されたネットワーク環境のための内蔵プロキシ設定 - -### 5. 🧠 記憶拡張エージェント(Memory-Augmented Agents) -QuantDinger のエージェントは毎回「ゼロから」ではありません。バックエンドに **ローカル記憶DB + 反省(検証)ループ** を内蔵し、過去の経験を検索して system prompt に注入します(RAG 風)。 - -- **これは何か**:経験検索によるプロンプト拡張(※モデルの学習/微調整ではありません) -- **保存先**:ローカル SQLite(`backend_api_python/data/memory/`) - -#### フロー図(リクエスト → 記憶閉ループ) - -```mermaid -flowchart TB - %% ===== 🌐 入口層 ===== - subgraph Entry["🌐 API エントリー"] - A["📡 POST /api/analysis/multi"] - A2["🔄 POST /api/analysis/reflect"] - end - - %% ===== ⚙️ サービス層 ===== - subgraph Service["⚙️ サービス編成"] - B[AnalysisService] - C[AgentCoordinator] - D["📊 コンテキスト構築
price · kline · news · indicators"] - end - - %% ===== 🤖 マルチエージェントワークフロー ===== - subgraph Agents["🤖 マルチエージェントワークフロー"] - - subgraph P1["📈 Phase 1 · 多次元分析(並列)"] - E1["🔍 MarketAnalyst
テクニカル"] - E2["📑 FundamentalAnalyst
ファンダメンタル"] - E3["📰 NewsAnalyst
ニュース"] - E4["💭 SentimentAnalyst
センチメント"] - E5["⚠️ RiskAnalyst
リスク評価"] - end - - subgraph P2["🎯 Phase 2 · 強気弱気論争(並列)"] - F1["🐂 BullResearcher
強気の論点"] - F2["🐻 BearResearcher
弱気の論点"] - end - - subgraph P3["💹 Phase 3 · 取引判断"] - G["🎰 TraderAgent
総合判定 → BUY / SELL / HOLD"] - end - - end - - %% ===== 🧠 記憶層 ===== - subgraph Memory["🧠 ローカル SQLite 記憶庫(data/memory/)"] - M1[("market_analyst")] - M2[("fundamental")] - M3[("news_analyst")] - M4[("sentiment")] - M5[("risk_analyst")] - M6[("bull_researcher")] - M7[("bear_researcher")] - M8[("trader_agent")] - end - - %% ===== 🔄 反省ループ ===== - subgraph Reflect["🔄 反省ループ(オプション)"] - R[ReflectionService] - RR[("reflection_records.db")] - W["⏰ ReflectionWorker"] - end - - %% ===== メインフロー ===== - A --> B --> C --> D - D --> P1 --> P2 --> P3 - - %% ===== 記憶読み書き ===== - E1 <-.-> M1 - E2 <-.-> M2 - E3 <-.-> M3 - E4 <-.-> M4 - E5 <-.-> M5 - F1 <-.-> M6 - F2 <-.-> M7 - G <-.-> M8 - - %% ===== 反省フロー ===== - C --> R --> RR - W --> RR - W -.->|"検証 + 学習"| M8 - A2 -.->|"手動復習"| M8 -``` - -#### 検索ランキング(簡略) -\[ -score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_score -\] - -#### 学習の入口 -- **手動復習(推奨)**:`POST /api/analysis/reflect` で実際の結果(returns/result)を記憶へ -- **自動検証(任意)**:`ENABLE_REFLECTION_WORKER=true`、`REFLECTION_WORKER_INTERVAL_SEC` で定期検証→記憶へ反映 - -#### 主な環境変数(`.env`) -- `ENABLE_AGENT_MEMORY`, `AGENT_MEMORY_TOP_K`, `AGENT_MEMORY_CANDIDATE_LIMIT` -- `AGENT_MEMORY_ENABLE_VECTOR`, `AGENT_MEMORY_EMBEDDING_DIM` -- `AGENT_MEMORY_HALF_LIFE_DAYS`, `AGENT_MEMORY_W_SIM`, `AGENT_MEMORY_W_RECENCY`, `AGENT_MEMORY_W_RETURNS` -- `ENABLE_REFLECTION_WORKER`, `REFLECTION_WORKER_INTERVAL_SEC` - -### 6. 戦略ランタイム - -- **スレッドベースのエグゼキューター**: 戦略実行のための独立したスレッドプール -- **自動復元**: システム再起動後に実行中の戦略を再開 -- **注文キュー**: 注文実行のためのバックグラウンドワーカー - -### 7. マルチLLMプロバイダーサポート - -QuantDinger は複数の AI プロバイダーをサポートし、自動検出機能を備えています: - -| プロバイダー | 特徴 | -|------------|------| -| **OpenRouter** | マルチモデルゲートウェイ(デフォルト)、100+ モデル | -| **OpenAI** | GPT-4o, GPT-4o-mini | -| **Google Gemini** | Gemini 1.5 Flash/Pro | -| **DeepSeek** | DeepSeek Chat(コスパ良好) | -| **xAI Grok** | Grok Beta | - -`.env` でお好みのプロバイダーの API キーを設定するだけで、システムが利用可能なプロバイダーを自動検出します。 - -### 8. インジケーターコミュニティ -*共有、発見、取引。* - -- **公開と共有**: Pythonインジケーターをコミュニティと共有 -- **購入システム**: 他のユーザーからプレミアムインジケーターを購入 -- **評価とレビュー**: 購入したインジケーターを評価・レビュー -- **管理者レビュー**: 品質管理のためのモデレーションシステム - -### 9. ユーザー管理とセキュリティ - -- **マルチユーザーサポート**:PostgreSQL ベースのユーザーアカウント、ロールベースの権限管理 -- **OAuth ログイン**:Google および GitHub OAuth 統合 -- **メール認証**:メール認証コードによる登録とパスワードリセット -- **セキュリティ機能**:Cloudflare Turnstile キャプチャ、IP/アカウントレート制限 -- **デモモード**:公開デモ用の読み取り専用モード - -### 10. 技術スタック - -- **バックエンド**: Python (Flask) + PostgreSQL + Redis(オプション) -- **フロントエンド**: Vue 2 + Ant Design Vue + KlineCharts/ECharts -- **デプロイ**: Docker Compose -- **現在のバージョン**: V2.1.1 ([更新履歴](docs/CHANGELOG.md)) - ---- - -## 🔌 対応取引所とブローカー - -QuantDingerは複数の市場タイプに対して複数の実行方法をサポートしています: - -### 暗号資産取引所(直接API) - -| 取引所 | 市場 | -|:--------:|:---------| -| Binance | 現物, 先物, マージン | -| OKX | 現物, 無期限, オプション | -| Bitget | 現物, 先物, コピートレーディング | -| Bybit | 現物, リニア先物 | -| Coinbase Exchange | 現物 | -| Kraken | 現物, 先物 | -| KuCoin | 現物, 先物 | -| Gate.io | 現物, 先物 | -| Bitfinex | 現物, デリバティブ | -| Deepcoin | 現物, 無期限 | - -### 伝統的なブローカー - -| ブローカー | 市場 | プラットフォーム | -|:------:|:--------|:---------| -| **Interactive Brokers (IBKR)** | 米国株式, 香港株式 | TWS / IB Gateway 🆕 | -| **MetaTrader 5 (MT5)** | 外国為替 | MT5ターミナル 🆕 | - -### 市場データ(CCXT経由) - -Bybit、Gate.io、Kraken、KuCoin、HTX、および100以上のその他の取引所が市場データ用にサポートされています。 - ---- - -### 多言語サポート - -QuantDingerは、包括的な国際化対応により、世界中のユーザー向けに構築されています。 - -全てのUI要素、エラーメッセージ、ドキュメントは完全に翻訳されています。言語はブラウザの設定に基づいて自動検出されますが、アプリ内で手動で切り替えることも可能です。 - ---- - -### 対応市場 - -| 市場タイプ | データソース | 取引 | -|-------------|--------------|---------| -| **暗号資産** | Binance, OKX, Bitget, + 100 取引所 | ✅ 完全サポート | -| **米国株** | Yahoo Finance, Finnhub, Tiingo | ✅ IBKR経由 🆕 | -| **香港株** | AkShare, East Money | ✅ IBKR経由 🆕 | -| **中国株(A株)** | AkShare, East Money | ⚡ データのみ | -| **FX** | Finnhub, OANDA | ✅ MT5経由 🆕 | -| **先物** | 取引所API, AkShare | ⚡ データのみ | - ---- - -### アーキテクチャ (現在のリポジトリ) - -```text -┌─────────────────────────────┐ -│ quantdinger_vue │ -│ (Vue 2 + Ant Design Vue) │ -└──────────────┬──────────────┘ - │ HTTP (/api/*) - ▼ -┌─────────────────────────────┐ -│ backend_api_python │ -│ (Flask + 戦略ランタイム) │ -└──────────────┬──────────────┘ - │ - ├─ SQLite (quantdinger.db) - ├─ Redis (オプション キャッシュ) - └─ データプロバイダー / LLMs / 取引所 -``` - ---- - -### リポジトリ構造 - -```text -. -├─ backend_api_python/ # Flask API + AI + バックテスト + 戦略ランタイム -│ ├─ app/ -│ ├─ env.example # ローカル設定用に .env にコピー -│ ├─ requirements.txt -│ └─ run.py # エントリーポイント -└─ quantdinger_vue/ # Vue 2 UI (開発サーバーは /api をバックエンドにプロキシ) -``` - ---- - -## クイックスタート - -### オプション 1: Docker デプロイ (推奨) - -QuantDingerを実行する最速の方法です。 - -#### 1. ワンクリック起動 - -**Linux / macOS** -```bash -git clone https://github.com/brokermr810/QuantDinger.git && \ -cd QuantDinger && \ -cp backend_api_python/env.example backend_api_python/.env && \ -docker-compose up -d --build -``` - -**Windows (PowerShell)** -```powershell -git clone https://github.com/brokermr810/QuantDinger.git -cd QuantDinger -Copy-Item backend_api_python\env.example -Destination backend_api_python\.env -docker-compose up -d --build -``` - -#### 2. アクセスと設定 - -- **フロントエンド UI**: http://localhost:8888 -- **デフォルトアカウント**: `quantdinger` / `123456` - -> **注意**: AI機能や本番環境のセキュリティのために、`backend_api_python/.env` を編集し(`OPENROUTER_API_KEY`の追加、パスワードの変更)、`docker-compose restart backend` でサービスを再起動してください。 - -#### 3. アプリケーションへのアクセス - -- **フロントエンド UI**: http://localhost -- **バックエンド API**: http://localhost:5000 - -#### Docker コマンドリファレンス - -```bash -# 実行状態の表示 -docker-compose ps - -# ログの表示 -docker-compose logs -f - -# サービスの停止 -docker-compose down - -# 停止してボリュームを削除 (警告: データベースが削除されます!) -docker-compose down -v -``` - -#### データの永続化 - -以下のデータはホストにマウントされ、コンテナの再起動後も保持されます: - -```yaml -volumes: - - ./backend_api_python/logs:/app/logs # ログ - - ./backend_api_python/data:/app/data # データディレクトリ(quantdinger.db を含む) - - ./backend_api_python/.env:/app/.env # 設定ファイル -``` - ---- - -### オプション 2: ローカル開発 - -**前提条件** -- Python 3.10+ (推奨) -- Node.js 16+ (推奨) - -#### 1. バックエンドの起動 (Flask API) - -```bash -cd backend_api_python -pip install -r requirements.txt -cp env.example .env # Windows: copy env.example .env -python run.py -``` - -バックエンドは `http://localhost:5000` で利用可能になります。 - -#### 2. フロントエンドの起動 (Vue UI) - -```bash -cd quantdinger_vue -npm install -npm run serve -``` - -フロントエンド開発サーバーは `http://localhost:8000` で実行され、`/api/*` を `http://localhost:5000` にプロキシします。 - ---- - -### 設定 (.env) - -`backend_api_python/env.example` をテンプレートとして使用してください。一般的な設定: - -- **認証**: `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD` -- **サーバー**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG` -- **データベース**: `DATABASE_URL` (PostgreSQL 接続文字列) -- **AI / LLM**: `LLM_PROVIDER` (openrouter/openai/google/deepseek/grok), 各プロバイダー API キー -- **OAuth**: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` -- **セキュリティ**: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `ENABLE_REGISTRATION` -- **ウェブ検索**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY` -- **注文執行**: `ORDER_MODE` (maker/market), `MAKER_WAIT_SEC`, `MAKER_OFFSET_BPS` -- **プロキシ (オプション)**: `PROXY_PORT` または `PROXY_URL` -- **バックグラウンドサービス**: `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR` - ---- - -## 🤝 コミュニティとサポート - -- **貢献**: [貢献ガイド](CONTRIBUTING.md) · [貢献者](CONTRIBUTORS.md) -- **Telegram**: [QuantDingerグループ](https://t.me/quantdinger) -- **Discord**: [サーバーに参加](https://discord.gg/vwJ8zxFh9Q) -- **📺 動画デモ**: [プロジェクト紹介](https://youtu.be/HPTVpqL7knM) -- **YouTube**: [@quantdinger](https://youtube.com/@quantdinger) -- **Email**: [brokermr810@gmail.com](mailto:brokermr810@gmail.com) -- **GitHub Issues**: [バグ報告 / 機能リクエスト](https://github.com/brokermr810/QuantDinger/issues) - ---- - -## 💼 商用ライセンス & スポンサー(Commercial License & Sponsorship) - -QuantDinger のコードは **Apache License 2.0** で提供されています。ただし **Apache 2.0 は商標権を付与しません**。QuantDinger の名称/ロゴ/ブランドは商標およびブランドポリシーの対象です(コードライセンスとは別扱い): - -- **著作権/帰属表示**:LICENSE/NOTICE など、必要な著作権・ライセンス告知は保持してください。 -- **商標(名称/ロゴ/ブランド)**:QuantDinger の名称/ロゴ/ブランドを用いて、出所の誤認や背書きを示唆してはいけません。改変版を再配布する場合、書面許可がない限り QuantDinger ブランド表示の削除/置換が必要になることがあります。 - -再配布物で QuantDinger のブランド表示を**保持/変更**したい場合(UI 上のロゴ使用などを含む)、**商用ライセンス**についてお問い合わせください。 - -参照:`TRADEMARKS.md` - -### 商用ライセンスで得られるもの - -- 合意に基づく **ブランド/表示の商用許諾** -- **運用サポート**:デプロイ、アップグレード、障害対応、保守ガイダンス -- **コンサルティング**:アーキテクチャレビュー、性能チューニング、戦略ワークフロー相談 -- **スポンサー枠**:スポンサーとしてロゴ/広告掲載(README/サイト/アプリ内など合意に基づく) - -### 連絡先 - -- **Telegram**: [QuantDinger Group](https://t.me/worldinbroker) -- **Email**: [brokermr810@gmail.com](mailto:brokermr810@gmail.com) - ---- - -### 💝 直接サポート(寄付) - -あなたのご支援は QuantDinger の維持・改善に役立ちます。 - -**暗号通貨寄付 (ERC-20 / BEP-20 / Polygon / Arbitrum)** - -``` -0x96fa4962181bea077f8c7240efe46afbe73641a7 -``` - -

- USDT - ETH -

- ---- - -### 🎓 サポートパートナー - -量的金融の教育と研究を推進する学術機関や組織からのサポートを光栄に思います。 - -
- - - - -
- - インディアナ大学量的金融学会 - -

- 量的金融学会 (QFS)
- インディアナ大学ブルーミントン校
- 次世代の量的金融専門家を育成 -
-
- -> 💡 **サポートパートナーになりませんか?** 大学、研究機関、組織との協力を歓迎します。[brokermr810@gmail.com](mailto:brokermr810@gmail.com) または [Telegram](https://t.me/worldinbroker) でお問い合わせください。 - ---- - -### 謝辞 - -QuantDingerは、Flask, Pandas, CCXT, Vue.js, Ant Design Vue, KlineChartsなどの素晴らしいオープンソースプロジェクトの上に成り立っています。 - -すべてのメンテナとコントリビューターに感謝します! ❤️ - diff --git a/README_KO.md b/README_KO.md deleted file mode 100644 index 4872c80..0000000 --- a/README_KO.md +++ /dev/null @@ -1,643 +0,0 @@ -
- 🇺🇸 English | - 🇨🇳 简体中文 | - 🇹🇼 繁體中文 | - 🇯🇵 日本語 | - 🇰🇷 한국어 -
-
- -
- - QuantDinger Logo - - -

QuantDinger

- -

- 차세대 AI 퀀트 트레이딩 플랫폼 -

- -

- 🤖 AI 네이티브 · 🐍 비주얼 파이썬 · 🌍 글로벌 멀티 마켓 · 🔒 프라이버시 우선 -

-

- AI 코파일럿과 함께하는 구축, 백테스팅 및 트레이딩 플랫폼. PineScript보다 강력하고, SaaS보다 똑똑합니다. -

- -

- 공식 커뮤니티 · - 라이브 데모 · - 📺 동영상 데모 · - 🌟 참여하기 -

- -

- License - Python - Vue - Docker - Stars -

- -

- Telegram Group - Discord - X -

-
- ---- - -## 📖 소개 - -### QuantDinger란 무엇인가요? - -QuantDinger는 **로컬 우선, 프라이버시 우선의 셀프 호스팅 정량 거래 인프라**입니다. 귀하의 머신/서버에서 실행되며, **PostgreSQL 기반 멀티 유저 계정**을 제공하면서도 전략, 거래 데이터 및 API 키를 완전히 제어할 수 있습니다. - -### 왜 로컬 우선인가요? - -데이터와 전략을 클라우드에 잠그는 SaaS 플랫폼과 달리, QuantDinger는 로컬에서 실행됩니다. 귀하의 전략, 거래 로그, API 키 및 분석 결과는 모두 귀하의 머신에 저장됩니다. 벤더 잠금 없음, 구독료 없음, 데이터 유출 없음. - -### 누구를 위한 것인가요? - -QuantDinger는 다음을 위한 사용자를 위해 구축되었습니다: -- 데이터 주권과 프라이버시를 중시하는 트레이더, 연구원 및 엔지니어 -- 투명하고 감사 가능한 거래 인프라를 원하는 사람 -- 마케팅보다 엔지니어링을 선호하는 사람 -- 완전한 워크플로우가 필요한 사람: 데이터, 분석, 백테스팅 및 실행 - -### 핵심 기능 - -QuantDinger는 웹에서 금융 정보를 수집하고, 로컬 시장 데이터와 결합하여 분석 보고서를 생성하는 내장 **LLM 기반 멀티 에이전트 리서치 시스템**을 포함합니다. 이것은 전략 개발, 백테스팅 및 실거래 워크플로우와 통합됩니다. - -### 핵심 가치 - -- **🔓 Apache 2.0 오픈소스(코드)**: 허용적이며 상업적으로 친화적입니다. Apache 2.0 범위에서 fork/수정이 가능하지만, 라이선스/저작권 고지 등 필요한 표기는 유지해야 합니다. -- **🐍 파이썬 네이티브 & 비주얼**: 표준 파이썬으로 지표를 작성(PineScript보다 쉬움)하고 AI의 지원을 받으세요. 차트에서 신호를 직접 시각화하여 "로컬 버전의 TradingView" 경험을 구축하세요. -- **🤖 AI 루프 최적화**: 전략을 실행할 뿐만 아니라, AI가 백테스트 결과를 분석하여 매개변수 조정(손절매/이익실현/MACD 설정)을 제안하고 폐루프 최적화를 형성합니다. -- **🌍 글로벌 마켓 액세스**: 암호화폐(실거래), 미국/중국 주식, 외환 및 선물(데이터/알림)을 지원하는 통합 시스템. -- **⚡ Docker & 클린 아키텍처**: 4줄 명령어로 빠른 배포. 모던 기술 스택(Vue + Python), 클린 아키텍처, 관심사 분리. - ---- - -## 📺 동영상 데모 - -
- - QuantDinger 프로젝트 소개 동영상 - -

위 동영상을 클릭하여 QuantDinger 프로젝트 소개를 시청하세요

-
- ---- - -## 📚 문서 - -### 개발 가이드 -- [Python 전략 개발 가이드](docs/STRATEGY_DEV_GUIDE_KO.md) -- [Interactive Brokers (IBKR) 트레이딩 가이드](docs/IBKR_TRADING_GUIDE_EN.md) 🆕 -- [MetaTrader 5 (MT5) 트레이딩 가이드](docs/MT5_TRADING_GUIDE_EN.md) 🆕 - -### 알림 설정 -- [Telegram 알림 설정](docs/NOTIFICATION_TELEGRAM_CONFIG_EN.md) -- [이메일 (SMTP) 알림 설정](docs/NOTIFICATION_EMAIL_CONFIG_EN.md) -- [SMS (Twilio) 알림 설정](docs/NOTIFICATION_SMS_CONFIG_EN.md) - -## 📸 비주얼 투어 - -
-

🗺️ 시스템 아키텍처 개요

-

QuantDinger의 AI 기반 리서치, 백테스팅 및 자동화 거래 기능 전체 개요.

- QuantDinger 시스템 토폴로지 -
- -
- -
-

📊 전문 퀀트 대시보드

-

시장 역학, 자산 및 전략 상태에 대한 실시간 모니터링.

- QuantDinger Dashboard -
- -
- - - - - - - - - - - - - -
-

🤖 AI 딥 리서치

-

시장 심리 및 기술적 분석을 위한 멀티 에이전트 협업.

- AI Market Analysis -
-

💬 스마트 트레이딩 어시스턴트

-

즉각적인 시장 통찰력을 위한 자연어 인터페이스.

- Trading Assistant -
-

📈 대화형 지표 분석

-

드래그 앤 드롭 분석이 가능한 풍부한 기술적 지표 라이브러리.

- Indicator Analysis -
-

🐍 Python 전략 생성

-

AI 지원 전략 코딩이 가능한 내장 에디터.

- Code Generation -
-

📊 포트폴리오 모니터

-

포지션 추적, 알림 설정, 이메일/Telegram으로 AI 분석 보고서 수신.

- Portfolio Monitor -
- ---- - -## ✨ 주요 기능 - -### 1. 비주얼 파이썬 전략 워크벤치 -*PineScript보다 강력하고, SaaS보다 똑똑합니다.* - -- **파이썬 네이티브**: 파이썬으로 지표와 전략을 작성하세요. PineScript와 같은 전용 언어 대신 완전한 파이썬 생태계(Pandas, Numpy, TA-Lib)를 활용하세요. -- **"Mini-TradingView" 경험**: 내장된 K-라인 차트에서 파이썬 지표를 직접 실행하세요. 과거 데이터에서 매수/매도 신호를 시각적으로 디버깅하세요. -- **AI 지원 코딩**: 내장 AI가 복잡한 로직을 작성하도록 하세요. 아이디어에서 코드까지 단 몇 초면 됩니다. - -### 2. 완전한 거래 라이프사이클 -*지표에서 실행까지, 매끄럽게.* - -1. **지표**: 시장 진입/청산 신호를 정의하세요. -2. **전략 설정**: 위험 관리 규칙(포지션 사이징, 손절매, 이익실현)을 추가하세요. -3. **백테스트 & AI 최적화**: 백테스트를 실행하고, 풍부한 성과 지표를 확인하며, **AI가 결과를 분석하여 개선 사항을 제안**하도록 하세요(예: "MACD 임계값을 X로 조정"). -4. **실행 모드**: - - **실거래**: - - **암호화폐**: 직접 API 실행, 10개 이상의 거래소(Binance, OKX, Bitget, Bybit 등) 지원 - - **미국/홍콩 주식**: Interactive Brokers (IBKR) 경유 🆕 - - **외환**: MetaTrader 5 (MT5) 경유 🆕 - - **신호 알림**: 실거래를 지원하지 않는 시장(A주/선물)의 경우, Telegram, Discord, Email, SMS 또는 Webhook을 통해 신호를 전송하세요. - -### 3. AI 분석 시스템 -*빠르고, 정확하며, 전문적인 리포트.* - -QuantDinger는 효율적인 AI 분석 시스템을 탑재: - -- **빠른 분석 모드**: 단일 LLM 호출 아키텍처로 빠르고 정확한 분석 (복잡한 멀티 에이전트 시스템 대체) -- **글로벌 시장 통합**: 실시간 시장 데이터, 히트맵, 경제 캘린더를 분석 페이지에 통합 -- **ATR 기반 거래 수준**: 기술적 분석(ATR, 지지/저항선)에 기반한 손절매 및 이익실현 권장 -- **분석 메모리**: 이력 검토 및 지속적 학습을 위한 분석 결과 저장 -- **전략 통합**: AI 분석을 전략의 "시장 필터"로 사용 가능 - -### 4. 범용 데이터 엔진 - -QuantDinger는 여러 시장에 걸친 통합 데이터 인터페이스를 제공합니다: - -- **암호화폐**: 거래를 위한 직접 API 연결(10개 이상의 거래소)및 시장 데이터를 위한 CCXT 통합(100개 이상의 소스) -- **주식**: Yahoo Finance, Finnhub, Tiingo(미국 주식)및 AkShare(중국/홍콩 주식) -- **선물/외환**: OANDA 및 주요 선물 데이터 소스 -- **프록시 지원**: 제한된 네트워크 환경을 위한 내장 프록시 구성 - -### 5. 🧠 메모리 증강 에이전트(Memory-Augmented Agents) -QuantDinger의 에이전트는 매번 “처음부터” 시작하지 않습니다. 백엔드에는 **로컬 메모리 DB + 리플렉션(검증) 루프**가 내장되어 있어, 과거 경험을 검색해 system prompt에 주입하는 RAG 스타일의 강화가 동작합니다. - -- **무엇인가**: 경험 검색 기반 프롬프트 강화(모델 파인튜닝/학습이 아님) -- **저장 위치**: 로컬 SQLite (`backend_api_python/data/memory/`) - -#### 흐름도(요청 → 메모리 폐루프) - -```mermaid -flowchart TB - %% ===== 🌐 진입 레이어 ===== - subgraph Entry["🌐 API 진입점"] - A["📡 POST /api/analysis/multi"] - A2["🔄 POST /api/analysis/reflect"] - end - - %% ===== ⚙️ 서비스 레이어 ===== - subgraph Service["⚙️ 서비스 오케스트레이션"] - B[AnalysisService] - C[AgentCoordinator] - D["📊 컨텍스트 구성
price · kline · news · indicators"] - end - - %% ===== 🤖 멀티에이전트 워크플로우 ===== - subgraph Agents["🤖 멀티에이전트 워크플로우"] - - subgraph P1["📈 Phase 1 · 다차원 분석(병렬)"] - E1["🔍 MarketAnalyst
기술적 분석"] - E2["📑 FundamentalAnalyst
펀더멘털"] - E3["📰 NewsAnalyst
뉴스"] - E4["💭 SentimentAnalyst
심리"] - E5["⚠️ RiskAnalyst
리스크 평가"] - end - - subgraph P2["🎯 Phase 2 · 강세/약세 논쟁(병렬)"] - F1["🐂 BullResearcher
강세 논거"] - F2["🐻 BearResearcher
약세 논거"] - end - - subgraph P3["💹 Phase 3 · 거래 결정"] - G["🎰 TraderAgent
종합 판단 → BUY / SELL / HOLD"] - end - - end - - %% ===== 🧠 메모리 레이어 ===== - subgraph Memory["🧠 로컬 SQLite 메모리(data/memory/)"] - M1[("market_analyst")] - M2[("fundamental")] - M3[("news_analyst")] - M4[("sentiment")] - M5[("risk_analyst")] - M6[("bull_researcher")] - M7[("bear_researcher")] - M8[("trader_agent")] - end - - %% ===== 🔄 리플렉션 루프 ===== - subgraph Reflect["🔄 리플렉션 루프(옵션)"] - R[ReflectionService] - RR[("reflection_records.db")] - W["⏰ ReflectionWorker"] - end - - %% ===== 메인 흐름 ===== - A --> B --> C --> D - D --> P1 --> P2 --> P3 - - %% ===== 메모리 읽기/쓰기 ===== - E1 <-.-> M1 - E2 <-.-> M2 - E3 <-.-> M3 - E4 <-.-> M4 - E5 <-.-> M5 - F1 <-.-> M6 - F2 <-.-> M7 - G <-.-> M8 - - %% ===== 리플렉션 흐름 ===== - C --> R --> RR - W --> RR - W -.->|"검증 + 학습"| M8 - A2 -.->|"수동 회고"| M8 -``` - -#### 검색 랭킹(간략) -\[ -score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_score -\] - -#### 학습 경로 -- **수동 회고(추천)**: `POST /api/analysis/reflect`로 실제 결과(returns/result)를 메모리에 기록 -- **자동 검증(옵션)**: `ENABLE_REFLECTION_WORKER=true`, `REFLECTION_WORKER_INTERVAL_SEC`로 주기적 검증→메모리 반영 - -#### 주요 환경 변수(`.env`) -- `ENABLE_AGENT_MEMORY`, `AGENT_MEMORY_TOP_K`, `AGENT_MEMORY_CANDIDATE_LIMIT` -- `AGENT_MEMORY_ENABLE_VECTOR`, `AGENT_MEMORY_EMBEDDING_DIM` -- `AGENT_MEMORY_HALF_LIFE_DAYS`, `AGENT_MEMORY_W_SIM`, `AGENT_MEMORY_W_RECENCY`, `AGENT_MEMORY_W_RETURNS` -- `ENABLE_REFLECTION_WORKER`, `REFLECTION_WORKER_INTERVAL_SEC` - -### 6. 전략 런타임 - -- **스레드 기반 실행기**: 전략 실행을 위한 독립적인 스레드 풀 -- **자동 복구**: 시스템 재시작 후 실행 중인 전략 재개 -- **주문 대기열**: 주문 실행을 위한 백그라운드 워커 - -### 7. 멀티 LLM 제공자 지원 - -QuantDinger는 자동 감지 기능을 갖춘 여러 AI 제공자를 지원합니다: - -| 제공자 | 특징 | -|--------|------| -| **OpenRouter** | 멀티모델 게이트웨이(기본값), 100+ 모델 | -| **OpenAI** | GPT-4o, GPT-4o-mini | -| **Google Gemini** | Gemini 1.5 Flash/Pro | -| **DeepSeek** | DeepSeek Chat(가성비 좋음) | -| **xAI Grok** | Grok Beta | - -`.env`에서 선호하는 제공자의 API 키만 설정하면 시스템이 사용 가능한 제공자를 자동 감지합니다. - -### 8. 인디케이터 커뮤니티 -*공유, 발견, 거래.* - -- **게시 및 공유**: 파이썬 인디케이터를 커뮤니티와 공유 -- **구매 시스템**: 다른 사용자로부터 프리미엄 인디케이터 구매 -- **평점 및 리뷰**: 구매한 인디케이터 평가 및 리뷰 -- **관리자 검토**: 품질 관리를 위한 모더레이션 시스템 - -### 9. 사용자 관리 및 보안 - -- **멀티유저 지원**: PostgreSQL 기반 사용자 계정, 역할 기반 권한 관리 -- **OAuth 로그인**: Google 및 GitHub OAuth 통합 -- **이메일 인증**: 이메일 인증 코드를 통한 등록 및 비밀번호 재설정 -- **보안 기능**: Cloudflare Turnstile 캡차, IP/계정 속도 제한 -- **데모 모드**: 공개 데모를 위한 읽기 전용 모드 - -### 10. 기술 스택 - -- **백엔드**: Python (Flask) + PostgreSQL + Redis(옵션) -- **프론트엔드**: Vue 2 + Ant Design Vue + KlineCharts/ECharts -- **배포**: Docker Compose -- **현재 버전**: V2.1.1 ([변경 로그](docs/CHANGELOG.md)) - ---- - -## 🔌 지원되는 거래소 및 브로커 - -QuantDinger는 다양한 시장 유형에 대해 여러 실행 방법을 지원합니다: - -### 암호화폐 거래소(직접 API) - -| 거래소 | 시장 | -|:--------:|:---------| -| Binance | 현물, 선물, 마진 | -| OKX | 현물, 무기한, 옵션 | -| Bitget | 현물, 선물, 카피 트레이딩 | -| Bybit | 현물, 선형 선물 | -| Coinbase Exchange | 현물 | -| Kraken | 현물, 선물 | -| KuCoin | 현물, 선물 | -| Gate.io | 현물, 선물 | -| Bitfinex | 현물, 파생상품 | -| Deepcoin | 현물, 무기한 | - -### 전통적인 브로커 - -| 브로커 | 시장 | 플랫폼 | -|:------:|:--------|:---------| -| **Interactive Brokers (IBKR)** | 미국 주식, 홍콩 주식 | TWS / IB Gateway 🆕 | -| **MetaTrader 5 (MT5)** | 외환 | MT5 터미널 🆕 | - -### 시장 데이터(CCXT 경유) - -Bybit, Gate.io, Kraken, KuCoin, HTX 및 100개 이상의 기타 거래소가 시장 데이터용으로 지원됩니다. - ---- - -### 다국어 지원 - -QuantDinger는 포괄적인 국제화를 통해 글로벌 사용자를 위해 구축되었습니다: - -

- English - Simplified Chinese - Traditional Chinese - Japanese - Korean - German - French - Thai - Vietnamese - Arabic -

- -모든 UI 요소, 오류 메시지 및 문서는 완벽하게 번역되어 있습니다. 언어는 브라우저 설정에 따라 자동으로 감지되거나 앱에서 수동으로 전환할 수 있습니다. - ---- - -### 지원되는 시장 - -| 시장 유형 | 데이터 소스 | 거래 | -|-------------|--------------|---------| -| **암호화폐** | Binance, OKX, Bitget, + 100 거래소 | ✅ 완전 지원 | -| **미국 주식** | Yahoo Finance, Finnhub, Tiingo | ✅ IBKR 경유 🆕 | -| **홍콩 주식** | AkShare, East Money | ✅ IBKR 경유 🆕 | -| **중국 주식(A주)** | AkShare, East Money | ⚡ 데이터만 | -| **외환** | Finnhub, OANDA | ✅ MT5 경유 🆕 | -| **선물** | 거래소 API, AkShare | ⚡ 데이터만 | - ---- - -### 아키텍처 (현재 레포지토리) - -```text -┌─────────────────────────────┐ -│ quantdinger_vue │ -│ (Vue 2 + Ant Design Vue) │ -└──────────────┬──────────────┘ - │ HTTP (/api/*) - ▼ -┌─────────────────────────────┐ -│ backend_api_python │ -│ (Flask + 전략 런타임) │ -└──────────────┬──────────────┘ - │ - ├─ SQLite (quantdinger.db) - ├─ Redis (옵션 캐시) - └─ 데이터 제공자 / LLMs / 거래소 -``` - ---- - -### 레포지토리 구조 - -```text -. -├─ backend_api_python/ # Flask API + AI + 백테스트 + 전략 런타임 -│ ├─ app/ -│ ├─ env.example # 로컬 구성을 위해 .env로 복사 -│ ├─ requirements.txt -│ └─ run.py # 엔트리 포인트 -└─ quantdinger_vue/ # Vue 2 UI (개발 서버는 /api를 백엔드로 프록시) -``` - ---- - -## 빠른 시작 - -### 옵션 1: Docker 배포 (권장) - -QuantDinger를 실행하는 가장 빠른 방법입니다. - -#### 1. 원클릭 시작 - -**Linux / macOS** -```bash -git clone https://github.com/brokermr810/QuantDinger.git && \ -cd QuantDinger && \ -cp backend_api_python/env.example backend_api_python/.env && \ -docker-compose up -d --build -``` - -**Windows (PowerShell)** -```powershell -git clone https://github.com/brokermr810/QuantDinger.git -cd QuantDinger -Copy-Item backend_api_python\env.example -Destination backend_api_python\.env -docker-compose up -d --build -``` - -#### 2. 접속 및 구성 - -- **프론트엔드 UI**: http://localhost:8888 -- **기본 계정**: `quantdinger` / `123456` - -> **참고**: AI 기능이나 프로덕션 환경의 보안을 위해, `backend_api_python/.env`를 편집하여( `OPENROUTER_API_KEY` 추가, 비밀번호 변경) `docker-compose restart backend`로 서비스를 재시작하십시오. - -#### 3. 애플리케이션 액세스 - -- **프론트엔드 UI**: http://localhost -- **백엔드 API**: http://localhost:5000 - -#### Docker 명령 참조 - -```bash -# 실행 상태 보기 -docker-compose ps - -# 로그 보기 -docker-compose logs -f - -# 서비스 중지 -docker-compose down - -# 볼륨 중지 및 제거 (경고: 데이터베이스가 삭제됩니다!) -docker-compose down -v -``` - -#### 데이터 지속성 - -다음 데이터는 호스트에 마운트되며 컨테이너 재시작 후에도 유지됩니다: - -```yaml -volumes: - - ./backend_api_python/logs:/app/logs # 로그 - - ./backend_api_python/data:/app/data # 데이터 디렉토리(quantdinger.db 포함) - - ./backend_api_python/.env:/app/.env # 구성 파일 -``` - ---- - -### 옵션 2: 로컬 개발 - -**전제 조건** -- Python 3.10+ 권장 -- Node.js 16+ 권장 - -#### 1. 백엔드 시작 (Flask API) - -```bash -cd backend_api_python -pip install -r requirements.txt -cp env.example .env # Windows: copy env.example .env -python run.py -``` - -백엔드는 `http://localhost:5000`에서 사용할 수 있습니다. - -#### 2. 프론트엔드 시작 (Vue UI) - -```bash -cd quantdinger_vue -npm install -npm run serve -``` - -프론트엔드 개발 서버는 `http://localhost:8000`에서 실행되며 `/api/*`를 `http://localhost:5000`으로 프록시합니다. - ---- - -### 구성 (.env) - -`backend_api_python/env.example`을 템플릿으로 사용하십시오. 일반적인 설정은 다음과 같습니다: - -- **인증**: `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD` -- **서버**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG` -- **데이터베이스**: `DATABASE_URL` (PostgreSQL 연결 문자열) -- **AI / LLM**: `LLM_PROVIDER` (openrouter/openai/google/deepseek/grok), 각 제공자 API 키 -- **OAuth**: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` -- **보안**: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `ENABLE_REGISTRATION` -- **웹 검색**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY` -- **주문 실행**: `ORDER_MODE` (maker/market), `MAKER_WAIT_SEC`, `MAKER_OFFSET_BPS` -- **프록시 (옵션)**: `PROXY_PORT` 또는 `PROXY_URL` -- **백그라운드 서비스**: `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR` - ---- - -## 🤝 커뮤니티 및 지원 - -- **기여**: [기여 가이드](CONTRIBUTING.md) · [기여자](CONTRIBUTORS.md) -- **Telegram**: [QuantDinger 그룹](https://t.me/quantdinger) -- **Discord**: [서버 참여](https://discord.gg/vwJ8zxFh9Q) -- **📺 동영상 데모**: [프로젝트 소개](https://youtu.be/HPTVpqL7knM) -- **YouTube**: [@quantdinger](https://youtube.com/@quantdinger) -- **Email**: [brokermr810@gmail.com](mailto:brokermr810@gmail.com) -- **GitHub Issues**: [버그 신고 / 기능 요청](https://github.com/brokermr810/QuantDinger/issues) - ---- - -## 💼 상용 라이선스 & 스폰서(Commercial License & Sponsorship) - -QuantDinger의 코드는 **Apache License 2.0**으로 제공됩니다. 다만 **Apache 2.0은 상표권을 부여하지 않습니다**. QuantDinger의 이름/로고/브랜딩은 상표 및 브랜드 정책의 적용을 받으며(코드 라이선스와 별개): - -- **저작권/표기**: LICENSE/NOTICE 등 필요한 저작권·라이선스 고지는 유지해야 합니다. -- **상표(이름/로고/브랜딩)**: QuantDinger의 이름/로고/브랜딩을 사용해 출처를 오인시키거나 보증(endorsement)을 암시해서는 안 됩니다. 수정 버전을 재배포할 때는 서면 허가가 없는 한 QuantDinger 브랜딩을 제거/대체해야 할 수 있습니다. - -재배포물에서 QuantDinger 브랜딩을 **유지/수정**하고 싶다면(UI 로고 사용 포함) **상용 라이선스**를 문의해 주세요. - -참고: `TRADEMARKS.md` - -### 상용 라이선스로 제공되는 것 - -- 합의된 범위 내 **브랜딩/표시의 상용 허가** -- **운영 지원**: 배포, 업그레이드, 장애 대응, 유지보수 가이드 -- **컨설팅**: 아키텍처 리뷰, 성능 튜닝, 전략 워크플로우 컨설팅 -- **스폰서 옵션**: 스폰서로서 로고/광고 노출(README/웹사이트/앱 내 등 합의에 따름) - -### 연락처 - -- **Telegram**: [QuantDinger Group](https://t.me/worldinbroker) -- **Email**: [brokermr810@gmail.com](mailto:brokermr810@gmail.com) - ---- - -### 💝 직접 지원(기부) - -귀하의 기여는 QuantDinger의 유지 및 개선에 도움이 됩니다. - -**암호화폐 기부 (ERC-20 / BEP-20 / Polygon / Arbitrum)** - -``` -0x96fa4962181bea077f8c7240efe46afbe73641a7 -``` - -

- USDT - ETH -

- ---- - -### 🎓 지원 파트너 - -양적 금융 교육 및 연구를 발전시키는 학술 기관과 조직의 지원을 받게 되어 자랑스럽습니다. - -
- - - - -
- - 인디애나 대학교 양적금융학회 - -

- 양적금융학회 (QFS)
- 인디애나 대학교 블루밍턴
- 차세대 양적 금융 전문가 양성 -
-
- -> 💡 **지원 파트너가 되는 데 관심이 있으신가요?** 대학, 연구 기관 및 조직과의 협력을 환영합니다. [brokermr810@gmail.com](mailto:brokermr810@gmail.com) 또는 [Telegram](https://t.me/worldinbroker)으로 연락주세요. - ---- - -### 감사의 말 - -QuantDinger는 Flask, Pandas, CCXT, Vue.js, Ant Design Vue, KlineCharts 등과 같은 훌륭한 오픈 소스 프로젝트의 어깨 위에 서 있습니다. - -모든 관리자와 기여자에게 감사드립니다! ❤️ - diff --git a/README_TW.md b/README_TW.md deleted file mode 100644 index ce05000..0000000 --- a/README_TW.md +++ /dev/null @@ -1,644 +0,0 @@ -
- 🇺🇸 English | - 🇨🇳 简体中文 | - 🇹🇼 繁體中文 | - 🇯🇵 日本語 | - 🇰🇷 한국어 -
-
- -
- - QuantDinger Logo - - -

QuantDinger

- -

- 下一代 AI 量化交易平台 -

- -

- 🤖 AI 原生 · 🐍 可視化 Python · 🌍 全球多市場 · 🔒 隱私優先 -

-

- 擁有 AI 副駕駛的構建、回測與交易平台。比 PineScript 更強,比 SaaS 更智能。 -

- -

- 官方社區 · - 在線演示 · - 📺 視頻演示 · - 🌟 加入我們 -

- -

- License - Python - Vue - Docker - Stars -

- -

- Telegram Group - Discord - X -

-
- ---- - -## 📖 簡介 - -### QuantDinger 是什麼? - -QuantDinger 是一個**本地優先、隱私優先、自託管的量化交易基礎設施**。它運行在你的機器/伺服器上,提供 **PostgreSQL 支持的多用戶帳號體系**,同時讓你完全控制自己的策略、交易數據和 API 密鑰。 - -### 為什麼選擇本地優先? - -與將你的數據和策略鎖定在雲端的 SaaS 平台不同,QuantDinger 在本地運行。你的策略、交易日誌、API 密鑰和分析結果都保留在你的機器上。沒有供應商鎖定,沒有訂閱費用,沒有數據洩露風險。 - -### 適合誰使用? - -QuantDinger 為以下用戶而構建: -- 重視數據主權和隱私的交易員、研究員和工程師 -- 需要透明、可審計的交易基礎設施 -- 更偏好工程而非營銷 -- 需要完整的工作流:數據、分析、回測和執行 - -### 核心功能 - -QuantDinger 包含一個內置的**基於 LLM 的多智能體研究系統**,能夠從網絡收集金融情報,結合本地市場數據,生成分析報告。這與策略開發、回測和實盤交易工作流無縫集成。 - -### 核心價值 - -- **🔓 Apache 2.0 開源(代碼)**:寬鬆且商業友好。你可以在 Apache 2.0 下 fork/修改代碼,但需保留許可與署名等必要聲明。 -- **🐍 Python 原生 & 可視化**:使用標準 Python 編寫指標(比 PineScript 更簡單),並由 AI 輔助。直接在圖表上可視化信號——打造「本地版 TradingView」體驗。 -- **🤖 AI 閉環優化**:不僅運行策略,AI 還會分析回測結果並建議參數調整(止損/止盈/MACD 設置),形成閉環優化。 -- **🌍 全球市場接入**:統一系統支持加密貨幣(實盤)、美股/A股、外匯和期貨(數據/通知)。 -- **⚡ Docker & 清晰架構**:4 行命令極速部署。現代技術棧(Vue + Python),架構清晰,關注點分離。 - ---- - -## 📺 視頻演示 - -
- - QuantDinger 專案介紹視頻 - -

點擊上方視頻觀看 QuantDinger 專案介紹

-
- ---- - -## 📚 文檔 - -### 開發指南 -- [Python 策略開發指南](docs/STRATEGY_DEV_GUIDE_TW.md) -- [盈透證券 (IBKR) 實盤交易指南](docs/IBKR_TRADING_GUIDE_CN.md) 🆕 -- [MetaTrader 5 (MT5) 外匯實盤交易指南](docs/MT5_TRADING_GUIDE_CN.md) 🆕 - -### 通知配置 -- [Telegram 通知配置](docs/NOTIFICATION_TELEGRAM_CONFIG_CH.md) -- [郵件 (SMTP) 通知配置](docs/NOTIFICATION_EMAIL_CONFIG_CH.md) -- [簡訊 (Twilio) 通知配置](docs/NOTIFICATION_SMS_CONFIG_CH.md) - -## 📸 功能預覽 - -
-

🗺️ 系統架構總覽

-

QuantDinger AI 驅動的研究、回測和自動化交易功能全景圖。

- QuantDinger 系統拓撲圖 -
- -
- -
-

📊 專業量化儀表盤

-

實時監控市場動態、資產狀況和策略狀態。

- QuantDinger Dashboard -
- -
- - - - - - - - - - - - - -
-

🤖 AI 深度投研

-

多智能體協作進行市場情緒與技術分析。

- AI Market Analysis -
-

💬 智能交易助手

-

通過自然語言接口獲取即時市場洞察。

- Trading Assistant -
-

📈 交互式指標分析

-

豐富的技術指標庫,支持拖拽式分析。

- Indicator Analysis -
-

🐍 Python 策略生成

-

內置編輯器,支持 AI 輔助策略代碼編寫。

- Code Generation -
-

📊 資產監測

-

追蹤持倉、設置預警,並通過郵件/Telegram 接收 AI 分析報告。

- Portfolio Monitor -
- ---- - -## ✨ 關鍵特性 - -### 1. 可視化 Python 策略工作台 -*比 PineScript 更強,比 SaaS 更智能。* - -- **Python 原生**:用 Python 編寫指標和策略。利用完整的 Python 生態(Pandas, Numpy, TA-Lib),而不是 PineScript 這種專用語言。 -- **"Mini-TradingView" 體驗**:直接在內置的 K 線圖上運行你的 Python 指標。在歷史數據上可視化調試買賣信號。 -- **AI 輔助編程**:讓內置 AI 為你編寫複雜的邏輯。從創意到代碼只需幾秒鐘。 - -### 2. 完整的交易生命週期 -*從指標到執行,無縫銜接。* - -1. **指標**:定義你的市場入場/出場信號。 -2. **策略配置**:附加風險管理規則(倉位管理、止損、止盈)。 -3. **回測 & AI 優化**:運行回測,查看豐富的性能指標,並**讓 AI 分析結果以建議改進**(例如:「調整 MACD 閾值為 X」)。 -4. **執行模式**: - - **實盤交易**: - - **加密貨幣**:直接 API 執行,支持 10+ 交易所(Binance, OKX, Bitget, Bybit 等) - - **美股/港股**:通過盈透證券 (IBKR) 🆕 - - **外匯**:通過 MetaTrader 5 (MT5) 🆕 - - **信號通知**:針對不支持實盤交易的市場(A股/期貨),通過 Telegram, Discord, Email, SMS 或 Webhook 發送信號。 - -### 3. AI 智能分析 -*快速、準確、專業的分析報告。* - -QuantDinger 具備精簡高效的 AI 分析系統: - -- **快速分析模式**:單次 LLM 調用架構,快速準確地生成分析(取代複雜的多智能體系統) -- **全球市場集成**:實時市場數據、熱力圖和財經日曆集成到分析頁面 -- **基於 ATR 的交易建議**:止損和止盈建議基於技術分析(ATR、支撐/阻力位) -- **分析記憶系統**:存儲分析結果用於歷史回顧和持續學習 -- **策略集成**:AI 分析可作為策略的「市場過濾器」 - -### 4. 通用數據引擎 - -QuantDinger 提供跨多個市場的統一數據接口: - -- **加密貨幣**:直接 API 連接進行交易(10+ 交易所)和 CCXT 集成獲取行情數據(100+ 數據源) -- **股票**:Yahoo Finance、Finnhub、Tiingo(美股)和 AkShare(A股/港股) -- **期貨/外匯**:OANDA 和主要期貨數據源 -- **代理支持**:內置代理配置,適應受限網絡環境 - -### 5. 🧠 AI 記憶增強系統(Memory-Augmented Agents) -QuantDinger 的多智能體不是「每次從零開始」。後端內建**本地記憶庫 + 反思閉環**:在生成提示詞(prompt)時檢索過往經驗並注入到 system prompt,並可在事後驗證/復盤後把結果寫回記憶庫。 - -- **本質**:RAG 風格「經驗檢索增強」,**不是**訓練/微調模型權重(零外部向量庫依賴)。 -- **隱私**:所有記憶與反思記錄預設落盤在本地 SQLite:`backend_api_python/data/memory/`。 - -#### 邏輯圖(從請求到記憶閉環) - -```mermaid -flowchart TB - %% ===== 🌐 入口層 ===== - subgraph Entry["🌐 API 入口"] - A["📡 POST /api/analysis/multi"] - A2["🔄 POST /api/analysis/reflect"] - end - - %% ===== ⚙️ 服務編排層 ===== - subgraph Service["⚙️ 服務編排"] - B[AnalysisService] - C[AgentCoordinator] - D["📊 建構上下文
price · kline · news · indicators"] - end - - %% ===== 🤖 多智能體工作流 ===== - subgraph Agents["🤖 多智能體工作流"] - - subgraph P1["📈 Phase 1 · 多維分析(並行)"] - E1["🔍 MarketAnalyst
技術面分析"] - E2["📑 FundamentalAnalyst
基本面分析"] - E3["📰 NewsAnalyst
新聞輿情"] - E4["💭 SentimentAnalyst
市場情緒"] - E5["⚠️ RiskAnalyst
風險評估"] - end - - subgraph P2["🎯 Phase 2 · 多空博弈(並行)"] - F1["🐂 BullResearcher
看多論據"] - F2["🐻 BearResearcher
看空論據"] - end - - subgraph P3["💹 Phase 3 · 交易決策"] - G["🎰 TraderAgent
綜合研判 → BUY / SELL / HOLD"] - end - - end - - %% ===== 🧠 記憶層 ===== - subgraph Memory["🧠 本地記憶庫 SQLite(data/memory/)"] - M1[("market_analyst")] - M2[("fundamental")] - M3[("news_analyst")] - M4[("sentiment")] - M5[("risk_analyst")] - M6[("bull_researcher")] - M7[("bear_researcher")] - M8[("trader_agent")] - end - - %% ===== 🔄 反思閉環 ===== - subgraph Reflect["🔄 反思閉環(可選)"] - R[ReflectionService] - RR[("reflection_records.db")] - W["⏰ ReflectionWorker"] - end - - %% ===== 主流程 ===== - A --> B --> C --> D - D --> P1 --> P2 --> P3 - - %% ===== 記憶讀寫 ===== - E1 <-.-> M1 - E2 <-.-> M2 - E3 <-.-> M3 - E4 <-.-> M4 - E5 <-.-> M5 - F1 <-.-> M6 - F2 <-.-> M7 - G <-.-> M8 - - %% ===== 反思流程 ===== - C --> R --> RR - W --> RR - W -.->|"驗證 + 學習"| M8 - A2 -.->|"手動復盤"| M8 -``` - -#### 檢索排序(簡化) -\[ -score = w_{sim}\cdot sim + w_{recency}\cdot recency + w_{returns}\cdot returns\_score -\] - -#### 兩條「學習」通道 -- **手動復盤(推薦)**:`POST /api/analysis/reflect` 寫入真實交易結果(returns/result) -- **自動反思(可選)**:`ENABLE_REFLECTION_WORKER=true` 後,背景任務按 `REFLECTION_WORKER_INTERVAL_SEC` 驗證到期記錄並寫回記憶 - -#### 关键環境變量(`.env`) -- `ENABLE_AGENT_MEMORY`, `AGENT_MEMORY_TOP_K`, `AGENT_MEMORY_CANDIDATE_LIMIT` -- `AGENT_MEMORY_ENABLE_VECTOR`, `AGENT_MEMORY_EMBEDDING_DIM` -- `AGENT_MEMORY_HALF_LIFE_DAYS`, `AGENT_MEMORY_W_SIM`, `AGENT_MEMORY_W_RECENCY`, `AGENT_MEMORY_W_RETURNS` -- `ENABLE_REFLECTION_WORKER`, `REFLECTION_WORKER_INTERVAL_SEC` - -### 6. 策略運行時 - -- **基於線程的執行器**:獨立的線程池用於策略執行 -- **自動恢復**:系統重啟後恢復運行中的策略 -- **訂單隊列**:後台工作線程用於訂單執行 - -### 7. 多LLM提供商支援 - -QuantDinger 支援多個 AI 提供商,具備自動檢測功能: - -| 提供商 | 特點 | -|--------|------| -| **OpenRouter** | 多模型閘道(預設),100+ 模型 | -| **OpenAI** | GPT-4o, GPT-4o-mini | -| **Google Gemini** | Gemini 1.5 Flash/Pro | -| **DeepSeek** | DeepSeek Chat(高性價比) | -| **xAI Grok** | Grok Beta | - -只需在 `.env` 中配置您首選提供商的 API 金鑰,系統會自動檢測可用提供商。 - -### 8. 指標社區 -*分享、發現、交易指標。* - -- **發佈與分享**:與社區分享你的 Python 指標 -- **購買系統**:從其他使用者購買優質付費指標 -- **評分與評論**:對購買的指標進行評分和評論 -- **管理員審核**:品質控制的審核系統 - -### 9. 使用者管理與安全 - -- **多使用者支援**:基於 PostgreSQL 的使用者帳戶,支援基於角色的權限管理 -- **OAuth 登入**:Google 和 GitHub OAuth 整合 -- **郵箱驗證**:透過郵箱驗證碼進行註冊和密碼重設 -- **安全功能**:Cloudflare Turnstile 人機驗證、IP/帳戶速率限制 -- **示範模式**:用於公開示範的唯讀模式 - -### 10. 技術棧 - -- **後端**:Python (Flask) + PostgreSQL + Redis(可選) -- **前端**:Vue 2 + Ant Design Vue + KlineCharts/ECharts -- **部署**:Docker Compose -- **當前版本**:V2.1.1 ([更新日誌](docs/CHANGELOG.md)) - ---- - -## 🔌 支持的交易所和券商 - -QuantDinger 支持多種市場類型的執行方式: - -### 加密貨幣交易所(直接 API) - -| 交易所 | 市場 | -|:--------:|:---------| -| Binance | 現貨, 合約, 槓桿 | -| OKX | 現貨, 永續, 期權 | -| Bitget | 現貨, 合約, 跟單交易 | -| Bybit | 現貨, 線性合約 | -| Coinbase Exchange | 現貨 | -| Kraken | 現貨, 合約 | -| KuCoin | 現貨, 合約 | -| Gate.io | 現貨, 合約 | -| Bitfinex | 現貨, 衍生品 | -| Deepcoin | 現貨, 永續 | - -### 傳統券商 - -| 券商 | 市場 | 平台 | -|:------:|:--------|:---------| -| **盈透證券 (IBKR)** | 美股, 港股 | TWS / IB Gateway 🆕 | -| **MetaTrader 5 (MT5)** | 外匯 | MT5 終端 🆕 | - -### 行情數據(通過 CCXT) - -Bybit、Gate.io、Kraken、KuCoin、HTX 以及 100+ 其他交易所用於行情數據。 - - ---- - -### 多語言支持 - -QuantDinger 為全球用戶構建,提供全面的國際化支持: - -

- English - Simplified Chinese - Traditional Chinese - Japanese - Korean - German - French - Thai - Vietnamese - Arabic -

- -所有 UI 元素、錯誤信息和文檔均已完全翻譯。語言會根據瀏覽器設置自動檢測,也可以在應用中手動切換。 - ---- - -### 支持的市場 - -| 市場類型 | 數據源 | 交易 | -|-------------|--------------|---------| -| **加密貨幣** | Binance, OKX, Bitget, + 100 交易所 | ✅ 全面支持 | -| **美股** | Yahoo Finance, Finnhub, Tiingo | ✅ 通過盈透證券 🆕 | -| **港股** | AkShare, 東方財富 | ✅ 通過盈透證券 🆕 | -| **A股** | AkShare, 東方財富 | ⚡ 僅數據 | -| **外匯** | Finnhub, OANDA | ✅ 通過 MT5 🆕 | -| **期貨** | 交易所 API, AkShare | ⚡ 僅數據 | - ---- - -### 架構 (當前倉庫) - -```text -┌─────────────────────────────┐ -│ quantdinger_vue │ -│ (Vue 2 + Ant Design Vue) │ -└──────────────┬──────────────┘ - │ HTTP (/api/*) - ▼ -┌─────────────────────────────┐ -│ backend_api_python │ -│ (Flask + 策略運行時) │ -└──────────────┬──────────────┘ - │ - ├─ SQLite (quantdinger.db) - ├─ Redis (可選緩存) - └─ 數據提供商 / LLMs / 交易所 -``` - ---- - -### 倉庫目錄結構 - -```text -. -├─ backend_api_python/ # Flask API + AI + 回測 + 策略運行時 -│ ├─ app/ -│ ├─ env.example # 複製為 .env 進行本地配置 -│ ├─ requirements.txt -│ └─ run.py # 入口點 -└─ quantdinger_vue/ # Vue 2 UI (開發服務器代理 /api -> 後端) -``` - ---- - -## 快速開始 - -### 選項 1: Docker 部署 (推薦) - -運行 QuantDinger 最快的方式。 - -#### 1. 一鍵啟動 - -**Linux / macOS** -```bash -git clone https://github.com/brokermr810/QuantDinger.git && \ -cd QuantDinger && \ -cp backend_api_python/env.example backend_api_python/.env && \ -docker-compose up -d --build -``` - -**Windows (PowerShell)** -```powershell -git clone https://github.com/brokermr810/QuantDinger.git -cd QuantDinger -Copy-Item backend_api_python\env.example -Destination backend_api_python\.env -docker-compose up -d --build -``` - -#### 2. 訪問與配置 - -- **前端 UI**: http://localhost:8888 -- **默認賬號**: `quantdinger` / `123456` - -> **注意**:為了使用 AI 功能或生產環境安全,請編輯 `backend_api_python/.env`(添加 `OPENROUTER_API_KEY`,修改密碼),然後執行 `docker-compose restart backend` 重啟服務。 - -#### 3. 訪問應用 - -- **前端 UI**: http://localhost -- **後端 API**: http://localhost:5000 - -#### Docker 命令參考 - -```bash -# 查看運行狀態 -docker-compose ps - -# 查看日誌 -docker-compose logs -f - -# 停止服務 -docker-compose down - -# 停止並刪除卷 (警告:會刪除數據庫!) -docker-compose down -v -``` - -#### 數據持久化 - -以下數據掛載到主機,重啟容器後依然保留: - -```yaml -volumes: - - ./backend_api_python/logs:/app/logs # 日誌 - - ./backend_api_python/data:/app/data # 數據目錄(包含 quantdinger.db) - - ./backend_api_python/.env:/app/.env # 配置文件 -``` - ---- - -### 選項 2: 本地開發 - -**先決條件** -- 推薦 Python 3.10+ -- 推薦 Node.js 16+ - -#### 1. 啟動後端 (Flask API) - -```bash -cd backend_api_python -pip install -r requirements.txt -cp env.example .env # Windows: copy env.example .env -python run.py -``` - -後端將在 `http://localhost:5000` 上可用。 - -#### 2. 啟動前端 (Vue UI) - -```bash -cd quantdinger_vue -npm install -npm run serve -``` - -前端開發服務器運行在 `http://localhost:8000` 並將 `/api/*` 代理到 `http://localhost:5000`。 - ---- - -### 配置 (.env) - -使用 `backend_api_python/env.example` 作為模板。常用設置包括: - -- **認證**: `SECRET_KEY`, `ADMIN_USER`, `ADMIN_PASSWORD` -- **伺服器**: `PYTHON_API_HOST`, `PYTHON_API_PORT`, `PYTHON_API_DEBUG` -- **資料庫**: `DATABASE_URL` (PostgreSQL 連接字串) -- **AI / LLM**: `LLM_PROVIDER` (openrouter/openai/google/deepseek/grok), 各提供商 API 金鑰 -- **OAuth**: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET` -- **安全**: `TURNSTILE_SITE_KEY`, `TURNSTILE_SECRET_KEY`, `ENABLE_REGISTRATION` -- **網路搜尋**: `SEARCH_PROVIDER`, `SEARCH_GOOGLE_*`, `SEARCH_BING_API_KEY` -- **訂單執行**: `ORDER_MODE` (maker/market), `MAKER_WAIT_SEC`, `MAKER_OFFSET_BPS` -- **代理 (可選)**: `PROXY_PORT` 或 `PROXY_URL` -- **後台服務**: `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR` - ---- - -## 🤝 社區與支持 - -- **貢獻**: [貢獻指南](CONTRIBUTING.md) · [貢獻者](CONTRIBUTORS.md) -- **Telegram**: [QuantDinger 群組](https://t.me/quantdinger) -- **Discord**: [加入服務器](https://discord.gg/vwJ8zxFh9Q) -- **📺 視頻演示**: [專案介紹](https://youtu.be/HPTVpqL7knM) -- **YouTube**: [@quantdinger](https://youtube.com/@quantdinger) -- **Email**: [brokermr810@gmail.com](mailto:brokermr810@gmail.com) -- **GitHub Issues**: [報告 Bug / 功能請求](https://github.com/brokermr810/QuantDinger/issues) - ---- - -## 💼 商業授權與贊助(Commercial License & Sponsorship) - -QuantDinger 的代碼使用 **Apache License 2.0** 授權。但請注意:**Apache 2.0 不授予商標權**。QuantDinger 的名稱/Logo/品牌標識受商標與品牌政策約束(與代碼許可分離): - -- **版權/署名**:你必須保留必要的版權與許可聲明(例如倉庫中的 LICENSE/NOTICE 等,以及代碼中的署名資訊)。 -- **商標(名稱/Logo/品牌)**:你不得使用 QuantDinger 的名稱/Logo/品牌來暗示背書或誤導來源;若再發佈修改版,一般需要移除/替換 QuantDinger 品牌標識,除非獲得書面許可。 - -若你希望在再發佈版本中**保留/修改 QuantDinger 品牌展示**(包括 UI 品牌、Logo 使用等),請聯繫我們獲取 **商業授權**。 - -另見:`TRADEMARKS.md` - -### 商業授權可獲得 - -- **品牌/版權展示的商用授權**(以雙方約定為準) -- **運維支持**:部署、升級、故障處理與維護建議 -- **諮詢服務**:架構評審、性能調優、策略工作流諮詢 -- **贊助商權益**:成為專案贊助商,可按約定展示你的 Logo/廣告(README/官網/應用內等) - -### 聯繫方式 - -- **Telegram**: [QuantDinger Group](https://t.me/worldinbroker) -- **Email**: [brokermr810@gmail.com](mailto:brokermr810@gmail.com) - ---- - -### 💝 直接支持(捐贈) - -你的貢獻幫助我們維護和改進 QuantDinger。 - -**加密貨幣捐贈 (ERC-20 / BEP-20 / Polygon / Arbitrum)** - -``` -0x96fa4962181bea077f8c7240efe46afbe73641a7 -``` - -

- USDT - ETH -

- ---- - -### 🎓 支持夥伴 - -我們很榮幸獲得推動量化金融教育和研究的學術機構和組織的支持。 - -
- - - - -
- - 印第安納大學量化金融學會 - -

- 量化金融學會 (QFS)
- 印第安納大學布盧明頓分校
- 培養下一代量化金融專業人才 -
-
- -> 💡 **有興趣成為支持夥伴嗎?** 我們歡迎與大學、研究機構和組織合作。請透過 [brokermr810@gmail.com](mailto:brokermr810@gmail.com) 或 [Telegram](https://t.me/worldinbroker) 聯繫我們。 - ---- - -### 致謝 - -QuantDinger 站在這些偉大的開源項目肩膀之上:Flask, Pandas, CCXT, Vue.js, Ant Design Vue, KlineCharts 等。 - -感謝所有維護者和貢獻者! ❤️ - diff --git a/backend_api_python/Dockerfile b/backend_api_python/Dockerfile index 620094e..aba5ea0 100644 --- a/backend_api_python/Dockerfile +++ b/backend_api_python/Dockerfile @@ -1,21 +1,30 @@ # QuantDinger Backend API Dockerfile -FROM python:3.12-slim +FROM python:3.12-slim-bookworm # Set working directory WORKDIR /app -# Install system dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ - gcc \ - libffi-dev \ +# Use HTTPS mirror and install minimal runtime dependencies. +# Keep image lean to avoid long downloads of full compiler toolchains. +RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/*.sources 2>/dev/null || true \ + && apt-get update \ + && apt-get install -y --no-install-recommends --fix-missing \ curl \ + # Build deps (needed for some pyproject-based packages like ed25519-blake2b) + build-essential \ + python3-dev \ + libffi-dev \ + libssl-dev \ && rm -rf /var/lib/apt/lists/* # Copy dependency file COPY requirements.txt . # Install Python dependencies -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir --prefer-binary --timeout 120 -r requirements.txt \ + # Remove build deps to keep image small + && apt-get purge -y --auto-remove build-essential python3-dev libffi-dev libssl-dev \ + && rm -rf /var/lib/apt/lists/* # Copy application code COPY . . diff --git a/backend_api_python/README.md b/backend_api_python/README.md index 0495184..73eaac1 100644 --- a/backend_api_python/README.md +++ b/backend_api_python/README.md @@ -4,7 +4,7 @@ Flask-based backend for QuantDinger: market data, indicators, AI analysis, backt ## What you get -- **Multi-market data layer**: factory-based providers (crypto / US stocks / CN&HK stocks / futures, etc.) +- **Multi-market data layer**: factory-based providers (crypto / US stocks / forex / futures, etc.) - **Indicators + backtesting**: persisted runs/history in PostgreSQL - **AI multi-agent analysis**: optional web search + OpenRouter LLM integration - **Strategy runtime**: thread-based executor, with optional auto-restore on startup diff --git a/backend_api_python/app/data/market_symbols_seed.py b/backend_api_python/app/data/market_symbols_seed.py index a883c11..e89817b 100644 --- a/backend_api_python/app/data/market_symbols_seed.py +++ b/backend_api_python/app/data/market_symbols_seed.py @@ -28,7 +28,7 @@ def get_hot_symbols(market: str, limit: int = 10) -> List[Dict]: Get hot symbols for a market. Args: - market: Market name (e.g., 'Crypto', 'USStock', 'AShare') + market: Market name (e.g., 'Crypto', 'USStock', 'Forex') limit: Maximum number of results Returns: @@ -100,18 +100,12 @@ def search_symbols(market: str, keyword: str, limit: int = 20) -> List[Dict]: def _normalize_for_match(market: str, symbol: str) -> str: - """Normalize symbol for matching (padding digits for A-Share/H-Share).""" + """Normalize symbol for matching.""" m = (market or '').strip() s = (symbol or '').strip().upper() if not m or not s: return s - # A-Share codes are usually 6 digits - if m == 'AShare' and s.isdigit() and len(s) < 6: - s = s.zfill(6) - # H-Share codes are often 5 digits - if m == 'HShare' and s.isdigit() and len(s) < 5: - s = s.zfill(5) return s diff --git a/backend_api_python/app/data_sources/__init__.py b/backend_api_python/app/data_sources/__init__.py index 9ef889b..9e309ef 100644 --- a/backend_api_python/app/data_sources/__init__.py +++ b/backend_api_python/app/data_sources/__init__.py @@ -6,12 +6,10 @@ - 熔断器保护 (circuit_breaker) - 数据缓存 (cache_manager) - 防封禁策略 (rate_limiter) -- 多数据源自动切换 (data_manager) """ from app.data_sources.factory import DataSourceFactory from app.data_sources.circuit_breaker import ( CircuitBreaker, - get_ashare_circuit_breaker, get_realtime_circuit_breaker ) from app.data_sources.cache_manager import ( @@ -22,24 +20,16 @@ from app.data_sources.cache_manager import ( ) from app.data_sources.rate_limiter import ( RateLimiter, - get_eastmoney_limiter, - get_tencent_limiter, - get_akshare_limiter, get_random_user_agent, random_sleep, retry_with_backoff ) -from app.data_sources.data_manager import ( - AShareDataManager, - get_ashare_data_manager -) __all__ = [ # 工厂 'DataSourceFactory', # 熔断器 'CircuitBreaker', - 'get_ashare_circuit_breaker', 'get_realtime_circuit_breaker', # 缓存 'DataCache', @@ -48,14 +38,7 @@ __all__ = [ 'get_stock_info_cache', # 限流器 'RateLimiter', - 'get_eastmoney_limiter', - 'get_tencent_limiter', - 'get_akshare_limiter', 'get_random_user_agent', 'random_sleep', 'retry_with_backoff', - # 数据管理器 - 'AShareDataManager', - 'get_ashare_data_manager', ] - diff --git a/backend_api_python/app/data_sources/cache_manager.py b/backend_api_python/app/data_sources/cache_manager.py index fdb8878..2d5dfbf 100644 --- a/backend_api_python/app/data_sources/cache_manager.py +++ b/backend_api_python/app/data_sources/cache_manager.py @@ -178,11 +178,11 @@ class DataCache: # 全局缓存实例 # ============================================ -# A股实时行情缓存(20分钟TTL,全市场数据量大) -_ashare_realtime_cache = DataCache( - name="ashare_realtime", +# 实时行情缓存(20分钟TTL) +_realtime_cache = DataCache( + name="realtime", default_ttl=1200.0, # 20分钟 - max_size=6000 # 约5000+股票 + max_size=6000 ) # K线数据缓存(5分钟TTL,按需缓存) @@ -202,7 +202,7 @@ _stock_info_cache = DataCache( def get_realtime_cache() -> DataCache: """获取实时行情缓存""" - return _ashare_realtime_cache + return _realtime_cache def get_kline_cache() -> DataCache: diff --git a/backend_api_python/app/data_sources/circuit_breaker.py b/backend_api_python/app/data_sources/circuit_breaker.py index b0e3e1c..20f52bd 100644 --- a/backend_api_python/app/data_sources/circuit_breaker.py +++ b/backend_api_python/app/data_sources/circuit_breaker.py @@ -161,13 +161,6 @@ class CircuitBreaker: # 全局熔断器实例 # ============================================ -# A股数据源熔断器(标准策略) -_ashare_circuit_breaker = CircuitBreaker( - failure_threshold=3, # 连续失败3次熔断 - cooldown_seconds=300.0, # 冷却5分钟 - half_open_max_calls=1 -) - # 实时行情熔断器(更严格的策略) _realtime_circuit_breaker = CircuitBreaker( failure_threshold=2, # 连续失败2次熔断 @@ -176,11 +169,6 @@ _realtime_circuit_breaker = CircuitBreaker( ) -def get_ashare_circuit_breaker() -> CircuitBreaker: - """获取A股数据源熔断器""" - return _ashare_circuit_breaker - - def get_realtime_circuit_breaker() -> CircuitBreaker: """获取实时行情熔断器""" return _realtime_circuit_breaker diff --git a/backend_api_python/app/data_sources/cn_stock.py b/backend_api_python/app/data_sources/cn_stock.py deleted file mode 100644 index 8f3ae01..0000000 --- a/backend_api_python/app/data_sources/cn_stock.py +++ /dev/null @@ -1,877 +0,0 @@ -""" -CN/HK stock data source. -Supports A-Share and H-Share with multiple public sources. - -改进版本(参考 daily_stock_analysis 项目): -- 多数据源自动切换(按优先级) -- 熔断器保护 -- 数据缓存 -- 防封禁策略(随机休眠+UA轮换) - -Priority (AShare): Eastmoney > Tencent > Sina > Akshare > yfinance -Priority (HShare): Tencent > Eastmoney > yfinance > akshare -""" -import json -from typing import Dict, List, Any, Optional, Tuple -from datetime import datetime, timedelta -import requests - -import yfinance as yf - -from app.data_sources.base import BaseDataSource -from app.data_sources.us_stock import USStockDataSource -from app.data_sources.data_manager import get_ashare_data_manager, AShareDataManager -from app.data_sources.circuit_breaker import get_ashare_circuit_breaker, get_realtime_circuit_breaker -from app.data_sources.rate_limiter import get_request_headers, get_tencent_limiter, get_eastmoney_limiter -from app.utils.logger import get_logger -from app.utils.http import get_retry_session - -logger = get_logger(__name__) - -# Optional dependency: akshare -try: - import akshare as ak # type: ignore - HAS_AKSHARE = True - logger.debug("akshare is available") -except ImportError: - HAS_AKSHARE = False - # Keep it quiet to avoid noisy startup logs on Windows. - logger.debug("akshare is not installed; akshare-based features are disabled") - - -class TencentDataMixin: - """Tencent quote API mixin (mostly for H-Share and legacy fallback).""" - - # 腾讯 K 线周期映射(注意:腾讯分钟级接口不支持240分钟,4H需要特殊处理) - TENCENT_PERIOD_MAP = { - '1m': 1, - '5m': 5, - '15m': 15, - '30m': 30, - '1H': 60, - '1D': 'day', - '1W': 'week' - } - - def _fetch_tencent_kline( - self, - symbol_code: str, - timeframe: str, - limit: int - ) -> List[Dict[str, Any]]: - """ - 使用腾讯财经接口获取K线数据 - - Args: - symbol_code: 腾讯格式的代码 (sh600000, sz000001, hk00700) - timeframe: 时间周期 - limit: 数据条数 - """ - klines = [] - - # 4H 需要特殊处理:获取1H数据然后聚合 - if timeframe == '4H': - return self._fetch_and_aggregate_4h(symbol_code, limit) - - try: - period = self.TENCENT_PERIOD_MAP.get(timeframe) - if period is None: - logger.warning(f"Unsupported timeframe: {timeframe}") - return [] - - # 构建请求URL - if isinstance(period, int): - # 分钟级数据 - url = f"http://ifzq.gtimg.cn/appstock/app/kline/mkline?param={symbol_code},m{period},,{limit}" - else: - # 日线/周线数据 - url = f"http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={symbol_code},{period},,,{limit},qfq" - - # logger.info(f"腾讯财经请求: {symbol_code}, 周期: {timeframe}, URL: {url[:80]}...") - - session = get_retry_session() - response = session.get(url, timeout=10) - - if response.status_code != 200: - logger.warning(f"Tencent quote returned status: {response.status_code}") - return [] - - data = response.json() - - # 解析响应数据 - if data.get('code') == 0 and 'data' in data: - stock_data = data['data'].get(symbol_code) - if stock_data: - # 分钟级数据格式 - if isinstance(period, int): - candles = stock_data.get(f'm{period}', []) - else: - # 日线/周线数据格式 - candles = stock_data.get('qfqday', stock_data.get('day', [])) - - for candle in candles: - if len(candle) >= 5: - # 解析时间 - time_str = str(candle[0]) - try: - if len(time_str) == 12: # 分钟级: 202411301430 - dt = datetime.strptime(time_str, '%Y%m%d%H%M') - elif len(time_str) == 10: # 日线: 2024-11-30 - dt = datetime.strptime(time_str, '%Y-%m-%d') - else: - continue - - klines.append(self.format_kline( - timestamp=int(dt.timestamp()), - open_price=float(candle[1]), - high=float(candle[3]), - low=float(candle[4]), - close=float(candle[2]), - volume=float(candle[5]) if len(candle) > 5 else 0 - )) - except (ValueError, IndexError) as e: - logger.debug(f"Failed to parse kline candle: {candle}, error: {e}") - continue - - # logger.info(f"腾讯财经返回 {len(klines)} 条数据") - else: - logger.warning(f"Tencent quote returned unexpected data: code={data.get('code')}") - - except Exception as e: - logger.error(f"Tencent quote fetch failed: {e}") - import traceback - logger.error(traceback.format_exc()) - - return klines - - def _fetch_and_aggregate_4h(self, symbol_code: str, limit: int) -> List[Dict[str, Any]]: - """获取1H数据并聚合为4H""" - # 获取足够多的1H数据 - hour_klines = self._fetch_tencent_kline(symbol_code, '1H', limit * 4 + 10) - - if not hour_klines: - return [] - - # 按4小时聚合 - aggregated = [] - i = 0 - while i < len(hour_klines): - # 取4根K线 - batch = hour_klines[i:i+4] - if len(batch) < 4: - break - - aggregated.append(self.format_kline( - timestamp=batch[0]['time'], - open_price=batch[0]['open'], - high=max(k['high'] for k in batch), - low=min(k['low'] for k in batch), - close=batch[-1]['close'], - volume=sum(k['volume'] for k in batch) - )) - i += 4 - - # logger.info(f"聚合生成 {len(aggregated)} 条 4H 数据") - return aggregated[-limit:] if len(aggregated) > limit else aggregated - - -class AShareDataSource(BaseDataSource, TencentDataMixin): - """ - A-Share data source. - - 改进版本:使用 AShareDataManager 实现多数据源自动切换 - - 熔断器保护 - - 数据缓存 - - 防封禁策略 - """ - - name = "AShare" - - # akshare 时间周期映射 - AKSHARE_PERIOD_MAP = { - '1D': 'daily', - '1W': 'weekly' - } - - # 东方财富 K 线周期映射 - EM_PERIOD_MAP = { - '1m': '1', - '5m': '5', - '15m': '15', - '30m': '30', - '1H': '60', - '4H': '240', - '1D': '101', - '1W': '102', - } - - def __init__(self): - self.us_stock_source = USStockDataSource() - # 使用新的数据管理器 - self._data_manager = get_ashare_data_manager() - # 熔断器和限流器 - self._circuit_breaker = get_ashare_circuit_breaker() - self._em_limiter = get_eastmoney_limiter() - - def get_kline( - self, - symbol: str, - timeframe: str, - limit: int, - before_time: Optional[int] = None - ) -> List[Dict[str, Any]]: - """ - Fetch A-Share Kline data. - - 改进版本:使用数据管理器自动切换数据源 - """ - # 使用新的数据管理器获取数据(自动切换数据源) - klines, source = self._data_manager.get_kline( - symbol=symbol, - timeframe=timeframe, - limit=limit, - before_time=before_time - ) - - if klines: - self.log_result(symbol, klines, timeframe) - return klines - - # 如果数据管理器失败,使用传统方式作为最后备选 - logger.warning(f"[AShare] 数据管理器获取 {symbol} 失败,尝试传统方式") - - # 传统方式:直接调用东方财富 - klines = self._fetch_eastmoney_ashare_legacy(symbol, timeframe, limit) - if klines: - klines = self.filter_and_limit(klines, limit, before_time) - self.log_result(symbol, klines, timeframe) - return klines - - # Fallback: yfinance (daily/weekly) - if timeframe in ('1D', '1W'): - yahoo_symbol = self._to_yahoo_symbol(symbol) - if yahoo_symbol: - klines = self.us_stock_source.get_kline(yahoo_symbol, timeframe, limit, before_time) - if klines: - return klines - - logger.warning(f"AShare {symbol} data fetch failed") - return [] - - def _fetch_eastmoney_ashare_legacy( - self, - symbol: str, - timeframe: str, - limit: int - ) -> List[Dict[str, Any]]: - """ - 传统方式获取东方财富数据(兜底用) - 不使用新的熔断器和限流器,保持原有逻辑 - """ - return self._fetch_eastmoney_ashare(symbol, timeframe, limit) - - def _to_tencent_symbol(self, symbol: str) -> Optional[str]: - """转换为腾讯财经格式""" - if symbol.startswith('6'): - return f"sh{symbol}" - elif symbol.startswith('0') or symbol.startswith('3'): - return f"sz{symbol}" - elif symbol.startswith('4') or symbol.startswith('8'): - return f"bj{symbol}" # 北交所 - return None - - def _to_yahoo_symbol(self, symbol: str) -> Optional[str]: - """转换为 Yahoo Finance 格式""" - if symbol.startswith('6'): - return f"{symbol}.SS" - elif symbol.startswith('0') or symbol.startswith('3'): - return f"{symbol}.SZ" - elif symbol.startswith('4') or symbol.startswith('8'): - return f"{symbol}.BJ" - return None - - def _fetch_eastmoney_ashare( - self, - symbol: str, - timeframe: str, - limit: int - ) -> List[Dict[str, Any]]: - """使用东方财富获取A股数据""" - klines = [] - - period = self.EM_PERIOD_MAP.get(timeframe) - if not period: - logger.warning(f"Eastmoney unsupported timeframe: {timeframe}") - return [] - - try: - # 确定市场代码: 上海=1, 深圳=0, 北交所=0 - if symbol.startswith('6'): - secid = f"1.{symbol}" - else: - secid = f"0.{symbol}" - - # 东方财富K线接口 - url = f"https://push2his.eastmoney.com/api/qt/stock/kline/get" - params = { - 'secid': secid, - 'fields1': 'f1,f2,f3,f4,f5,f6', - 'fields2': 'f51,f52,f53,f54,f55,f56,f57', - 'klt': period, - 'fqt': '1', # 前复权 - 'end': '20500101', - 'lmt': limit, - } - - # logger.info(f"东方财富A股请求: {symbol}, 周期: {timeframe}") - - # 添加浏览器请求头 - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Referer': 'https://quote.eastmoney.com/', - 'Accept': 'application/json, text/plain, */*', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - } - - session = get_retry_session() - response = session.get(url, params=params, headers=headers, timeout=15) - - if response.status_code != 200: - logger.warning(f"Eastmoney HTTP status: {response.status_code}") - return [] - - data = response.json() - - # 解析响应 - if data.get('data') and data['data'].get('klines'): - for line in data['data']['klines']: - try: - parts = line.split(',') - if len(parts) >= 6: - time_str = parts[0] - if ' ' in time_str: - dt = datetime.strptime(time_str, '%Y-%m-%d %H:%M') - else: - dt = datetime.strptime(time_str, '%Y-%m-%d') - - klines.append(self.format_kline( - timestamp=int(dt.timestamp()), - open_price=float(parts[1]), - high=float(parts[3]), - low=float(parts[4]), - close=float(parts[2]), - volume=float(parts[5]) - )) - except (ValueError, IndexError) as e: - logger.debug(f"Failed to parse Eastmoney data line: {line}, error: {e}") - continue - - # logger.info(f"东方财富返回 {len(klines)} 条A股数据") - else: - logger.warning("Eastmoney returned no data") - - except Exception as e: - logger.error(f"Eastmoney A-share fetch failed: {e}") - import traceback - logger.error(traceback.format_exc()) - - return klines - - def _fetch_akshare( - self, - symbol: str, - timeframe: str, - limit: int, - before_time: Optional[int] - ) -> List[Dict[str, Any]]: - """使用 akshare 获取数据""" - klines = [] - - try: - period = self.AKSHARE_PERIOD_MAP.get(timeframe, 'daily') - - # 计算日期范围 - if before_time: - end_date = datetime.fromtimestamp(before_time).strftime('%Y%m%d') - else: - end_date = datetime.now().strftime('%Y%m%d') - - days = limit * 2 if timeframe == '1D' else limit * 10 - start_date = (datetime.now() - timedelta(days=days)).strftime('%Y%m%d') - - # logger.info(f"使用 akshare 获取A股: {symbol}, 周期: {period}") - - df = ak.stock_zh_a_hist( - symbol=symbol, - period=period, - start_date=start_date, - end_date=end_date, - adjust="qfq" # 前复权 - ) - - if df is not None and not df.empty: - df = df.tail(limit) - for _, row in df.iterrows(): - ts = int(datetime.strptime(str(row['日期']), '%Y-%m-%d').timestamp()) - klines.append(self.format_kline( - timestamp=ts, - open_price=row['开盘'], - high=row['最高'], - low=row['最低'], - close=row['收盘'], - volume=row['成交量'] - )) - # logger.info(f"akshare 返回 {len(klines)} 条A股数据") - - except Exception as e: - logger.error(f"Akshare A-share fetch failed: {e}") - import traceback - logger.error(traceback.format_exc()) - - return klines - - def get_ticker(self, symbol: str) -> Dict[str, Any]: - """ - 获取A股实时报价 - - 改进版本:使用数据管理器自动切换数据源 - - 熔断器保护 - - 数据缓存(60秒TTL) - - 多数据源自动切换 - - Returns: - dict: { - 'last': 当前价格, - 'change': 涨跌额, - 'changePercent': 涨跌幅, - 'high': 最高价, - 'low': 最低价, - 'open': 开盘价, - 'previousClose': 昨收价 - } - """ - symbol = (symbol or '').strip() - - # 使用数据管理器获取实时报价(自动切换数据源) - quote, source = self._data_manager.get_realtime_quote(symbol) - if quote and quote.get('last', 0) > 0: - return quote - - # 如果数据管理器失败,使用传统方式作为兜底 - logger.debug(f"[AShare] 数据管理器获取 {symbol} 实时报价失败,尝试传统方式") - return self._get_ticker_legacy(symbol) - - def _get_ticker_legacy(self, symbol: str) -> Dict[str, Any]: - """ - 传统方式获取实时报价(兜底用) - - 保持原有逻辑,不使用熔断器和限流器 - """ - # 优先使用东方财富实时行情 API - try: - # 判断市场 - if symbol.startswith('6'): - secid = f"1.{symbol}" # 上海 - elif symbol.startswith('0') or symbol.startswith('3'): - secid = f"0.{symbol}" # 深圳 - elif symbol.startswith('4') or symbol.startswith('8'): - secid = f"0.{symbol}" # 北交所 - else: - secid = f"1.{symbol}" - - # 东方财富实时行情接口 - url = "https://push2.eastmoney.com/api/qt/stock/get" - params = { - 'secid': secid, - 'fields': 'f43,f44,f45,f46,f47,f48,f57,f58,f60,f169,f170', - } - - session = get_retry_session() - response = session.get(url, params=params, timeout=10) - if response.status_code == 200: - data = response.json() - if data and data.get('data'): - d = data['data'] - last_price = d.get('f43', 0) - if last_price and last_price > 0: - divisor = 100 if last_price > 1000 else 1 - return { - 'last': last_price / divisor, - 'high': d.get('f44', 0) / divisor, - 'low': d.get('f45', 0) / divisor, - 'open': d.get('f46', 0) / divisor, - 'previousClose': d.get('f60', 0) / divisor, - 'change': d.get('f169', 0) / divisor, - 'changePercent': d.get('f170', 0) / 100 - } - except Exception as e: - logger.debug(f"Eastmoney ticker failed for {symbol}: {e}") - - # 降级使用腾讯实时报价 - try: - tencent_symbol = self._to_tencent_symbol(symbol) - if tencent_symbol: - url = f"http://qt.gtimg.cn/q={tencent_symbol}" - response = requests.get(url, timeout=10) - content = response.content.decode('gbk', errors='ignore') - if '="' in content: - data_str = content.split('="')[1].strip('";\n') - if data_str: - parts = data_str.split('~') - if len(parts) > 32: - return { - 'last': float(parts[3]) if parts[3] else 0, - 'change': float(parts[31]) if parts[31] else 0, - 'changePercent': float(parts[32]) if parts[32] else 0, - 'high': float(parts[33]) if len(parts) > 33 and parts[33] else 0, - 'low': float(parts[34]) if len(parts) > 34 and parts[34] else 0, - 'open': float(parts[5]) if len(parts) > 5 and parts[5] else 0, - 'previousClose': float(parts[4]) if parts[4] else 0 - } - except Exception as e: - logger.debug(f"Tencent ticker failed for {symbol}: {e}") - - return {'last': 0, 'symbol': symbol} - - -class HShareDataSource(BaseDataSource, TencentDataMixin): - """港股数据源""" - - name = "HShare" - - def __init__(self): - self.us_stock_source = USStockDataSource() - - def get_kline( - self, - symbol: str, - timeframe: str, - limit: int, - before_time: Optional[int] = None - ) -> List[Dict[str, Any]]: - """获取港股K线数据""" - klines = [] - - # 方案1: 腾讯财经 (港股日线/周线首选,稳定可靠) - if timeframe in ('1D', '1W'): - tencent_symbol = self._to_tencent_symbol(symbol) - if tencent_symbol: - # logger.info(f"尝试使用腾讯财经获取港股: {tencent_symbol}") - klines = self._fetch_tencent_kline(tencent_symbol, timeframe, limit) - if klines: - klines = self.filter_and_limit(klines, limit, before_time) - self.log_result(symbol, klines, timeframe) - return klines - - # 方案2: 东方财富 (支持所有周期,但可能有地域限制) - klines = self._fetch_eastmoney_kline(symbol, timeframe, limit) - if klines: - klines = self.filter_and_limit(klines, limit, before_time) - self.log_result(symbol, klines, timeframe) - return klines - - # 方案3: 尝试 yfinance (日线级别备选) - if timeframe in ('1D', '1W'): - yahoo_symbol = self._to_yahoo_symbol(symbol) - if yahoo_symbol: - # logger.info(f"尝试使用 yfinance 获取港股: {yahoo_symbol}") - klines = self.us_stock_source.get_kline(yahoo_symbol, timeframe, limit, before_time) - if klines: - # logger.info(f"yfinance 成功获取 {len(klines)} 条港股数据") - return klines - - # 方案4: 尝试 akshare (日线级别) - if HAS_AKSHARE and timeframe in ('1D', '1W'): - klines = self._fetch_akshare(symbol, timeframe, limit, before_time) - if klines: - return klines - - # 分钟级数据获取失败提示 - if timeframe not in ('1D', '1W'): - logger.warning(f"HK stock {symbol}: minute-level data is not supported (data source limitations)") - else: - logger.warning(f"HK stock {symbol}: data fetch failed (timeframe: {timeframe})") - return klines - - def _to_tencent_symbol(self, symbol: str) -> str: - """转换为腾讯财经格式""" - # 港股代码补齐到5位 - padded = symbol.zfill(5) - return f"hk{padded}" - - def _to_yahoo_symbol(self, symbol: str) -> str: - """转换为 Yahoo Finance 格式""" - # 港股代码补齐到4位 - padded = symbol.zfill(4) - return f"{padded}.HK" - - def _fetch_eastmoney_kline( - self, - symbol: str, - timeframe: str, - limit: int - ) -> List[Dict[str, Any]]: - """使用东方财富获取港股分钟级数据""" - klines = [] - - # 东方财富 K 线周期映射 - em_period_map = { - '1m': '1', - '5m': '5', - '15m': '15', - '30m': '30', - '1H': '60', - '4H': '240', - '1D': '101', - '1W': '102', - } - - period = em_period_map.get(timeframe) - if not period: - logger.warning(f"Eastmoney unsupported timeframe: {timeframe}") - return [] - - try: - # 港股代码补齐到5位 - hk_symbol = symbol.zfill(5) - # 东方财富港股代码格式: 116.00700 (116是港股市场代码) - secid = f"116.{hk_symbol}" - - # 东方财富K线接口 - url = f"https://push2his.eastmoney.com/api/qt/stock/kline/get" - params = { - 'secid': secid, - 'fields1': 'f1,f2,f3,f4,f5,f6', - 'fields2': 'f51,f52,f53,f54,f55,f56,f57', - 'klt': period, # K线类型 - 'fqt': '1', # 前复权 - 'end': '20500101', - 'lmt': limit, - } - - # logger.info(f"东方财富港股请求: {hk_symbol}, 周期: {timeframe}") - - # 添加浏览器请求头,避免被拒绝 - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Referer': 'https://quote.eastmoney.com/', - 'Accept': 'application/json, text/plain, */*', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - } - - session = get_retry_session() - response = session.get(url, params=params, headers=headers, timeout=15) - - if response.status_code != 200: - logger.warning(f"Eastmoney HTTP status: {response.status_code}") - return [] - - data = response.json() - - # 解析响应 - if data.get('data') and data['data'].get('klines'): - for line in data['data']['klines']: - try: - # 格式: "2025-11-28 15:00,400.0,401.0,399.0,400.5,1000,100000" - # 日期,开盘,收盘,最高,最低,成交量,成交额 - parts = line.split(',') - if len(parts) >= 6: - time_str = parts[0] - # 解析时间 - if ' ' in time_str: - dt = datetime.strptime(time_str, '%Y-%m-%d %H:%M') - else: - dt = datetime.strptime(time_str, '%Y-%m-%d') - - klines.append(self.format_kline( - timestamp=int(dt.timestamp()), - open_price=float(parts[1]), - high=float(parts[3]), - low=float(parts[4]), - close=float(parts[2]), - volume=float(parts[5]) - )) - except (ValueError, IndexError) as e: - logger.debug(f"Failed to parse Eastmoney data line: {line}, error: {e}") - continue - - # logger.info(f"东方财富返回 {len(klines)} 条港股数据") - else: - logger.warning("Eastmoney returned no data") - - except Exception as e: - logger.error(f"Eastmoney HK stock fetch failed: {e}") - import traceback - logger.error(traceback.format_exc()) - - return klines - - def _fetch_akshare( - self, - symbol: str, - timeframe: str, - limit: int, - before_time: Optional[int] - ) -> List[Dict[str, Any]]: - """使用 akshare 获取港股数据""" - klines = [] - - try: - # 计算日期范围 - if before_time: - end_date = datetime.fromtimestamp(before_time).strftime('%Y%m%d') - else: - end_date = datetime.now().strftime('%Y%m%d') - - days = limit * 2 if timeframe == '1D' else limit * 10 - start_date = (datetime.now() - timedelta(days=days)).strftime('%Y%m%d') - - # 港股代码补齐到5位 - hk_symbol = symbol.zfill(5) - - # logger.info(f"使用 akshare 获取港股: {hk_symbol}") - - df = ak.stock_hk_hist( - symbol=hk_symbol, - period="daily", - start_date=start_date, - end_date=end_date, - adjust="qfq" - ) - - if df is not None and not df.empty: - df = df.tail(limit) - for _, row in df.iterrows(): - ts = int(datetime.strptime(str(row['日期']), '%Y-%m-%d').timestamp()) - klines.append(self.format_kline( - timestamp=ts, - open_price=row['开盘'], - high=row['最高'], - low=row['最低'], - close=row['收盘'], - volume=row['成交量'] - )) - # logger.info(f"akshare 返回 {len(klines)} 条港股数据") - - except Exception as e: - logger.error(f"Akshare HK stock fetch failed: {e}") - import traceback - logger.error(traceback.format_exc()) - - return klines - - def get_ticker(self, symbol: str) -> Dict[str, Any]: - """ - 获取港股实时报价 - - 使用腾讯财经实时行情API获取实时报价 - - Returns: - dict: { - 'last': 当前价格, - 'change': 涨跌额, - 'changePercent': 涨跌幅, - 'high': 最高价, - 'low': 最低价, - 'open': 开盘价, - 'previousClose': 昨收价 - } - """ - symbol = (symbol or '').strip() - - # 使用腾讯财经实时报价 - try: - tencent_symbol = self._to_tencent_symbol(symbol) - url = f"http://qt.gtimg.cn/q={tencent_symbol}" - response = requests.get(url, timeout=10) - content = response.content.decode('gbk', errors='ignore') - if '="' in content: - data_str = content.split('="')[1].strip('";\n') - if data_str: - parts = data_str.split('~') - if len(parts) > 32: - return { - 'last': float(parts[3]) if parts[3] else 0, - 'change': float(parts[31]) if parts[31] else 0, - 'changePercent': float(parts[32]) if parts[32] else 0, - 'high': float(parts[33]) if len(parts) > 33 and parts[33] else 0, - 'low': float(parts[34]) if len(parts) > 34 and parts[34] else 0, - 'open': float(parts[5]) if len(parts) > 5 and parts[5] else 0, - 'previousClose': float(parts[4]) if parts[4] else 0 - } - except Exception as e: - logger.debug(f"Tencent ticker failed for {symbol}: {e}") - - # 降级使用东方财富 - try: - hk_symbol = symbol.zfill(5) - secid = f"116.{hk_symbol}" - - url = "https://push2.eastmoney.com/api/qt/stock/get" - params = { - 'secid': secid, - 'fields': 'f43,f44,f45,f46,f47,f48,f57,f58,f60,f169,f170', - } - - session = get_retry_session() - response = session.get(url, params=params, timeout=10) - if response.status_code == 200: - data = response.json() - if data and data.get('data'): - d = data['data'] - last_price = d.get('f43', 0) - if last_price and last_price > 0: - divisor = 1000 if last_price > 10000 else 100 if last_price > 1000 else 1 - return { - 'last': last_price / divisor, - 'high': d.get('f44', 0) / divisor, - 'low': d.get('f45', 0) / divisor, - 'open': d.get('f46', 0) / divisor, - 'previousClose': d.get('f60', 0) / divisor, - 'change': d.get('f169', 0) / divisor, - 'changePercent': d.get('f170', 0) / 100 - } - except Exception as e: - logger.debug(f"Eastmoney ticker failed for {symbol}: {e}") - - # 第三备选: yfinance - try: - import yfinance as yf - # 港股在 yfinance 中的格式是 XXXX.HK - yf_symbol = f"{symbol.zfill(4)}.HK" - ticker = yf.Ticker(yf_symbol) - - # Try fast_info first (faster) - try: - info = ticker.fast_info - if hasattr(info, 'last_price') and info.last_price and info.last_price > 0: - return { - 'last': float(info.last_price), - 'change': float(info.last_price - info.previous_close) if hasattr(info, 'previous_close') and info.previous_close else 0, - 'changePercent': float((info.last_price - info.previous_close) / info.previous_close * 100) if hasattr(info, 'previous_close') and info.previous_close else 0, - 'high': float(info.day_high) if hasattr(info, 'day_high') and info.day_high else float(info.last_price), - 'low': float(info.day_low) if hasattr(info, 'day_low') and info.day_low else float(info.last_price), - 'open': float(info.open) if hasattr(info, 'open') and info.open else float(info.last_price), - 'previousClose': float(info.previous_close) if hasattr(info, 'previous_close') and info.previous_close else 0 - } - except Exception: - pass - - # Fallback to history - hist = ticker.history(period="2d") - if hist is not None and not hist.empty: - current = float(hist['Close'].iloc[-1]) - prev_close = float(hist['Close'].iloc[-2]) if len(hist) > 1 else current - return { - 'last': current, - 'change': current - prev_close, - 'changePercent': (current - prev_close) / prev_close * 100 if prev_close else 0, - 'high': float(hist['High'].iloc[-1]), - 'low': float(hist['Low'].iloc[-1]), - 'open': float(hist['Open'].iloc[-1]), - 'previousClose': prev_close - } - except Exception as e: - logger.debug(f"yfinance ticker failed for {symbol}: {e}") - - return {'last': 0, 'symbol': symbol} diff --git a/backend_api_python/app/data_sources/data_manager.py b/backend_api_python/app/data_sources/data_manager.py deleted file mode 100644 index 20a2b5e..0000000 --- a/backend_api_python/app/data_sources/data_manager.py +++ /dev/null @@ -1,629 +0,0 @@ -# -*- coding: utf-8 -*- -""" -=================================== -A股数据源管理器 (Data Manager) -=================================== - -参考 daily_stock_analysis 项目的 DataFetcherManager 实现 -统一管理多个A股数据源,实现自动故障切换 - -数据源优先级: -1. 东方财富 (Eastmoney) - 数据最全,首选 -2. 腾讯财经 (Tencent) - 稳定可靠 -3. 新浪财经 (Sina) - 轻量级 -4. Akshare - 功能丰富,但容易被封 -5. yfinance - 国际数据源,兜底 -""" - -import logging -from typing import Dict, List, Any, Optional, Tuple -from datetime import datetime -import requests - -from app.data_sources.circuit_breaker import ( - CircuitBreaker, - get_ashare_circuit_breaker, - get_realtime_circuit_breaker -) -from app.data_sources.cache_manager import ( - DataCache, - get_realtime_cache, - get_kline_cache, - generate_kline_cache_key -) -from app.data_sources.rate_limiter import ( - RateLimiter, - get_eastmoney_limiter, - get_tencent_limiter, - get_akshare_limiter, - get_request_headers, - retry_with_backoff -) -from app.utils.logger import get_logger -from app.utils.http import get_retry_session - -logger = get_logger(__name__) - - -# ============================================ -# 数据源常量 -# ============================================ - -class DataSource: - """数据源标识""" - EASTMONEY = "eastmoney" - TENCENT = "tencent" - SINA = "sina" - AKSHARE = "akshare" - YFINANCE = "yfinance" - - -# 数据源优先级(数字越小优先级越高) -DATA_SOURCE_PRIORITY = { - DataSource.EASTMONEY: 0, - DataSource.TENCENT: 1, - DataSource.SINA: 2, - DataSource.AKSHARE: 3, - DataSource.YFINANCE: 4, -} - - -# ============================================ -# A股数据管理器 -# ============================================ - -class AShareDataManager: - """ - A股数据源管理器 - - 功能: - 1. 多数据源自动切换(按优先级) - 2. 熔断器保护 - 3. 数据缓存 - 4. 防封禁策略 - """ - - # 东方财富 K 线周期映射 - EM_PERIOD_MAP = { - '1m': '1', - '5m': '5', - '15m': '15', - '30m': '30', - '1H': '60', - '4H': '240', - '1D': '101', - '1W': '102', - } - - def __init__(self): - # 熔断器 - self._circuit_breaker = get_ashare_circuit_breaker() - self._realtime_cb = get_realtime_circuit_breaker() - - # 缓存 - self._realtime_cache = get_realtime_cache() - self._kline_cache = get_kline_cache() - - # 限流器 - self._em_limiter = get_eastmoney_limiter() - self._tencent_limiter = get_tencent_limiter() - self._akshare_limiter = get_akshare_limiter() - - # Akshare 可用性检查 - self._has_akshare = self._check_akshare() - - def _check_akshare(self) -> bool: - """检查 akshare 是否可用""" - try: - import akshare - return True - except ImportError: - logger.debug("akshare 未安装,相关功能已禁用") - return False - - def get_kline( - self, - symbol: str, - timeframe: str, - limit: int, - before_time: Optional[int] = None, - use_cache: bool = True - ) -> Tuple[List[Dict[str, Any]], str]: - """ - 获取K线数据(自动切换数据源) - - Args: - symbol: 股票代码 - timeframe: 时间周期 - limit: 数据条数 - before_time: 获取此时间之前的数据 - use_cache: 是否使用缓存 - - Returns: - (K线数据列表, 成功的数据源名称) - """ - # 检查缓存 - if use_cache: - cache_key = generate_kline_cache_key(symbol, timeframe, limit, before_time) - cached = self._kline_cache.get(cache_key) - if cached: - logger.debug(f"[缓存命中] K线数据 {symbol}:{timeframe}") - return cached, "cache" - - errors = [] - - # 按优先级尝试各个数据源 - sources = [ - (DataSource.EASTMONEY, self._fetch_eastmoney_kline), - (DataSource.TENCENT, self._fetch_tencent_kline), - (DataSource.AKSHARE, self._fetch_akshare_kline), - (DataSource.YFINANCE, self._fetch_yfinance_kline), - ] - - for source_name, fetch_func in sources: - # 检查熔断器 - if not self._circuit_breaker.is_available(source_name): - logger.debug(f"[熔断] {source_name} 处于熔断状态,跳过") - continue - - # 跳过不可用的 akshare - if source_name == DataSource.AKSHARE and not self._has_akshare: - continue - - try: - logger.debug(f"[数据源] 尝试 {source_name} 获取 {symbol}") - klines = fetch_func(symbol, timeframe, limit, before_time) - - if klines: - self._circuit_breaker.record_success(source_name) - - # 更新缓存 - if use_cache: - cache_key = generate_kline_cache_key(symbol, timeframe, limit, before_time) - self._kline_cache.set(cache_key, klines) - - logger.info(f"[数据源] {source_name} 成功获取 {symbol} {len(klines)} 条数据") - return klines, source_name - - except Exception as e: - error_msg = f"{source_name}: {str(e)}" - errors.append(error_msg) - self._circuit_breaker.record_failure(source_name, str(e)) - logger.warning(f"[数据源] {error_msg}") - - # 所有数据源都失败 - logger.error(f"[数据源] 所有数据源获取 {symbol} 失败: {errors}") - return [], "" - - def _fetch_eastmoney_kline( - self, - symbol: str, - timeframe: str, - limit: int, - before_time: Optional[int] = None - ) -> List[Dict[str, Any]]: - """使用东方财富获取K线数据""" - klines = [] - - period = self.EM_PERIOD_MAP.get(timeframe) - if not period: - raise ValueError(f"Eastmoney 不支持时间周期: {timeframe}") - - # 限流 - self._em_limiter.wait() - - # 确定市场代码 - if symbol.startswith('6'): - secid = f"1.{symbol}" # 上海 - else: - secid = f"0.{symbol}" # 深圳 - - url = "https://push2his.eastmoney.com/api/qt/stock/kline/get" - params = { - 'secid': secid, - 'fields1': 'f1,f2,f3,f4,f5,f6', - 'fields2': 'f51,f52,f53,f54,f55,f56,f57', - 'klt': period, - 'fqt': '1', # 前复权 - 'end': '20500101', - 'lmt': limit, - } - - headers = get_request_headers(referer='https://quote.eastmoney.com/') - - session = get_retry_session() - response = session.get(url, params=params, headers=headers, timeout=15) - - if response.status_code != 200: - raise ConnectionError(f"HTTP {response.status_code}") - - data = response.json() - - if data.get('data') and data['data'].get('klines'): - for line in data['data']['klines']: - try: - parts = line.split(',') - if len(parts) >= 6: - time_str = parts[0] - if ' ' in time_str: - dt = datetime.strptime(time_str, '%Y-%m-%d %H:%M') - else: - dt = datetime.strptime(time_str, '%Y-%m-%d') - - klines.append({ - 'time': int(dt.timestamp()), - 'open': round(float(parts[1]), 4), - 'high': round(float(parts[3]), 4), - 'low': round(float(parts[4]), 4), - 'close': round(float(parts[2]), 4), - 'volume': round(float(parts[5]), 2) - }) - except (ValueError, IndexError): - continue - - # 过滤和排序 - klines.sort(key=lambda x: x['time']) - if before_time: - klines = [k for k in klines if k['time'] < before_time] - if len(klines) > limit: - klines = klines[-limit:] - - return klines - - def _fetch_tencent_kline( - self, - symbol: str, - timeframe: str, - limit: int, - before_time: Optional[int] = None - ) -> List[Dict[str, Any]]: - """使用腾讯财经获取K线数据""" - # 腾讯周期映射 - period_map = { - '1m': 1, '5m': 5, '15m': 15, '30m': 30, - '1H': 60, '1D': 'day', '1W': 'week' - } - - period = period_map.get(timeframe) - if period is None: - raise ValueError(f"腾讯财经不支持时间周期: {timeframe}") - - # 转换代码格式 - if symbol.startswith('6'): - tencent_symbol = f"sh{symbol}" - elif symbol.startswith('0') or symbol.startswith('3'): - tencent_symbol = f"sz{symbol}" - else: - tencent_symbol = f"bj{symbol}" - - # 限流 - self._tencent_limiter.wait() - - # 构建URL - if isinstance(period, int): - url = f"http://ifzq.gtimg.cn/appstock/app/kline/mkline?param={tencent_symbol},m{period},,{limit}" - else: - url = f"http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param={tencent_symbol},{period},,,{limit},qfq" - - response = requests.get(url, timeout=10) - - if response.status_code != 200: - raise ConnectionError(f"HTTP {response.status_code}") - - data = response.json() - klines = [] - - if data.get('code') == 0 and 'data' in data: - stock_data = data['data'].get(tencent_symbol) - if stock_data: - if isinstance(period, int): - candles = stock_data.get(f'm{period}', []) - else: - candles = stock_data.get('qfqday', stock_data.get('day', [])) - - for candle in candles: - if len(candle) >= 5: - try: - time_str = str(candle[0]) - if len(time_str) == 12: - dt = datetime.strptime(time_str, '%Y%m%d%H%M') - elif len(time_str) == 10: - dt = datetime.strptime(time_str, '%Y-%m-%d') - else: - continue - - klines.append({ - 'time': int(dt.timestamp()), - 'open': round(float(candle[1]), 4), - 'high': round(float(candle[3]), 4), - 'low': round(float(candle[4]), 4), - 'close': round(float(candle[2]), 4), - 'volume': round(float(candle[5]), 2) if len(candle) > 5 else 0 - }) - except (ValueError, IndexError): - continue - - # 过滤和排序 - klines.sort(key=lambda x: x['time']) - if before_time: - klines = [k for k in klines if k['time'] < before_time] - if len(klines) > limit: - klines = klines[-limit:] - - return klines - - def _fetch_akshare_kline( - self, - symbol: str, - timeframe: str, - limit: int, - before_time: Optional[int] = None - ) -> List[Dict[str, Any]]: - """使用 Akshare 获取K线数据""" - if not self._has_akshare: - raise RuntimeError("akshare 未安装") - - import akshare as ak - from datetime import timedelta - - # Akshare 只支持日线/周线 - period_map = {'1D': 'daily', '1W': 'weekly'} - period = period_map.get(timeframe) - if not period: - raise ValueError(f"Akshare 不支持时间周期: {timeframe}") - - # 限流 - self._akshare_limiter.wait() - - # 计算日期范围 - if before_time: - end_date = datetime.fromtimestamp(before_time).strftime('%Y%m%d') - else: - end_date = datetime.now().strftime('%Y%m%d') - - days = limit * 2 if timeframe == '1D' else limit * 10 - start_date = (datetime.now() - timedelta(days=days)).strftime('%Y%m%d') - - df = ak.stock_zh_a_hist( - symbol=symbol, - period=period, - start_date=start_date, - end_date=end_date, - adjust="qfq" - ) - - klines = [] - if df is not None and not df.empty: - df = df.tail(limit) - for _, row in df.iterrows(): - ts = int(datetime.strptime(str(row['日期']), '%Y-%m-%d').timestamp()) - klines.append({ - 'time': ts, - 'open': round(float(row['开盘']), 4), - 'high': round(float(row['最高']), 4), - 'low': round(float(row['最低']), 4), - 'close': round(float(row['收盘']), 4), - 'volume': round(float(row['成交量']), 2) - }) - - return klines - - def _fetch_yfinance_kline( - self, - symbol: str, - timeframe: str, - limit: int, - before_time: Optional[int] = None - ) -> List[Dict[str, Any]]: - """使用 yfinance 获取K线数据(兜底)""" - import yfinance as yf - - # 转换为 Yahoo 格式 - if symbol.startswith('6'): - yahoo_symbol = f"{symbol}.SS" - elif symbol.startswith('0') or symbol.startswith('3'): - yahoo_symbol = f"{symbol}.SZ" - else: - yahoo_symbol = f"{symbol}.SS" - - # 周期映射 - period_map = { - '1D': ('1d', f'{limit}d'), - '1W': ('1wk', f'{limit * 7}d'), - '1H': ('1h', f'{limit}d'), - } - - interval, period = period_map.get(timeframe, ('1d', f'{limit}d')) - - ticker = yf.Ticker(yahoo_symbol) - df = ticker.history(period=period, interval=interval) - - klines = [] - if df is not None and not df.empty: - df = df.tail(limit) - for idx, row in df.iterrows(): - ts = int(idx.timestamp()) - klines.append({ - 'time': ts, - 'open': round(float(row['Open']), 4), - 'high': round(float(row['High']), 4), - 'low': round(float(row['Low']), 4), - 'close': round(float(row['Close']), 4), - 'volume': round(float(row['Volume']), 2) - }) - - return klines - - def get_realtime_quote( - self, - symbol: str, - use_cache: bool = True - ) -> Tuple[Dict[str, Any], str]: - """ - 获取实时报价(自动切换数据源) - - Args: - symbol: 股票代码 - use_cache: 是否使用缓存 - - Returns: - (报价数据字典, 成功的数据源名称) - """ - # 检查缓存 - if use_cache: - cached = self._realtime_cache.get(f"quote:{symbol}") - if cached: - return cached, "cache" - - errors = [] - - # 按优先级尝试各个数据源 - sources = [ - (DataSource.EASTMONEY, self._fetch_eastmoney_quote), - (DataSource.TENCENT, self._fetch_tencent_quote), - (DataSource.AKSHARE, self._fetch_akshare_quote), - ] - - for source_name, fetch_func in sources: - if not self._realtime_cb.is_available(source_name): - continue - - if source_name == DataSource.AKSHARE and not self._has_akshare: - continue - - try: - quote = fetch_func(symbol) - if quote and quote.get('last', 0) > 0: - self._realtime_cb.record_success(source_name) - - # 更新缓存 - if use_cache: - self._realtime_cache.set(f"quote:{symbol}", quote, ttl=60.0) - - return quote, source_name - - except Exception as e: - errors.append(f"{source_name}: {str(e)}") - self._realtime_cb.record_failure(source_name, str(e)) - - logger.warning(f"[实时报价] 所有数据源获取 {symbol} 失败") - return {'last': 0, 'symbol': symbol}, "" - - def _fetch_eastmoney_quote(self, symbol: str) -> Dict[str, Any]: - """使用东方财富获取实时报价""" - if symbol.startswith('6'): - secid = f"1.{symbol}" - else: - secid = f"0.{symbol}" - - url = "https://push2.eastmoney.com/api/qt/stock/get" - params = { - 'secid': secid, - 'fields': 'f43,f44,f45,f46,f47,f48,f57,f58,f60,f169,f170', - } - - self._em_limiter.wait() - - session = get_retry_session() - response = session.get(url, params=params, headers=get_request_headers(), timeout=10) - - if response.status_code == 200: - data = response.json() - if data and data.get('data'): - d = data['data'] - last_price = d.get('f43', 0) - if last_price and last_price > 0: - divisor = 100 if last_price > 1000 else 1 - return { - 'last': last_price / divisor, - 'high': d.get('f44', 0) / divisor, - 'low': d.get('f45', 0) / divisor, - 'open': d.get('f46', 0) / divisor, - 'previousClose': d.get('f60', 0) / divisor, - 'change': d.get('f169', 0) / divisor, - 'changePercent': d.get('f170', 0) / 100 - } - - return {} - - def _fetch_tencent_quote(self, symbol: str) -> Dict[str, Any]: - """使用腾讯财经获取实时报价""" - if symbol.startswith('6'): - tencent_symbol = f"sh{symbol}" - else: - tencent_symbol = f"sz{symbol}" - - url = f"http://qt.gtimg.cn/q={tencent_symbol}" - - self._tencent_limiter.wait() - - response = requests.get(url, timeout=10) - content = response.content.decode('gbk', errors='ignore') - - if '="' in content: - data_str = content.split('="')[1].strip('";\n') - if data_str: - parts = data_str.split('~') - if len(parts) > 32: - return { - 'last': float(parts[3]) if parts[3] else 0, - 'change': float(parts[31]) if parts[31] else 0, - 'changePercent': float(parts[32]) if parts[32] else 0, - 'high': float(parts[33]) if len(parts) > 33 and parts[33] else 0, - 'low': float(parts[34]) if len(parts) > 34 and parts[34] else 0, - 'open': float(parts[5]) if len(parts) > 5 and parts[5] else 0, - 'previousClose': float(parts[4]) if parts[4] else 0 - } - - return {} - - def _fetch_akshare_quote(self, symbol: str) -> Dict[str, Any]: - """使用 Akshare 获取实时报价""" - if not self._has_akshare: - return {} - - import akshare as ak - - self._akshare_limiter.wait() - - df = ak.stock_zh_a_spot_em() - if df is not None and not df.empty: - row = df[df['代码'] == symbol] - if not row.empty: - row = row.iloc[0] - return { - 'last': float(row.get('最新价', 0) or 0), - 'change': float(row.get('涨跌额', 0) or 0), - 'changePercent': float(row.get('涨跌幅', 0) or 0), - 'high': float(row.get('最高', 0) or 0), - 'low': float(row.get('最低', 0) or 0), - 'open': float(row.get('今开', 0) or 0), - 'previousClose': float(row.get('昨收', 0) or 0) - } - - return {} - - def get_status(self) -> Dict[str, Any]: - """获取数据管理器状态""" - return { - 'circuit_breaker': self._circuit_breaker.get_status(), - 'realtime_circuit_breaker': self._realtime_cb.get_status(), - 'realtime_cache_stats': self._realtime_cache.stats(), - 'kline_cache_stats': self._kline_cache.stats(), - 'has_akshare': self._has_akshare, - } - - -# ============================================ -# 全局实例 -# ============================================ - -_ashare_data_manager: Optional[AShareDataManager] = None - - -def get_ashare_data_manager() -> AShareDataManager: - """获取A股数据管理器单例""" - global _ashare_data_manager - if _ashare_data_manager is None: - _ashare_data_manager = AShareDataManager() - return _ashare_data_manager diff --git a/backend_api_python/app/data_sources/factory.py b/backend_api_python/app/data_sources/factory.py index 0f70e4a..826bf4d 100644 --- a/backend_api_python/app/data_sources/factory.py +++ b/backend_api_python/app/data_sources/factory.py @@ -21,7 +21,7 @@ class DataSourceFactory: 获取指定市场的数据源 Args: - market: 市场类型 (Crypto, USStock, AShare, HShare) + market: 市场类型 (Crypto, USStock, Forex, Futures) Returns: 数据源实例 @@ -55,12 +55,6 @@ class DataSourceFactory: elif market == 'USStock': from app.data_sources.us_stock import USStockDataSource return USStockDataSource() - elif market == 'AShare': - from app.data_sources.cn_stock import AShareDataSource - return AShareDataSource() - elif market == 'HShare': - from app.data_sources.cn_stock import HShareDataSource - return HShareDataSource() elif market == 'Forex': from app.data_sources.forex import ForexDataSource return ForexDataSource() diff --git a/backend_api_python/app/routes/__init__.py b/backend_api_python/app/routes/__init__.py index 507ad06..c2bb55d 100644 --- a/backend_api_python/app/routes/__init__.py +++ b/backend_api_python/app/routes/__init__.py @@ -24,6 +24,7 @@ def register_routes(app: Flask): from app.routes.global_market import global_market_bp from app.routes.community import community_bp from app.routes.fast_analysis import fast_analysis_bp + from app.routes.billing import billing_bp app.register_blueprint(health_bp) app.register_blueprint(auth_bp, url_prefix='/api/auth') # Auth routes @@ -42,4 +43,5 @@ def register_routes(app: Flask): app.register_blueprint(mt5_bp, url_prefix='/api/mt5') app.register_blueprint(global_market_bp, url_prefix='/api/global-market') app.register_blueprint(community_bp, url_prefix='/api/community') - app.register_blueprint(fast_analysis_bp, url_prefix='/api/fast-analysis') \ No newline at end of file + app.register_blueprint(fast_analysis_bp, url_prefix='/api/fast-analysis') + app.register_blueprint(billing_bp, url_prefix='/api/billing') \ No newline at end of file diff --git a/backend_api_python/app/routes/billing.py b/backend_api_python/app/routes/billing.py new file mode 100644 index 0000000..bad35f5 --- /dev/null +++ b/backend_api_python/app/routes/billing.py @@ -0,0 +1,105 @@ +""" +Billing APIs - 会员购买/套餐配置(Mock支付) + +当前版本先实现“快速商业闭环”的最小可用: +- 从系统设置(.env)读取 3 档会员(包月/包年/永久)金额与赠送积分配置 +- 用户在前端购买后立即开通/发放积分(后续可替换为真实支付网关) +""" + +from flask import Blueprint, jsonify, request, g + +from app.utils.auth import login_required +from app.utils.logger import get_logger +from app.services.billing_service import get_billing_service +from app.services.usdt_payment_service import get_usdt_payment_service + +logger = get_logger(__name__) + +billing_bp = Blueprint("billing", __name__) + + +@billing_bp.route("/plans", methods=["GET"]) +@login_required +def get_membership_plans(): + """Get membership plan configuration + current user's billing snapshot.""" + try: + user_id = getattr(g, "user_id", None) + svc = get_billing_service() + plans = svc.get_membership_plans() + billing_info = svc.get_user_billing_info(user_id) if user_id else {} + return jsonify({"code": 1, "msg": "success", "data": {"plans": plans, "billing": billing_info}}) + except Exception as e: + logger.error(f"get_membership_plans failed: {e}", exc_info=True) + return jsonify({"code": 0, "msg": str(e), "data": None}), 500 + + +@billing_bp.route("/purchase", methods=["POST"]) +@login_required +def purchase_membership(): + """ + Purchase membership (mock: immediate activation). + + Body: + { plan: "monthly" | "yearly" | "lifetime" } + """ + try: + user_id = getattr(g, "user_id", None) + data = request.get_json() or {} + plan = (data.get("plan") or "").strip().lower() + if not plan: + return jsonify({"code": 0, "msg": "missing_plan", "data": None}), 400 + + success, msg, out = get_billing_service().purchase_membership(user_id, plan) + if success: + return jsonify({"code": 1, "msg": msg, "data": out}) + return jsonify({"code": 0, "msg": msg, "data": out}), 400 + except Exception as e: + logger.error(f"purchase_membership failed: {e}", exc_info=True) + return jsonify({"code": 0, "msg": str(e), "data": None}), 500 + + +# ========================= +# USDT Pay (方案B) +# ========================= + + +@billing_bp.route("/usdt/create", methods=["POST"]) +@login_required +def usdt_create_order(): + """ + Create USDT order for membership plan (per-order address). + + Body: + { plan: "monthly"|"yearly"|"lifetime" } + """ + try: + user_id = getattr(g, "user_id", None) + data = request.get_json() or {} + plan = (data.get("plan") or "").strip().lower() + if not plan: + return jsonify({"code": 0, "msg": "missing_plan", "data": None}), 400 + + ok, msg, out = get_usdt_payment_service().create_order(user_id, plan) + if ok: + return jsonify({"code": 1, "msg": "success", "data": out}) + return jsonify({"code": 0, "msg": msg, "data": out}), 400 + except Exception as e: + logger.error(f"usdt_create_order failed: {e}", exc_info=True) + return jsonify({"code": 0, "msg": str(e), "data": None}), 500 + + +@billing_bp.route("/usdt/order/", methods=["GET"]) +@login_required +def usdt_get_order(order_id: int): + """Get my USDT order; refresh chain status by default.""" + try: + user_id = getattr(g, "user_id", None) + refresh = str(request.args.get("refresh", "1")).lower() in ("1", "true", "yes") + ok, msg, out = get_usdt_payment_service().get_order(user_id, order_id, refresh=refresh) + if ok: + return jsonify({"code": 1, "msg": "success", "data": out}) + return jsonify({"code": 0, "msg": msg, "data": out}), 404 + except Exception as e: + logger.error(f"usdt_get_order failed: {e}", exc_info=True) + return jsonify({"code": 0, "msg": str(e), "data": None}), 500 + diff --git a/backend_api_python/app/routes/fast_analysis.py b/backend_api_python/app/routes/fast_analysis.py index 0856907..18435d8 100644 --- a/backend_api_python/app/routes/fast_analysis.py +++ b/backend_api_python/app/routes/fast_analysis.py @@ -23,7 +23,7 @@ def analyze(): POST /api/fast-analysis/analyze Body: { - "market": "Crypto" | "USStock" | "AShare" | "Forex" | ..., + "market": "Crypto" | "USStock" | "Forex" | ..., "symbol": "BTC/USDT" | "AAPL" | ..., "language": "zh-CN" | "en-US" (optional), "model": "openai/gpt-4o" (optional), diff --git a/backend_api_python/app/routes/global_market.py b/backend_api_python/app/routes/global_market.py index 1e74661..3f2c65f 100644 --- a/backend_api_python/app/routes/global_market.py +++ b/backend_api_python/app/routes/global_market.py @@ -2,7 +2,7 @@ Global Market Dashboard APIs. Provides aggregated global market data including: -- Major indices (US, China, Hong Kong, Europe, Japan) +- Major indices (US, Europe, Japan, Korea, Australia, India) - Forex pairs - Crypto prices - Market heatmap data (crypto, stocks, forex) @@ -53,7 +53,7 @@ CACHE_TTL = { "market_news": 180, # 3分钟 - 新闻 "economic_calendar": 3600, # 1小时 - 日历事件 "market_sentiment": 21600, # 6小时 - 宏观情绪变化缓慢 - "trading_opportunities": 60, # 1分钟 - 交易机会需要较新 + "trading_opportunities": 3600, # 1小时 - 每小时更新一次 } @@ -257,12 +257,6 @@ def _fetch_stock_indices() -> List[Dict[str, Any]]: {"symbol": "^GSPC", "name_cn": "标普500", "name_en": "S&P 500", "region": "US", "flag": "🇺🇸", "lat": 40.7, "lng": -74.0}, {"symbol": "^DJI", "name_cn": "道琼斯", "name_en": "Dow Jones", "region": "US", "flag": "🇺🇸", "lat": 38.5, "lng": -77.0}, {"symbol": "^IXIC", "name_cn": "纳斯达克", "name_en": "NASDAQ", "region": "US", "flag": "🇺🇸", "lat": 37.5, "lng": -122.4}, - # China Markets - 坐标错开 - {"symbol": "000001.SS", "name_cn": "上证指数", "name_en": "SSE Composite", "region": "CN", "flag": "🇨🇳", "lat": 31.2, "lng": 121.5}, - {"symbol": "399001.SZ", "name_cn": "深证成指", "name_en": "SZSE Component", "region": "CN", "flag": "🇨🇳", "lat": 22.5, "lng": 114.1}, - {"symbol": "399006.SZ", "name_cn": "创业板指", "name_en": "ChiNext", "region": "CN", "flag": "🇨🇳", "lat": 25.0, "lng": 117.0}, - # Hong Kong - 只保留恒生指数 - {"symbol": "^HSI", "name_cn": "恒生指数", "name_en": "Hang Seng", "region": "HK", "flag": "🇭🇰", "lat": 22.3, "lng": 114.2}, # Europe {"symbol": "^GDAXI", "name_cn": "德国DAX", "name_en": "DAX", "region": "EU", "flag": "🇩🇪", "lat": 50.1109, "lng": 8.6821}, {"symbol": "^FTSE", "name_cn": "英国富时100", "name_en": "FTSE 100", "region": "EU", "flag": "🇬🇧", "lat": 51.5074, "lng": -0.1278}, @@ -977,12 +971,12 @@ def _fetch_financial_news(lang: str = "all") -> Dict[str, List[Dict[str, Any]]]: # Chinese news queries cn_queries = [ - "A股市场最新消息", "加密货币新闻", "美联储利率", - "中国经济数据", - "港股市场动态", + "美股市场最新消息", "外汇市场分析", + "全球经济数据", + "期货市场动态", ] # English news queries @@ -1106,42 +1100,6 @@ def _get_economic_calendar() -> List[Dict[str, Any]]: "impact_desc": "加息利空欧股,利多欧元", "impact_desc_en": "Rate hike: bearish EU stocks, bullish EUR" }, - { - "name": "中国GDP年率", - "name_en": "China GDP y/y", - "country": "CN", - "importance": "high", - "forecast": "5.2%", - "previous": "5.0%", - "impact_if_above": "bullish", - "impact_if_below": "bearish", - "impact_desc": "GDP高于预期利多A股和港股", - "impact_desc_en": "Above forecast: bullish A-shares and HK stocks" - }, - { - "name": "中国CPI年率", - "name_en": "China CPI y/y", - "country": "CN", - "importance": "medium", - "forecast": "0.3%", - "previous": "0.1%", - "impact_if_above": "neutral", - "impact_if_below": "bearish", - "impact_desc": "通胀过低反映需求不足,利空股市", - "impact_desc_en": "Low inflation reflects weak demand, bearish stocks" - }, - { - "name": "中国PMI", - "name_en": "China Manufacturing PMI", - "country": "CN", - "importance": "medium", - "forecast": "50.2", - "previous": "49.8", - "impact_if_above": "bullish", - "impact_if_below": "bearish", - "impact_desc": "PMI>50表示扩张,利多A股和大宗商品", - "impact_desc_en": "PMI>50 = expansion, bullish A-shares and commodities" - }, { "name": "日本央行利率决议", "name_en": "BoJ Interest Rate Decision", @@ -1634,80 +1592,265 @@ def market_sentiment(): return jsonify({"code": 0, "msg": str(e), "data": None}), 500 +def _fetch_stock_opportunity_prices() -> List[Dict[str, Any]]: + """Fetch popular US stock prices for opportunity scanning.""" + stocks = [ + {"symbol": "AAPL", "name": "Apple"}, + {"symbol": "MSFT", "name": "Microsoft"}, + {"symbol": "GOOGL", "name": "Alphabet"}, + {"symbol": "AMZN", "name": "Amazon"}, + {"symbol": "TSLA", "name": "Tesla"}, + {"symbol": "NVDA", "name": "NVIDIA"}, + {"symbol": "META", "name": "Meta"}, + {"symbol": "NFLX", "name": "Netflix"}, + {"symbol": "AMD", "name": "AMD"}, + {"symbol": "CRM", "name": "Salesforce"}, + {"symbol": "COIN", "name": "Coinbase"}, + {"symbol": "BABA", "name": "Alibaba"}, + {"symbol": "NIO", "name": "NIO"}, + {"symbol": "PLTR", "name": "Palantir"}, + {"symbol": "INTC", "name": "Intel"}, + ] + + try: + import yfinance as yf + + symbols = [s["symbol"] for s in stocks] + tickers = yf.Tickers(" ".join(symbols)) + + result = [] + for stock in stocks: + try: + ticker = tickers.tickers.get(stock["symbol"]) + if ticker: + hist = ticker.history(period="2d") + if len(hist) >= 2: + prev_close = float(hist["Close"].iloc[-2]) + current = float(hist["Close"].iloc[-1]) + change = ((current - prev_close) / prev_close) * 100 + elif len(hist) == 1: + current = float(hist["Close"].iloc[-1]) + change = 0 + else: + continue + + result.append({ + "symbol": stock["symbol"], + "name": stock["name"], + "price": round(current, 2), + "change": round(change, 2) + }) + except Exception as e: + logger.debug(f"Failed to fetch stock {stock['symbol']}: {e}") + + return result + except Exception as e: + logger.error(f"Failed to fetch stock opportunity prices: {e}") + return [] + + +def _analyze_opportunities_crypto(opportunities: list): + """Scan crypto market for trading opportunities.""" + crypto_data = _get_cached("crypto_prices") + if not crypto_data: + crypto_data = _fetch_crypto_prices() + if crypto_data: + _set_cached("crypto_prices", crypto_data) + + for coin in (crypto_data or [])[:20]: + change = _safe_float(coin.get("change_24h", 0)) + change_7d = _safe_float(coin.get("change_7d", 0)) + symbol = coin.get("symbol", "") + name = coin.get("name", "") + price = _safe_float(coin.get("price", 0)) + + signal = None + strength = "medium" + reason = "" + impact = "neutral" + + if change > 15: + signal = "overbought" + strength = "strong" + reason = f"24h涨幅{change:.1f}%,7日涨幅{change_7d:.1f}%,短期超买风险" + impact = "bearish" + elif change > 8: + signal = "bullish_momentum" + strength = "medium" + reason = f"24h涨幅{change:.1f}%,上涨动能强劲" + impact = "bullish" + elif change < -15: + signal = "oversold" + strength = "strong" + reason = f"24h跌幅{abs(change):.1f}%,可能超卖反弹" + impact = "bullish" + elif change < -8: + signal = "bearish_momentum" + strength = "medium" + reason = f"24h跌幅{abs(change):.1f}%,下跌趋势明显" + impact = "bearish" + + if signal: + opportunities.append({ + "symbol": symbol, + "name": name, + "price": price, + "change_24h": change, + "change_7d": change_7d, + "signal": signal, + "strength": strength, + "reason": reason, + "impact": impact, + "market": "Crypto", + "timestamp": int(time.time()) + }) + + +def _analyze_opportunities_stocks(opportunities: list): + """Scan US stocks for trading opportunities.""" + stock_data = _get_cached("stock_opportunity_prices") + if not stock_data: + stock_data = _fetch_stock_opportunity_prices() + if stock_data: + _set_cached("stock_opportunity_prices", stock_data, 3600) + + for stock in (stock_data or []): + change = _safe_float(stock.get("change", 0)) + symbol = stock.get("symbol", "") + name = stock.get("name", "") + price = _safe_float(stock.get("price", 0)) + + signal = None + strength = "medium" + reason = "" + impact = "neutral" + + # US stocks: smaller thresholds than crypto + if change > 5: + signal = "overbought" + strength = "strong" + reason = f"日涨幅{change:.1f}%,短期涨幅较大,注意回调风险" + impact = "bearish" + elif change > 3: + signal = "bullish_momentum" + strength = "medium" + reason = f"日涨幅{change:.1f}%,上涨动能强劲" + impact = "bullish" + elif change < -5: + signal = "oversold" + strength = "strong" + reason = f"日跌幅{abs(change):.1f}%,可能超卖反弹" + impact = "bullish" + elif change < -3: + signal = "bearish_momentum" + strength = "medium" + reason = f"日跌幅{abs(change):.1f}%,下跌趋势明显" + impact = "bearish" + + if signal: + opportunities.append({ + "symbol": symbol, + "name": name, + "price": price, + "change_24h": change, + "signal": signal, + "strength": strength, + "reason": reason, + "impact": impact, + "market": "USStock", + "timestamp": int(time.time()) + }) + + +def _analyze_opportunities_forex(opportunities: list): + """Scan forex pairs for trading opportunities.""" + forex_data = _get_cached("forex_pairs") + if not forex_data: + forex_data = _fetch_forex_pairs() + if forex_data: + _set_cached("forex_pairs", forex_data, 3600) + + for pair in (forex_data or []): + change = _safe_float(pair.get("change", 0)) + symbol = pair.get("symbol", pair.get("name", "")) + name = pair.get("name_cn", pair.get("name", "")) + price = _safe_float(pair.get("price", 0)) + + signal = None + strength = "medium" + reason = "" + impact = "neutral" + + # Forex: even smaller thresholds + if change > 1.5: + signal = "overbought" + strength = "strong" + reason = f"日涨幅{change:.2f}%,汇率波动剧烈,注意回调" + impact = "bearish" + elif change > 0.8: + signal = "bullish_momentum" + strength = "medium" + reason = f"日涨幅{change:.2f}%,上涨动能较强" + impact = "bullish" + elif change < -1.5: + signal = "oversold" + strength = "strong" + reason = f"日跌幅{abs(change):.2f}%,汇率波动剧烈,可能反弹" + impact = "bullish" + elif change < -0.8: + signal = "bearish_momentum" + strength = "medium" + reason = f"日跌幅{abs(change):.2f}%,下跌趋势明显" + impact = "bearish" + + if signal: + opportunities.append({ + "symbol": symbol, + "name": name, + "price": price, + "change_24h": change, + "signal": signal, + "strength": strength, + "reason": reason, + "impact": impact, + "market": "Forex", + "timestamp": int(time.time()) + }) + + @global_market_bp.route("/opportunities", methods=["GET"]) @login_required def trading_opportunities(): """ - Scan for trading opportunities based on technical indicators. + Scan for trading opportunities across Crypto, US Stocks, and Forex. + Cached for 1 hour. Pass ?force=true to skip cache. """ try: - cached = _get_cached("trading_opportunities", 60) - if cached: - return jsonify({"code": 1, "msg": "success", "data": cached}) - + force = request.args.get("force", "").lower() in ("true", "1") + + if not force: + cached = _get_cached("trading_opportunities") + if cached: + return jsonify({"code": 1, "msg": "success", "data": cached}) + opportunities = [] - - # Get crypto data - crypto_data = _get_cached("crypto_prices") - if not crypto_data: - crypto_data = _fetch_crypto_prices() - - # Analyze crypto for opportunities - for coin in crypto_data[:15]: - change = coin.get("change_24h", 0) - change_7d = coin.get("change_7d", 0) - symbol = coin.get("symbol", "") - name = coin.get("name", "") - price = coin.get("price", 0) - - signal = None - strength = "medium" - reason = "" - impact = "neutral" - - if change > 15: - signal = "overbought" - strength = "strong" - reason = f"24h涨幅{change:.1f}%,7日涨幅{change_7d:.1f}%,短期超买风险" - impact = "bearish" - elif change > 8: - signal = "bullish_momentum" - strength = "medium" - reason = f"24h涨幅{change:.1f}%,上涨动能强劲" - impact = "bullish" - elif change < -15: - signal = "oversold" - strength = "strong" - reason = f"24h跌幅{abs(change):.1f}%,可能超卖反弹" - impact = "bullish" - elif change < -8: - signal = "bearish_momentum" - strength = "medium" - reason = f"24h跌幅{abs(change):.1f}%,下跌趋势明显" - impact = "bearish" - - if signal: - opportunities.append({ - "symbol": symbol, - "name": name, - "price": price, - "change_24h": change, - "change_7d": change_7d, - "signal": signal, - "strength": strength, - "reason": reason, - "impact": impact, - "market": "crypto", - "timestamp": int(time.time()) - }) - - # Sort by absolute change + + # 1) Crypto + _analyze_opportunities_crypto(opportunities) + + # 2) US Stocks + _analyze_opportunities_stocks(opportunities) + + # 3) Forex + _analyze_opportunities_forex(opportunities) + + # Sort by absolute change descending opportunities.sort(key=lambda x: abs(x.get("change_24h", 0)), reverse=True) - - _set_cached("trading_opportunities", opportunities, 60) - + + _set_cached("trading_opportunities", opportunities, 3600) + return jsonify({"code": 1, "msg": "success", "data": opportunities}) - + except Exception as e: logger.error(f"trading_opportunities failed: {e}", exc_info=True) return jsonify({"code": 0, "msg": str(e), "data": None}), 500 diff --git a/backend_api_python/app/routes/ibkr.py b/backend_api_python/app/routes/ibkr.py index 6b00cfd..7c9049b 100644 --- a/backend_api_python/app/routes/ibkr.py +++ b/backend_api_python/app/routes/ibkr.py @@ -1,7 +1,7 @@ """ Interactive Brokers API Routes -Standalone API endpoints for US and Hong Kong stock trading. +Standalone API endpoints for US stock trading. """ from flask import Blueprint, request, jsonify @@ -235,7 +235,7 @@ def place_order(): "symbol": "AAPL", // Required, symbol code "side": "buy", // Required, buy or sell "quantity": 10, // Required, number of shares - "marketType": "USStock", // Optional, USStock or HShare, default USStock + "marketType": "USStock", // Optional, default USStock "orderType": "market", // Optional, market or limit, default market "price": 150.00 // Required for limit orders } diff --git a/backend_api_python/app/routes/indicator.py b/backend_api_python/app/routes/indicator.py index a75a39c..61de52c 100644 --- a/backend_api_python/app/routes/indicator.py +++ b/backend_api_python/app/routes/indicator.py @@ -76,6 +76,8 @@ def _row_to_indicator(row: Dict[str, Any], user_id: int) -> Dict[str, Any]: "publish_to_community": row.get("publish_to_community") if row.get("publish_to_community") is not None else 0, "pricing_type": row.get("pricing_type") or "free", "price": row.get("price") if row.get("price") is not None else 0, + # VIP-free indicator flag (community publishing) + "vip_free": 1 if (row.get("vip_free") or 0) else 0, # Local mode: encryption is not supported; keep field for frontend compatibility (always 0). "is_encrypted": 0, "preview_image": row.get("preview_image") or "", @@ -131,12 +133,17 @@ def get_indicators(): with get_db_connection() as db: cur = db.cursor() + # Best-effort schema upgrade for VIP-free indicators + try: + cur.execute("ALTER TABLE qd_indicator_codes ADD COLUMN IF NOT EXISTS vip_free BOOLEAN DEFAULT FALSE") + except Exception: + pass # Get user's own indicators (both purchased and custom). cur.execute( """ SELECT id, user_id, is_buy, end_time, name, code, description, - publish_to_community, pricing_type, price, is_encrypted, preview_image, + publish_to_community, pricing_type, price, is_encrypted, preview_image, vip_free, createtime, updatetime, created_at, updated_at FROM qd_indicator_codes WHERE user_id = ? @@ -178,6 +185,7 @@ def save_indicator(): description = (data.get("description") or "").strip() publish_to_community = 1 if data.get("publishToCommunity") or data.get("publish_to_community") else 0 pricing_type = (data.get("pricingType") or data.get("pricing_type") or "free").strip() or "free" + vip_free = 1 if (data.get("vipFree") or data.get("vip_free")) else 0 try: price = float(data.get("price") or 0) except Exception: @@ -206,6 +214,11 @@ def save_indicator(): with get_db_connection() as db: cur = db.cursor() + # Best-effort schema upgrade for VIP-free indicators + try: + cur.execute("ALTER TABLE qd_indicator_codes ADD COLUMN IF NOT EXISTS vip_free BOOLEAN DEFAULT FALSE") + except Exception: + pass if indicator_id and indicator_id > 0: # 检查是否从未发布改为发布,需要设置审核状态 if publish_to_community: @@ -224,11 +237,12 @@ def save_indicator(): UPDATE qd_indicator_codes SET name = ?, code = ?, description = ?, publish_to_community = ?, pricing_type = ?, price = ?, preview_image = ?, + vip_free = ?, review_status = ?, review_note = '', reviewed_at = NOW(), reviewed_by = ?, updatetime = ?, updated_at = NOW() WHERE id = ? AND user_id = ? AND (is_buy IS NULL OR is_buy = 0) """, - (name, code, description, publish_to_community, pricing_type, price, preview_image, + (name, code, description, publish_to_community, pricing_type, price, preview_image, vip_free, new_review_status, user_id if is_admin else None, now, indicator_id, user_id), ) else: @@ -238,10 +252,11 @@ def save_indicator(): UPDATE qd_indicator_codes SET name = ?, code = ?, description = ?, publish_to_community = ?, pricing_type = ?, price = ?, preview_image = ?, + vip_free = ?, updatetime = ?, updated_at = NOW() WHERE id = ? AND user_id = ? AND (is_buy IS NULL OR is_buy = 0) """, - (name, code, description, publish_to_community, pricing_type, price, preview_image, now, indicator_id, user_id), + (name, code, description, publish_to_community, pricing_type, price, preview_image, vip_free, now, indicator_id, user_id), ) else: # 取消发布,清除审核状态 @@ -250,6 +265,7 @@ def save_indicator(): UPDATE qd_indicator_codes SET name = ?, code = ?, description = ?, publish_to_community = ?, pricing_type = ?, price = ?, preview_image = ?, + vip_free = 0, review_status = NULL, review_note = '', reviewed_at = NULL, reviewed_by = NULL, updatetime = ?, updated_at = NOW() WHERE id = ? AND user_id = ? AND (is_buy IS NULL OR is_buy = 0) @@ -265,11 +281,11 @@ def save_indicator(): """ INSERT INTO qd_indicator_codes (user_id, is_buy, end_time, name, code, description, - publish_to_community, pricing_type, price, preview_image, review_status, + publish_to_community, pricing_type, price, preview_image, vip_free, review_status, createtime, updatetime, created_at, updated_at) - VALUES (?, 0, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + VALUES (?, 0, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) """, - (user_id, name, code, description, publish_to_community, pricing_type, price, preview_image, review_status, now, now), + (user_id, name, code, description, publish_to_community, pricing_type, price, preview_image, vip_free, review_status, now, now), ) indicator_id = int(cur.lastrowid or 0) db.commit() diff --git a/backend_api_python/app/routes/kline.py b/backend_api_python/app/routes/kline.py index a009dd7..c04b846 100644 --- a/backend_api_python/app/routes/kline.py +++ b/backend_api_python/app/routes/kline.py @@ -20,7 +20,7 @@ def get_kline(): 获取K线数据 参数: - market: 市场类型 (Crypto, USStock, AShare, HShare, Forex, Futures) + market: 市场类型 (Crypto, USStock, Forex, Futures) symbol: 交易对/股票代码 timeframe: 时间周期 (1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W) limit: 数据条数 (默认300) diff --git a/backend_api_python/app/routes/market.py b/backend_api_python/app/routes/market.py index ff1391f..bbf28e0 100644 --- a/backend_api_python/app/routes/market.py +++ b/backend_api_python/app/routes/market.py @@ -83,7 +83,7 @@ def get_public_config(): @market_bp.route('/types', methods=['GET']) def get_market_types(): """Return supported market types for the add-watchlist modal.""" - desired_order = ['USStock', 'Crypto', 'Forex', 'Futures', 'HShare', 'AShare'] + desired_order = ['USStock', 'Crypto', 'Forex', 'Futures'] order_rank = {v: i for i, v in enumerate(desired_order)} def _normalize_item(x): @@ -495,22 +495,11 @@ def get_stock_name(): stock_name = symbol # 默认使用代码 try: - if market in ['USStock', 'AShare', 'HShare']: + if market == 'USStock': # 对于股票,尝试获取基本信息 import yfinance as yf - # 转换symbol格式 - if market == 'USStock': - yf_symbol = symbol - elif market == 'AShare': - yf_symbol = symbol + '.SS' if symbol.startswith('6') else symbol + '.SZ' - elif market == 'HShare': - # 港股需要补齐4位数字并添加.HK - hk_code = symbol.zfill(4) - yf_symbol = hk_code + '.HK' - else: - yf_symbol = symbol - + yf_symbol = symbol ticker = yf.Ticker(yf_symbol) info = ticker.info diff --git a/backend_api_python/app/routes/settings.py b/backend_api_python/app/routes/settings.py index 4234bcc..1d9fefd 100644 --- a/backend_api_python/app/routes/settings.py +++ b/backend_api_python/app/routes/settings.py @@ -18,42 +18,19 @@ settings_bp = Blueprint('settings', __name__) ENV_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env') # 配置项定义(分组)- 按功能模块划分,每个配置项包含描述 +# --------------------------------------------------------------- +# 精简原则: +# - 部署级配置(host/port/debug)不在 UI 暴露,用户通过 .env 或 docker-compose 设置 +# - 内部调优参数(超时/重试/tick间隔/向量维度等)使用默认值即可,不暴露给普通用户 +# - 只保留用户真正需要配置的功能开关和 API Key +# --------------------------------------------------------------- CONFIG_SCHEMA = { - # ==================== 1. 服务配置 ==================== - 'server': { - 'title': 'Server Configuration', - 'icon': 'cloud-server', - 'order': 1, - 'items': [ - { - 'key': 'PYTHON_API_HOST', - 'label': 'Listen Address', - 'type': 'text', - 'default': '0.0.0.0', - 'description': 'Server listen address. 0.0.0.0 allows external access, 127.0.0.1 for local only' - }, - { - 'key': 'PYTHON_API_PORT', - 'label': 'Port', - 'type': 'number', - 'default': '5000', - 'description': 'Server listen port, default 5000' - }, - { - 'key': 'PYTHON_API_DEBUG', - 'label': 'Debug Mode', - 'type': 'boolean', - 'default': 'False', - 'description': 'Enable debug mode for development. Disable in production' - }, - ] - }, - # ==================== 2. 安全认证 ==================== + # ==================== 1. 安全认证 ==================== 'auth': { 'title': 'Security & Authentication', 'icon': 'lock', - 'order': 2, + 'order': 1, 'items': [ { 'key': 'SECRET_KEY', @@ -86,11 +63,11 @@ CONFIG_SCHEMA = { ] }, - # ==================== 3. AI/LLM 配置 ==================== + # ==================== 2. AI/LLM 配置 ==================== 'ai': { 'title': 'AI / LLM Configuration', 'icon': 'robot', - 'order': 3, + 'order': 2, 'items': [ { 'key': 'LLM_PROVIDER', @@ -235,13 +212,6 @@ CONFIG_SCHEMA = { 'default': '0.7', 'description': 'Model creativity (0-1). Lower = more deterministic' }, - { - 'key': 'OPENROUTER_TIMEOUT', - 'label': 'Request Timeout (sec)', - 'type': 'number', - 'default': '300', - 'description': 'API request timeout in seconds' - }, { 'key': 'AI_MODELS_JSON', 'label': 'Custom Models (JSON)', @@ -253,33 +223,19 @@ CONFIG_SCHEMA = { ] }, - # ==================== 4. 实盘交易 ==================== + # ==================== 3. 实盘交易 ==================== 'trading': { 'title': 'Live Trading', 'icon': 'stock', - 'order': 4, + 'order': 3, 'items': [ - { - 'key': 'ENABLE_PENDING_ORDER_WORKER', - 'label': 'Enable Order Worker', - 'type': 'boolean', - 'default': 'True', - 'description': 'Enable background order processing worker for live trading' - }, - { - 'key': 'PENDING_ORDER_STALE_SEC', - 'label': 'Order Stale Timeout (sec)', - 'type': 'number', - 'default': '90', - 'description': 'Mark pending order as stale after this many seconds' - }, { 'key': 'ORDER_MODE', 'label': 'Order Execution Mode', 'type': 'select', - 'options': ['maker', 'market'], - 'default': 'maker', - 'description': 'maker: Limit order first (lower fees), market: Market order (instant fill)' + 'options': ['market', 'maker'], + 'default': 'market', + 'description': 'market: Market order (instant fill, recommended), maker: Limit order first (lower fees but may not fill)' }, { 'key': 'MAKER_WAIT_SEC', @@ -288,95 +244,30 @@ CONFIG_SCHEMA = { 'default': '10', 'description': 'Wait time for limit order fill before switching to market order' }, - { - 'key': 'MAKER_OFFSET_BPS', - 'label': 'Limit Order Offset (bps)', - 'type': 'number', - 'default': '2', - 'description': 'Price offset in basis points (1bps=0.01%). Buy: price*(1-offset), Sell: price*(1+offset)' - }, ] }, - # ==================== 5. 策略执行 ==================== - 'strategy': { - 'title': 'Strategy Execution', - 'icon': 'fund', - 'order': 5, - 'items': [ - { - 'key': 'DISABLE_RESTORE_RUNNING_STRATEGIES', - 'label': 'Disable Auto Restore', - 'type': 'boolean', - 'default': 'False', - 'description': 'Disable automatic restore of running strategies on server restart' - }, - { - 'key': 'STRATEGY_TICK_INTERVAL_SEC', - 'label': 'Tick Interval (sec)', - 'type': 'number', - 'default': '10', - 'description': 'Strategy main loop tick interval in seconds' - }, - { - 'key': 'PRICE_CACHE_TTL_SEC', - 'label': 'Price Cache TTL (sec)', - 'type': 'number', - 'default': '10', - 'description': 'Time-to-live for cached price data in seconds' - }, - ] - }, - - # ==================== 6. 数据源配置 ==================== + # ==================== 4. 数据源配置 ==================== 'data_source': { 'title': 'Data Sources', 'icon': 'database', - 'order': 6, + 'order': 4, 'items': [ - { - 'key': 'DATA_SOURCE_TIMEOUT', - 'label': 'Default Timeout (sec)', - 'type': 'number', - 'default': '30', - 'description': 'Default timeout for all data source requests' - }, - { - 'key': 'DATA_SOURCE_RETRY', - 'label': 'Retry Count', - 'type': 'number', - 'default': '3', - 'description': 'Number of retry attempts on data source failure' - }, - { - 'key': 'DATA_SOURCE_RETRY_BACKOFF', - 'label': 'Retry Backoff (sec)', - 'type': 'number', - 'default': '0.5', - 'description': 'Backoff time between retry attempts' - }, { 'key': 'CCXT_DEFAULT_EXCHANGE', - 'label': 'CCXT Default Exchange', + 'label': 'Default Crypto Exchange', 'type': 'text', 'default': 'coinbase', 'link': 'https://github.com/ccxt/ccxt#supported-cryptocurrency-exchange-markets', 'link_text': 'settings.link.supportedExchanges', - 'description': 'Default exchange for CCXT crypto data (binance, coinbase, okx, etc.)' - }, - { - 'key': 'CCXT_TIMEOUT', - 'label': 'CCXT Timeout (ms)', - 'type': 'number', - 'default': '10000', - 'description': 'CCXT request timeout in milliseconds' + 'description': 'Default exchange for crypto data (binance, coinbase, okx, etc.)' }, { 'key': 'CCXT_PROXY', - 'label': 'CCXT Proxy', + 'label': 'Crypto Data Proxy', 'type': 'text', 'required': False, - 'description': 'Proxy URL for CCXT requests (e.g. socks5h://127.0.0.1:1080)' + 'description': 'Proxy URL for crypto data requests (e.g. socks5h://127.0.0.1:1080)' }, { 'key': 'FINNHUB_API_KEY', @@ -387,20 +278,6 @@ CONFIG_SCHEMA = { 'link_text': 'settings.link.freeRegister', 'description': 'Finnhub API key for US stock data (free tier available)' }, - { - 'key': 'FINNHUB_TIMEOUT', - 'label': 'Finnhub Timeout (sec)', - 'type': 'number', - 'default': '10', - 'description': 'Finnhub API request timeout' - }, - { - 'key': 'FINNHUB_RATE_LIMIT', - 'label': 'Finnhub Rate Limit', - 'type': 'number', - 'default': '60', - 'description': 'Finnhub API rate limit (requests per minute)' - }, { 'key': 'TIINGO_API_KEY', 'label': 'Tiingo API Key', @@ -408,37 +285,16 @@ CONFIG_SCHEMA = { 'required': False, 'link': 'https://www.tiingo.com/account/api/token', 'link_text': 'settings.link.getToken', - 'description': 'Tiingo API key for Forex/Metals data (free tier does not support 1-minute data)' - }, - { - 'key': 'TIINGO_TIMEOUT', - 'label': 'Tiingo Timeout (sec)', - 'type': 'number', - 'default': '10', - 'description': 'Tiingo API request timeout' - }, - { - 'key': 'AKSHARE_TIMEOUT', - 'label': 'Akshare Timeout (sec)', - 'type': 'number', - 'default': '30', - 'description': 'Akshare API timeout for China A-share data' - }, - { - 'key': 'YFINANCE_TIMEOUT', - 'label': 'YFinance Timeout (sec)', - 'type': 'number', - 'default': '30', - 'description': 'Yahoo Finance API timeout' + 'description': 'Tiingo API key for Forex/Metals data' }, ] }, - # ==================== 7. 邮件配置 (公共 SMTP) ==================== + # ==================== 5. 邮件配置 ==================== 'email': { 'title': 'Email (SMTP)', 'icon': 'mail', - 'order': 7, + 'order': 5, 'items': [ { 'key': 'SMTP_HOST', @@ -452,7 +308,7 @@ CONFIG_SCHEMA = { 'label': 'SMTP Port', 'type': 'number', 'default': '587', - 'description': 'SMTP port (587 for TLS, 465 for SSL, 25 for plain)' + 'description': 'SMTP port (587 for TLS, 465 for SSL)' }, { 'key': 'SMTP_USER', @@ -492,11 +348,11 @@ CONFIG_SCHEMA = { ] }, - # ==================== 9. 短信配置 ==================== + # ==================== 6. 短信配置 ==================== 'sms': { 'title': 'SMS (Twilio)', 'icon': 'phone', - 'order': 8, + 'order': 6, 'items': [ { 'key': 'TWILIO_ACCOUNT_SID', @@ -524,11 +380,11 @@ CONFIG_SCHEMA = { ] }, - # ==================== 9. AI Agent 配置 ==================== + # ==================== 7. AI Agent ==================== 'agent': { 'title': 'AI Agent', 'icon': 'experiment', - 'order': 9, + 'order': 7, 'items': [ { 'key': 'ENABLE_AGENT_MEMORY', @@ -537,62 +393,6 @@ CONFIG_SCHEMA = { 'default': 'True', 'description': 'Enable AI agent memory for learning from past trades' }, - { - 'key': 'AGENT_MEMORY_ENABLE_VECTOR', - 'label': 'Enable Vector Search', - 'type': 'boolean', - 'default': 'True', - 'description': 'Enable local vector similarity search for memory retrieval' - }, - { - 'key': 'AGENT_MEMORY_EMBEDDING_DIM', - 'label': 'Embedding Dimension', - 'type': 'number', - 'default': '256', - 'description': 'Vector embedding dimension for memory storage' - }, - { - 'key': 'AGENT_MEMORY_TOP_K', - 'label': 'Retrieval Top-K', - 'type': 'number', - 'default': '5', - 'description': 'Number of similar memories to retrieve' - }, - { - 'key': 'AGENT_MEMORY_CANDIDATE_LIMIT', - 'label': 'Candidate Limit', - 'type': 'number', - 'default': '500', - 'description': 'Maximum candidates for similarity search' - }, - { - 'key': 'AGENT_MEMORY_HALF_LIFE_DAYS', - 'label': 'Recency Half-life (days)', - 'type': 'number', - 'default': '30', - 'description': 'Time decay half-life for memory recency scoring' - }, - { - 'key': 'AGENT_MEMORY_W_SIM', - 'label': 'Similarity Weight', - 'type': 'number', - 'default': '0.75', - 'description': 'Weight for similarity score in memory ranking (0-1)' - }, - { - 'key': 'AGENT_MEMORY_W_RECENCY', - 'label': 'Recency Weight', - 'type': 'number', - 'default': '0.20', - 'description': 'Weight for recency score in memory ranking (0-1)' - }, - { - 'key': 'AGENT_MEMORY_W_RETURNS', - 'label': 'Returns Weight', - 'type': 'number', - 'default': '0.05', - 'description': 'Weight for returns score in memory ranking (0-1)' - }, { 'key': 'ENABLE_REFLECTION_WORKER', 'label': 'Enable Auto Reflection', @@ -600,59 +400,30 @@ CONFIG_SCHEMA = { 'default': 'False', 'description': 'Enable background worker for automatic trade reflection' }, - { - 'key': 'REFLECTION_WORKER_INTERVAL_SEC', - 'label': 'Reflection Interval (sec)', - 'type': 'number', - 'default': '86400', - 'description': 'Interval between automatic reflection runs (default: 24h)' - }, ] }, - # ==================== 10. 网络代理 ==================== + # ==================== 8. 网络代理 ==================== 'network': { 'title': 'Network & Proxy', 'icon': 'global', - 'order': 10, + 'order': 8, 'items': [ - { - 'key': 'PROXY_HOST', - 'label': 'Proxy Host', - 'type': 'text', - 'default': '127.0.0.1', - 'description': 'Proxy server hostname or IP' - }, - { - 'key': 'PROXY_PORT', - 'label': 'Proxy Port', - 'type': 'text', - 'required': False, - 'description': 'Proxy server port (leave empty to disable proxy)' - }, - { - 'key': 'PROXY_SCHEME', - 'label': 'Proxy Protocol', - 'type': 'select', - 'options': ['socks5h', 'socks5', 'http', 'https'], - 'default': 'socks5h', - 'description': 'Proxy protocol type. socks5h: SOCKS5 with DNS resolution' - }, { 'key': 'PROXY_URL', - 'label': 'Full Proxy URL', + 'label': 'Proxy URL', 'type': 'text', 'required': False, - 'description': 'Complete proxy URL (overrides above settings if set)' + 'description': 'Global proxy URL (e.g. socks5h://127.0.0.1:1080 or http://proxy:8080)' }, ] }, - # ==================== 11. 搜索配置 ==================== + # ==================== 9. 搜索配置 ==================== 'search': { 'title': 'Web Search', 'icon': 'search', - 'order': 11, + 'order': 9, 'items': [ { 'key': 'SEARCH_PROVIDER', @@ -660,16 +431,8 @@ CONFIG_SCHEMA = { 'type': 'select', 'options': ['bocha', 'tavily', 'google', 'bing', 'none'], 'default': 'bocha', - 'description': 'Web search provider for AI research features. Bocha recommended for A-share news' + 'description': 'Web search provider for AI research features' }, - { - 'key': 'SEARCH_MAX_RESULTS', - 'label': 'Max Results', - 'type': 'number', - 'default': '10', - 'description': 'Maximum search results to return' - }, - # Tavily Search API { 'key': 'TAVILY_API_KEYS', 'label': 'Tavily API Keys', @@ -677,9 +440,8 @@ CONFIG_SCHEMA = { 'required': False, 'link': 'https://tavily.com/', 'link_text': 'settings.link.getApiKey', - 'description': 'Tavily Search API keys, comma-separated for rotation. Free 1000 requests/month' + 'description': 'Tavily Search API keys (comma-separated). Free 1000 req/month' }, - # Bocha Search API { 'key': 'BOCHA_API_KEYS', 'label': 'Bocha API Keys', @@ -687,60 +449,16 @@ CONFIG_SCHEMA = { 'required': False, 'link': 'https://bochaai.com/', 'link_text': 'settings.link.getApiKey', - 'description': 'Bocha Search API keys, comma-separated for rotation. Best for A-share news' - }, - # SerpAPI - { - 'key': 'SERPAPI_KEYS', - 'label': 'SerpAPI Keys', - 'type': 'password', - 'required': False, - 'link': 'https://serpapi.com/', - 'link_text': 'settings.link.getApiKey', - 'description': 'SerpAPI keys for Google/Bing search, comma-separated for rotation' - }, - { - 'key': 'SEARCH_GOOGLE_API_KEY', - 'label': 'Google API Key', - 'type': 'password', - 'required': False, - 'link': 'https://developers.google.com/custom-search/v1/introduction', - 'link_text': 'settings.link.applyApi', - 'description': 'Google Custom Search JSON API key' - }, - { - 'key': 'SEARCH_GOOGLE_CX', - 'label': 'Google Search Engine ID', - 'type': 'text', - 'required': False, - 'link': 'https://programmablesearchengine.google.com/controlpanel/all', - 'link_text': 'settings.link.createSearchEngine', - 'description': 'Google Programmable Search Engine ID (CX)' - }, - { - 'key': 'SEARCH_BING_API_KEY', - 'label': 'Bing API Key', - 'type': 'password', - 'required': False, - 'link': 'https://www.microsoft.com/en-us/bing/apis/bing-web-search-api', - 'link_text': 'settings.link.applyApi', - 'description': 'Microsoft Bing Web Search API key' - }, - { - 'key': 'INTERNAL_API_KEY', - 'label': 'Internal API Key', - 'type': 'password', - 'required': False, - 'description': 'Internal API authentication key for service-to-service calls' + 'description': 'Bocha Search API keys (comma-separated)' }, ] }, - # ==================== 12. 注册与安全 ==================== + # ==================== 10. 注册与 OAuth ==================== 'security': { - 'title': 'Registration & Security', + 'title': 'Registration & OAuth', 'icon': 'safety', - 'order': 12, + 'order': 10, 'items': [ { 'key': 'ENABLE_REGISTRATION', @@ -749,6 +467,13 @@ CONFIG_SCHEMA = { 'default': 'True', 'description': 'Allow new users to register accounts' }, + { + 'key': 'FRONTEND_URL', + 'label': 'Frontend URL', + 'type': 'text', + 'default': 'http://localhost:8080', + 'description': 'Frontend URL for OAuth redirects' + }, { 'key': 'TURNSTILE_SITE_KEY', 'label': 'Turnstile Site Key', @@ -756,7 +481,7 @@ CONFIG_SCHEMA = { 'required': False, 'link': 'https://dash.cloudflare.com/?to=/:account/turnstile', 'link_text': 'settings.link.getTurnstileKey', - 'description': 'Cloudflare Turnstile site key for CAPTCHA verification' + 'description': 'Cloudflare Turnstile site key for CAPTCHA' }, { 'key': 'TURNSTILE_SECRET_KEY', @@ -765,193 +490,198 @@ CONFIG_SCHEMA = { 'required': False, 'description': 'Cloudflare Turnstile secret key' }, - { - 'key': 'FRONTEND_URL', - 'label': 'Frontend URL', - 'type': 'text', - 'default': 'http://localhost:8080', - 'description': 'Frontend URL for OAuth redirects' - }, { 'key': 'GOOGLE_CLIENT_ID', - 'label': 'Google Client ID', + 'label': 'Google OAuth Client ID', 'type': 'text', 'required': False, 'link': 'https://console.cloud.google.com/apis/credentials', 'link_text': 'settings.link.getGoogleCredentials', - 'description': 'Google OAuth Client ID' + 'description': 'Google OAuth Client ID for Google login' }, { 'key': 'GOOGLE_CLIENT_SECRET', - 'label': 'Google Client Secret', + 'label': 'Google OAuth Secret', 'type': 'password', 'required': False, 'description': 'Google OAuth Client Secret' }, - { - 'key': 'GOOGLE_REDIRECT_URI', - 'label': 'Google Redirect URI', - 'type': 'text', - 'default': 'http://localhost:5000/api/auth/oauth/google/callback', - 'description': 'Google OAuth callback URL' - }, { 'key': 'GITHUB_CLIENT_ID', - 'label': 'GitHub Client ID', + 'label': 'GitHub OAuth Client ID', 'type': 'text', 'required': False, 'link': 'https://github.com/settings/developers', 'link_text': 'settings.link.getGithubCredentials', - 'description': 'GitHub OAuth Client ID' + 'description': 'GitHub OAuth Client ID for GitHub login' }, { 'key': 'GITHUB_CLIENT_SECRET', - 'label': 'GitHub Client Secret', + 'label': 'GitHub OAuth Secret', 'type': 'password', 'required': False, 'description': 'GitHub OAuth Client Secret' }, - { - 'key': 'GITHUB_REDIRECT_URI', - 'label': 'GitHub Redirect URI', - 'type': 'text', - 'default': 'http://localhost:5000/api/auth/oauth/github/callback', - 'description': 'GitHub OAuth callback URL' - }, - { - 'key': 'SECURITY_IP_MAX_ATTEMPTS', - 'label': 'IP Max Failed Attempts', - 'type': 'number', - 'default': '10', - 'description': 'Block IP after this many failed login attempts' - }, - { - 'key': 'SECURITY_IP_WINDOW_MINUTES', - 'label': 'IP Window (minutes)', - 'type': 'number', - 'default': '5', - 'description': 'Time window for counting IP failed attempts' - }, - { - 'key': 'SECURITY_IP_BLOCK_MINUTES', - 'label': 'IP Block Duration (minutes)', - 'type': 'number', - 'default': '15', - 'description': 'How long to block IP after exceeding limit' - }, - { - 'key': 'SECURITY_ACCOUNT_MAX_ATTEMPTS', - 'label': 'Account Max Failed Attempts', - 'type': 'number', - 'default': '5', - 'description': 'Lock account after this many failed login attempts' - }, - { - 'key': 'SECURITY_ACCOUNT_WINDOW_MINUTES', - 'label': 'Account Window (minutes)', - 'type': 'number', - 'default': '60', - 'description': 'Time window for counting account failed attempts' - }, - { - 'key': 'SECURITY_ACCOUNT_BLOCK_MINUTES', - 'label': 'Account Block Duration (minutes)', - 'type': 'number', - 'default': '30', - 'description': 'How long to lock account after exceeding limit' - }, - { - 'key': 'VERIFICATION_CODE_EXPIRE_MINUTES', - 'label': 'Verification Code Expiry (minutes)', - 'type': 'number', - 'default': '10', - 'description': 'Email verification code validity period' - }, - { - 'key': 'VERIFICATION_CODE_RATE_LIMIT', - 'label': 'Code Rate Limit (seconds)', - 'type': 'number', - 'default': '60', - 'description': 'Minimum time between verification code requests per email' - }, - { - 'key': 'VERIFICATION_CODE_IP_HOURLY_LIMIT', - 'label': 'Code Hourly Limit per IP', - 'type': 'number', - 'default': '10', - 'description': 'Maximum verification codes per IP per hour' - }, - { - 'key': 'VERIFICATION_CODE_MAX_ATTEMPTS', - 'label': 'Code Max Attempts', - 'type': 'number', - 'default': '5', - 'description': 'Maximum attempts to verify a code before lockout' - }, - { - 'key': 'VERIFICATION_CODE_LOCK_MINUTES', - 'label': 'Code Lock Minutes', - 'type': 'number', - 'default': '30', - 'description': 'Lockout duration after exceeding max attempts' - }, ] }, - # ==================== 13. 计费配置 ==================== + # ==================== 11. 计费配置 ==================== 'billing': { 'title': 'Billing & Credits', 'icon': 'dollar', - 'order': 13, + 'order': 11, 'items': [ { 'key': 'BILLING_ENABLED', 'label': 'Enable Billing', 'type': 'boolean', 'default': 'False', - 'description': 'Enable billing system. When enabled, users need credits to use certain features' + 'description': 'Enable billing system. Users need credits to use certain features' }, { 'key': 'BILLING_VIP_BYPASS', - 'label': 'VIP Free', + 'label': 'VIP Bypass (Legacy)', 'type': 'boolean', - 'default': 'True', - 'description': 'VIP users can use all paid features for free during VIP period' + 'default': 'False', + 'description': 'Legacy switch. If enabled, VIP users bypass ALL feature credit costs. Recommended OFF: VIP should only unlock VIP-free indicators.' + }, + + # ===== Membership Plans (3 tiers) ===== + { + 'key': 'MEMBERSHIP_MONTHLY_PRICE_USD', + 'label': 'Monthly Membership Price (USD)', + 'type': 'number', + 'default': '19.9', + 'description': 'Monthly membership price in USD (mock payment in current version)' + }, + { + 'key': 'MEMBERSHIP_MONTHLY_CREDITS', + 'label': 'Monthly Membership Bonus Credits', + 'type': 'number', + 'default': '500', + 'description': 'Credits granted immediately after purchasing monthly membership' + }, + { + 'key': 'MEMBERSHIP_YEARLY_PRICE_USD', + 'label': 'Yearly Membership Price (USD)', + 'type': 'number', + 'default': '199', + 'description': 'Yearly membership price in USD (mock payment in current version)' + }, + { + 'key': 'MEMBERSHIP_YEARLY_CREDITS', + 'label': 'Yearly Membership Bonus Credits', + 'type': 'number', + 'default': '8000', + 'description': 'Credits granted immediately after purchasing yearly membership' + }, + { + 'key': 'MEMBERSHIP_LIFETIME_PRICE_USD', + 'label': 'Lifetime Membership Price (USD)', + 'type': 'number', + 'default': '499', + 'description': 'Lifetime membership price in USD (mock payment in current version)' + }, + { + 'key': 'MEMBERSHIP_LIFETIME_MONTHLY_CREDITS', + 'label': 'Lifetime Membership Monthly Credits', + 'type': 'number', + 'default': '800', + 'description': 'Credits granted every 30 days for lifetime members' + }, + + # ===== USDT Pay (方案B:每单独立地址) ===== + { + 'key': 'USDT_PAY_ENABLED', + 'label': 'Enable USDT Pay', + 'type': 'boolean', + 'default': 'False', + 'description': 'Enable USDT scan-to-pay flow (per-order unique address)' + }, + { + 'key': 'USDT_PAY_CHAIN', + 'label': 'USDT Chain', + 'type': 'select', + 'default': 'TRC20', + 'options': ['TRC20'], + 'description': 'Currently only TRC20 is supported' + }, + { + 'key': 'USDT_TRC20_XPUB', + 'label': 'TRC20 XPUB (Watch-only)', + 'type': 'password', + 'required': False, + 'description': 'Watch-only xpub used to derive per-order deposit addresses. Do NOT paste private key.' + }, + { + 'key': 'USDT_TRC20_CONTRACT', + 'label': 'USDT TRC20 Contract', + 'type': 'text', + 'default': 'TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj', + 'description': 'USDT contract address on TRON' + }, + { + 'key': 'TRONGRID_BASE_URL', + 'label': 'TronGrid Base URL', + 'type': 'text', + 'default': 'https://api.trongrid.io', + 'description': 'TronGrid API base URL' + }, + { + 'key': 'TRONGRID_API_KEY', + 'label': 'TronGrid API Key', + 'type': 'password', + 'required': False, + 'description': 'Optional TronGrid API key for higher rate limits' + }, + { + 'key': 'USDT_PAY_CONFIRM_SECONDS', + 'label': 'Confirm Delay (sec)', + 'type': 'number', + 'default': '30', + 'description': 'Delay before marking a paid transaction as confirmed (TRC20)' + }, + { + 'key': 'USDT_PAY_EXPIRE_MINUTES', + 'label': 'Order Expire (min)', + 'type': 'number', + 'default': '30', + 'description': 'USDT payment order expiration time in minutes' }, { 'key': 'BILLING_COST_AI_ANALYSIS', 'label': 'AI Analysis Cost', 'type': 'number', 'default': '10', - 'description': 'Credits consumed per AI analysis request' + 'description': 'Credits per AI analysis request' }, { 'key': 'BILLING_COST_STRATEGY_RUN', 'label': 'Strategy Run Cost', 'type': 'number', 'default': '5', - 'description': 'Credits consumed when starting a strategy' + 'description': 'Credits per strategy start' }, { 'key': 'BILLING_COST_BACKTEST', 'label': 'Backtest Cost', 'type': 'number', 'default': '3', - 'description': 'Credits consumed per backtest run' + 'description': 'Credits per backtest run' }, { 'key': 'BILLING_COST_PORTFOLIO_MONITOR', 'label': 'Portfolio Monitor Cost', 'type': 'number', 'default': '8', - 'description': 'Credits consumed per portfolio AI monitoring run' + 'description': 'Credits per portfolio AI monitoring run' }, { 'key': 'RECHARGE_TELEGRAM_URL', 'label': 'Recharge Telegram URL', 'type': 'text', 'default': 'https://t.me/your_support_bot', - 'description': 'Telegram customer service URL for recharge inquiries' + 'description': 'Telegram URL for recharge inquiries' }, { 'key': 'CREDITS_REGISTER_BONUS', @@ -965,44 +695,23 @@ CONFIG_SCHEMA = { 'label': 'Referral Bonus', 'type': 'number', 'default': '50', - 'description': 'Credits awarded to referrer when someone signs up with their code' + 'description': 'Credits awarded to referrer for each signup' }, ] }, - # ==================== 14. 应用配置 ==================== + # ==================== 12. 应用功能 ==================== 'app': { 'title': 'Application', 'icon': 'appstore', - 'order': 14, + 'order': 12, 'items': [ { 'key': 'CORS_ORIGINS', 'label': 'CORS Origins', 'type': 'text', 'default': '*', - 'description': 'Allowed CORS origins (* for all, or comma-separated list)' - }, - { - 'key': 'RATE_LIMIT', - 'label': 'Rate Limit (req/min)', - 'type': 'number', - 'default': '100', - 'description': 'API rate limit per IP per minute' - }, - { - 'key': 'ENABLE_CACHE', - 'label': 'Enable Cache', - 'type': 'boolean', - 'default': 'False', - 'description': 'Enable response caching for improved performance' - }, - { - 'key': 'ENABLE_REQUEST_LOG', - 'label': 'Enable Request Log', - 'type': 'boolean', - 'default': 'True', - 'description': 'Log all API requests for debugging' + 'description': 'Allowed CORS origins (* for all, or comma-separated URLs)' }, { 'key': 'ENABLE_AI_ANALYSIS', diff --git a/backend_api_python/app/routes/user.py b/backend_api_python/app/routes/user.py index 88edd62..2b3e322 100644 --- a/backend_api_python/app/routes/user.py +++ b/backend_api_python/app/routes/user.py @@ -985,11 +985,61 @@ def get_system_strategies(): 'updated_at': updated_at }) - # Compute summary stats - all_running = [i for i in items if i['status'] == 'running'] - total_capital = sum(i['initial_capital'] for i in items) - total_system_pnl = sum(i['total_pnl'] for i in items) - total_running = len(all_running) + # Compute summary stats from all matched strategies (not just current page items). + with get_db_connection() as db: + cur = db.cursor() + + # Aggregate strategy counts/capital by execution mode and running status. + agg_sql = f""" + SELECT + COUNT(*) AS total_strategies, + COALESCE(SUM(s.initial_capital), 0) AS total_capital, + COALESCE(SUM(CASE WHEN s.status = 'running' THEN 1 ELSE 0 END), 0) AS running_strategies, + COALESCE(SUM(CASE WHEN s.execution_mode = 'live' THEN 1 ELSE 0 END), 0) AS live_strategies, + COALESCE(SUM(CASE WHEN s.execution_mode = 'signal' THEN 1 ELSE 0 END), 0) AS signal_strategies, + COALESCE(SUM(CASE WHEN s.status = 'running' AND s.execution_mode = 'live' THEN 1 ELSE 0 END), 0) AS running_live_strategies, + COALESCE(SUM(CASE WHEN s.status = 'running' AND s.execution_mode = 'signal' THEN 1 ELSE 0 END), 0) AS running_signal_strategies, + COALESCE(SUM(CASE WHEN s.execution_mode = 'live' THEN s.initial_capital ELSE 0 END), 0) AS live_capital, + COALESCE(SUM(CASE WHEN s.execution_mode = 'signal' THEN s.initial_capital ELSE 0 END), 0) AS signal_capital + FROM qd_strategies_trading s + LEFT JOIN qd_users u ON u.id = s.user_id + {where_clause} + """ + cur.execute(agg_sql, tuple(params)) + agg_row = cur.fetchone() or {} + + # Aggregate unrealized pnl from current positions. + unreal_sql = f""" + SELECT COALESCE(SUM(p.unrealized_pnl), 0) AS total_unrealized, + COALESCE(SUM(CASE WHEN s.execution_mode = 'live' THEN p.unrealized_pnl ELSE 0 END), 0) AS live_unrealized, + COALESCE(SUM(CASE WHEN s.execution_mode = 'signal' THEN p.unrealized_pnl ELSE 0 END), 0) AS signal_unrealized + FROM qd_strategy_positions p + JOIN qd_strategies_trading s ON s.id = p.strategy_id + LEFT JOIN qd_users u ON u.id = s.user_id + {where_clause} + """ + cur.execute(unreal_sql, tuple(params)) + unreal_row = cur.fetchone() or {} + + # Aggregate realized pnl from trade history. + realized_sql = f""" + SELECT COALESCE(SUM(t.profit), 0) AS total_realized, + COALESCE(SUM(CASE WHEN s.execution_mode = 'live' THEN t.profit ELSE 0 END), 0) AS live_realized, + COALESCE(SUM(CASE WHEN s.execution_mode = 'signal' THEN t.profit ELSE 0 END), 0) AS signal_realized + FROM qd_strategy_trades t + JOIN qd_strategies_trading s ON s.id = t.strategy_id + LEFT JOIN qd_users u ON u.id = s.user_id + {where_clause} + """ + cur.execute(realized_sql, tuple(params)) + realized_row = cur.fetchone() or {} + cur.close() + + total_capital = float(agg_row.get('total_capital') or 0) + total_running = int(agg_row.get('running_strategies') or 0) + total_system_pnl = float(unreal_row.get('total_unrealized') or 0) + float(realized_row.get('total_realized') or 0) + live_pnl = float(unreal_row.get('live_unrealized') or 0) + float(realized_row.get('live_realized') or 0) + signal_pnl = float(unreal_row.get('signal_unrealized') or 0) + float(realized_row.get('signal_realized') or 0) return jsonify({ 'code': 1, @@ -1000,11 +1050,19 @@ def get_system_strategies(): 'page': page, 'page_size': page_size, 'summary': { - 'total_strategies': total, + 'total_strategies': int(agg_row.get('total_strategies') or total), 'running_strategies': total_running, 'total_capital': round(total_capital, 2), 'total_pnl': round(total_system_pnl, 4), - 'total_roi': round((total_system_pnl / total_capital * 100) if total_capital > 0 else 0, 2) + 'total_roi': round((total_system_pnl / total_capital * 100) if total_capital > 0 else 0, 2), + 'live_strategies': int(agg_row.get('live_strategies') or 0), + 'signal_strategies': int(agg_row.get('signal_strategies') or 0), + 'running_live_strategies': int(agg_row.get('running_live_strategies') or 0), + 'running_signal_strategies': int(agg_row.get('running_signal_strategies') or 0), + 'live_capital': round(float(agg_row.get('live_capital') or 0), 2), + 'signal_capital': round(float(agg_row.get('signal_capital') or 0), 2), + 'live_pnl': round(live_pnl, 4), + 'signal_pnl': round(signal_pnl, 4) } } }) diff --git a/backend_api_python/app/services/billing_service.py b/backend_api_python/app/services/billing_service.py index d90f124..aac2a6d 100644 --- a/backend_api_python/app/services/billing_service.py +++ b/backend_api_python/app/services/billing_service.py @@ -10,7 +10,7 @@ Billing Service - 统一计费服务 """ import os import time -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta from decimal import Decimal from typing import Dict, Any, Optional, Tuple @@ -27,7 +27,8 @@ BILLING_CONFIG_PREFIX = 'BILLING_' DEFAULT_BILLING_CONFIG = { # 全局开关 'enabled': False, # 是否启用计费 - 'vip_bypass': True, # VIP用户是否免费 + # IMPORTANT: VIP 不再默认免扣积分(VIP 仅对“VIP免费指标”生效) + 'vip_bypass': False, # VIP用户是否免费(功能计费层面的旁路,默认关闭) # 各功能积分消耗(0表示免费) 'cost_ai_analysis': 10, # AI分析 每次消耗积分 @@ -127,10 +128,15 @@ class BillingService: try: with get_db_connection() as db: cur = db.cursor() - cur.execute( - "SELECT vip_expires_at FROM qd_users WHERE id = ?", - (user_id,) - ) + # Ensure lifetime membership monthly credits are granted (best-effort, silent on failure). + self._ensure_membership_schema_best_effort(cur) + self._grant_lifetime_monthly_credits_best_effort(cur, user_id) + try: + db.commit() + except Exception: + pass + + cur.execute("SELECT vip_expires_at FROM qd_users WHERE id = ?", (user_id,)) row = cur.fetchone() cur.close() @@ -152,6 +158,298 @@ class BillingService: except Exception as e: logger.error(f"get_user_vip_status failed: {e}") return False, None + + # ==================== Membership Plans (VIP) ==================== + + def get_membership_plans(self) -> Dict[str, Any]: + """ + Get membership plans from .env (configured via Settings UI). + + Plan keys: + - monthly: price_usd, credits_once, duration_days + - yearly: price_usd, credits_once, duration_days + - lifetime: price_usd, credits_monthly (granted every 30 days) + """ + def _f(key: str, default: float) -> float: + try: + return float(os.getenv(key, str(default)).strip()) + except Exception: + return float(default) + + def _i(key: str, default: int) -> int: + try: + return int(float(os.getenv(key, str(default)).strip())) + except Exception: + return int(default) + + return { + "monthly": { + "plan": "monthly", + "price_usd": _f("MEMBERSHIP_MONTHLY_PRICE_USD", 19.9), + "credits_once": _i("MEMBERSHIP_MONTHLY_CREDITS", 500), + "duration_days": 30, + }, + "yearly": { + "plan": "yearly", + "price_usd": _f("MEMBERSHIP_YEARLY_PRICE_USD", 199.0), + "credits_once": _i("MEMBERSHIP_YEARLY_CREDITS", 8000), + "duration_days": 365, + }, + "lifetime": { + "plan": "lifetime", + "price_usd": _f("MEMBERSHIP_LIFETIME_PRICE_USD", 499.0), + # Lifetime: monthly credits granted periodically + "credits_monthly": _i("MEMBERSHIP_LIFETIME_MONTHLY_CREDITS", 800), + }, + } + + def purchase_membership(self, user_id: int, plan: str) -> Tuple[bool, str, Dict[str, Any]]: + """ + Purchase membership plan (mock payment: immediately activates). + + NOTE: Real payment gateway can be integrated later; this function is the single activation point. + """ + plan = (plan or "").strip().lower() + plans = self.get_membership_plans() + if plan not in plans: + return False, "invalid_plan", {} + + try: + with get_db_connection() as db: + cur = db.cursor() + self._ensure_membership_schema_best_effort(cur) + self._ensure_membership_orders_table_best_effort(cur) + + now = datetime.now(timezone.utc) + + # Read current VIP expiry to support stacking for monthly/yearly. + cur.execute("SELECT vip_expires_at FROM qd_users WHERE id = ?", (user_id,)) + row = cur.fetchone() or {} + current_expires = row.get("vip_expires_at") + if isinstance(current_expires, str) and current_expires: + try: + current_expires = datetime.fromisoformat(current_expires.replace("Z", "+00:00")) + except Exception: + current_expires = None + if current_expires and current_expires.tzinfo is None: + current_expires = current_expires.replace(tzinfo=timezone.utc) + + base_time = current_expires if (current_expires and current_expires > now) else now + + vip_expires_at = None + vip_plan = plan + vip_is_lifetime = False + + if plan in ("monthly", "yearly"): + days = int(plans[plan].get("duration_days") or (30 if plan == "monthly" else 365)) + vip_expires_at = base_time + timedelta(days=days) + else: + # Lifetime: set very long expiry + mark lifetime flag + vip_expires_at = now + timedelta(days=365 * 100) + vip_is_lifetime = True + + # Create order record (mock paid) + order_plan = plan + order_price_usd = float(plans[plan].get("price_usd") or 0) + order_id = None + try: + cur.execute( + """ + INSERT INTO qd_membership_orders + (user_id, plan, price_usd, status, created_at, paid_at) + VALUES (?, ?, ?, 'paid', NOW(), NOW()) + RETURNING id + """, + (user_id, order_plan, order_price_usd), + ) + row2 = cur.fetchone() or {} + order_id = row2.get("id") + except Exception: + # Fallback for DB drivers without RETURNING support + cur.execute( + """ + INSERT INTO qd_membership_orders + (user_id, plan, price_usd, status, created_at, paid_at) + VALUES (?, ?, ?, 'paid', NOW(), NOW()) + """, + (user_id, order_plan, order_price_usd), + ) + order_id = getattr(cur, "lastrowid", None) + order_ref = str(order_id or "") + + # Update user VIP fields + cur.execute( + """ + UPDATE qd_users + SET vip_expires_at = ?, + vip_plan = ?, + vip_is_lifetime = ?, + updated_at = NOW() + WHERE id = ? + """, + (vip_expires_at, vip_plan, 1 if vip_is_lifetime else 0, user_id), + ) + + # Credits grants + if plan in ("monthly", "yearly"): + credits_once = int(plans[plan].get("credits_once") or 0) + if credits_once > 0: + # Use add_credits to update balance and log + # NOTE: add_credits opens its own connection, so we do a direct update here for atomicity. + self._add_credits_in_tx(cur, user_id, credits_once, action="membership_bonus", + remark=f"Membership bonus ({plan})", reference_id=order_ref) + else: + # Lifetime: grant first month's credits immediately and set last grant time + monthly_credits = int(plans["lifetime"].get("credits_monthly") or 0) + if monthly_credits > 0: + self._add_credits_in_tx(cur, user_id, monthly_credits, action="membership_monthly", + remark="Lifetime membership monthly credits", reference_id=order_ref) + try: + cur.execute( + "UPDATE qd_users SET vip_monthly_credits_last_grant = ?, updated_at = NOW() WHERE id = ?", + (now, user_id), + ) + except Exception: + # Column may not exist; ignore + pass + + # VIP log entry (for audit) + cur.execute( + """ + INSERT INTO qd_credits_log + (user_id, action, amount, balance_after, remark, operator_id, reference_id, created_at) + VALUES (?, 'membership_purchase', 0, + (SELECT credits FROM qd_users WHERE id = ?), + ?, NULL, ?, NOW()) + """, + (user_id, user_id, f"Membership purchased: {plan}", order_ref), + ) + + db.commit() + cur.close() + + return True, "success", { + "order_id": order_id, + "plan": plan, + "vip_expires_at": vip_expires_at.isoformat() if vip_expires_at else None, + } + except Exception as e: + logger.error(f"purchase_membership failed: {e}", exc_info=True) + return False, f"error:{str(e)}", {} + + def _ensure_membership_schema_best_effort(self, cur): + """Best-effort schema upgrade for membership fields on qd_users.""" + try: + # vip_plan / vip_is_lifetime / vip_monthly_credits_last_grant + cur.execute("ALTER TABLE qd_users ADD COLUMN IF NOT EXISTS vip_plan VARCHAR(20) DEFAULT ''") + cur.execute("ALTER TABLE qd_users ADD COLUMN IF NOT EXISTS vip_is_lifetime BOOLEAN DEFAULT FALSE") + cur.execute("ALTER TABLE qd_users ADD COLUMN IF NOT EXISTS vip_monthly_credits_last_grant TIMESTAMP") + except Exception: + # Ignore schema upgrade failures (e.g., insufficient privileges) + pass + + def _ensure_membership_orders_table_best_effort(self, cur): + """Best-effort create membership orders table (mock payment).""" + try: + cur.execute( + """ + CREATE TABLE IF NOT EXISTS qd_membership_orders ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + plan VARCHAR(20) NOT NULL, + price_usd DECIMAL(10,2) DEFAULT 0, + status VARCHAR(20) DEFAULT 'paid', + created_at TIMESTAMP DEFAULT NOW(), + paid_at TIMESTAMP + ) + """ + ) + except Exception: + pass + + def _add_credits_in_tx(self, cur, user_id: int, amount: int, action: str, remark: str, reference_id: str = ''): + """Add credits within an existing DB transaction and write qd_credits_log.""" + try: + cur.execute("SELECT credits FROM qd_users WHERE id = ?", (user_id,)) + row = cur.fetchone() or {} + credits = Decimal(str(row.get("credits", 0) or 0)) + new_balance = credits + Decimal(str(amount)) + + cur.execute("UPDATE qd_users SET credits = ?, updated_at = NOW() WHERE id = ?", (float(new_balance), user_id)) + cur.execute( + """ + INSERT INTO qd_credits_log + (user_id, action, amount, balance_after, remark, operator_id, reference_id, created_at) + VALUES (?, ?, ?, ?, ?, NULL, ?, NOW()) + """, + (user_id, action, amount, float(new_balance), remark, reference_id), + ) + except Exception as e: + logger.debug(f"_add_credits_in_tx failed: {e}", exc_info=True) + + def _grant_lifetime_monthly_credits_best_effort(self, cur, user_id: int): + """Grant lifetime monthly credits if due (best-effort).""" + try: + plans = self.get_membership_plans() + monthly_credits = int(plans.get("lifetime", {}).get("credits_monthly") or 0) + if monthly_credits <= 0: + return + + cur.execute( + "SELECT vip_is_lifetime, vip_expires_at, vip_monthly_credits_last_grant FROM qd_users WHERE id = ?", + (user_id,), + ) + row = cur.fetchone() or {} + if not row.get("vip_is_lifetime"): + return + + expires_at = row.get("vip_expires_at") + if isinstance(expires_at, str) and expires_at: + try: + expires_at = datetime.fromisoformat(expires_at.replace("Z", "+00:00")) + except Exception: + expires_at = None + if expires_at and expires_at.tzinfo is None: + expires_at = expires_at.replace(tzinfo=timezone.utc) + now = datetime.now(timezone.utc) + if expires_at and expires_at <= now: + return + + last = row.get("vip_monthly_credits_last_grant") + if isinstance(last, str) and last: + try: + last = datetime.fromisoformat(last.replace("Z", "+00:00")) + except Exception: + last = None + if last and last.tzinfo is None: + last = last.replace(tzinfo=timezone.utc) + + # First time: do nothing (purchase flow already grants), but set last to now if missing + if not last: + cur.execute( + "UPDATE qd_users SET vip_monthly_credits_last_grant = ?, updated_at = NOW() WHERE id = ?", + (now, user_id), + ) + return + + # Use 30-day periods. Catch up up to 6 periods max to avoid abuse. + delta_days = int((now - last).total_seconds() // 86400) + periods = delta_days // 30 + if periods <= 0: + return + if periods > 6: + periods = 6 + + total = monthly_credits * periods + self._add_credits_in_tx(cur, user_id, total, action="membership_monthly", + remark=f"Lifetime membership monthly credits x{periods}", reference_id="") + cur.execute( + "UPDATE qd_users SET vip_monthly_credits_last_grant = ?, updated_at = NOW() WHERE id = ?", + (now, user_id), + ) + except Exception: + # Best-effort; never break caller + pass def check_and_consume(self, user_id: int, feature: str, reference_id: str = '') -> Tuple[bool, str]: """ @@ -420,7 +718,7 @@ class BillingService: 'is_vip': is_vip, 'vip_expires_at': vip_expires_at.isoformat() if vip_expires_at else None, 'billing_enabled': config.get('enabled', False), - 'vip_bypass': config.get('vip_bypass', True), + 'vip_bypass': config.get('vip_bypass', False), # Public support link for credits recharge / VIP purchase 'recharge_telegram_url': os.getenv('RECHARGE_TELEGRAM_URL', '').strip() or 'https://t.me/your_support_bot', # 功能费用(供前端显示) diff --git a/backend_api_python/app/services/community_service.py b/backend_api_python/app/services/community_service.py index 19aaebe..a7f80d3 100644 --- a/backend_api_python/app/services/community_service.py +++ b/backend_api_python/app/services/community_service.py @@ -3,6 +3,7 @@ Community Service - 指标社区服务 处理指标市场、购买、评论等功能。 """ +import json import time from decimal import Decimal from typing import Dict, Any, List, Optional, Tuple @@ -19,6 +20,15 @@ class CommunityService: def __init__(self): self.billing = get_billing_service() + # Best-effort: ensure vip_free column exists (for old databases) + try: + with get_db_connection() as db: + cur = db.cursor() + cur.execute("ALTER TABLE qd_indicator_codes ADD COLUMN IF NOT EXISTS vip_free BOOLEAN DEFAULT FALSE") + db.commit() + cur.close() + except Exception: + pass # ========================================== # 指标市场 @@ -78,7 +88,7 @@ class CommunityService: # 获取列表(联表查询作者信息) query_sql = f""" SELECT - i.id, i.name, i.description, i.pricing_type, i.price, + i.id, i.name, i.description, i.pricing_type, i.price, COALESCE(i.vip_free, FALSE) as vip_free, i.preview_image, i.purchase_count, i.avg_rating, i.rating_count, i.view_count, i.created_at, i.updated_at, u.id as author_id, u.username as author_username, @@ -115,6 +125,7 @@ class CommunityService: 'description': row['description'][:200] if row['description'] else '', 'pricing_type': row['pricing_type'] or 'free', 'price': float(row['price'] or 0), + 'vip_free': bool(row.get('vip_free') or False), 'preview_image': row['preview_image'] or '', 'purchase_count': row['purchase_count'] or 0, 'avg_rating': float(row['avg_rating'] or 0), @@ -152,7 +163,7 @@ class CommunityService: # 获取指标信息 cur.execute(""" SELECT - i.id, i.name, i.description, i.pricing_type, i.price, + i.id, i.name, i.description, i.pricing_type, i.price, COALESCE(i.vip_free, FALSE) as vip_free, i.preview_image, i.purchase_count, i.avg_rating, i.rating_count, i.view_count, i.publish_to_community, i.created_at, i.updated_at, i.user_id, @@ -196,6 +207,7 @@ class CommunityService: 'description': row['description'] or '', 'pricing_type': row['pricing_type'] or 'free', 'price': float(row['price'] or 0), + 'vip_free': bool(row.get('vip_free') or False), 'preview_image': row['preview_image'] or '', 'purchase_count': row['purchase_count'] or 0, 'avg_rating': float(row['avg_rating'] or 0), @@ -234,7 +246,7 @@ class CommunityService: # 1. 获取指标信息 cur.execute(""" - SELECT id, user_id, name, code, description, pricing_type, price, + SELECT id, user_id, name, code, description, pricing_type, price, COALESCE(vip_free, FALSE) as vip_free, preview_image, is_encrypted FROM qd_indicator_codes WHERE id = ? AND publish_to_community = 1 @@ -248,6 +260,11 @@ class CommunityService: seller_id = indicator['user_id'] price = float(indicator['price'] or 0) pricing_type = indicator['pricing_type'] or 'free' + vip_free = bool(indicator.get('vip_free') or False) + is_vip, _ = self.billing.get_user_vip_status(buyer_id) + + # VIP-free indicator: VIP users can get it without credits charge + effective_price = 0.0 if (vip_free and is_vip) else price # 2. 检查是否购买自己的指标 if seller_id == buyer_id: @@ -264,17 +281,17 @@ class CommunityService: return False, 'already_purchased', {} # 4. 如果是付费指标,检查并扣除积分 - if pricing_type != 'free' and price > 0: + if pricing_type != 'free' and effective_price > 0: buyer_credits = self.billing.get_user_credits(buyer_id) - if buyer_credits < price: + if buyer_credits < effective_price: cur.close() return False, 'insufficient_credits', { - 'required': price, + 'required': effective_price, 'current': float(buyer_credits) } # 扣除买家积分 - new_buyer_balance = buyer_credits - Decimal(str(price)) + new_buyer_balance = buyer_credits - Decimal(str(effective_price)) cur.execute( "UPDATE qd_users SET credits = ?, updated_at = NOW() WHERE id = ?", (float(new_buyer_balance), buyer_id) @@ -285,12 +302,12 @@ class CommunityService: INSERT INTO qd_credits_log (user_id, action, amount, balance_after, feature, reference_id, remark, created_at) VALUES (?, 'indicator_purchase', ?, ?, 'indicator_purchase', ?, ?, NOW()) - """, (buyer_id, -price, float(new_buyer_balance), str(indicator_id), + """, (buyer_id, -effective_price, float(new_buyer_balance), str(indicator_id), f"购买指标: {indicator['name']}")) # 给卖家增加积分(可配置抽成比例,这里先100%给卖家) seller_credits = self.billing.get_user_credits(seller_id) - new_seller_balance = seller_credits + Decimal(str(price)) + new_seller_balance = seller_credits + Decimal(str(effective_price)) cur.execute( "UPDATE qd_users SET credits = ?, updated_at = NOW() WHERE id = ?", (float(new_seller_balance), seller_id) @@ -301,7 +318,7 @@ class CommunityService: INSERT INTO qd_credits_log (user_id, action, amount, balance_after, feature, reference_id, remark, created_at) VALUES (?, 'indicator_sale', ?, ?, 'indicator_sale', ?, ?, NOW()) - """, (seller_id, price, float(new_seller_balance), str(indicator_id), + """, (seller_id, effective_price, float(new_seller_balance), str(indicator_id), f"出售指标: {indicator['name']}")) # 5. 创建购买记录 @@ -309,16 +326,16 @@ class CommunityService: INSERT INTO qd_indicator_purchases (indicator_id, buyer_id, seller_id, price, created_at) VALUES (?, ?, ?, ?, NOW()) - """, (indicator_id, buyer_id, seller_id, price)) + """, (indicator_id, buyer_id, seller_id, effective_price)) # 6. 复制指标到买家账户 now_ts = int(time.time()) cur.execute(""" INSERT INTO qd_indicator_codes (user_id, is_buy, end_time, name, code, description, - publish_to_community, pricing_type, price, is_encrypted, preview_image, + publish_to_community, pricing_type, price, is_encrypted, preview_image, vip_free, createtime, updatetime, created_at, updated_at) - VALUES (?, 1, 0, ?, ?, ?, 0, 'free', 0, ?, ?, ?, ?, NOW(), NOW()) + VALUES (?, 1, 0, ?, ?, ?, 0, 'free', 0, ?, ?, 0, ?, ?, NOW(), NOW()) """, ( buyer_id, indicator['name'], @@ -339,8 +356,8 @@ class CommunityService: db.commit() cur.close() - logger.info(f"User {buyer_id} purchased indicator {indicator_id} for {price} credits") - return True, 'success', {'indicator_name': indicator['name'], 'price': price} + logger.info(f"User {buyer_id} purchased indicator {indicator_id} for {effective_price} credits (vip_free={vip_free}, is_vip={is_vip})") + return True, 'success', {'indicator_name': indicator['name'], 'price': price, 'charged': effective_price, 'vip_free': vip_free} except Exception as e: logger.error(f"purchase_indicator failed: {e}") @@ -864,14 +881,16 @@ class CommunityService: return {'pending': 0, 'approved': 0, 'rejected': 0} # ========================================== - # 实盘表现(聚合回测数据) + # 实盘表现(聚合回测 + 实盘交易数据) # ========================================== - + def get_indicator_performance(self, indicator_id: int) -> Dict[str, Any]: """ 获取指标的实盘表现统计 - - 目前基于回测数据统计,未来可扩展为实盘交易数据 + + 数据来源: + 1. qd_backtest_runs - 回测记录(result_json 内含 totalReturn / winRate 等) + 2. qd_strategy_trades + qd_strategies_trading - 真实实盘交易记录 """ default_result = { 'strategy_count': 0, @@ -881,49 +900,125 @@ class CommunityService: 'avg_return': 0, 'max_drawdown': 0 } - + try: with get_db_connection() as db: cur = db.cursor() - - # 首先检查回测记录表是否存在 - cur.execute(""" - SELECT COUNT(*) as cnt FROM information_schema.tables - WHERE table_name = 'qd_backtest_runs' - """) - table_exists = cur.fetchone() - if not table_exists or table_exists['cnt'] == 0: - cur.close() - return default_result - - # 从回测记录中统计该指标的表现 - # 使用 indicator_id 字段匹配 - cur.execute(""" - SELECT - COUNT(*) as run_count, - AVG(CASE WHEN total_return IS NOT NULL THEN total_return ELSE 0 END) as avg_return, - AVG(CASE WHEN win_rate IS NOT NULL THEN win_rate ELSE 0 END) as avg_win_rate, - AVG(CASE WHEN max_drawdown IS NOT NULL THEN max_drawdown ELSE 0 END) as avg_drawdown, - SUM(CASE WHEN trade_count IS NOT NULL THEN trade_count ELSE 0 END) as total_trades - FROM qd_backtest_runs - WHERE indicator_id = ? - """, (indicator_id,)) - - row = cur.fetchone() + + # ---------- Part 1: 回测数据(从 result_json 解析) ---------- + bt_returns = [] + bt_win_rates = [] + bt_drawdowns = [] + bt_trade_counts = [] + + try: + cur.execute(""" + SELECT result_json + FROM qd_backtest_runs + WHERE indicator_id = %s AND status = 'success' + AND result_json IS NOT NULL AND result_json != '' + """, (indicator_id,)) + rows = cur.fetchall() + + for row in rows: + try: + rj = json.loads(row['result_json']) if isinstance(row['result_json'], str) else {} + tr = float(rj.get('totalReturn', 0) or 0) + wr = float(rj.get('winRate', 0) or 0) + md = float(rj.get('maxDrawdown', 0) or 0) + tc = int(rj.get('totalTrades', 0) or 0) + bt_returns.append(tr) + bt_win_rates.append(wr) + bt_drawdowns.append(md) + bt_trade_counts.append(tc) + except (json.JSONDecodeError, TypeError, ValueError): + continue + except Exception: + logger.debug("Backtest runs query skipped or failed", exc_info=True) + + bt_run_count = len(bt_returns) + + # ---------- Part 2: 实盘交易数据 ---------- + live_strategy_count = 0 + live_trade_count = 0 + live_win_rate = 0.0 + live_total_profit = 0.0 + + try: + # 找出使用该指标的策略(indicator_config JSON 中 indicator_id 匹配) + cur.execute(""" + SELECT id FROM qd_strategies_trading + WHERE indicator_config::text LIKE %s + """, (f'%"indicator_id": {indicator_id}%',)) + strategy_rows = cur.fetchall() + + # 也尝试匹配无空格的格式 + if not strategy_rows: + cur.execute(""" + SELECT id FROM qd_strategies_trading + WHERE indicator_config::text LIKE %s + """, (f'%"indicator_id":{indicator_id}%',)) + strategy_rows = cur.fetchall() + + if strategy_rows: + strategy_ids = [r['id'] for r in strategy_rows] + live_strategy_count = len(strategy_ids) + + placeholders = ','.join(['%s'] * len(strategy_ids)) + cur.execute(f""" + SELECT + COUNT(*) as trade_count, + SUM(CASE WHEN profit > 0 THEN 1 ELSE 0 END) as win_count, + SUM(profit) as total_profit + FROM qd_strategy_trades + WHERE strategy_id IN ({placeholders}) + AND profit != 0 + """, tuple(strategy_ids)) + trade_row = cur.fetchone() + + if trade_row and (trade_row['trade_count'] or 0) > 0: + live_trade_count = int(trade_row['trade_count'] or 0) + win_count = int(trade_row['win_count'] or 0) + live_win_rate = round(win_count / live_trade_count * 100, 2) if live_trade_count > 0 else 0.0 + live_total_profit = round(float(trade_row['total_profit'] or 0), 2) + except Exception: + logger.debug("Live trading query skipped or failed", exc_info=True) + cur.close() - - if not row or row['run_count'] == 0: + + # ---------- Combine results ---------- + total_strategy_count = bt_run_count + live_strategy_count + total_trade_count = sum(bt_trade_counts) + live_trade_count + + # 综合胜率:优先实盘 > 回测平均 + if live_trade_count > 0: + combined_win_rate = live_win_rate + elif bt_win_rates: + combined_win_rate = round(sum(bt_win_rates) / len(bt_win_rates), 2) + else: + combined_win_rate = 0.0 + + # 平均收益率(回测 totalReturn %) + avg_return = round(sum(bt_returns) / len(bt_returns), 2) if bt_returns else 0.0 + + # 总利润:优先用实盘绝对利润,无实盘则显示回测平均收益率 + combined_profit = live_total_profit if live_trade_count > 0 else avg_return + + # 最大回撤取回测中最差的(maxDrawdown 是负数,取最小即最差) + avg_drawdown = round(min(bt_drawdowns), 2) if bt_drawdowns else 0.0 + + if total_strategy_count == 0 and total_trade_count == 0: return default_result - + return { - 'strategy_count': row['run_count'] or 0, - 'trade_count': row['total_trades'] or 0, - 'win_rate': round(float(row['avg_win_rate'] or 0), 2), - 'total_profit': round(float(row['avg_return'] or 0), 2), - 'avg_return': round(float(row['avg_return'] or 0), 2), - 'max_drawdown': round(float(row['avg_drawdown'] or 0), 2) + 'strategy_count': total_strategy_count, + 'trade_count': total_trade_count, + 'win_rate': combined_win_rate, + 'total_profit': round(combined_profit, 2), + 'avg_return': avg_return, + 'max_drawdown': avg_drawdown } - + except Exception as e: logger.error(f"get_indicator_performance failed: {e}") return default_result diff --git a/backend_api_python/app/services/ibkr_trading/README.md b/backend_api_python/app/services/ibkr_trading/README.md index de89c41..9d703ea 100644 --- a/backend_api_python/app/services/ibkr_trading/README.md +++ b/backend_api_python/app/services/ibkr_trading/README.md @@ -1,6 +1,6 @@ # Interactive Brokers Trading Module -Supports US stocks and Hong Kong stocks trading via TWS or IB Gateway. +Supports US stocks trading via TWS or IB Gateway. ## Installation @@ -76,10 +76,10 @@ curl -X POST http://localhost:5000/api/ibkr/order \ -H "Content-Type: application/json" \ -d '{"symbol": "AAPL", "side": "buy", "quantity": 10, "marketType": "USStock"}' -# Limit order: sell 100 shares of Tencent +# Limit order: sell 100 shares of MSFT curl -X POST http://localhost:5000/api/ibkr/order \ -H "Content-Type: application/json" \ - -d '{"symbol": "0700.HK", "side": "sell", "quantity": 100, "marketType": "HShare", "orderType": "limit", "price": 300}' + -d '{"symbol": "MSFT", "side": "sell", "quantity": 100, "marketType": "USStock", "orderType": "limit", "price": 400}' ``` ### Get Positions @@ -93,7 +93,6 @@ curl http://localhost:5000/api/ibkr/positions | Market | Format | Examples | |--------|--------|----------| | US Stock | Ticker symbol | `AAPL`, `TSLA`, `GOOGL` | -| HK Stock | `XXXX.HK` or digits | `0700.HK`, `00700`, `700` | ## Important Notes diff --git a/backend_api_python/app/services/ibkr_trading/__init__.py b/backend_api_python/app/services/ibkr_trading/__init__.py index 8c985f3..d42131f 100644 --- a/backend_api_python/app/services/ibkr_trading/__init__.py +++ b/backend_api_python/app/services/ibkr_trading/__init__.py @@ -1,7 +1,7 @@ """ Interactive Brokers (IBKR) Trading Module -Supports US stocks and Hong Kong stocks trading via TWS or IB Gateway. +Supports US stocks trading via TWS or IB Gateway. Port Reference: - TWS Live: 7497, TWS Paper: 7496 diff --git a/backend_api_python/app/services/ibkr_trading/client.py b/backend_api_python/app/services/ibkr_trading/client.py index 815193d..3dd34c6 100644 --- a/backend_api_python/app/services/ibkr_trading/client.py +++ b/backend_api_python/app/services/ibkr_trading/client.py @@ -180,7 +180,7 @@ class IBKRClient: Args: symbol: Symbol code - market_type: Market type (USStock, HShare) + market_type: Market type (USStock) """ _ensure_ib_insync() @@ -219,7 +219,7 @@ class IBKRClient: symbol: Symbol code (e.g., AAPL, 0700.HK) side: Direction ("buy" or "sell") quantity: Number of shares - market_type: Market type ("USStock" or "HShare") + market_type: Market type ("USStock") Returns: OrderResult diff --git a/backend_api_python/app/services/ibkr_trading/symbols.py b/backend_api_python/app/services/ibkr_trading/symbols.py index 230238d..6f6a089 100644 --- a/backend_api_python/app/services/ibkr_trading/symbols.py +++ b/backend_api_python/app/services/ibkr_trading/symbols.py @@ -13,7 +13,7 @@ def normalize_symbol(symbol: str, market_type: str) -> Tuple[str, str, str]: Args: symbol: Symbol code in the system - market_type: Market type (USStock, HShare) + market_type: Market type (USStock) Returns: (ib_symbol, exchange, currency) @@ -26,22 +26,6 @@ def normalize_symbol(symbol: str, market_type: str) -> Tuple[str, str, str]: # Use SMART routing for best execution return symbol, "SMART", "USD" - elif market_type == "HShare": - # Hong Kong stock formats: - # - 0700.HK -> 700 - # - 00700 -> 700 - # - 700 -> 700 - ib_symbol = symbol - - # Remove .HK suffix - if ib_symbol.endswith(".HK"): - ib_symbol = ib_symbol[:-3] - - # Remove leading zeros - ib_symbol = ib_symbol.lstrip("0") or "0" - - return ib_symbol, "SEHK", "HKD" - else: # Default to US stock return symbol, "SMART", "USD" @@ -59,15 +43,6 @@ def parse_symbol(symbol: str) -> Tuple[str, Optional[str]]: """ symbol = (symbol or "").strip().upper() - # HK stock: ends with .HK or all digits - if symbol.endswith(".HK"): - return symbol, "HShare" - - # All digits (likely HK stock code) - clean = symbol.lstrip("0") - if clean.isdigit() and len(clean) <= 5: - return symbol, "HShare" - # Default to US stock return symbol, "USStock" @@ -83,8 +58,4 @@ def format_display_symbol(ib_symbol: str, exchange: str) -> str: Returns: Display symbol """ - if exchange == "SEHK": - # HK stock: pad to 4 digits, add .HK - padded = ib_symbol.zfill(4) - return f"{padded}.HK" return ib_symbol diff --git a/backend_api_python/app/services/kline.py b/backend_api_python/app/services/kline.py index 5fefa62..d78e876 100644 --- a/backend_api_python/app/services/kline.py +++ b/backend_api_python/app/services/kline.py @@ -30,7 +30,7 @@ class KlineService: 获取K线数据 Args: - market: 市场类型 (Crypto, USStock, AShare, HShare, Forex, Futures) + market: 市场类型 (Crypto, USStock, Forex, Futures) symbol: 交易对/股票代码 timeframe: 时间周期 limit: 数据条数 @@ -76,7 +76,7 @@ class KlineService: 获取实时价格(优先使用 ticker API,降级使用分钟 K 线) Args: - market: 市场类型 (Crypto, USStock, AShare, HShare, Forex, Futures) + market: 市场类型 (Crypto, USStock, Forex, Futures) symbol: 交易对/股票代码 force_refresh: 是否强制刷新(跳过缓存) diff --git a/backend_api_python/app/services/live_trading/execution.py b/backend_api_python/app/services/live_trading/execution.py index 3b2ba9c..c0f45e7 100644 --- a/backend_api_python/app/services/live_trading/execution.py +++ b/backend_api_python/app/services/live_trading/execution.py @@ -3,7 +3,7 @@ Translate a strategy signal into a direct-exchange order call. Supports: - Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex -- Traditional brokers: Interactive Brokers (IBKR) for US/HK stocks +- Traditional brokers: Interactive Brokers (IBKR) for US stocks - Forex brokers: MetaTrader 5 (MT5) """ @@ -203,7 +203,7 @@ def _place_ibkr_order( exchange_config: Optional[Dict[str, Any]] = None, ) -> LiveOrderResult: """ - Place order via IBKR for US/HK stocks. + Place order via IBKR for US stocks. Signal mapping for stocks (no short selling in this implementation): - open_long / add_long -> BUY diff --git a/backend_api_python/app/services/live_trading/factory.py b/backend_api_python/app/services/live_trading/factory.py index 21cfebb..867e79e 100644 --- a/backend_api_python/app/services/live_trading/factory.py +++ b/backend_api_python/app/services/live_trading/factory.py @@ -3,7 +3,7 @@ Factory for direct exchange clients. Supports: - Crypto exchanges: Binance, OKX, Bitget, Bybit, Coinbase, Kraken, KuCoin, Gate, Bitfinex -- Traditional brokers: Interactive Brokers (IBKR) for US/HK stocks +- Traditional brokers: Interactive Brokers (IBKR) for US stocks - Forex brokers: MetaTrader 5 (MT5) """ @@ -131,7 +131,7 @@ def create_client(exchange_config: Dict[str, Any], *, market_type: str = "swap") market_type=mt, ) - # Traditional brokers (IBKR for US/HK stocks only) + # Traditional brokers (IBKR for US stocks only) if exchange_id == "ibkr": # Note: Market category validation should be done at the caller level # This factory only creates clients based on exchange_id @@ -148,7 +148,7 @@ def create_client(exchange_config: Dict[str, Any], *, market_type: str = "swap") def create_ibkr_client(exchange_config: Dict[str, Any]): """ - Create IBKR client for US/HK stock trading. + Create IBKR client for US stock trading. exchange_config should contain: - ibkr_host: TWS/Gateway host (default: 127.0.0.1) diff --git a/backend_api_python/app/services/market_data_collector.py b/backend_api_python/app/services/market_data_collector.py index 962ec77..3b64f66 100644 --- a/backend_api_python/app/services/market_data_collector.py +++ b/backend_api_python/app/services/market_data_collector.py @@ -11,7 +11,7 @@ - 价格/K线: DataSourceFactory (已验证,与K线模块、自选列表一致) - 宏观数据: 复用 global_market.py (VIX, DXY, TNX, Fear&Greed等,带缓存) - 新闻: Finnhub API (结构化数据,无需深度阅读) -- 基本面: Finnhub (美股) / akshare (A股) / 固定描述 (加密) +- 基本面: Finnhub (美股) / 固定描述 (加密) """ import time @@ -59,12 +59,12 @@ class MarketDataCollector: except Exception as e: logger.warning(f"Finnhub client init failed: {e}") - # akshare + # akshare (optional, for supplementary data) try: import akshare as ak self._ak = ak except ImportError: - logger.info("akshare not installed, A-share data will be limited") + logger.info("akshare not installed") def collect_all( self, @@ -79,7 +79,7 @@ class MarketDataCollector: 采集所有市场数据 Args: - market: 市场类型 (USStock, Crypto, AShare, HShare, Forex, Futures) + market: 市场类型 (USStock, Crypto, Forex, Futures) symbol: 标的代码 timeframe: K线周期 include_macro: 是否包含宏观数据 @@ -124,7 +124,7 @@ class MarketDataCollector: } # 如果需要基本面,也并行获取 - if market in ('USStock', 'AShare', 'HShare'): + if market == 'USStock': core_futures[executor.submit(self._get_fundamental, market, symbol)] = "fundamental" core_futures[executor.submit(self._get_company, market, symbol)] = "company" elif market == 'Crypto': @@ -547,10 +547,6 @@ class MarketDataCollector: try: if market == 'USStock': return self._get_us_fundamental(symbol) - elif market == 'AShare': - return self._get_ashare_fundamental(symbol) - elif market == 'HShare': - return self._get_hshare_fundamental(symbol) except Exception as e: logger.warning(f"Fundamental data fetch failed for {market}:{symbol}: {e}") return None @@ -602,56 +598,6 @@ class MarketDataCollector: return result if result else None - def _get_ashare_fundamental(self, symbol: str) -> Optional[Dict[str, Any]]: - """A股基本面 - akshare""" - if not self._ak: - return None - - try: - # 个股指标 - df = self._ak.stock_individual_info_em(symbol=symbol) - if df is not None and not df.empty: - result = {} - for _, row in df.iterrows(): - item = row.get('item', '') - value = row.get('value', '') - if '市盈率' in item: - result['pe_ratio'] = value - elif '市净率' in item: - result['pb_ratio'] = value - elif '总市值' in item: - result['market_cap'] = value - elif 'ROE' in item or '净资产收益率' in item: - result['roe'] = value - elif '每股收益' in item: - result['eps'] = value - return result if result else None - except Exception as e: - logger.debug(f"akshare fundamental failed for {symbol}: {e}") - - return None - - def _get_hshare_fundamental(self, symbol: str) -> Optional[Dict[str, Any]]: - """港股基本面 - yfinance""" - try: - # 港股在yfinance的格式: 0700.HK, 9988.HK - yf_symbol = f"{symbol}.HK" - ticker = yf.Ticker(yf_symbol) - info = ticker.info or {} - - return { - 'pe_ratio': info.get('trailingPE'), - 'pb_ratio': info.get('priceToBook'), - 'market_cap': info.get('marketCap'), - 'dividend_yield': info.get('dividendYield'), - '52w_high': info.get('fiftyTwoWeekHigh'), - '52w_low': info.get('fiftyTwoWeekLow'), - } - except Exception as e: - logger.debug(f"yfinance HShare fundamental failed for {symbol}: {e}") - - return None - def _get_crypto_info(self, symbol: str) -> Optional[Dict[str, Any]]: """加密货币信息 (固定描述为主)""" # 常见加密货币的描述 @@ -717,19 +663,6 @@ class MarketDataCollector: 'website': profile.get('weburl'), } - elif market == 'AShare' and self._ak: - df = self._ak.stock_individual_info_em(symbol=symbol) - if df is not None and not df.empty: - result = {} - for _, row in df.iterrows(): - item = row.get('item', '') - value = row.get('value', '') - if '名称' in item or '简称' in item: - result['name'] = value - elif '行业' in item: - result['industry'] = value - return result if result else None - except Exception as e: logger.debug(f"Company info fetch failed for {market}:{symbol}: {e}") @@ -892,9 +825,8 @@ class MarketDataCollector: 策略(按优先级): 1. 结构化API (Finnhub) - 美股首选 - 2. akshare 多源 - A股首选(东方财富/新浪/同花顺/雪球) - 3. 搜索引擎 (Bocha/Tavily) - 补充搜索 - 4. 情绪分析 - Finnhub 社交媒体情绪 + 2. 搜索引擎 (Bocha/Tavily) - 补充搜索 + 3. 情绪分析 - Finnhub 社交媒体情绪 """ news_list = [] sentiment = {} @@ -912,7 +844,7 @@ class MarketDataCollector: elif market == 'Crypto': # 加密货币通用新闻 raw_news = self._finnhub_client.general_news('crypto', min_id=0) - elif market not in ('AShare', 'HShare'): + else: # 其他市场通用新闻 raw_news = self._finnhub_client.general_news('general', min_id=0) @@ -942,17 +874,7 @@ class MarketDataCollector: except Exception as e: logger.debug(f"Finnhub sentiment fetch failed: {e}") - # === 3) A股多源新闻 (akshare) === - if market == 'AShare' and self._ak: - ashare_news = self._get_ashare_news_multi_source(symbol) - news_list.extend(ashare_news) - - # === 4) 港股新闻 (akshare) === - if market == 'HShare' and self._ak: - hshare_news = self._get_hshare_news(symbol) - news_list.extend(hshare_news) - - # === 5) 搜索引擎补充 (如果新闻太少) === + # === 3) 搜索引擎补充 (如果新闻太少) === if len(news_list) < 5: search_news = self._get_news_from_search(market, symbol, company_name) news_list.extend(search_news) @@ -974,108 +896,6 @@ class MarketDataCollector: "sentiment": sentiment, } - def _get_ashare_news_multi_source(self, symbol: str) -> List[Dict[str, Any]]: - """ - A股多源新闻获取 - - 来源(按优先级): - 1. 东方财富个股新闻 (stock_news_em) - 2. 新浪财经滚动新闻 (stock_news_sina) - 3. 同花顺个股新闻 (stock_news_ths) - 4. 雪球热帖 (stock_xuqiu) - """ - news_list = [] - - # 1) 东方财富个股新闻 - try: - df = self._ak.stock_news_em(symbol=symbol) - if df is not None and not df.empty: - for _, row in df.head(8).iterrows(): - news_list.append({ - "datetime": str(row.get('发布时间', ''))[:16], - "headline": row.get('新闻标题', ''), - "summary": row.get('新闻内容', '')[:200] if row.get('新闻内容') else '', - "source": "东方财富", - "url": row.get('新闻链接', ''), - "sentiment": 'neutral', - }) - logger.debug(f"东方财富新闻: {len(df)} 条") - except Exception as e: - logger.debug(f"东方财富新闻获取失败: {e}") - - # 2) 新浪财经个股新闻 - try: - # 注意:akshare 的新浪新闻接口可能需要股票名称而非代码 - df = self._ak.stock_news_sina(symbol=symbol) - if df is not None and not df.empty: - for _, row in df.head(5).iterrows(): - title = row.get('title', '') or row.get('新闻标题', '') - if title and title not in [n.get('headline') for n in news_list]: - news_list.append({ - "datetime": str(row.get('time', row.get('发布时间', '')))[:16], - "headline": title, - "summary": row.get('content', row.get('新闻内容', ''))[:200] if row.get('content') or row.get('新闻内容') else '', - "source": "新浪财经", - "url": row.get('url', row.get('新闻链接', '')), - "sentiment": 'neutral', - }) - logger.debug(f"新浪财经新闻: {len(df)} 条") - except Exception as e: - logger.debug(f"新浪财经新闻获取失败: {e}") - - # 3) 同花顺个股新闻 - try: - df = self._ak.stock_news_ths(symbol=symbol) - if df is not None and not df.empty: - for _, row in df.head(5).iterrows(): - title = row.get('标题', '') or row.get('title', '') - if title and title not in [n.get('headline') for n in news_list]: - news_list.append({ - "datetime": str(row.get('发布时间', row.get('time', '')))[:16], - "headline": title, - "summary": row.get('内容', row.get('content', ''))[:200] if row.get('内容') or row.get('content') else '', - "source": "同花顺", - "url": row.get('链接', row.get('url', '')), - "sentiment": 'neutral', - }) - logger.debug(f"同花顺新闻: {len(df)} 条") - except Exception as e: - logger.debug(f"同花顺新闻获取失败: {e}") - - # 4) 雪球热帖(社区讨论) - try: - df = self._ak.stock_xuqiu(symbol=symbol) - if df is not None and not df.empty: - for _, row in df.head(3).iterrows(): - title = row.get('标题', '') or row.get('title', '') - if title and title not in [n.get('headline') for n in news_list]: - news_list.append({ - "datetime": str(row.get('发布时间', row.get('time', '')))[:16], - "headline": title, - "summary": row.get('内容摘要', row.get('content', ''))[:200] if row.get('内容摘要') or row.get('content') else '', - "source": "雪球", - "url": row.get('链接', row.get('url', '')), - "sentiment": 'neutral', - }) - logger.debug(f"雪球热帖: {len(df)} 条") - except Exception as e: - logger.debug(f"雪球热帖获取失败: {e}") - - return news_list - - def _get_hshare_news(self, symbol: str) -> List[Dict[str, Any]]: - """港股新闻获取""" - news_list = [] - - try: - # 港股新闻 (如果 akshare 支持) - df = self._ak.stock_hk_spot_em() - # 港股一般没有专门的新闻接口,可以通过搜索补充 - except Exception as e: - logger.debug(f"港股新闻获取失败: {e}") - - return news_list - def _get_news_from_search( self, market: str, symbol: str, company_name: str = None ) -> List[Dict[str, Any]]: diff --git a/backend_api_python/app/services/pending_order_worker.py b/backend_api_python/app/services/pending_order_worker.py index f9d3d14..57377c5 100644 --- a/backend_api_python/app/services/pending_order_worker.py +++ b/backend_api_python/app/services/pending_order_worker.py @@ -838,19 +838,19 @@ class PendingOrderWorker: market_category = str(cfg.get("market_category") or "Crypto").strip() # Validate market category and exchange_id combination for live trading - # AShare and Futures do not support live trading - if market_category in ("AShare", "Futures"): + # Futures does not support live trading + if market_category in ("Futures",): self._mark_failed(order_id=order_id, error=f"live_trading_not_supported_for_{market_category.lower()}") _console_print(f"[worker] order rejected: strategy_id={strategy_id} pending_id={order_id} {market_category} does not support live trading") _notify_live_best_effort(status="failed", error=f"live_trading_not_supported_for_{market_category.lower()}") return - # Validate IBKR only for USStock/HShare + # Validate IBKR only for USStock if exchange_id == "ibkr": - if market_category not in ("USStock", "HShare"): - self._mark_failed(order_id=order_id, error=f"ibkr_only_supports_usstock_hshare_got_{market_category.lower()}") - _console_print(f"[worker] order rejected: strategy_id={strategy_id} pending_id={order_id} IBKR only supports USStock/HShare, got {market_category}") - _notify_live_best_effort(status="failed", error=f"ibkr_only_supports_usstock_hshare_got_{market_category.lower()}") + if market_category not in ("USStock",): + self._mark_failed(order_id=order_id, error=f"ibkr_only_supports_usstock_got_{market_category.lower()}") + _console_print(f"[worker] order rejected: strategy_id={strategy_id} pending_id={order_id} IBKR only supports USStock, got {market_category}") + _notify_live_best_effort(status="failed", error=f"ibkr_only_supports_usstock_got_{market_category.lower()}") return # Validate MT5 only for Forex @@ -884,7 +884,7 @@ class PendingOrderWorker: _notify_live_best_effort(status="failed", error=f"create_client_failed:{e}") return - # Check if this is an IBKR client (US/HK stocks) + # Check if this is an IBKR client (US stocks) global IBKRClient if IBKRClient is None: try: @@ -961,7 +961,7 @@ class PendingOrderWorker: # Unified maker->market fallback settings # Priority: payload config > environment variable > default value - _default_order_mode = os.getenv("ORDER_MODE", "maker").strip().lower() + _default_order_mode = os.getenv("ORDER_MODE", "market").strip().lower() _default_maker_wait_sec = float(os.getenv("MAKER_WAIT_SEC", "10")) _default_maker_offset_bps = float(os.getenv("MAKER_OFFSET_BPS", "2")) @@ -1925,7 +1925,7 @@ class PendingOrderWorker: _console_print, ) -> None: """ - Execute order via Interactive Brokers for US/HK stocks. + Execute order via Interactive Brokers for US stocks. Simplified flow compared to crypto (no maker->market fallback): - Place market order directly @@ -1957,7 +1957,7 @@ class PendingOrderWorker: _notify_live_best_effort(status="failed", error=f"ibkr_unsupported_signal:{signal_type}") return - # Get market type (USStock or HShare) + # Get market type (USStock) market_type = str( payload.get("market_type") or payload.get("market_category") or diff --git a/backend_api_python/app/services/search.py b/backend_api_python/app/services/search.py index ceb03b0..3603f99 100644 --- a/backend_api_python/app/services/search.py +++ b/backend_api_python/app/services/search.py @@ -3,7 +3,7 @@ Search service v2.0 - 增强版搜索服务 整合多个搜索引擎,支持 API Key 轮换和故障转移 支持的搜索引擎(按优先级): -1. Bocha (博查) - 国内搜索优化,A股新闻推荐 +1. Bocha (博查) - 搜索优化 2. Tavily - 专为AI设计,免费1000次/月 3. SerpAPI - Google/Bing 结果抓取 4. Google CSE - 自定义搜索引擎 @@ -972,7 +972,7 @@ class SearchService: self, stock_code: str, stock_name: str, - market: str = "AShare", + market: str = "USStock", max_results: int = 5 ) -> SearchResponse: """ @@ -997,14 +997,14 @@ class SearchService: search_days = 1 # 根据市场类型构建搜索查询 - if market == "AShare": - query = f"{stock_name} {stock_code} 股票 最新消息 利好 利空" - elif market == "USStock": + if market == "USStock": query = f"{stock_name} {stock_code} stock news latest" elif market == "Crypto": query = f"{stock_name} crypto news price analysis" + elif market == "Forex": + query = f"{stock_name} {stock_code} forex news analysis" else: - query = f"{stock_name} {stock_code} 最新消息" + query = f"{stock_name} {stock_code} latest news" logger.info(f"搜索股票新闻: {stock_name}({stock_code}), market={market}, days={search_days}") diff --git a/backend_api_python/app/services/symbol_name.py b/backend_api_python/app/services/symbol_name.py index 25131de..236367b 100644 --- a/backend_api_python/app/services/symbol_name.py +++ b/backend_api_python/app/services/symbol_name.py @@ -6,8 +6,6 @@ Goal: from public data sources, then persist it into watchlist records. Notes: -- For A shares we prefer akshare when available (requested). -- For H shares we use Tencent quote API (no key required). - For US stocks we use Finnhub (if configured) or yfinance. - For Crypto/Forex/Futures we provide best-effort fallbacks. """ @@ -26,122 +24,13 @@ from app.data.market_symbols_seed import get_symbol_name as seed_get_symbol_name logger = get_logger(__name__) -try: - import akshare as ak # type: ignore - HAS_AKSHARE = True -except Exception: - ak = None - HAS_AKSHARE = False - def _normalize_symbol_for_market(market: str, symbol: str) -> str: m = (market or '').strip() s = (symbol or '').strip().upper() - if not m or not s: - return s - - if m == 'AShare' and s.isdigit(): - return s.zfill(6) - if m == 'HShare' and s.isdigit(): - return s.zfill(5) - return s -def _tencent_quote_code(market: str, symbol: str) -> Optional[str]: - """ - Convert symbol to Tencent quote code, e.g. - - AShare: sh600000 / sz000001 / bj430047 - - HShare: hk00700 - """ - m = (market or '').strip() - s = _normalize_symbol_for_market(m, symbol) - if not s: - return None - - if m == 'AShare': - if s.startswith('6'): - return f"sh{s}" - if s.startswith('0') or s.startswith('3'): - return f"sz{s}" - if s.startswith('4') or s.startswith('8'): - return f"bj{s}" - return None - - if m == 'HShare': - if s.isdigit(): - return f"hk{s}" - # allow already prefixed - if s.startswith('HK') and s[2:].isdigit(): - return f"hk{s[2:]}" - if s.startswith('HK') and len(s) > 2: - return f"hk{s[2:]}" - return f"hk{s}" - - return None - - -def _resolve_name_from_tencent(market: str, symbol: str) -> Optional[str]: - """ - Tencent quote endpoint: http://qt.gtimg.cn/q=sz000858 - Returns: - v_sz000858="51~五 粮 液~000858~..."; -> name is the 2nd field split by '~' - """ - code = _tencent_quote_code(market, symbol) - if not code: - return None - - try: - url = f"http://qt.gtimg.cn/q={code}" - resp = requests.get(url, timeout=5) - # Tencent often responds in GBK for Chinese names - resp.encoding = 'gbk' - text = resp.text or '' - - # Extract quoted payload - m = re.search(r'="([^"]*)"', text) - payload = m.group(1) if m else '' - if not payload: - return None - - parts = payload.split('~') - if len(parts) < 2: - return None - - name = (parts[1] or '').strip().replace(' ', '') - return name if name else None - except Exception as e: - logger.debug(f"Tencent name resolve failed: {market} {symbol}: {e}") - return None - - -def _resolve_name_from_akshare_ashare(symbol: str) -> Optional[str]: - """ - Resolve A-share name via akshare (no API key required). - """ - if not HAS_AKSHARE or ak is None: - return None - try: - # Prefer per-symbol endpoint (avoids fetching the whole market list). - if hasattr(ak, "stock_individual_info_em"): - df = ak.stock_individual_info_em(symbol=symbol) - if df is not None and not df.empty and 'item' in df.columns and 'value' in df.columns: - info = {str(r['item']).strip(): r['value'] for _, r in df.iterrows()} - name = str(info.get('股票简称') or info.get('证券简称') or '').strip() - return name if name else None - - # Fallback: spot list (may be slow / large) - if hasattr(ak, "stock_zh_a_spot_em"): - df2 = ak.stock_zh_a_spot_em() - if df2 is not None and not df2.empty: - row = df2[df2['代码'] == symbol].iloc[0] - name = str(row.get('名称') or '').strip() - return name if name else None - except Exception as e: - logger.debug(f"akshare name resolve failed (AShare {symbol}): {e}") - return None - return None - def _resolve_name_from_yfinance(symbol: str) -> Optional[str]: """ Best-effort company name via yfinance. @@ -211,13 +100,6 @@ def resolve_symbol_name(market: str, symbol: str) -> Optional[str]: return seed # 2) Market-specific - if m == 'AShare': - # Requested: use akshare for A shares, do not depend on Tencent by default. - return _resolve_name_from_akshare_ashare(s) - - if m == 'HShare': - return _resolve_name_from_tencent(m, s) - if m == 'USStock': # Prefer Finnhub if configured (more stable for company name), # otherwise fall back to yfinance. @@ -239,5 +121,3 @@ def resolve_symbol_name(market: str, symbol: str) -> Optional[str]: return s return None - - diff --git a/backend_api_python/app/services/trading_executor.py b/backend_api_python/app/services/trading_executor.py index 0258de0..08b005d 100644 --- a/backend_api_python/app/services/trading_executor.py +++ b/backend_api_python/app/services/trading_executor.py @@ -556,7 +556,7 @@ class TradingExecutor: trade_direction = 'long' # 现货只能做多 logger.info(f"Strategy {strategy_id} spot trading; force trade_direction=long") - # 获取市场类别(Crypto, USStock, Forex, Futures, AShare, HShare) + # 获取市场类别(Crypto, USStock, Forex, Futures) # 这决定了使用哪个数据源来获取价格和K线数据 market_category = (strategy.get('market_category') or 'Crypto').strip() logger.info(f"Strategy {strategy_id} market_category: {market_category}") @@ -1131,7 +1131,7 @@ class TradingExecutor: symbol: 交易对/代码 timeframe: 时间周期 limit: 数据条数 - market_category: 市场类型 (Crypto, USStock, Forex, Futures, AShare, HShare) + market_category: 市场类型 (Crypto, USStock, Forex, Futures) """ try: # 使用 KlineService 获取K线数据(自动处理缓存) @@ -1153,7 +1153,7 @@ class TradingExecutor: exchange: 交易所实例(信号模式下为 None) symbol: 交易对/代码 market_type: 交易类型 (swap/spot) - market_category: 市场类型 (Crypto, USStock, Forex, Futures, AShare, HShare) + market_category: 市场类型 (Crypto, USStock, Forex, Futures) """ # Local in-memory cache first cache_key = f"{market_category}:{(symbol or '').strip().upper()}" @@ -1173,7 +1173,7 @@ class TradingExecutor: try: # 根据 market_category 选择正确的数据源 - # 支持: Crypto, USStock, Forex, Futures, AShare, HShare + # 支持: Crypto, USStock, Forex, Futures ticker = DataSourceFactory.get_ticker(market_category, symbol) if ticker: price = float(ticker.get('last') or ticker.get('close') or 0) diff --git a/backend_api_python/app/services/usdt_payment_service.py b/backend_api_python/app/services/usdt_payment_service.py new file mode 100644 index 0000000..b55a653 --- /dev/null +++ b/backend_api_python/app/services/usdt_payment_service.py @@ -0,0 +1,366 @@ +""" +USDT Payment Service (方案B:每单独立地址 + 自动对账) + +MVP: +- 只支持 USDT-TRC20 +- 使用 XPUB 派生地址(服务端只保存 xpub,不保存私钥) +- 通过 TronGrid API 轮询到账(前端轮询订单状态时触发刷新) +""" + +import os +import time +from datetime import datetime, timezone, timedelta +from decimal import Decimal +from typing import Any, Dict, Optional, Tuple + +import requests + +from app.utils.db import get_db_connection +from app.utils.logger import get_logger +from app.services.billing_service import get_billing_service + +logger = get_logger(__name__) + + +class UsdtPaymentService: + def __init__(self): + self.billing = get_billing_service() + + # -------------------- Config -------------------- + + def _get_cfg(self) -> Dict[str, Any]: + return { + "enabled": str(os.getenv("USDT_PAY_ENABLED", "False")).lower() in ("1", "true", "yes"), + "chain": (os.getenv("USDT_PAY_CHAIN", "TRC20") or "TRC20").upper(), + "xpub_trc20": (os.getenv("USDT_TRC20_XPUB", "") or "").strip(), + "trongrid_base": (os.getenv("TRONGRID_BASE_URL", "https://api.trongrid.io") or "").strip().rstrip("/"), + "trongrid_key": (os.getenv("TRONGRID_API_KEY", "") or "").strip(), + "usdt_trc20_contract": (os.getenv("USDT_TRC20_CONTRACT", "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj") or "").strip(), + "confirm_seconds": int(float(os.getenv("USDT_PAY_CONFIRM_SECONDS", "30") or 30)), + "order_expire_minutes": int(float(os.getenv("USDT_PAY_EXPIRE_MINUTES", "30") or 30)), + } + + # -------------------- Schema -------------------- + + def _ensure_schema_best_effort(self, cur): + """Best-effort create table/columns for old databases.""" + try: + cur.execute( + """ + CREATE TABLE IF NOT EXISTS qd_usdt_orders ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES qd_users(id) ON DELETE CASCADE, + plan VARCHAR(20) NOT NULL, + chain VARCHAR(20) NOT NULL DEFAULT 'TRC20', + amount_usdt DECIMAL(20,6) NOT NULL DEFAULT 0, + address_index INTEGER NOT NULL DEFAULT 0, + address VARCHAR(80) NOT NULL DEFAULT '', + status VARCHAR(20) NOT NULL DEFAULT 'pending', + tx_hash VARCHAR(120) DEFAULT '', + paid_at TIMESTAMP, + confirmed_at TIMESTAMP, + expires_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + """ + ) + cur.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_usdt_orders_address_unique ON qd_usdt_orders(chain, address)") + cur.execute("CREATE INDEX IF NOT EXISTS idx_usdt_orders_user_id ON qd_usdt_orders(user_id)") + cur.execute("CREATE INDEX IF NOT EXISTS idx_usdt_orders_status ON qd_usdt_orders(status)") + except Exception: + pass + + # -------------------- Address derivation -------------------- + + def _derive_trc20_address_from_xpub(self, xpub: str, index: int) -> str: + """ + Derive TRON address from xpub. + + Requires bip_utils. + NOTE: + - Some wallets export account-level xpub at m/44'/195'/0' (level=3). + - Some export change-level xpub at m/44'/195'/0'/0 (level=4, external chain). + This function supports both by normalizing to change-level before AddressIndex(). + """ + try: + from bip_utils import Bip44, Bip44Coins, Bip44Changes + except Exception as e: + raise RuntimeError(f"bip_utils_missing:{e}") + + if not xpub: + raise RuntimeError("missing_xpub") + if index < 0: + raise RuntimeError("invalid_index") + + ctx = Bip44.FromExtendedKey(xpub, Bip44Coins.TRON) + lvl = int(ctx.Level()) + # Normalize to change-level (external chain) so we can derive addresses by index + if lvl == 3: + # account-level xpub: m/44'/195'/0' + ctx = ctx.Change(Bip44Changes.CHAIN_EXT) + elif lvl == 4: + # change-level xpub: m/44'/195'/0'/0 + pass + elif lvl == 5: + # address-level xpub: cannot derive other indexes + if index != 0: + raise RuntimeError("xpub_is_address_level") + return ctx.PublicKey().ToAddress() + else: + raise RuntimeError(f"unsupported_xpub_level:{lvl}") + + addr = ctx.AddressIndex(index).PublicKey().ToAddress() + return addr + + # -------------------- Orders -------------------- + + def create_order(self, user_id: int, plan: str) -> Tuple[bool, str, Dict[str, Any]]: + cfg = self._get_cfg() + if not cfg["enabled"]: + return False, "usdt_pay_disabled", {} + if cfg["chain"] != "TRC20": + return False, "unsupported_chain", {} + plan = (plan or "").strip().lower() + if plan not in ("monthly", "yearly", "lifetime"): + return False, "invalid_plan", {} + + plans = self.billing.get_membership_plans() + amount = Decimal(str(plans.get(plan, {}).get("price_usd") or 0)) + if amount <= 0: + return False, "invalid_amount", {} + + now = datetime.now(timezone.utc) + expires_at = now + timedelta(minutes=cfg["order_expire_minutes"]) + + try: + with get_db_connection() as db: + cur = db.cursor() + self._ensure_schema_best_effort(cur) + + # allocate next address index (simple monotonic) + cur.execute( + "SELECT COALESCE(MAX(address_index), -1) as max_idx FROM qd_usdt_orders WHERE chain = 'TRC20'" + ) + max_idx = cur.fetchone().get("max_idx") + next_idx = int(max_idx) + 1 + + address = self._derive_trc20_address_from_xpub(cfg["xpub_trc20"], next_idx) + + cur.execute( + """ + INSERT INTO qd_usdt_orders + (user_id, plan, chain, amount_usdt, address_index, address, status, expires_at, created_at, updated_at) + VALUES (?, ?, 'TRC20', ?, ?, ?, 'pending', ?, NOW(), NOW()) + RETURNING id + """, + (user_id, plan, float(amount), next_idx, address, expires_at), + ) + row = cur.fetchone() or {} + order_id = row.get("id") + db.commit() + cur.close() + + return True, "success", { + "order_id": order_id, + "plan": plan, + "chain": "TRC20", + "amount_usdt": str(amount), + "address": address, + "expires_at": expires_at.isoformat(), + } + except Exception as e: + logger.error(f"create_order failed: {e}", exc_info=True) + return False, f"error:{str(e)}", {} + + def get_order(self, user_id: int, order_id: int, refresh: bool = True) -> Tuple[bool, str, Dict[str, Any]]: + try: + with get_db_connection() as db: + cur = db.cursor() + self._ensure_schema_best_effort(cur) + + cur.execute( + """ + SELECT id, user_id, plan, chain, amount_usdt, address_index, address, status, tx_hash, + paid_at, confirmed_at, expires_at, created_at, updated_at + FROM qd_usdt_orders + WHERE id = ? AND user_id = ? + """, + (order_id, user_id), + ) + row = cur.fetchone() + if not row: + cur.close() + return False, "order_not_found", {} + + if refresh: + self._refresh_order_in_tx(cur, row) + db.commit() + # re-read + cur.execute( + """ + SELECT id, user_id, plan, chain, amount_usdt, address_index, address, status, tx_hash, + paid_at, confirmed_at, expires_at, created_at, updated_at + FROM qd_usdt_orders + WHERE id = ? AND user_id = ? + """, + (order_id, user_id), + ) + row = cur.fetchone() + + cur.close() + + return True, "success", self._row_to_dict(row) + except Exception as e: + logger.error(f"get_order failed: {e}", exc_info=True) + return False, f"error:{str(e)}", {} + + def _row_to_dict(self, row: Dict[str, Any]) -> Dict[str, Any]: + return { + "order_id": row.get("id"), + "plan": row.get("plan"), + "chain": row.get("chain"), + "amount_usdt": str(row.get("amount_usdt") or 0), + "address": row.get("address") or "", + "status": row.get("status") or "", + "tx_hash": row.get("tx_hash") or "", + "paid_at": row.get("paid_at").isoformat() if row.get("paid_at") else None, + "confirmed_at": row.get("confirmed_at").isoformat() if row.get("confirmed_at") else None, + "expires_at": row.get("expires_at").isoformat() if row.get("expires_at") else None, + "created_at": row.get("created_at").isoformat() if row.get("created_at") else None, + } + + # -------------------- Chain check -------------------- + + def _refresh_order_in_tx(self, cur, row: Dict[str, Any]) -> None: + cfg = self._get_cfg() + status = (row.get("status") or "").lower() + chain = (row.get("chain") or "").upper() + + expires_at = row.get("expires_at") + now = datetime.now(timezone.utc) + if expires_at and isinstance(expires_at, datetime): + exp = expires_at + if exp.tzinfo is None: + exp = exp.replace(tzinfo=timezone.utc) + if status == "pending" and exp <= now: + cur.execute("UPDATE qd_usdt_orders SET status = 'expired', updated_at = NOW() WHERE id = ?", (row["id"],)) + return + + if chain != "TRC20": + return + if status not in ("pending", "paid"): + return + + address = row.get("address") or "" + amount = Decimal(str(row.get("amount_usdt") or 0)) + if not address or amount <= 0: + return + + tx = self._find_trc20_usdt_incoming(address, amount, row.get("created_at")) + if not tx: + return + + tx_hash = tx.get("transaction_id") or "" + paid_at = datetime.now(timezone.utc) + cur.execute( + "UPDATE qd_usdt_orders SET status = 'paid', tx_hash = ?, paid_at = ?, updated_at = NOW() WHERE id = ? AND status = 'pending'", + (tx_hash, paid_at, row["id"]), + ) + + # Confirm after a short delay to reduce reorg/uncle risk (TRON usually stable) + # If already old enough, confirm now. + confirm_sec = int(cfg.get("confirm_seconds") or 30) + try: + if confirm_sec <= 0: + confirm_sec = 0 + # If transaction timestamp is available, use it + tx_ts = tx.get("block_timestamp") + if tx_ts: + tx_time = datetime.fromtimestamp(int(tx_ts) / 1000.0, tz=timezone.utc) + if (now - tx_time).total_seconds() >= confirm_sec: + self._confirm_and_activate_in_tx(cur, row["id"], row.get("user_id"), row.get("plan"), tx_hash) + else: + # no timestamp -> confirm immediately + self._confirm_and_activate_in_tx(cur, row["id"], row.get("user_id"), row.get("plan"), tx_hash) + except Exception: + # do not block + pass + + def _confirm_and_activate_in_tx(self, cur, order_id: int, user_id: int, plan: str, tx_hash: str) -> None: + # Mark confirmed if not already + cur.execute( + "UPDATE qd_usdt_orders SET status='confirmed', confirmed_at = NOW(), updated_at = NOW() WHERE id = ? AND status IN ('paid','pending')", + (order_id,), + ) + # Activate membership (idempotent-ish: billing_service stacks vip) + try: + # We use existing membership activation (writes qd_membership_orders + credits logs). + ok, msg, data = self.billing.purchase_membership(int(user_id), str(plan)) + logger.info(f"USDT activate membership: order={order_id} user={user_id} plan={plan} ok={ok} msg={msg}") + except Exception as e: + logger.error(f"USDT activate membership failed: order={order_id} err={e}", exc_info=True) + + def _find_trc20_usdt_incoming(self, address: str, amount_usdt: Decimal, created_at: Optional[datetime]) -> Optional[Dict[str, Any]]: + cfg = self._get_cfg() + base = cfg["trongrid_base"] + contract = cfg["usdt_trc20_contract"] + + url = f"{base}/v1/accounts/{address}/transactions/trc20" + headers = {} + if cfg["trongrid_key"]: + headers["TRON-PRO-API-KEY"] = cfg["trongrid_key"] + + params = { + "only_to": "true", + "limit": 50, + "contract_address": contract, + } + + try: + resp = requests.get(url, params=params, headers=headers, timeout=10) + if resp.status_code != 200: + return None + data = resp.json() or {} + items = data.get("data") or [] + # TRC20 USDT has 6 decimals + target = int((amount_usdt * Decimal("1000000")).to_integral_value()) + + min_ts = None + if created_at and isinstance(created_at, datetime): + ct = created_at + if ct.tzinfo is None: + ct = ct.replace(tzinfo=timezone.utc) + min_ts = int(ct.timestamp() * 1000) - 60_000 + + for it in items: + try: + if it.get("to") != address: + continue + if min_ts and int(it.get("block_timestamp") or 0) < min_ts: + continue + val = int(it.get("value") or 0) + if val != target: + continue + # basic checks + token = it.get("token_info") or {} + if str(token.get("symbol") or "").upper() != "USDT": + # some APIs omit symbol; contract filter should already ensure + pass + return it + except Exception: + continue + except Exception: + return None + return None + + +_svc = None + + +def get_usdt_payment_service() -> UsdtPaymentService: + global _svc + if _svc is None: + _svc = UsdtPaymentService() + return _svc + diff --git a/backend_api_python/env.example b/backend_api_python/env.example index cf83de0..9c0c7ca 100644 --- a/backend_api_python/env.example +++ b/backend_api_python/env.example @@ -55,9 +55,9 @@ PENDING_ORDER_STALE_SEC=90 # Live trading order execution settings # ========================= # Order execution mode: -# - "maker": Limit order first, then market order for remaining (default, lower fees) -# - "market": Market order only (immediate execution, higher fees) -ORDER_MODE=maker +# - "market": Market order only (immediate execution, recommended for stability) +# - "maker": Limit order first, then market order for remaining (lower fees, may not fill) +ORDER_MODE=market # How long to wait for limit order to fill before switching to market order (seconds) MAKER_WAIT_SEC=10 @@ -208,7 +208,7 @@ CCXT_DEFAULT_EXCHANGE=coinbase CCXT_TIMEOUT=10000 CCXT_PROXY= -# Akshare (CN/HK stocks if enabled) +# Akshare (optional) AKSHARE_TIMEOUT=30 # YFinance @@ -237,7 +237,7 @@ SEARCH_BING_API_KEY= # 支持多个 key 轮换,用逗号分隔: key1,key2,key3 TAVILY_API_KEYS= -# 博查 Bocha Search API (国内搜索优化,A股新闻推荐!) +# 博查 Bocha Search API (国内搜索优化) # Get your key at: https://bochaai.com/ # 支持多个 key 轮换,用逗号分隔: key1,key2,key3 BOCHA_API_KEYS= @@ -300,8 +300,9 @@ VERIFICATION_CODE_LOCK_MINUTES=30 # Enable billing system (启用计费系统) BILLING_ENABLED=False -# VIP users can use all paid features for free (VIP用户免费) -BILLING_VIP_BYPASS=True +# Legacy: VIP users bypass ALL paid feature credit costs (NOT recommended) +# 建议关闭:VIP 仅用于“VIP免费指标”,其它功能仍扣积分 +BILLING_VIP_BYPASS=False # Credits consumed per feature (各功能消耗积分数) BILLING_COST_AI_ANALYSIS=10 @@ -312,6 +313,41 @@ BILLING_COST_PORTFOLIO_MONITOR=8 # Telegram customer service URL for recharge (充值跳转的Telegram链接) RECHARGE_TELEGRAM_URL=https://t.me/quantdinger +# ========================= +# Membership Plans (会员套餐 - Mock支付配置) +# ========================= +# Price in USD +MEMBERSHIP_MONTHLY_PRICE_USD=19.9 +MEMBERSHIP_YEARLY_PRICE_USD=199 +MEMBERSHIP_LIFETIME_PRICE_USD=499 + +# Credits bonus +MEMBERSHIP_MONTHLY_CREDITS=500 +MEMBERSHIP_YEARLY_CREDITS=8000 + +# Lifetime: monthly credits granted every 30 days +MEMBERSHIP_LIFETIME_MONTHLY_CREDITS=800 + +# ========================= +# USDT Pay (Plan B: per-order unique address) +# ========================= +USDT_PAY_ENABLED=False +USDT_PAY_CHAIN=TRC20 + +# TRC20 (TRON) watch-only xpub (derive deposit addresses from index 0..N) +USDT_TRC20_XPUB= + +# USDT TRC20 contract on TRON (default) +USDT_TRC20_CONTRACT=TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj + +# TronGrid API +TRONGRID_BASE_URL=https://api.trongrid.io +TRONGRID_API_KEY= + +# Order confirmation delay and expiration +USDT_PAY_CONFIRM_SECONDS=30 +USDT_PAY_EXPIRE_MINUTES=30 + # New user registration bonus credits (新用户注册赠送积分) CREDITS_REGISTER_BONUS=100 diff --git a/backend_api_python/migrations/init.sql b/backend_api_python/migrations/init.sql index 6c1f96f..7156521 100644 --- a/backend_api_python/migrations/init.sql +++ b/backend_api_python/migrations/init.sql @@ -16,6 +16,9 @@ CREATE TABLE IF NOT EXISTS qd_users ( role VARCHAR(20) DEFAULT 'user', -- admin/manager/user/viewer credits DECIMAL(20,2) DEFAULT 0, -- 积分余额 vip_expires_at TIMESTAMP, -- VIP过期时间 + vip_plan VARCHAR(20) DEFAULT '', -- VIP套餐:monthly/yearly/lifetime + vip_is_lifetime BOOLEAN DEFAULT FALSE, -- 是否永久会员 + vip_monthly_credits_last_grant TIMESTAMP, -- 永久会员上次发放月度积分时间 email_verified BOOLEAN DEFAULT FALSE, -- 邮箱是否已验证 referred_by INTEGER, -- 邀请人ID notification_settings TEXT DEFAULT '', -- 用户通知配置 JSON (telegram_chat_id, default_channels等) @@ -51,6 +54,47 @@ CREATE INDEX IF NOT EXISTS idx_credits_log_user_id ON qd_credits_log(user_id); CREATE INDEX IF NOT EXISTS idx_credits_log_action ON qd_credits_log(action); CREATE INDEX IF NOT EXISTS idx_credits_log_created_at ON qd_credits_log(created_at); +-- ============================================================================= +-- 1.55. Membership Orders (会员订单 - Mock支付) +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS qd_membership_orders ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES qd_users(id) ON DELETE CASCADE, + plan VARCHAR(20) NOT NULL, -- monthly/yearly/lifetime + price_usd DECIMAL(10,2) DEFAULT 0, -- 订单金额(USD) + status VARCHAR(20) DEFAULT 'paid', -- paid/pending/failed/refunded (mock 默认 paid) + created_at TIMESTAMP DEFAULT NOW(), + paid_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_membership_orders_user_id ON qd_membership_orders(user_id); + +-- ============================================================================= +-- 1.56. USDT Orders (USDT 收款订单 - 每单独立地址) +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS qd_usdt_orders ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES qd_users(id) ON DELETE CASCADE, + plan VARCHAR(20) NOT NULL, -- monthly/yearly/lifetime + chain VARCHAR(20) NOT NULL DEFAULT 'TRC20',-- TRC20 (MVP) + amount_usdt DECIMAL(20,6) NOT NULL DEFAULT 0, + address_index INTEGER NOT NULL DEFAULT 0, -- HD 派生索引 + address VARCHAR(80) NOT NULL DEFAULT '', + status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending/paid/confirmed/expired/cancelled/failed + tx_hash VARCHAR(120) DEFAULT '', + paid_at TIMESTAMP, + confirmed_at TIMESTAMP, + expires_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_usdt_orders_address_unique ON qd_usdt_orders(chain, address); +CREATE INDEX IF NOT EXISTS idx_usdt_orders_user_id ON qd_usdt_orders(user_id); +CREATE INDEX IF NOT EXISTS idx_usdt_orders_status ON qd_usdt_orders(status); + -- ============================================================================= -- 1.6. Verification Codes (邮箱验证码) -- ============================================================================= @@ -300,6 +344,7 @@ CREATE TABLE IF NOT EXISTS qd_indicator_codes ( price numeric(10, 2) DEFAULT 0 NOT NULL, is_encrypted int4 DEFAULT 0 NOT NULL, preview_image varchar(500) DEFAULT ''::character varying NULL, + vip_free boolean DEFAULT false, -- VIP免费指标:VIP可免扣积分使用 createtime int8 NULL, updatetime int8 NULL, created_at timestamp DEFAULT now(), @@ -523,17 +568,6 @@ CREATE INDEX IF NOT EXISTS idx_market_symbols_is_hot ON qd_market_symbols(market -- Seed data: Hot symbols for each market INSERT INTO qd_market_symbols (market, symbol, name, exchange, currency, is_active, is_hot, sort_order) VALUES --- AShare (China A-Shares) -('AShare', '000001', '平安银行', 'SZSE', 'CNY', 1, 1, 100), -('AShare', '000002', '万科A', 'SZSE', 'CNY', 1, 1, 99), -('AShare', '600000', '浦发银行', 'SSE', 'CNY', 1, 1, 98), -('AShare', '600036', '招商银行', 'SSE', 'CNY', 1, 1, 97), -('AShare', '600519', '贵州茅台', 'SSE', 'CNY', 1, 1, 96), -('AShare', '000858', '五粮液', 'SZSE', 'CNY', 1, 1, 95), -('AShare', '002415', '海康威视', 'SZSE', 'CNY', 1, 1, 94), -('AShare', '300059', '东方财富', 'SZSE', 'CNY', 1, 1, 93), -('AShare', '000725', '京东方A', 'SZSE', 'CNY', 1, 1, 92), -('AShare', '002594', '比亚迪', 'SZSE', 'CNY', 1, 1, 91), -- USStock (US Stocks) ('USStock', 'AAPL', 'Apple Inc.', 'NASDAQ', 'USD', 1, 1, 100), ('USStock', 'MSFT', 'Microsoft Corporation', 'NASDAQ', 'USD', 1, 1, 99), @@ -545,17 +579,6 @@ INSERT INTO qd_market_symbols (market, symbol, name, exchange, currency, is_acti ('USStock', 'JPM', 'JPMorgan Chase & Co.', 'NYSE', 'USD', 1, 1, 93), ('USStock', 'V', 'Visa Inc.', 'NYSE', 'USD', 1, 1, 92), ('USStock', 'JNJ', 'Johnson & Johnson', 'NYSE', 'USD', 1, 1, 91), --- HShare (Hong Kong Stocks) -('HShare', '00700', 'Tencent Holdings', 'HKEX', 'HKD', 1, 1, 100), -('HShare', '09988', 'Alibaba Group', 'HKEX', 'HKD', 1, 1, 99), -('HShare', '03690', 'Meituan', 'HKEX', 'HKD', 1, 1, 98), -('HShare', '01810', 'Xiaomi Corporation', 'HKEX', 'HKD', 1, 1, 97), -('HShare', '02318', 'Ping An Insurance', 'HKEX', 'HKD', 1, 1, 96), -('HShare', '01398', 'ICBC', 'HKEX', 'HKD', 1, 1, 95), -('HShare', '00939', 'CCB', 'HKEX', 'HKD', 1, 1, 94), -('HShare', '01299', 'AIA Group', 'HKEX', 'HKD', 1, 1, 93), -('HShare', '02020', 'Anta Sports', 'HKEX', 'HKD', 1, 1, 92), -('HShare', '01024', 'Kuaishou Technology', 'HKEX', 'HKD', 1, 1, 91), -- Crypto ('Crypto', 'BTC/USDT', 'Bitcoin', 'Binance', 'USDT', 1, 1, 100), ('Crypto', 'ETH/USDT', 'Ethereum', 'Binance', 'USDT', 1, 1, 99), diff --git a/backend_api_python/requirements.txt b/backend_api_python/requirements.txt index 3978fca..e304065 100644 --- a/backend_api_python/requirements.txt +++ b/backend_api_python/requirements.txt @@ -11,11 +11,13 @@ pymysql>=1.0.2 SQLAlchemy>=2.0.0 PyJWT==2.8.0 python-dotenv>=1.0.1 +# HD wallet derivation (xpub -> address) for USDT payments +bip-utils>=2.9.0 # PostgreSQL support (multi-user mode) psycopg2-binary>=2.9.9 # Password hashing bcrypt>=4.1.0 -# Interactive Brokers trading (optional, for US/HK stock trading via TWS/IB Gateway) +# Interactive Brokers trading (optional, for US stock trading via TWS/IB Gateway) ib_insync>=0.9.86 # MetaTrader 5 trading (optional, for forex trading via MT5 terminal, Windows only) # Note: MetaTrader5 is Windows-only and not available on Linux/macOS diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f88f142..e075ab4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,184 @@ This document records version updates, new features, bug fixes, and database mig --- +## V2.2.1 (2026-02-27) + +### 🚀 New Features + +#### Membership & Billing System +- **Subscription Plans**: Monthly / Yearly / Lifetime tiers with configurable pricing and credit bundles +- **Credit System**: Each plan includes credits; lifetime members receive recurring monthly credit bonuses +- **Plan Management**: All plan prices, credits, and bonus amounts configurable via System Settings → Billing Configuration +- **Membership Orders**: Order tracking with status management (paid / pending / failed / refunded) + +#### USDT On-Chain Payment (TRC20) +- **HD Wallet Integration**: Per-order unique receiving address derived from xpub (BIP-32/44) — no private key on server +- **Automatic Reconciliation**: Background polling via TronGrid API detects incoming payments and confirms orders +- **Depth-Flexible xpub**: Supports both account-level (depth=3) and change-level (depth=4) xpub keys +- **Configurable Expiry**: Order expiration time and confirmation delay configurable in System Settings +- **Scan-to-Pay Modal**: Professional checkout UI with QR code, step indicator, real-time status, copy-to-clipboard, dark theme support + +#### VIP Free Indicators +- **VIP Free Tag**: Admins can mark community indicators as "VIP Free" when publishing +- **Zero-Credit Access**: VIP members can use VIP-free indicators without spending credits +- **Visual Badge**: VIP Free indicators display a distinct badge in the Indicator Market + +#### AI Trading Opportunities Radar +- **Multi-Market Scanning**: Auto-scans Crypto, US Stocks, and Forex markets every hour +- **Rolling Carousel**: Opportunities displayed in a rotating carousel with market-specific styling +- **Signal Classification**: BUY / SELL signals with percentage change and reason text +- **Multi-Language**: All radar card content fully internationalized + +#### Simplified Strategy Creation +- **Simple / Advanced Mode Toggle**: New users start with simplified mode, power users can switch to advanced +- **Smart Defaults**: 15-minute K-line period, 5x leverage, market order, sensible TP/SL percentages +- **Live Trading Disclaimer**: Mandatory risk acknowledgment checkbox before enabling live trading + +#### System Settings Simplification +- **Streamlined Configuration**: Removed redundant config groups (server, strategy); consolidated into essential categories +- **Market Order Default**: Changed default order mode to market order for reliable execution +- **Billing Config i18n**: All billing configuration items fully multi-language supported + +#### Indicator Market Performance Tracking +- **Live Performance Data**: Fixed aggregation to correctly parse backtest `result_json` and include live trade data +- **Combined Metrics**: Backtest return, live PnL, and win rate now properly displayed on indicator cards + +### 🐛 Bug Fixes +- Fixed "Live Performance" data showing all zeros in Indicator Market (incorrect SQL query referencing non-existent columns) +- Fixed incorrect entry price display in Position Records (was falling back to current price) +- Fixed inaccurate System Overview statistics for running strategies, total capital, and total PnL +- Fixed multiple duplicate i18n key issues in `zh-CN.js` and `en-US.js` causing ESLint build failures +- Fixed exposed i18n keys (`common.loading`, `common.noData`, `systemOverview.*`) not configured +- Fixed HTML nesting issues in trading assistant strategy creation form +- Fixed `ed25519-blake2b` build failure in Docker by adding temporary build dependencies +- Fixed "Current depth (3) is not suitable for deriving address" error for xpub — now compatible with both depth 3 and depth 4 + +### 🎨 UI/UX Improvements +- Removed "Total Analyses" / "Accuracy Rate" row from homepage AI Analysis section +- Removed "Search" and "Portfolio Checkup" features from AI Asset Analysis page +- Professional USDT checkout modal with custom header, step indicator, dual-column layout +- Dark theme and mobile responsive support for payment modal +- Trading Opportunities Radar carousel with smooth scrolling animation + +### 📋 Database Migration + +**Run the following SQL on your PostgreSQL database before deploying V2.2.1:** + +```sql +-- ============================================================ +-- QuantDinger V2.2.1 Database Migration +-- Membership, USDT Payment, VIP Free Indicators +-- ============================================================ + +-- 1. User Table: Add membership columns +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'qd_users' AND column_name = 'vip_plan' + ) THEN + ALTER TABLE qd_users ADD COLUMN vip_plan VARCHAR(20) DEFAULT ''; + RAISE NOTICE 'Added vip_plan column to qd_users'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'qd_users' AND column_name = 'vip_is_lifetime' + ) THEN + ALTER TABLE qd_users ADD COLUMN vip_is_lifetime BOOLEAN DEFAULT FALSE; + RAISE NOTICE 'Added vip_is_lifetime column to qd_users'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'qd_users' AND column_name = 'vip_monthly_credits_last_grant' + ) THEN + ALTER TABLE qd_users ADD COLUMN vip_monthly_credits_last_grant TIMESTAMP; + RAISE NOTICE 'Added vip_monthly_credits_last_grant column to qd_users'; + END IF; +END $$; + +-- 2. Indicator Codes: Add VIP Free flag +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'qd_indicator_codes' AND column_name = 'vip_free' + ) THEN + ALTER TABLE qd_indicator_codes ADD COLUMN vip_free BOOLEAN DEFAULT FALSE; + RAISE NOTICE 'Added vip_free column to qd_indicator_codes'; + END IF; +END $$; + +-- 3. Membership Orders table +CREATE TABLE IF NOT EXISTS qd_membership_orders ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES qd_users(id) ON DELETE CASCADE, + plan VARCHAR(20) NOT NULL, + price_usd DECIMAL(10,2) DEFAULT 0, + status VARCHAR(20) DEFAULT 'paid', + created_at TIMESTAMP DEFAULT NOW(), + paid_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_membership_orders_user_id ON qd_membership_orders(user_id); + +-- 4. USDT Orders table (on-chain payment tracking) +CREATE TABLE IF NOT EXISTS qd_usdt_orders ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES qd_users(id) ON DELETE CASCADE, + plan VARCHAR(20) NOT NULL, + chain VARCHAR(20) NOT NULL DEFAULT 'TRC20', + amount_usdt DECIMAL(20,6) NOT NULL DEFAULT 0, + address_index INTEGER NOT NULL DEFAULT 0, + address VARCHAR(80) NOT NULL DEFAULT '', + status VARCHAR(20) NOT NULL DEFAULT 'pending', + tx_hash VARCHAR(120) DEFAULT '', + paid_at TIMESTAMP, + confirmed_at TIMESTAMP, + expires_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_usdt_orders_address_unique ON qd_usdt_orders(chain, address); +CREATE INDEX IF NOT EXISTS idx_usdt_orders_user_id ON qd_usdt_orders(user_id); +CREATE INDEX IF NOT EXISTS idx_usdt_orders_status ON qd_usdt_orders(status); + +-- Migration Complete +DO $$ +BEGIN + RAISE NOTICE '✅ QuantDinger V2.2.1 database migration completed!'; +END $$; +``` + +**Migration Notes:** +- All statements use `IF NOT EXISTS` — safe to run multiple times +- No existing data is modified or deleted +- New `.env` variables required for USDT payment: `USDT_PAY_ENABLED`, `USDT_TRC20_XPUB`, `TRONGRID_API_KEY` +- New `.env` variables for membership pricing: `MEMBERSHIP_MONTHLY_PRICE_USD`, `MEMBERSHIP_MONTHLY_CREDITS`, etc. +- See `backend_api_python/env.example` for all new configuration options + +### 📝 Configuration Notes + +New environment variables (all optional, with defaults): + +| Variable | Default | Description | +|----------|---------|-------------| +| `MEMBERSHIP_MONTHLY_PRICE_USD` | `19.9` | Monthly plan price | +| `MEMBERSHIP_MONTHLY_CREDITS` | `500` | Credits included in monthly plan | +| `MEMBERSHIP_YEARLY_PRICE_USD` | `169` | Yearly plan price | +| `MEMBERSHIP_YEARLY_CREDITS` | `8000` | Credits included in yearly plan | +| `MEMBERSHIP_LIFETIME_PRICE_USD` | `499` | Lifetime plan price | +| `MEMBERSHIP_LIFETIME_CREDITS` | `30000` | Initial credits for lifetime plan | +| `MEMBERSHIP_LIFETIME_MONTHLY_BONUS` | `500` | Monthly bonus credits for lifetime members | +| `USDT_PAY_ENABLED` | `false` | Enable USDT TRC20 payment | +| `USDT_TRC20_XPUB` | _(empty)_ | TRC20 HD wallet xpub for address derivation | +| `TRONGRID_API_KEY` | _(empty)_ | TronGrid API key for on-chain monitoring | +| `USDT_ORDER_EXPIRE_MINUTES` | `30` | USDT order expiration time | + +--- + ## V2.1.3 (2026-02-XX) ### 🚀 New Features @@ -195,7 +373,7 @@ output = { - Fixed progress bar and timer not animating during AI analysis - Fixed missing i18n translations for various components - Fixed Tiingo API rate limit issues with caching -- Fixed A-share and H-share data fetching with multiple fallback sources +- Fixed data fetching with multiple fallback sources - Fixed watchlist price batch fetch timeout handling - Fixed heatmap multi-language support for commodities and forex - **Fixed AI analysis history not filtered by user** - All users were seeing the same history records; now each user only sees their own analysis history @@ -418,6 +596,9 @@ END $$; | Version | Date | Highlights | |---------|------|------------| +| V2.2.1 | 2026-02-27 | Membership & Billing, USDT TRC20 payment, VIP free indicators, AI Trading Radar, simplified strategy creation | +| V2.1.3 | 2026-02-XX | Cross-sectional strategy support | +| V2.1.2 | 2026-02-01 | Indicator parameters, cross-indicator calling | | V2.1.1 | 2026-01-31 | AI Analysis overhaul, Global Market integration, Indicator Community enhancements | --- diff --git a/docs/IBKR_TRADING_GUIDE_CN.md b/docs/IBKR_TRADING_GUIDE_CN.md deleted file mode 100644 index 75c9b20..0000000 --- a/docs/IBKR_TRADING_GUIDE_CN.md +++ /dev/null @@ -1,173 +0,0 @@ -# 盈透证券 (IBKR) 实盘交易指南 - -QuantDinger 支持通过盈透证券 TWS 或 IB Gateway 进行美股和港股的实盘交易。 - -## 概述 - -此功能可通过您的盈透证券账户实现美股和港股的自动化交易执行。配置完成后,您的交易策略可以通过 IBKR API 自动下单。 - -## 前置条件 - -- 盈透证券账户 -- 已安装 TWS (Trader Workstation) 或 IB Gateway -- 已订阅市场数据(用于实时报价) - -## 安装 - -`ib_insync` 库已包含在 `requirements.txt` 中。如需手动安装: - -```bash -pip install ib_insync -``` - -## 端口参考 - -| 客户端 | 实盘端口 | 模拟盘端口 | -|--------|----------|------------| -| TWS | 7497 | 7496 | -| IB Gateway | 4001 | 4002 | - -## TWS / IB Gateway 配置 - -1. 打开 TWS 或 IB Gateway -2. 进入 **配置** → **API** → **设置** -3. 启用以下选项: - - ✅ 启用 ActiveX 和 Socket 客户端 - - ✅ 仅允许来自本地主机的连接 -4. 设置 Socket 端口(参考上表) -5. 点击 应用 / 确定 - -## 策略配置 - -创建美股或港股策略时,在"实盘交易"部分配置 IBKR 连接: - -| 字段 | 说明 | 示例 | -|------|------|------| -| **券商** | 选择"盈透证券" | - | -| **主机地址** | TWS/Gateway 主机地址 | `127.0.0.1` | -| **端口** | TWS/Gateway API 端口 | `7497`(TWS 实盘) | -| **客户端 ID** | 唯一客户端标识 | `1` | -| **账户号** | 账户 ID(可选) | 留空自动选择 | - -## 代码格式 - -| 市场 | 格式 | 示例 | -|------|------|------| -| 美股 | 股票代码 | `AAPL`, `TSLA`, `GOOGL`, `MSFT` | -| 港股 | `XXXX.HK` 或数字 | `0700.HK`, `00700`, `700` | - -## 交易流程 - -``` -策略信号 → 待执行订单队列 → IBKR 执行 → 持仓更新 -``` - -1. 您的策略生成买入/卖出信号 -2. 信号作为待执行订单入队 -3. 后台工作线程连接 IBKR 并执行订单 -4. 更新持仓和交易记录 - -## 支持的信号类型 - -| 信号 | 动作 | 说明 | -|------|------|------| -| `open_long` | 买入 | 开多仓 | -| `add_long` | 买入 | 加多仓 | -| `close_long` | 卖出 | 平多仓 | -| `reduce_long` | 卖出 | 减多仓 | - -> **注意**:当前版本暂不支持做空交易。 - -## API 接口 - -### 连接管理 - -``` -GET /api/ibkr/status # 获取连接状态 -POST /api/ibkr/connect # 连接到 TWS/Gateway -POST /api/ibkr/disconnect # 断开连接 -``` - -### 账户查询 - -``` -GET /api/ibkr/account # 账户信息 -GET /api/ibkr/positions # 当前持仓 -GET /api/ibkr/orders # 未成交订单 -``` - -### 交易 - -``` -POST /api/ibkr/order # 下单 -DELETE /api/ibkr/order/ # 撤单 -``` - -### 行情数据 - -``` -GET /api/ibkr/quote?symbol=AAPL&marketType=USStock -``` - -## 使用示例 - -### 测试连接(通过 curl) - -```bash -curl -X POST http://localhost:5000/api/ibkr/connect \ - -H "Content-Type: application/json" \ - -d '{"host": "127.0.0.1", "port": 7497, "clientId": 1}' -``` - -### 下单 - -```bash -# 市价单:买入 10 股苹果 -curl -X POST http://localhost:5000/api/ibkr/order \ - -H "Content-Type: application/json" \ - -d '{"symbol": "AAPL", "side": "buy", "quantity": 10, "marketType": "USStock"}' - -# 限价单:卖出 100 股腾讯 -curl -X POST http://localhost:5000/api/ibkr/order \ - -H "Content-Type: application/json" \ - -d '{"symbol": "0700.HK", "side": "sell", "quantity": 100, "marketType": "HShare", "orderType": "limit", "price": 300}' -``` - -## 重要说明 - -1. **TWS/Gateway 必须运行**:交易前确保 TWS 或 IB Gateway 已启动并登录 -2. **市场数据订阅**:实时报价可能需要向 IBKR 订阅市场数据 -3. **客户端 ID**:如果多个程序连接同一个 TWS/Gateway,使用不同的 clientId -4. **账户选择**:如有多个子账户,请指定 `account` 参数 -5. **交易时间**:订单仅在市场交易时间执行 - -## 常见问题排查 - -| 错误 | 原因 | 解决方案 | -|------|------|----------| -| 连接失败 | TWS/Gateway 未运行 | 启动并登录 TWS/Gateway | -| 连接失败 | 端口错误 | 检查 TWS/Gateway 中的 API 端口设置 | -| 连接失败 | API 未启用 | 在 TWS/Gateway 设置中启用 Socket API | -| 客户端 ID 冲突 | 相同 clientId 已连接 | 使用不同的 clientId | -| 无效合约 | 代码格式错误 | 检查股票代码格式 | -| 订单被拒绝 | 资金/保证金不足 | 检查账户余额 | - -## Docker 部署 - -在 Docker 中运行 QuantDinger 时,TWS/IB Gateway 必须能从容器中访问: - -1. 在宿主机上运行 TWS/Gateway -2. 使用 `host.docker.internal` 作为主机地址(Docker Desktop) -3. 或配置 host 网络模式 - -## 安全建议 - -- 在 TWS/Gateway 中仅启用"仅允许来自本地主机的连接" -- 使用模拟盘账户进行测试 -- 在策略中设置适当的仓位限制 -- 定期监控您的账户 - -## 参见 - -- [Python 策略开发指南](STRATEGY_DEV_GUIDE_CN.md) -- [盈透证券 API 文档](https://interactivebrokers.github.io/tws-api/) diff --git a/docs/IBKR_TRADING_GUIDE_EN.md b/docs/IBKR_TRADING_GUIDE_EN.md index 7444479..d01b30b 100644 --- a/docs/IBKR_TRADING_GUIDE_EN.md +++ b/docs/IBKR_TRADING_GUIDE_EN.md @@ -1,10 +1,10 @@ # Interactive Brokers (IBKR) Trading Guide -QuantDinger supports US stocks and Hong Kong stocks live trading via Interactive Brokers TWS or IB Gateway. +QuantDinger supports US stocks live trading via Interactive Brokers TWS or IB Gateway. ## Overview -This feature enables automated trading execution for US and HK stock markets through your Interactive Brokers account. Once configured, your trading strategies can automatically place orders via the IBKR API. +This feature enables automated trading execution for US stock markets through your Interactive Brokers account. Once configured, your trading strategies can automatically place orders via the IBKR API. ## Prerequisites @@ -39,7 +39,7 @@ pip install ib_insync ## Strategy Configuration -When creating a strategy for US or HK stocks, configure the IBKR connection in the "Live Trading" section: +When creating a strategy for US stocks, configure the IBKR connection in the "Live Trading" section: | Field | Description | Example | |-------|-------------|---------| @@ -54,7 +54,6 @@ When creating a strategy for US or HK stocks, configure the IBKR connection in t | Market | Format | Examples | |--------|--------|----------| | US Stock | Ticker symbol | `AAPL`, `TSLA`, `GOOGL`, `MSFT` | -| HK Stock | `XXXX.HK` or digits | `0700.HK`, `00700`, `700` | ## Trading Flow @@ -126,11 +125,6 @@ curl -X POST http://localhost:5000/api/ibkr/connect \ curl -X POST http://localhost:5000/api/ibkr/order \ -H "Content-Type: application/json" \ -d '{"symbol": "AAPL", "side": "buy", "quantity": 10, "marketType": "USStock"}' - -# Limit order: sell 100 shares of Tencent -curl -X POST http://localhost:5000/api/ibkr/order \ - -H "Content-Type: application/json" \ - -d '{"symbol": "0700.HK", "side": "sell", "quantity": 100, "marketType": "HShare", "orderType": "limit", "price": 300}' ``` ## Important Notes diff --git a/quantdinger_vue/src/api/billing.js b/quantdinger_vue/src/api/billing.js new file mode 100644 index 0000000..7fcb73f --- /dev/null +++ b/quantdinger_vue/src/api/billing.js @@ -0,0 +1,39 @@ +import request from '@/utils/request' + +const billingApi = { + Plans: '/api/billing/plans', + Purchase: '/api/billing/purchase', + UsdtCreate: '/api/billing/usdt/create', + UsdtOrder: (id) => `/api/billing/usdt/order/${id}` +} + +export function getMembershipPlans () { + return request({ + url: billingApi.Plans, + method: 'get' + }) +} + +export function purchaseMembership (plan) { + return request({ + url: billingApi.Purchase, + method: 'post', + data: { plan } + }) +} + +export function createUsdtOrder (plan) { + return request({ + url: billingApi.UsdtCreate, + method: 'post', + data: { plan } + }) +} + +export function getUsdtOrder (orderId, refresh = true) { + return request({ + url: billingApi.UsdtOrder(orderId), + method: 'get', + params: { refresh: refresh ? 1 : 0 } + }) +} diff --git a/quantdinger_vue/src/api/global-market.js b/quantdinger_vue/src/api/global-market.js index 94495e2..2a76979 100644 --- a/quantdinger_vue/src/api/global-market.js +++ b/quantdinger_vue/src/api/global-market.js @@ -61,10 +61,11 @@ export function getMarketSentiment () { /** * Get trading opportunities based on technical analysis */ -export function getTradingOpportunities () { +export function getTradingOpportunities (params) { return request({ url: `${BASE_URL}/opportunities`, - method: 'get' + method: 'get', + params }) } diff --git a/quantdinger_vue/src/config/router.config.js b/quantdinger_vue/src/config/router.config.js index dc4137f..c682d3f 100644 --- a/quantdinger_vue/src/config/router.config.js +++ b/quantdinger_vue/src/config/router.config.js @@ -21,8 +21,16 @@ export const asyncRouterMap = [ path: '/ai-analysis/:pageNo([1-9]\\d*)?', name: 'Analysis', component: () => import('@/views/ai-analysis'), + hidden: true, meta: { title: 'menu.dashboard.analysis', keepAlive: false, icon: 'thunderbolt', permission: ['dashboard'] } }, + // AI资产分析(统一入口) + { + path: '/ai-asset-analysis', + name: 'AIAssetAnalysis', + component: () => import('@/views/ai-asset-analysis'), + meta: { title: 'menu.dashboard.aiAssetAnalysis', keepAlive: false, icon: 'appstore', permission: ['dashboard'] } + }, // 指标分析 { path: '/indicator-analysis', @@ -49,6 +57,7 @@ export const asyncRouterMap = [ path: '/portfolio', name: 'Portfolio', component: () => import('@/views/portfolio'), + hidden: true, meta: { title: 'menu.dashboard.portfolio', keepAlive: true, icon: 'fund', permission: ['dashboard'] } }, // 用户管理 (admin only) @@ -65,6 +74,13 @@ export const asyncRouterMap = [ component: () => import('@/views/profile'), meta: { title: 'menu.myProfile', keepAlive: false, icon: 'user', permission: ['dashboard'] } }, + // 会员/充值 + { + path: '/billing', + name: 'Billing', + component: () => import('@/views/billing'), + meta: { title: 'menu.billing', keepAlive: false, icon: 'wallet', permission: ['dashboard'] } + }, // 系统设置 (admin only) - 放在最后 { path: '/settings', diff --git a/quantdinger_vue/src/layouts/BasicLayout.vue b/quantdinger_vue/src/layouts/BasicLayout.vue index 2ed0ef0..c188b8b 100644 --- a/quantdinger_vue/src/layouts/BasicLayout.vue +++ b/quantdinger_vue/src/layouts/BasicLayout.vue @@ -119,7 +119,7 @@ diff --git a/quantdinger_vue/src/locales/lang/ar-SA.js b/quantdinger_vue/src/locales/lang/ar-SA.js index fef6a97..5e57ea4 100644 --- a/quantdinger_vue/src/locales/lang/ar-SA.js +++ b/quantdinger_vue/src/locales/lang/ar-SA.js @@ -420,9 +420,7 @@ const locale = { 'dashboard.analysis.modal.addStock.willAutoFetchName': 'سيحصل النظام تلقائيًا على الاسم', 'dashboard.analysis.modal.addStock.addDirectly': 'أضف مباشرة', 'dashboard.analysis.modal.addStock.nameWillBeFetched': 'سيتم التقاط الاسم تلقائيًا عند إضافته', - 'dashboard.analysis.market.AShare': 'أسهم', 'dashboard.analysis.market.USStock': 'الأسهم الأمريكية', - 'dashboard.analysis.market.HShare': 'أسهم هونج كونج', 'dashboard.analysis.market.Crypto': 'عملة مشفرة', 'dashboard.analysis.market.Forex': 'الفوركس', 'dashboard.analysis.market.Futures': 'العقود الآجلة', @@ -576,9 +574,7 @@ const locale = { 'dashboard.indicator.drawing.priceChannel': 'قناة السعر', 'dashboard.indicator.drawing.fibonacciLine': 'خطوط فيبوناتشي', 'dashboard.indicator.drawing.clearAll': 'مسح كافة الرسومات الخطية', - 'dashboard.indicator.market.AShare': 'أسهم', 'dashboard.indicator.market.USStock': 'الأسهم الأمريكية', - 'dashboard.indicator.market.HShare': 'أسهم هونج كونج', 'dashboard.indicator.market.Crypto': 'عملة مشفرة', 'dashboard.indicator.market.Forex': 'الفوركس', 'dashboard.indicator.market.Futures': 'العقود الآجلة', diff --git a/quantdinger_vue/src/locales/lang/de-DE.js b/quantdinger_vue/src/locales/lang/de-DE.js index 59bceee..485a032 100644 --- a/quantdinger_vue/src/locales/lang/de-DE.js +++ b/quantdinger_vue/src/locales/lang/de-DE.js @@ -421,9 +421,7 @@ const locale = { 'dashboard.analysis.modal.addStock.willAutoFetchName': 'Das System erhält den Namen automatisch', 'dashboard.analysis.modal.addStock.addDirectly': 'Direkt hinzufügen', 'dashboard.analysis.modal.addStock.nameWillBeFetched': 'Der Name wird beim Hinzufügen automatisch übernommen', - 'dashboard.analysis.market.AShare': 'A-Aktien', 'dashboard.analysis.market.USStock': 'US-Aktien', - 'dashboard.analysis.market.HShare': 'Aktien aus Hongkong', 'dashboard.analysis.market.Crypto': 'Kryptowährung', 'dashboard.analysis.market.Forex': 'Forex', 'dashboard.analysis.market.Futures': 'Futures', @@ -577,9 +575,7 @@ const locale = { 'dashboard.indicator.drawing.priceChannel': 'Preiskanal', 'dashboard.indicator.drawing.fibonacciLine': 'Fibonacci-Linien', 'dashboard.indicator.drawing.clearAll': 'Löschen Sie alle Strichzeichnungen', - 'dashboard.indicator.market.AShare': 'A-Aktien', 'dashboard.indicator.market.USStock': 'US-Aktien', - 'dashboard.indicator.market.HShare': 'Aktien aus Hongkong', 'dashboard.indicator.market.Crypto': 'Kryptowährung', 'dashboard.indicator.market.Forex': 'Forex', 'dashboard.indicator.market.Futures': 'Futures', @@ -1022,7 +1018,6 @@ const locale = { 'trading-assistant.form.step3Signal': 'Signalzustellung', 'trading-assistant.form.marketCategory': 'Marktkategorie', 'trading-assistant.form.marketCategoryHint': 'Wählen Sie zuerst den Markt. Nur Krypto kann Live-Trading aktivieren; andere Märkte sind nur Signal.', - 'trading-assistant.market.AShare': 'A-Aktien', 'trading-assistant.market.USStock': 'US-Aktien', 'trading-assistant.market.Crypto': 'Krypto', 'trading-assistant.market.Forex': 'Forex', diff --git a/quantdinger_vue/src/locales/lang/en-US.js b/quantdinger_vue/src/locales/lang/en-US.js index af0eeae..98fae48 100644 --- a/quantdinger_vue/src/locales/lang/en-US.js +++ b/quantdinger_vue/src/locales/lang/en-US.js @@ -17,6 +17,8 @@ const locale = { 'common.add': 'Add', 'common.close': 'Close', 'common.ok': 'OK', + 'common.loading': 'Loading...', + 'common.noData': 'No data', 'submit': 'Submit', 'save': 'Save', 'submit.ok': 'Submit successfully', @@ -25,6 +27,7 @@ const locale = { 'menu.home': 'Home', 'menu.dashboard': 'Dashboard', 'menu.dashboard.analysis': 'AI Analysis', + 'menu.dashboard.aiAssetAnalysis': 'AI Asset Analysis', 'menu.dashboard.aiQuant': 'AI Quant', 'menu.dashboard.indicator': 'Indicator Analysis', 'menu.dashboard.community': 'Indicator Market', @@ -36,6 +39,76 @@ const locale = { 'menu.dashboard.signalRobot': 'Signal Robot', 'menu.dashboard.monitor': 'Monitor', 'menu.dashboard.workplace': 'Workplace', + 'aiAssetAnalysis.title': 'AI Asset Analysis', + 'aiAssetAnalysis.subtitle': 'Unify portfolio monitoring, instant analysis, and scheduled tasks into one smooth workflow.', + 'aiAssetAnalysis.actions.quickAnalysis': 'Start Analysis Now', + 'aiAssetAnalysis.actions.monitorTasks': 'Manage Monitor Tasks', + 'aiAssetAnalysis.actions.openStandalone': 'Open Standalone Page', + 'aiAssetAnalysis.quickBar.title': 'Quick Analyze', + 'aiAssetAnalysis.quickBar.placeholder': 'Select a symbol from asset pool', + 'aiAssetAnalysis.quickBar.useInQuick': 'Use in Instant Analysis', + 'aiAssetAnalysis.quickBar.runNow': 'Analyze Now', + 'aiAssetAnalysis.history.title': 'Recent Analysis', + 'aiAssetAnalysis.history.empty': 'No analysis history yet', + 'aiAssetAnalysis.actions.enterQuick': 'Go to Instant Analysis', + 'aiAssetAnalysis.actions.enterMonitor': 'Go to Asset Pool & Tasks', + 'aiAssetAnalysis.flow.poolTitle': 'Build Asset Pool', + 'aiAssetAnalysis.flow.poolDesc': 'Add positions and watch targets first to build one unified analysis pool.', + 'aiAssetAnalysis.flow.poolAction': 'Manage Asset Pool', + 'aiAssetAnalysis.flow.quickTitle': 'Instant Analysis', + 'aiAssetAnalysis.flow.quickDesc': 'Run AI analysis for any symbol in one click and get suggestions quickly.', + 'aiAssetAnalysis.flow.quickAction': 'Run Analysis', + 'aiAssetAnalysis.flow.autoTitle': 'Scheduled Monitoring', + 'aiAssetAnalysis.flow.autoDesc': 'Set recurring AI analysis tasks and deliver reports through notification channels.', + 'aiAssetAnalysis.flow.autoAction': 'Configure Tasks', + 'aiAssetAnalysis.tabs.quick': 'Instant Analysis', + 'aiAssetAnalysis.tabs.monitor': 'Asset Pool & Scheduled Tasks', + 'aiAssetAnalysis.tabLead.quick': 'Best for ad-hoc decisions: select a symbol and analyze immediately.', + 'aiAssetAnalysis.tabLead.monitor': 'Best for continuous tracking: maintain positions and set monitor ranges/frequency.', + // AI Asset Analysis - Performance Stats + 'aiAssetAnalysis.stats.totalAnalyses': 'Total Analyses', + 'aiAssetAnalysis.stats.accuracy': 'Accuracy', + 'aiAssetAnalysis.stats.avgReturn': 'Avg Return', + 'aiAssetAnalysis.stats.satisfaction': 'Satisfaction', + 'aiAssetAnalysis.stats.decisions': 'Decision Dist.', + // AI Asset Analysis - Trading Opportunities + 'aiAssetAnalysis.opportunities.title': 'AI Opportunity Radar', + 'aiAssetAnalysis.opportunities.empty': 'No opportunities found', + 'aiAssetAnalysis.opportunities.analyze': 'Analyze', + 'aiAssetAnalysis.opportunities.updateHint': 'Updates hourly', + 'aiAssetAnalysis.opportunities.signal.overbought': 'Overbought', + 'aiAssetAnalysis.opportunities.signal.oversold': 'Oversold', + 'aiAssetAnalysis.opportunities.signal.bullish_momentum': 'Bullish', + 'aiAssetAnalysis.opportunities.signal.bearish_momentum': 'Bearish', + 'aiAssetAnalysis.opportunities.market.Crypto': '🪙 Crypto', + 'aiAssetAnalysis.opportunities.market.USStock': '📈 US Stock', + 'aiAssetAnalysis.opportunities.market.Forex': '💱 Forex', + 'aiAssetAnalysis.opportunities.reason.crypto.overbought': '24h +{change}%, 7d +{change7d}%, overbought risk', + 'aiAssetAnalysis.opportunities.reason.crypto.bullish_momentum': '24h +{change}%, strong bullish momentum', + 'aiAssetAnalysis.opportunities.reason.crypto.oversold': '24h -{change}%, possible oversold bounce', + 'aiAssetAnalysis.opportunities.reason.crypto.bearish_momentum': '24h -{change}%, clear bearish trend', + 'aiAssetAnalysis.opportunities.reason.usstock.overbought': 'Day +{change}%, large gain, watch for pullback', + 'aiAssetAnalysis.opportunities.reason.usstock.bullish_momentum': 'Day +{change}%, strong bullish momentum', + 'aiAssetAnalysis.opportunities.reason.usstock.oversold': 'Day -{change}%, possible oversold bounce', + 'aiAssetAnalysis.opportunities.reason.usstock.bearish_momentum': 'Day -{change}%, clear bearish trend', + 'aiAssetAnalysis.opportunities.reason.forex.overbought': 'Day +{change}%, volatile, watch for reversal', + 'aiAssetAnalysis.opportunities.reason.forex.bullish_momentum': 'Day +{change}%, bullish momentum', + 'aiAssetAnalysis.opportunities.reason.forex.oversold': 'Day -{change}%, volatile, possible bounce', + 'aiAssetAnalysis.opportunities.reason.forex.bearish_momentum': 'Day -{change}%, clear bearish trend', + // AI Asset Analysis - Portfolio Checkup + 'aiAssetAnalysis.checkup.title': 'Portfolio Checkup', + 'aiAssetAnalysis.checkup.btn': 'Checkup', + 'aiAssetAnalysis.checkup.desc': 'Run AI analysis on all positions in your portfolio to quickly understand the current status and get recommendations for each asset.', + 'aiAssetAnalysis.checkup.start': 'Start Checkup', + 'aiAssetAnalysis.checkup.progress': 'Analyzing {current}/{total}...', + 'aiAssetAnalysis.checkup.noPositions': 'No positions in your portfolio. Please add assets first.', + 'aiAssetAnalysis.checkup.complete': 'Checkup Complete', + 'aiAssetAnalysis.checkup.failed': 'Failed', + 'aiAssetAnalysis.checkup.rerun': 'Run Again', + // AI Asset Analysis - Global Search + 'aiAssetAnalysis.search.hint': 'Search', + 'aiAssetAnalysis.search.placeholder': 'Search any symbol... (Ctrl+K)', + 'aiAssetAnalysis.search.noResults': 'No results found. Try a different keyword.', 'menu.form': 'Form', 'menu.form.basic-form': 'Basic Form', 'menu.form.step-form': 'Step Form', @@ -558,9 +631,7 @@ const locale = { 'dashboard.analysis.modal.addStock.willAutoFetchName': 'Name will be fetched automatically', 'dashboard.analysis.modal.addStock.addDirectly': 'Add Directly', 'dashboard.analysis.modal.addStock.nameWillBeFetched': 'Name will be fetched automatically when adding', - 'dashboard.analysis.market.AShare': 'A-Share', 'dashboard.analysis.market.USStock': 'US Stock', - 'dashboard.analysis.market.HShare': 'H-Share', 'dashboard.analysis.market.Crypto': 'Cryptocurrency', 'dashboard.analysis.market.Forex': 'Forex', 'dashboard.analysis.market.Futures': 'Futures', @@ -735,9 +806,7 @@ const locale = { 'dashboard.indicator.drawing.priceChannel': 'Price Channel', 'dashboard.indicator.drawing.fibonacciLine': 'Fibonacci Line', 'dashboard.indicator.drawing.clearAll': 'Clear All Drawings', - 'dashboard.indicator.market.AShare': 'A-Share', 'dashboard.indicator.market.USStock': 'US Stock', - 'dashboard.indicator.market.HShare': 'H-Share', 'dashboard.indicator.market.Crypto': 'Cryptocurrency', 'dashboard.indicator.market.Forex': 'Forex', 'dashboard.indicator.market.Futures': 'Futures', @@ -1301,9 +1370,17 @@ const locale = { 'trading-assistant.form.step3': 'Strategy Parameters', 'trading-assistant.form.step2Params': 'Parameters', 'trading-assistant.form.step3Signal': 'Signal Delivery', + 'trading-assistant.form.simpleMode': 'Simple', + 'trading-assistant.form.advancedMode': 'Advanced', + 'trading-assistant.form.simpleModeHint': 'Quick setup with recommended defaults', + 'trading-assistant.form.advancedModeHint': 'Customize all parameters for pros', + 'trading-assistant.form.simpleStep1': 'Select Indicator & Pair', + 'trading-assistant.form.simpleStep2': 'Launch Mode', + 'trading-assistant.form.simpleDefaultsHint': 'Defaults (expand to customize)', + 'trading-assistant.form.showAdvancedSettings': 'Show Advanced Settings', + 'trading-assistant.form.hideAdvancedSettings': 'Hide Advanced Settings', 'trading-assistant.form.marketCategory': 'Market Category', 'trading-assistant.form.marketCategoryHint': 'Select a market first. Only Crypto can enable live trading; other markets are signal-only.', - 'trading-assistant.market.AShare': 'A-Share', 'trading-assistant.market.USStock': 'US Stock', 'trading-assistant.market.Crypto': 'Crypto', 'trading-assistant.market.Forex': 'Forex', @@ -2232,7 +2309,6 @@ const locale = { 'settings.field.CCXT_DEFAULT_EXCHANGE': 'CCXT Default Exchange', 'settings.field.CCXT_TIMEOUT': 'CCXT Timeout (ms)', 'settings.field.CCXT_PROXY': 'CCXT Proxy', - 'settings.field.AKSHARE_TIMEOUT': 'Akshare Timeout (sec)', 'settings.field.YFINANCE_TIMEOUT': 'YFinance Timeout (sec)', 'settings.field.TIINGO_API_KEY': 'Tiingo API Key', 'settings.field.TIINGO_TIMEOUT': 'Tiingo Timeout (sec)', @@ -2249,9 +2325,9 @@ const locale = { // Settings descriptions (config item descriptions) // Note: These are optional since backend already provides English descriptions - 'settings.desc.SEARCH_PROVIDER': 'Web search provider for AI research. Bocha recommended for A-share news', + 'settings.desc.SEARCH_PROVIDER': 'Web search provider for AI research', 'settings.desc.TAVILY_API_KEYS': 'Tavily Search API keys, comma-separated for rotation. Free 1000 requests/month', - 'settings.desc.BOCHA_API_KEYS': 'Bocha Search API keys, comma-separated for rotation. Best for A-share news', + 'settings.desc.BOCHA_API_KEYS': 'Bocha Search API keys, comma-separated for rotation', 'settings.desc.SERPAPI_KEYS': 'SerpAPI keys for Google/Bing search, comma-separated for rotation', 'settings.desc.ORDER_MODE': 'maker: Limit order first (lower fees), market: Market order (instant fill)', 'settings.desc.MAKER_WAIT_SEC': 'Wait time for limit order fill before switching to market order', @@ -2473,7 +2549,7 @@ const locale = { 'profile.credits.vipExpires': 'VIP expires on', 'profile.credits.vipExpired': 'VIP expired', 'profile.credits.noVip': 'Not a VIP', - 'profile.credits.hint': 'AI analysis and other features consume credits. VIP users get free access.', + 'profile.credits.hint': 'AI analysis/backtest/monitoring will consume credits; VIP only makes VIP-free indicators free to use.', // Profile - Credits Log 'profile.creditsLog': 'Credits History', @@ -2591,11 +2667,13 @@ const locale = { 'systemOverview.realized': 'Real', 'systemOverview.unrealized': 'Unreal', 'systemOverview.symbols': 'symbols', + 'systemOverview.live': 'Live', + 'systemOverview.signal': 'Signal Only', // Settings - Billing 'settings.group.billing': 'Billing & Credits', 'settings.field.BILLING_ENABLED': 'Enable Billing', - 'settings.field.BILLING_VIP_BYPASS': 'VIP Free Access', + 'settings.field.BILLING_VIP_BYPASS': 'VIP Bypass (Legacy)', 'settings.field.BILLING_COST_AI_ANALYSIS': 'AI Analysis Cost', 'settings.field.BILLING_COST_STRATEGY_RUN': 'Strategy Run Cost', 'settings.field.BILLING_COST_BACKTEST': 'Backtest Cost', @@ -2603,8 +2681,22 @@ const locale = { 'settings.field.CREDITS_REGISTER_BONUS': 'Register Bonus', 'settings.field.CREDITS_REFERRAL_BONUS': 'Referral Bonus', 'settings.field.RECHARGE_TELEGRAM_URL': 'Recharge Telegram URL', + 'settings.field.MEMBERSHIP_MONTHLY_PRICE_USD': 'Monthly Membership Price (USD)', + 'settings.field.MEMBERSHIP_MONTHLY_CREDITS': 'Monthly Membership Bonus Credits', + 'settings.field.MEMBERSHIP_YEARLY_PRICE_USD': 'Yearly Membership Price (USD)', + 'settings.field.MEMBERSHIP_YEARLY_CREDITS': 'Yearly Membership Bonus Credits', + 'settings.field.MEMBERSHIP_LIFETIME_PRICE_USD': 'Lifetime Membership Price (USD)', + 'settings.field.MEMBERSHIP_LIFETIME_MONTHLY_CREDITS': 'Lifetime Monthly Credits', + 'settings.field.USDT_PAY_ENABLED': 'Enable USDT Pay', + 'settings.field.USDT_PAY_CHAIN': 'USDT Chain', + 'settings.field.USDT_TRC20_XPUB': 'TRC20 XPUB (Watch-only)', + 'settings.field.USDT_TRC20_CONTRACT': 'USDT TRC20 Contract', + 'settings.field.TRONGRID_BASE_URL': 'TronGrid Base URL', + 'settings.field.TRONGRID_API_KEY': 'TronGrid API Key', + 'settings.field.USDT_PAY_CONFIRM_SECONDS': 'Confirm Delay (sec)', + 'settings.field.USDT_PAY_EXPIRE_MINUTES': 'Order Expire (min)', 'settings.desc.BILLING_ENABLED': 'Enable billing system. Users need credits to use certain features when enabled', - 'settings.desc.BILLING_VIP_BYPASS': 'VIP users can use all paid features for free during VIP period', + 'settings.desc.BILLING_VIP_BYPASS': 'Legacy switch. If enabled, VIP users bypass ALL feature credit costs. Recommended OFF: VIP should only unlock VIP-free indicators.', 'settings.desc.BILLING_COST_AI_ANALYSIS': 'Credits consumed per AI analysis request', 'settings.desc.BILLING_COST_STRATEGY_RUN': 'Credits consumed when starting a strategy', 'settings.desc.BILLING_COST_BACKTEST': 'Credits consumed per backtest run', @@ -2614,6 +2706,20 @@ const locale = { 'settings.desc.VERIFICATION_CODE_MAX_ATTEMPTS': 'Maximum attempts to verify a code before lockout', 'settings.desc.VERIFICATION_CODE_LOCK_MINUTES': 'Lockout duration after exceeding max attempts', 'settings.desc.RECHARGE_TELEGRAM_URL': 'Telegram customer service URL for recharge inquiries', + 'settings.desc.MEMBERSHIP_MONTHLY_PRICE_USD': 'Monthly membership price in USD (mock payment in current version).', + 'settings.desc.MEMBERSHIP_MONTHLY_CREDITS': 'Credits granted immediately after purchasing monthly membership.', + 'settings.desc.MEMBERSHIP_YEARLY_PRICE_USD': 'Yearly membership price in USD (mock payment in current version).', + 'settings.desc.MEMBERSHIP_YEARLY_CREDITS': 'Credits granted immediately after purchasing yearly membership.', + 'settings.desc.MEMBERSHIP_LIFETIME_PRICE_USD': 'Lifetime membership price in USD (mock payment in current version).', + 'settings.desc.MEMBERSHIP_LIFETIME_MONTHLY_CREDITS': 'Credits granted every 30 days for lifetime members (first grant happens immediately on purchase).', + 'settings.desc.USDT_PAY_ENABLED': 'Enable USDT scan-to-pay flow (per-order unique address + auto reconciliation).', + 'settings.desc.USDT_PAY_CHAIN': 'Currently only TRC20 is supported.', + 'settings.desc.USDT_TRC20_XPUB': 'Watch-only xpub used to derive per-order deposit addresses. Do NOT paste private key/seed.', + 'settings.desc.USDT_TRC20_CONTRACT': 'USDT contract address on TRON (default prefilled).', + 'settings.desc.TRONGRID_BASE_URL': 'TronGrid API base URL (default is fine).', + 'settings.desc.TRONGRID_API_KEY': 'Optional. For higher TronGrid rate limits and stability.', + 'settings.desc.USDT_PAY_CONFIRM_SECONDS': 'After a payment is detected, wait N seconds before marking it confirmed.', + 'settings.desc.USDT_PAY_EXPIRE_MINUTES': 'Order expiration time; users need to regenerate an order after it expires.', // Global Market 'globalMarket.title': 'Global Market Dashboard', @@ -2990,7 +3096,61 @@ const locale = { 'aiQuant.template.news': '📰 News Driven', 'aiQuant.template.custom': '✏️ Custom', 'aiQuant.hint.dataProvided': 'System auto-provides: real-time price, indicators (RSI/MACD/MA), recent news, macro data. AI analyzes these with your prompt to determine direction.', - 'aiQuant.hint.liveWarning': 'Live mode will trade with REAL money! Make sure you have configured exchange API and understand the risks!' + 'aiQuant.hint.liveWarning': 'Live mode will trade with REAL money! Make sure you have configured exchange API and understand the risks!', + + // Trading Assistant - Live Trading Disclaimer + 'trading-assistant.liveDisclaimer.title': 'Live Trading Disclaimer', + 'trading-assistant.liveDisclaimer.content': 'Live trading involves significant risk and may result in partial or total loss of funds. The platform does not guarantee returns or profits. You are responsible for your own decisions and outcomes.', + 'trading-assistant.liveDisclaimer.agree': 'I have read and understood the disclaimer and still want to enable live trading', + 'trading-assistant.liveDisclaimer.required': 'Please accept the disclaimer before enabling live trading', + 'trading-assistant.liveDisclaimer.blockTitle': 'Please accept the disclaimer first', + 'trading-assistant.liveDisclaimer.blockDesc': 'You must accept the disclaimer to configure live trading connection and order settings.', + + // Billing / Membership + 'menu.billing': 'Membership', + 'billing.title': 'Membership / Credits', + 'billing.desc': 'Choose a plan to activate VIP and receive bonus credits.', + 'billing.snapshot.credits': 'Current Credits', + 'billing.snapshot.vip': 'VIP Status', + 'billing.snapshot.notVip': 'Not VIP', + 'billing.snapshot.expires': 'Expires', + 'billing.vipRule.title': 'VIP Benefit', + 'billing.vipRule.desc': 'VIP has only one special permission: VIP-free indicators can be used without credits deduction. Other paid features/indicators still consume credits.', + 'billing.plan.monthly': 'Monthly', + 'billing.plan.yearly': 'Yearly', + 'billing.plan.lifetime': 'Lifetime', + 'billing.perMonth': 'month', + 'billing.perYear': 'year', + 'billing.once': 'one-time', + 'billing.credits': 'Credits', + 'billing.lifetimeMonthly': 'Monthly bonus', + 'billing.buyNow': 'Buy Now', + 'billing.purchaseSuccess': 'Purchase successful', + 'billing.purchaseFailed': 'Purchase failed', + 'billing.usdt.title': 'USDT Scan to Pay', + 'billing.usdt.hintTitle': 'Scan with your wallet and send USDT', + 'billing.usdt.hintDesc': 'Make sure the network and amount are correct (TRC20 only for now). Membership will be activated automatically after payment is confirmed.', + 'billing.usdt.chain': 'Chain', + 'billing.usdt.amount': 'Amount', + 'billing.usdt.address': 'Deposit Address', + 'billing.usdt.copyAddress': 'Copy Address', + 'billing.usdt.copyAmount': 'Copy Amount', + 'billing.usdt.refresh': 'Refresh', + 'billing.usdt.expires': 'Expires', + 'billing.usdt.paidSuccess': 'Payment confirmed. Membership activated.', + 'billing.usdt.status.pending': 'Waiting for payment', + 'billing.usdt.status.paid': 'Payment detected', + 'billing.usdt.status.confirmed': 'Confirmed', + 'billing.usdt.status.expired': 'Expired', + 'billing.usdt.status.cancelled': 'Cancelled', + 'billing.usdt.status.failed': 'Failed', + + // Community + 'community.vipFree': 'VIP Free', + + // Indicator publish + 'dashboard.indicator.publish.vipFree': 'VIP Free', + 'dashboard.indicator.publish.vipFreeHint': 'When enabled: VIP users can use this indicator for free (non-VIP still need to purchase).' } export default { diff --git a/quantdinger_vue/src/locales/lang/fr-FR.js b/quantdinger_vue/src/locales/lang/fr-FR.js index e84ca57..af8d638 100644 --- a/quantdinger_vue/src/locales/lang/fr-FR.js +++ b/quantdinger_vue/src/locales/lang/fr-FR.js @@ -421,9 +421,7 @@ const locale = { 'dashboard.analysis.modal.addStock.willAutoFetchName': 'Le système obtiendra automatiquement le nom', 'dashboard.analysis.modal.addStock.addDirectly': 'Ajouter directement', 'dashboard.analysis.modal.addStock.nameWillBeFetched': 'Le nom sera récupéré automatiquement une fois ajouté', - 'dashboard.analysis.market.AShare': 'Un partage', 'dashboard.analysis.market.USStock': 'Actions américaines', - 'dashboard.analysis.market.HShare': 'Actions de Hong Kong', 'dashboard.analysis.market.Crypto': 'crypto-monnaie', 'dashboard.analysis.market.Forex': 'Forex', 'dashboard.analysis.market.Futures': 'Contrats à terme', @@ -577,9 +575,7 @@ const locale = { 'dashboard.indicator.drawing.priceChannel': 'canal de prix', 'dashboard.indicator.drawing.fibonacciLine': 'Lignes de Fibonacci', 'dashboard.indicator.drawing.clearAll': 'Effacer tous les dessins au trait', - 'dashboard.indicator.market.AShare': 'Un partage', 'dashboard.indicator.market.USStock': 'Actions américaines', - 'dashboard.indicator.market.HShare': 'Actions de Hong Kong', 'dashboard.indicator.market.Crypto': 'crypto-monnaie', 'dashboard.indicator.market.Forex': 'Forex', 'dashboard.indicator.market.Futures': 'Contrats à terme', diff --git a/quantdinger_vue/src/locales/lang/ja-JP.js b/quantdinger_vue/src/locales/lang/ja-JP.js index 7309e8b..7daf1fb 100644 --- a/quantdinger_vue/src/locales/lang/ja-JP.js +++ b/quantdinger_vue/src/locales/lang/ja-JP.js @@ -421,9 +421,7 @@ const locale = { 'dashboard.analysis.modal.addStock.willAutoFetchName': 'システムが自動的に名前を取得します', 'dashboard.analysis.modal.addStock.addDirectly': '直接追加', 'dashboard.analysis.modal.addStock.nameWillBeFetched': '名前は追加時に自動的に取得されます', - 'dashboard.analysis.market.AShare': 'A株', 'dashboard.analysis.market.USStock': '米国株', - 'dashboard.analysis.market.HShare': '香港株', 'dashboard.analysis.market.Crypto': '暗号通貨', 'dashboard.analysis.market.Forex': '外国為替', 'dashboard.analysis.market.Futures': '先物', @@ -577,9 +575,7 @@ const locale = { 'dashboard.indicator.drawing.priceChannel': '価格チャネル', 'dashboard.indicator.drawing.fibonacciLine': 'フィボナッチライン', 'dashboard.indicator.drawing.clearAll': '線画をすべてクリアする', - 'dashboard.indicator.market.AShare': 'A株', 'dashboard.indicator.market.USStock': '米国株', - 'dashboard.indicator.market.HShare': '香港株', 'dashboard.indicator.market.Crypto': '暗号通貨', 'dashboard.indicator.market.Forex': '外国為替', 'dashboard.indicator.market.Futures': '先物', diff --git a/quantdinger_vue/src/locales/lang/ko-KR.js b/quantdinger_vue/src/locales/lang/ko-KR.js index 1abd96f..9b01e7d 100644 --- a/quantdinger_vue/src/locales/lang/ko-KR.js +++ b/quantdinger_vue/src/locales/lang/ko-KR.js @@ -420,9 +420,7 @@ const locale = { 'dashboard.analysis.modal.addStock.willAutoFetchName': '시스템에서 자동으로 이름을 가져옵니다.', 'dashboard.analysis.modal.addStock.addDirectly': '직접 추가', 'dashboard.analysis.modal.addStock.nameWillBeFetched': '추가되면 이름이 자동으로 선택됩니다.', - 'dashboard.analysis.market.AShare': 'A주', 'dashboard.analysis.market.USStock': '미국 주식', - 'dashboard.analysis.market.HShare': '홍콩 주식', 'dashboard.analysis.market.Crypto': '암호화폐', 'dashboard.analysis.market.Forex': '외환', 'dashboard.analysis.market.Futures': '선물', @@ -576,9 +574,7 @@ const locale = { 'dashboard.indicator.drawing.priceChannel': '가격 채널', 'dashboard.indicator.drawing.fibonacciLine': '피보나치 라인', 'dashboard.indicator.drawing.clearAll': '모든 선화 지우기', - 'dashboard.indicator.market.AShare': 'A주', 'dashboard.indicator.market.USStock': '미국 주식', - 'dashboard.indicator.market.HShare': '홍콩 주식', 'dashboard.indicator.market.Crypto': '암호화폐', 'dashboard.indicator.market.Forex': '외환', 'dashboard.indicator.market.Futures': '선물', diff --git a/quantdinger_vue/src/locales/lang/th-TH.js b/quantdinger_vue/src/locales/lang/th-TH.js index a6ac03d..c1200b9 100644 --- a/quantdinger_vue/src/locales/lang/th-TH.js +++ b/quantdinger_vue/src/locales/lang/th-TH.js @@ -421,9 +421,7 @@ const locale = { 'dashboard.analysis.modal.addStock.willAutoFetchName': 'ระบบจะรับชื่อให้อัตโนมัติ', 'dashboard.analysis.modal.addStock.addDirectly': 'เพิ่มโดยตรง', 'dashboard.analysis.modal.addStock.nameWillBeFetched': 'ชื่อจะถูกหยิบขึ้นมาโดยอัตโนมัติเมื่อเพิ่ม', - 'dashboard.analysis.market.AShare': 'มีหุ้น', 'dashboard.analysis.market.USStock': 'หุ้นสหรัฐ', - 'dashboard.analysis.market.HShare': 'หุ้นฮ่องกง', 'dashboard.analysis.market.Crypto': 'สกุลเงินดิจิทัล', 'dashboard.analysis.market.Forex': 'ฟอเร็กซ์', 'dashboard.analysis.market.Futures': 'ฟิวเจอร์ส', @@ -577,9 +575,7 @@ const locale = { 'dashboard.indicator.drawing.priceChannel': 'ช่องราคา', 'dashboard.indicator.drawing.fibonacciLine': 'เส้นฟีโบนักชี', 'dashboard.indicator.drawing.clearAll': 'ล้างภาพวาดเส้นทั้งหมด', - 'dashboard.indicator.market.AShare': 'มีหุ้น', 'dashboard.indicator.market.USStock': 'หุ้นสหรัฐ', - 'dashboard.indicator.market.HShare': 'หุ้นฮ่องกง', 'dashboard.indicator.market.Crypto': 'สกุลเงินดิจิทัล', 'dashboard.indicator.market.Forex': 'ฟอเร็กซ์', 'dashboard.indicator.market.Futures': 'ฟิวเจอร์ส', diff --git a/quantdinger_vue/src/locales/lang/vi-VN.js b/quantdinger_vue/src/locales/lang/vi-VN.js index ed53382..41110ee 100644 --- a/quantdinger_vue/src/locales/lang/vi-VN.js +++ b/quantdinger_vue/src/locales/lang/vi-VN.js @@ -830,12 +830,8 @@ const locale = { 'dashboard.analysis.modal.addStock.nameWillBeFetched': 'Tên sẽ được tự động lấy khi thêm', -'dashboard.analysis.market.AShare': 'Cổ phiếu loại A', - 'dashboard.analysis.market.USStock': 'Cổ phiếu Mỹ', -'dashboard.analysis.market.HShare': 'Cổ phiếu Hồng Kông cổ phiếu', - 'dashboard.analysis.market.Crypto': 'Tiền điện tử', 'dashboard.analysis.market.Forex': 'Ngoại hối', @@ -1138,12 +1134,8 @@ const locale = { 'dashboard.indicator.drawing.clearAll': 'xóa tất cả các đường đã vẽ', -'dashboard.indicator.market.AShare': 'cổ phiếu A', - 'dashboard.indicator.market.USStock': 'cổ phiếu Mỹ', -'dashboard.indicator.market.HShare': 'cổ phiếu Hồng Kông', - 'dashboard.indicator.market.Crypto': 'tiền điện tử', 'dashboard.indicator.market.Forex': 'ngoại hối', diff --git a/quantdinger_vue/src/locales/lang/zh-CN.js b/quantdinger_vue/src/locales/lang/zh-CN.js index 48cf2b1..574a19c 100644 --- a/quantdinger_vue/src/locales/lang/zh-CN.js +++ b/quantdinger_vue/src/locales/lang/zh-CN.js @@ -17,6 +17,8 @@ const locale = { 'common.add': '添加', 'common.close': '关闭', 'common.ok': '确定', + 'common.loading': '加载中...', + 'common.noData': '暂无数据', 'submit': '提交', 'save': '保存', 'submit.ok': '提交成功', @@ -27,6 +29,7 @@ const locale = { 'menu.dashboard.indicator': '指标分析', 'menu.dashboard.community': '指标市场', 'menu.dashboard.analysis': 'AI 分析', + 'menu.dashboard.aiAssetAnalysis': 'AI资产分析', 'menu.dashboard.aiQuant': 'AI 量化', 'menu.dashboard.tradingAssistant': '交易助手', 'menu.dashboard.portfolio': '资产监测', @@ -36,6 +39,76 @@ const locale = { 'menu.dashboard.signalRobot': '信号机器人', 'menu.dashboard.monitor': '监控页', 'menu.dashboard.workplace': '工作台', + 'aiAssetAnalysis.title': 'AI资产分析', + 'aiAssetAnalysis.subtitle': '把资产监测、即时分析、定时任务放到一个工作流里,操作更直观、反馈更及时。', + 'aiAssetAnalysis.actions.quickAnalysis': '立即开始分析', + 'aiAssetAnalysis.actions.monitorTasks': '管理监控任务', + 'aiAssetAnalysis.actions.openStandalone': '在独立页面打开', + 'aiAssetAnalysis.quickBar.title': '快捷分析', + 'aiAssetAnalysis.quickBar.placeholder': '从资产池选择标的', + 'aiAssetAnalysis.quickBar.useInQuick': '带入即时分析', + 'aiAssetAnalysis.quickBar.runNow': '一键分析', + 'aiAssetAnalysis.history.title': '最近分析', + 'aiAssetAnalysis.history.empty': '暂无分析记录', + 'aiAssetAnalysis.actions.enterQuick': '进入即时分析', + 'aiAssetAnalysis.actions.enterMonitor': '进入定时任务与资产池', + 'aiAssetAnalysis.flow.poolTitle': '构建资产池', + 'aiAssetAnalysis.flow.poolDesc': '先添加持仓与关注标的,形成统一分析对象池。', + 'aiAssetAnalysis.flow.poolAction': '去管理资产池', + 'aiAssetAnalysis.flow.quickTitle': '即时分析', + 'aiAssetAnalysis.flow.quickDesc': '对任意标的一键发起 AI 分析,快速拿到决策建议。', + 'aiAssetAnalysis.flow.quickAction': '去即时分析', + 'aiAssetAnalysis.flow.autoTitle': '定时监控', + 'aiAssetAnalysis.flow.autoDesc': '配置定时任务自动运行 AI 分析,并推送报告到通知渠道。', + 'aiAssetAnalysis.flow.autoAction': '去配置任务', + 'aiAssetAnalysis.tabs.quick': '即时分析', + 'aiAssetAnalysis.tabs.monitor': '资产池与定时任务', + 'aiAssetAnalysis.tabLead.quick': '适合临时判断:选择标的后立刻分析并查看历史结果。', + 'aiAssetAnalysis.tabLead.monitor': '适合持续跟踪:维护持仓、设置监控范围与执行频率。', + // AI资产分析 - 准确率看板 + 'aiAssetAnalysis.stats.totalAnalyses': '分析总数', + 'aiAssetAnalysis.stats.accuracy': '准确率', + 'aiAssetAnalysis.stats.avgReturn': '平均回报', + 'aiAssetAnalysis.stats.satisfaction': '用户满意度', + 'aiAssetAnalysis.stats.decisions': '决策分布', + // AI资产分析 - 交易机会 + 'aiAssetAnalysis.opportunities.title': 'AI 交易机会雷达', + 'aiAssetAnalysis.opportunities.empty': '暂无交易机会', + 'aiAssetAnalysis.opportunities.analyze': '分析', + 'aiAssetAnalysis.opportunities.updateHint': '每小时更新', + 'aiAssetAnalysis.opportunities.signal.overbought': '超买', + 'aiAssetAnalysis.opportunities.signal.oversold': '超卖', + 'aiAssetAnalysis.opportunities.signal.bullish_momentum': '看涨', + 'aiAssetAnalysis.opportunities.signal.bearish_momentum': '看跌', + 'aiAssetAnalysis.opportunities.market.Crypto': '🪙 加密', + 'aiAssetAnalysis.opportunities.market.USStock': '📈 美股', + 'aiAssetAnalysis.opportunities.market.Forex': '💱 外汇', + 'aiAssetAnalysis.opportunities.reason.crypto.overbought': '24h涨幅{change}%,7日涨幅{change7d}%,短期超买风险', + 'aiAssetAnalysis.opportunities.reason.crypto.bullish_momentum': '24h涨幅{change}%,上涨动能强劲', + 'aiAssetAnalysis.opportunities.reason.crypto.oversold': '24h跌幅{change}%,可能超卖反弹', + 'aiAssetAnalysis.opportunities.reason.crypto.bearish_momentum': '24h跌幅{change}%,下跌趋势明显', + 'aiAssetAnalysis.opportunities.reason.usstock.overbought': '日涨幅{change}%,短期涨幅较大,注意回调风险', + 'aiAssetAnalysis.opportunities.reason.usstock.bullish_momentum': '日涨幅{change}%,上涨动能强劲', + 'aiAssetAnalysis.opportunities.reason.usstock.oversold': '日跌幅{change}%,可能超卖反弹', + 'aiAssetAnalysis.opportunities.reason.usstock.bearish_momentum': '日跌幅{change}%,下跌趋势明显', + 'aiAssetAnalysis.opportunities.reason.forex.overbought': '日涨幅{change}%,汇率波动剧烈,注意回调', + 'aiAssetAnalysis.opportunities.reason.forex.bullish_momentum': '日涨幅{change}%,上涨动能较强', + 'aiAssetAnalysis.opportunities.reason.forex.oversold': '日跌幅{change}%,汇率波动剧烈,可能反弹', + 'aiAssetAnalysis.opportunities.reason.forex.bearish_momentum': '日跌幅{change}%,下跌趋势明显', + // AI资产分析 - 一键组合体检 + 'aiAssetAnalysis.checkup.title': '组合体检', + 'aiAssetAnalysis.checkup.btn': '组合体检', + 'aiAssetAnalysis.checkup.desc': '一键对您资产池中的所有持仓进行 AI 分析,快速了解每个标的的当前状态和操作建议。', + 'aiAssetAnalysis.checkup.start': '开始体检', + 'aiAssetAnalysis.checkup.progress': '正在分析 {current}/{total}...', + 'aiAssetAnalysis.checkup.noPositions': '资产池中暂无持仓,请先添加资产。', + 'aiAssetAnalysis.checkup.complete': '体检完成', + 'aiAssetAnalysis.checkup.failed': '分析失败', + 'aiAssetAnalysis.checkup.rerun': '重新体检', + // AI资产分析 - 全局搜索 + 'aiAssetAnalysis.search.hint': '搜索', + 'aiAssetAnalysis.search.placeholder': '搜索任意标的... (Ctrl+K)', + 'aiAssetAnalysis.search.noResults': '未找到匹配结果,请尝试其他关键词', 'menu.form': '表单页', 'menu.form.basic-form': '基础表单', 'menu.form.step-form': '分步表单', @@ -558,9 +631,7 @@ const locale = { 'dashboard.analysis.modal.addStock.willAutoFetchName': '系统将自动获取名称', 'dashboard.analysis.modal.addStock.addDirectly': '直接添加', 'dashboard.analysis.modal.addStock.nameWillBeFetched': '名称将在添加时自动获取', - 'dashboard.analysis.market.AShare': 'A股', 'dashboard.analysis.market.USStock': '美股', - 'dashboard.analysis.market.HShare': '港股', 'dashboard.analysis.market.Crypto': '加密货币', 'dashboard.analysis.market.Forex': '外汇', 'dashboard.analysis.market.Futures': '期货', @@ -735,9 +806,7 @@ const locale = { 'dashboard.indicator.drawing.priceChannel': '价格通道', 'dashboard.indicator.drawing.fibonacciLine': '斐波那契线', 'dashboard.indicator.drawing.clearAll': '清除所有画线', - 'dashboard.indicator.market.AShare': 'A股', 'dashboard.indicator.market.USStock': '美股', - 'dashboard.indicator.market.HShare': '港股', 'dashboard.indicator.market.Crypto': '加密货币', 'dashboard.indicator.market.Forex': '外汇', 'dashboard.indicator.market.Futures': '期货', @@ -1210,6 +1279,15 @@ const locale = { 'trading-assistant.form.step3': '策略参数', 'trading-assistant.form.step2Params': '策略参数', 'trading-assistant.form.step3Signal': '信号通知', + 'trading-assistant.form.simpleMode': '简易模式', + 'trading-assistant.form.advancedMode': '高级模式', + 'trading-assistant.form.simpleModeHint': '快速创建策略,使用推荐默认参数', + 'trading-assistant.form.advancedModeHint': '自定义全部参数,适合专业用户', + 'trading-assistant.form.simpleStep1': '选择指标 & 交易对', + 'trading-assistant.form.simpleStep2': '启动方式', + 'trading-assistant.form.simpleDefaultsHint': '默认参数(可展开修改)', + 'trading-assistant.form.showAdvancedSettings': '展开高级设置', + 'trading-assistant.form.hideAdvancedSettings': '收起高级设置', 'trading-assistant.form.indicator': '选择指标', 'trading-assistant.form.indicatorHint': '只能选择您已购买或创建的技术指标', 'trading-assistant.form.qdtCostHints': '使用策略将消耗QDT,请确保账户有足够的QDT余额', @@ -2282,7 +2360,7 @@ const locale = { 'profile.credits.vipExpires': 'VIP有效期至', 'profile.credits.vipExpired': 'VIP已过期', 'profile.credits.noVip': '非VIP用户', - 'profile.credits.hint': '使用AI分析等功能会消耗积分,VIP用户免费', + 'profile.credits.hint': '使用AI分析/回测/监控等功能会消耗积分;VIP仅可免费使用VIP免费指标。', // Profile - Credits Log (消费记录) 'profile.creditsLog': '消费记录', @@ -2400,11 +2478,13 @@ const locale = { 'systemOverview.realized': '已实现', 'systemOverview.unrealized': '未实现', 'systemOverview.symbols': '个交易对', + 'systemOverview.live': '实盘', + 'systemOverview.signal': '仅通知', // Settings - Billing 'settings.group.billing': '计费配置', 'settings.field.BILLING_ENABLED': '启用计费', - 'settings.field.BILLING_VIP_BYPASS': 'VIP免费', + 'settings.field.BILLING_VIP_BYPASS': 'VIP旁路(旧)', 'settings.field.BILLING_COST_AI_ANALYSIS': 'AI分析消耗', 'settings.field.BILLING_COST_STRATEGY_RUN': '策略运行消耗', 'settings.field.BILLING_COST_BACKTEST': '回测消耗', @@ -2412,8 +2492,22 @@ const locale = { 'settings.field.CREDITS_REGISTER_BONUS': '注册奖励', 'settings.field.CREDITS_REFERRAL_BONUS': '邀请奖励', 'settings.field.RECHARGE_TELEGRAM_URL': '充值Telegram链接', + 'settings.field.MEMBERSHIP_MONTHLY_PRICE_USD': '包月会员价格(USD)', + 'settings.field.MEMBERSHIP_MONTHLY_CREDITS': '包月会员赠送积分', + 'settings.field.MEMBERSHIP_YEARLY_PRICE_USD': '包年会员价格(USD)', + 'settings.field.MEMBERSHIP_YEARLY_CREDITS': '包年会员赠送积分', + 'settings.field.MEMBERSHIP_LIFETIME_PRICE_USD': '永久会员价格(USD)', + 'settings.field.MEMBERSHIP_LIFETIME_MONTHLY_CREDITS': '永久会员每月赠送积分', + 'settings.field.USDT_PAY_ENABLED': '启用USDT收款', + 'settings.field.USDT_PAY_CHAIN': 'USDT网络', + 'settings.field.USDT_TRC20_XPUB': 'TRC20 XPUB(仅观察)', + 'settings.field.USDT_TRC20_CONTRACT': 'USDT TRC20 合约地址', + 'settings.field.TRONGRID_BASE_URL': 'TronGrid Base URL', + 'settings.field.TRONGRID_API_KEY': 'TronGrid API Key', + 'settings.field.USDT_PAY_CONFIRM_SECONDS': '确认延迟(秒)', + 'settings.field.USDT_PAY_EXPIRE_MINUTES': '订单过期(分钟)', 'settings.desc.BILLING_ENABLED': '启用计费系统。启用后,用户使用某些功能需要消耗积分', - 'settings.desc.BILLING_VIP_BYPASS': 'VIP用户在有效期内可免费使用所有付费功能', + 'settings.desc.BILLING_VIP_BYPASS': '旧开关:开启后VIP将旁路所有功能扣积分(不推荐)。建议关闭,VIP仅用于“VIP免费指标”。', 'settings.desc.BILLING_COST_AI_ANALYSIS': '每次AI分析消耗的积分数', 'settings.desc.BILLING_COST_STRATEGY_RUN': '启动策略时消耗的积分数', 'settings.desc.BILLING_COST_BACKTEST': '每次回测消耗的积分数', @@ -2423,6 +2517,20 @@ const locale = { 'settings.desc.VERIFICATION_CODE_MAX_ATTEMPTS': '验证码验证失败的最大尝试次数,超过后将锁定', 'settings.desc.VERIFICATION_CODE_LOCK_MINUTES': '验证码验证失败次数超过限制后的锁定时长(分钟)', 'settings.desc.RECHARGE_TELEGRAM_URL': '用户点击充值时跳转的Telegram客服链接', + 'settings.desc.MEMBERSHIP_MONTHLY_PRICE_USD': '包月会员的价格(USD)。当前为模拟支付,后续可接入真实支付网关。', + 'settings.desc.MEMBERSHIP_MONTHLY_CREDITS': '购买包月会员后立即赠送到账号的积分数量。', + 'settings.desc.MEMBERSHIP_YEARLY_PRICE_USD': '包年会员的价格(USD)。当前为模拟支付,后续可接入真实支付网关。', + 'settings.desc.MEMBERSHIP_YEARLY_CREDITS': '购买包年会员后立即赠送到账号的积分数量。', + 'settings.desc.MEMBERSHIP_LIFETIME_PRICE_USD': '永久会员的价格(USD)。当前为模拟支付,后续可接入真实支付网关。', + 'settings.desc.MEMBERSHIP_LIFETIME_MONTHLY_CREDITS': '永久会员每30天自动发放一次的积分数量(首次购买会立刻发放一次)。', + 'settings.desc.USDT_PAY_ENABLED': '开启后,会员购买页会提供 USDT 扫码支付(每单独立地址 + 自动对账)。', + 'settings.desc.USDT_PAY_CHAIN': '当前版本仅支持 TRC20。', + 'settings.desc.USDT_TRC20_XPUB': '仅观察 xpub,用于派生每个订单的收款地址。不要填写私钥/助记词。', + 'settings.desc.USDT_TRC20_CONTRACT': 'TRON 链上 USDT 合约地址(默认已填写)。', + 'settings.desc.TRONGRID_BASE_URL': 'TronGrid API 基础地址(默认即可)。', + 'settings.desc.TRONGRID_API_KEY': '可选。用于提高 TronGrid 调用额度/稳定性。', + 'settings.desc.USDT_PAY_CONFIRM_SECONDS': '检测到到账后,等待指定秒数再标记“确认”,降低极端回滚风险。', + 'settings.desc.USDT_PAY_EXPIRE_MINUTES': '用户扫码支付的订单有效期,过期后需重新生成订单。', // Global Market 'globalMarket.title': '全球金融面板', @@ -2799,7 +2907,61 @@ const locale = { 'aiQuant.template.news': '📰 新闻驱动', 'aiQuant.template.custom': '✏️ 自定义', 'aiQuant.hint.dataProvided': '系统自动提供:实时价格、技术指标(RSI/MACD/均线)、最近新闻、宏观数据。AI将基于这些数据和您的提示词判断方向。', - 'aiQuant.hint.liveWarning': '实盘模式将使用真实资金交易,请确保已配置交易所API并充分了解风险!' + 'aiQuant.hint.liveWarning': '实盘模式将使用真实资金交易,请确保已配置交易所API并充分了解风险!', + + // 交易助手 - 实盘免责声明 + 'trading-assistant.liveDisclaimer.title': '实盘交易免责声明', + 'trading-assistant.liveDisclaimer.content': '实盘交易存在较高风险,可能导致部分或全部资金损失。平台不保证收益、不承诺盈利。你需自行评估风险并对交易结果承担全部责任。', + 'trading-assistant.liveDisclaimer.agree': '我已阅读并理解上述免责声明,仍选择开启实盘交易', + 'trading-assistant.liveDisclaimer.required': '开启实盘前请先勾选免责声明确认', + 'trading-assistant.liveDisclaimer.blockTitle': '请先确认免责声明', + 'trading-assistant.liveDisclaimer.blockDesc': '勾选免责声明后才可继续配置实盘连接与下单参数。', + + // Billing / Membership + 'menu.billing': '会员充值', + 'billing.title': '开通会员 / 充值积分', + 'billing.desc': '选择适合你的会员套餐,开通后会赠送积分。', + 'billing.snapshot.credits': '当前积分', + 'billing.snapshot.vip': 'VIP 状态', + 'billing.snapshot.notVip': '非VIP', + 'billing.snapshot.expires': '到期时间', + 'billing.vipRule.title': 'VIP 权益说明', + 'billing.vipRule.desc': 'VIP 仅拥有一个特殊权限:可免费使用「VIP免费指标」。其它付费功能/指标仍会正常扣积分。', + 'billing.plan.monthly': '包月会员', + 'billing.plan.yearly': '包年会员', + 'billing.plan.lifetime': '永久会员', + 'billing.perMonth': '月', + 'billing.perYear': '年', + 'billing.once': '一次性', + 'billing.credits': '积分', + 'billing.lifetimeMonthly': '每月赠送', + 'billing.buyNow': '立即购买', + 'billing.purchaseSuccess': '购买成功', + 'billing.purchaseFailed': '购买失败', + 'billing.usdt.title': 'USDT 扫码支付', + 'billing.usdt.hintTitle': '请使用钱包扫码并完成转账', + 'billing.usdt.hintDesc': '请确保网络与金额正确(当前仅支持 TRC20)。支付到账后系统会自动开通会员并发放积分。', + 'billing.usdt.chain': '网络', + 'billing.usdt.amount': '金额', + 'billing.usdt.address': '收款地址', + 'billing.usdt.copyAddress': '复制地址', + 'billing.usdt.copyAmount': '复制金额', + 'billing.usdt.refresh': '刷新状态', + 'billing.usdt.expires': '过期时间', + 'billing.usdt.paidSuccess': '支付确认成功,已开通会员', + 'billing.usdt.status.pending': '等待支付', + 'billing.usdt.status.paid': '已检测到账', + 'billing.usdt.status.confirmed': '已确认', + 'billing.usdt.status.expired': '已过期', + 'billing.usdt.status.cancelled': '已取消', + 'billing.usdt.status.failed': '失败', + + // Community + 'community.vipFree': 'VIP免费', + + // Indicator publish + 'dashboard.indicator.publish.vipFree': 'VIP免费', + 'dashboard.indicator.publish.vipFreeHint': '开启后:VIP用户可免费使用该指标(非VIP仍需购买)。' } export default { diff --git a/quantdinger_vue/src/locales/lang/zh-TW.js b/quantdinger_vue/src/locales/lang/zh-TW.js index e955007..ca38656 100644 --- a/quantdinger_vue/src/locales/lang/zh-TW.js +++ b/quantdinger_vue/src/locales/lang/zh-TW.js @@ -464,9 +464,7 @@ const locale = { 'dashboard.analysis.modal.addStock.willAutoFetchName': '系統將自動獲取名稱', 'dashboard.analysis.modal.addStock.addDirectly': '直接添加', 'dashboard.analysis.modal.addStock.nameWillBeFetched': '名稱將在添加時自動獲取', - 'dashboard.analysis.market.AShare': 'A股', 'dashboard.analysis.market.USStock': '美股', - 'dashboard.analysis.market.HShare': '港股', 'dashboard.analysis.market.Crypto': '加密貨幣', 'dashboard.analysis.market.Forex': '外匯', 'dashboard.analysis.market.Futures': '期貨', @@ -621,9 +619,7 @@ const locale = { 'dashboard.indicator.drawing.priceChannel': '價格通道', 'dashboard.indicator.drawing.fibonacciLine': '斐波那契線', 'dashboard.indicator.drawing.clearAll': '清除所有畫線', - 'dashboard.indicator.market.AShare': 'A股', 'dashboard.indicator.market.USStock': '美股', - 'dashboard.indicator.market.HShare': '港股', 'dashboard.indicator.market.Crypto': '加密貨幣', 'dashboard.indicator.market.Forex': '外匯', 'dashboard.indicator.market.Futures': '期貨', diff --git a/quantdinger_vue/src/views/ai-analysis/index.vue b/quantdinger_vue/src/views/ai-analysis/index.vue index 2d87ae0..7f3f4e1 100644 --- a/quantdinger_vue/src/views/ai-analysis/index.vue +++ b/quantdinger_vue/src/views/ai-analysis/index.vue @@ -1,5 +1,5 @@