refactor deployment config and exchange integrations

Simplify runtime configuration and remove legacy database and settings surface so new installs are easier to operate.
Refresh deployment assets, docs, and order execution behavior to keep the packaged app aligned with the current backend.

Made-with: Cursor
This commit is contained in:
Dinger
2026-03-21 18:32:04 +08:00
parent 9473e50d59
commit 05f07ee544
138 changed files with 1530 additions and 860 deletions
+3 -3
View File
@@ -5,12 +5,12 @@
**/.git
.DS_Store
# Do not send local node_modules / build outputs (Docker runs npm ci + build)
# Do not send unused frontend source build artifacts
QuantDinger-Vue-src/node_modules
QuantDinger-Vue-src/dist
# Pre-built copy used for local nginx tests only — image builds from source
frontend/dist
# The open-source repo ships prebuilt frontend assets in `frontend/dist`,
# and the frontend image copies them directly, so keep this directory included.
# Backend & DB not needed for frontend stage
backend_api_python
+21
View File
@@ -0,0 +1,21 @@
#
# Project-root Docker/Compose overrides
# Copy to `.env` only when you need custom ports or want to switch image source.
#
# Optional custom ports
# FRONTEND_PORT=8888
# BACKEND_PORT=127.0.0.1:5000
# DB_PORT=127.0.0.1:5432
# Global image source switch
# Leave empty for official Docker Hub:
# IMAGE_PREFIX=
#
# Common alternatives (pick one, keep trailing slash):
# IMAGE_PREFIX=docker.m.daocloud.io/library/
# IMAGE_PREFIX=docker.xuanyuan.me/library/
#
# Important:
# - This file lives in the project root and is used by docker-compose build/pull.
# - `backend_api_python/.env` is runtime config for the backend container only.
+74 -13
View File
@@ -70,25 +70,40 @@
git clone https://github.com/brokermr810/QuantDinger.git
cd QuantDinger
# 2. Configure (edit admin password & AI API key)
# 2. Copy backend config
cp backend_api_python/env.example backend_api_python/.env
# 3. IMPORTANT: Generate and set a secure SECRET_KEY
# Linux/Mac:
python3 -c "import secrets; print(secrets.token_hex(32))" | xargs -I {} sed -i 's|SECRET_KEY=.*|SECRET_KEY={}|' backend_api_python/.env
# Or manually edit backend_api_python/.env and replace SECRET_KEY value
# Windows PowerShell:
# $key = python -c "import secrets; print(secrets.token_hex(32))"
# (Get-Content backend_api_python\.env) -replace 'SECRET_KEY=.*', "SECRET_KEY=$key" | Set-Content backend_api_python\.env
# 3. Generate and write a secure SECRET_KEY
./scripts/generate-secret-key.sh
# 4. Launch!
docker-compose up -d --build
```
> **Linux/macOS**:
> - Copy backend config:
> `cp backend_api_python/env.example backend_api_python/.env`
> - Need more knobs? Scroll to the lower "Advanced / rarely changed" section inside:
> `backend_api_python/env.example`
> - Optional: if Docker Hub is slow/unreachable in your network, copy root Docker config:
> `cp .env.example .env`
> - Generate and write `SECRET_KEY`:
> `./scripts/generate-secret-key.sh`
> - Launch:
> `docker-compose up -d --build`
> **Windows PowerShell**:
> - Copy: `Copy-Item backend_api_python\env.example -Destination backend_api_python\.env`
> - Generate SECRET_KEY: `python -c "import secrets; print(secrets.token_hex(32))"` then edit `.env` manually
> - Copy backend config:
> `Copy-Item backend_api_python\env.example -Destination backend_api_python\.env`
> - Need more knobs? Scroll to the lower "Advanced / rarely changed" section inside:
> `backend_api_python\env.example`
> - Optional: if Docker Hub is slow/unreachable in your network, copy root Docker config:
> `Copy-Item .env.example -Destination .env`
> - Generate and write `SECRET_KEY`:
> `$key = py -c "import secrets; print(secrets.token_hex(32))"`
> `(Get-Content backend_api_python\.env) -replace '^SECRET_KEY=.*$', "SECRET_KEY=$key" | Set-Content backend_api_python\.env -Encoding UTF8`
> - Launch:
> `docker-compose up -d --build`
> **⚠️ Security Note**: The container will **NOT start** if `SECRET_KEY` is using the default value. This prevents insecure deployments.
@@ -161,6 +176,42 @@ FRONTEND_PORT=3000 # Default: 8888
BACKEND_PORT=127.0.0.1:5001 # Default: 5000
```
**Alternative image sources** — Default uses Docker Hub. If pulling base images fails in your region/network, copy `.env.example` to `.env` and change one line:
```ini
IMAGE_PREFIX=docker.m.daocloud.io/library/
```
Other common choices:
```ini
IMAGE_PREFIX=
IMAGE_PREFIX=docker.xuanyuan.me/library/
```
**Docker troubleshooting**
```text
1. Image source switching is controlled by the project-root `.env`, not `backend_api_python/.env`.
2. The open-source repo ships prebuilt frontend files in `frontend/dist`, so frontend deployment does not require Node.js.
3. On Windows, if backend logs show:
exec /usr/local/bin/docker-entrypoint.sh: no such file or directory
it is usually caused by shell/line-ending compatibility in the image layer. Rebuild the backend image with:
docker-compose build --no-cache backend
4. If frontend logs show:
host not found in upstream "backend"
it usually means the backend container failed first. Fix backend, then restart frontend:
docker-compose restart frontend
5. If frontend build fails with:
COPY frontend/dist ... not found
check `.dockerignore` and make sure `frontend/dist` is NOT excluded from the Docker build context.
6. If saving settings from the admin panel fails with:
Read-only file system: '/app/.env'
make sure `backend_api_python/.env` is mounted read-write in `docker-compose.yml`.
7. For Docker deployments, a local proxy must use `host.docker.internal`, not `127.0.0.1`.
Example:
PROXY_URL=socks5h://host.docker.internal:10808
8. If exchange logs say a symbol is "not found" after proxy/network fixes, it may be a market-symbol mapping issue
on that exchange (for example, renamed tokens), not a general network failure.
```
</details>
---
@@ -370,6 +421,14 @@ flowchart TB
| **Forex** | MetaTrader 5 (MT5), OANDA | ✅ Via MT5 |
| **Futures** | Exchange APIs | ⚡ Data + Notify |
### Exchange Rebate Links
If you are opening a new exchange account, use a rebate link from the start. Many traders actively look for fee-rebate links anyway, and these links give you a straightforward 20% trading fee discount at no extra cost.
- **OKX**: [Open account with 20% fee rebate](https://www.promooboost.com/join/QUANTDINGER)
- **Bitget**: [Open account with 20% fee rebate](https://partner.bitget.com/bg/dinger)
- **Bitget (backup link)**: [Alternate 20% fee rebate signup link](https://partner.hdmune.cn/bg/7r4xz8kd)
---
## 🏗️ Architecture & Configuration
@@ -427,7 +486,8 @@ QuantDinger/
<details>
<summary><b>⚙️ Configuration Reference (.env)</b></summary>
Use `backend_api_python/env.example` as template:
Use `backend_api_python/env.example` as the simplified template.
The upper part is for first-time deployment, and the lower "Advanced / rarely changed" section contains optional tuning.
| Category | Key Variables |
|----------|-----------|
@@ -438,7 +498,7 @@ Use `backend_api_python/env.example` as template:
| **Security** | `TURNSTILE_SITE_KEY`, `ENABLE_REGISTRATION` |
| **Membership** | `MEMBERSHIP_MONTHLY_PRICE_USD`, `MEMBERSHIP_MONTHLY_CREDITS` |
| **USDT Payment** | `USDT_PAY_ENABLED`, `USDT_TRC20_XPUB`, `TRONGRID_API_KEY` |
| **Proxy** | `PROXY_PORT` or `PROXY_URL` |
| **Proxy** | `PROXY_URL` |
| **Workers** | `ENABLE_PENDING_ORDER_WORKER`, `ENABLE_PORTFOLIO_MONITOR` |
</details>
@@ -470,6 +530,7 @@ All detailed guides are in the [`docs/`](docs/) folder:
|----------|-------------|
| [Changelog](docs/CHANGELOG.md) | Version history & migration notes |
| [Multi-User Setup](docs/multi-user-setup.md) | PostgreSQL multi-user deployment |
| [Cloud Deployment](docs/CLOUD_DEPLOYMENT_EN.md) | Cloud server deployment with domain, HTTPS, and reverse proxy |
### Strategy Development
+5 -2
View File
@@ -1,5 +1,7 @@
# QuantDinger Backend API Dockerfile
FROM python:3.12-slim-bookworm
# BASE_IMAGE can be overridden from docker-compose/.env.
ARG BASE_IMAGE=python:3.12-slim-bookworm
FROM ${BASE_IMAGE}
# Set working directory
WORKDIR /app
@@ -31,7 +33,8 @@ COPY . .
# Copy and set up entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
RUN sed -i 's/\r$//' /usr/local/bin/docker-entrypoint.sh \
&& chmod +x /usr/local/bin/docker-entrypoint.sh
# Create log and data directories
RUN mkdir -p logs data/memory
+1 -1
View File
@@ -208,7 +208,7 @@ gunicorn -c gunicorn_config.py "run:app"
## Troubleshooting
- **Database connection failed**: Check `DATABASE_URL` format and PostgreSQL service status
- **Outbound requests fail**: Configure `PROXY_PORT` or `PROXY_URL` in `.env`
- **Outbound requests fail**: Configure `PROXY_URL` in `.env`
- **Disable auto-restore**: Set `DISABLE_RESTORE_RUNNING_STRATEGIES=true`
- **Disable pending-order worker**: Set `ENABLE_PENDING_ORDER_WORKER=false`
-12
View File
@@ -81,18 +81,6 @@ class MetaAPIKeys(type):
return [k.strip() for k in val.split(',') if k.strip()]
return []
@property
def BOCHA_API_KEYS(cls):
"""Bocha Search API keys (comma-separated for rotation)"""
env_val = os.getenv('BOCHA_API_KEYS', '').strip()
if env_val:
return [k.strip() for k in env_val.split(',') if k.strip()]
from app.utils.config_loader import load_addon_config
val = load_addon_config().get('bocha', {}).get('api_keys', '')
if val:
return [k.strip() for k in val.split(',') if k.strip()]
return []
@property
def SERPAPI_KEYS(cls):
"""SerpAPI keys (comma-separated for rotation)"""
+2 -19
View File
@@ -130,30 +130,13 @@ class MetaCCXTConfig(type):
@property
def PROXY(cls):
from app.utils.config_loader import load_addon_config
val = load_addon_config().get('ccxt', {}).get('proxy')
if val:
return val
# 1) Direct CCXT proxy env
ccxt_proxy = (os.getenv('CCXT_PROXY') or '').strip()
if ccxt_proxy:
return ccxt_proxy
# 2) Local proxy helpers from backend_api_python/.env
# 1) Local proxy helpers from backend_api_python/.env
# PROXY_URL has the highest priority if provided.
proxy_url = (os.getenv('PROXY_URL') or '').strip()
if proxy_url:
return proxy_url
# Build from parts: PROXY_SCHEME/PROXY_HOST/PROXY_PORT
proxy_port = (os.getenv('PROXY_PORT') or '').strip()
if proxy_port:
proxy_scheme = (os.getenv('PROXY_SCHEME') or 'socks5h').strip()
proxy_host = (os.getenv('PROXY_HOST') or '127.0.0.1').strip()
return f"{proxy_scheme}://{proxy_host}:{proxy_port}"
# 3) Standard proxy envs (fallback)
# 2) Standard proxy envs (fallback)
for key in ['HTTPS_PROXY', 'HTTP_PROXY', 'ALL_PROXY']:
v = (os.getenv(key) or '').strip()
if v:
-15
View File
@@ -65,12 +65,6 @@ class MetaConfig(type):
# ==================== 安全配置 ====================
@property
def CORS_ORIGINS(cls):
from app.utils.config_loader import load_addon_config
val = load_addon_config().get('app', {}).get('cors_origins')
return val if val else os.getenv('CORS_ORIGINS', '*')
@property
def RATE_LIMIT(cls):
from app.utils.config_loader import load_addon_config
@@ -95,15 +89,6 @@ class MetaConfig(type):
return bool(val)
return os.getenv('ENABLE_REQUEST_LOG', 'True').lower() == 'true'
@property
def ENABLE_AI_ANALYSIS(cls):
from app.utils.config_loader import load_addon_config
val = load_addon_config().get('app', {}).get('enable_ai_analysis')
if val is not None:
return bool(val)
return os.getenv('ENABLE_AI_ANALYSIS', 'True').lower() == 'true'
class Config(metaclass=MetaConfig):
"""应用配置类"""
+62 -88
View File
@@ -65,7 +65,7 @@ CONFIG_SCHEMA = {
# ==================== 2. AI/LLM 配置 ====================
'ai': {
'title': 'AI / LLM Configuration',
'title': 'AI / LLM & Search',
'icon': 'robot',
'order': 2,
'items': [
@@ -212,14 +212,6 @@ CONFIG_SCHEMA = {
'default': '0.7',
'description': 'Model creativity (0-1). Lower = more deterministic'
},
{
'key': 'AI_MODELS_JSON',
'label': 'Custom Models (JSON)',
'type': 'text',
'default': '{}',
'required': False,
'description': 'Custom model list in JSON format for model selector'
},
{
'key': 'AI_ANALYSIS_CONSENSUS_TIMEFRAMES',
'label': 'Consensus Timeframes',
@@ -228,6 +220,66 @@ CONFIG_SCHEMA = {
'required': False,
'description': 'Multi-timeframe consensus for fast AI analysis. Comma-separated, e.g. "1D,4H"'
},
{
'key': 'SEARCH_PROVIDER',
'label': 'Search Provider',
'type': 'select',
'options': ['tavily', 'google', 'bing', 'none'],
'default': 'google',
'description': 'News / web search provider used by AI analysis. Configure both LLM and search to get full AI analysis results'
},
{
'key': 'SEARCH_MAX_RESULTS',
'label': 'Search Max Results',
'type': 'number',
'default': '10',
'description': 'Maximum number of search/news results returned per AI analysis request'
},
{
'key': 'TAVILY_API_KEYS',
'label': 'Tavily API Keys',
'type': 'password',
'required': False,
'link': 'https://tavily.com/',
'link_text': 'settings.link.getApiKey',
'description': 'Tavily search API keys (comma-separated). Recommended lightweight search source for AI analysis'
},
{
'key': 'SEARCH_GOOGLE_API_KEY',
'label': 'Google Search API Key',
'type': 'password',
'required': False,
'link': 'https://console.cloud.google.com/apis/credentials',
'link_text': 'settings.link.getApiKey',
'description': 'Google Custom Search JSON API key'
},
{
'key': 'SEARCH_GOOGLE_CX',
'label': 'Google Search Engine ID (CX)',
'type': 'text',
'required': False,
'link': 'https://programmablesearchengine.google.com/',
'link_text': 'settings.link.getApiKey',
'description': 'Google Programmable Search Engine ID'
},
{
'key': 'SEARCH_BING_API_KEY',
'label': 'Bing Search API Key',
'type': 'password',
'required': False,
'link': 'https://portal.azure.com/',
'link_text': 'settings.link.getApiKey',
'description': 'Microsoft Bing Web Search API key'
},
{
'key': 'SERPAPI_KEYS',
'label': 'SerpAPI Keys',
'type': 'password',
'required': False,
'link': 'https://serpapi.com/',
'link_text': 'settings.link.getApiKey',
'description': 'SerpAPI keys (comma-separated)'
},
]
},
@@ -270,13 +322,6 @@ CONFIG_SCHEMA = {
'link_text': 'settings.link.supportedExchanges',
'description': 'Default exchange for crypto data (binance, coinbase, okx, etc.)'
},
{
'key': 'CCXT_PROXY',
'label': 'Crypto Data Proxy',
'type': 'text',
'required': False,
'description': 'Proxy URL for crypto data requests (e.g. socks5h://127.0.0.1:1080)'
},
{
'key': 'FINNHUB_API_KEY',
'label': 'Finnhub API Key',
@@ -422,42 +467,7 @@ CONFIG_SCHEMA = {
'label': 'Proxy URL',
'type': 'text',
'required': False,
'description': 'Global proxy URL (e.g. socks5h://127.0.0.1:1080 or http://proxy:8080)'
},
]
},
# ==================== 9. 搜索配置 ====================
'search': {
'title': 'Web Search',
'icon': 'search',
'order': 9,
'items': [
{
'key': 'SEARCH_PROVIDER',
'label': 'Search Provider',
'type': 'select',
'options': ['bocha', 'tavily', 'google', 'bing', 'none'],
'default': 'bocha',
'description': 'Web search provider for AI research features'
},
{
'key': 'TAVILY_API_KEYS',
'label': 'Tavily API Keys',
'type': 'password',
'required': False,
'link': 'https://tavily.com/',
'link_text': 'settings.link.getApiKey',
'description': 'Tavily Search API keys (comma-separated). Free 1000 req/month'
},
{
'key': 'BOCHA_API_KEYS',
'label': 'Bocha API Keys',
'type': 'password',
'required': False,
'link': 'https://bochaai.com/',
'link_text': 'settings.link.getApiKey',
'description': 'Bocha Search API keys (comma-separated)'
'description': 'Global outbound proxy URL. Used by requests and by crypto data requests when a proxy is needed.'
},
]
},
@@ -546,13 +556,6 @@ CONFIG_SCHEMA = {
'default': 'False',
'description': 'Enable billing system. Users need credits to use certain features'
},
{
'key': 'BILLING_VIP_BYPASS',
'label': 'VIP Bypass (Legacy)',
'type': 'boolean',
'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) =====
{
@@ -684,13 +687,6 @@ CONFIG_SCHEMA = {
'default': '8',
'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 URL for recharge inquiries'
},
{
'key': 'CREDITS_REGISTER_BONUS',
'label': 'Register Bonus',
@@ -708,28 +704,6 @@ CONFIG_SCHEMA = {
]
},
# ==================== 12. 应用功能 ====================
'app': {
'title': 'Application',
'icon': 'appstore',
'order': 12,
'items': [
{
'key': 'CORS_ORIGINS',
'label': 'CORS Origins',
'type': 'text',
'default': '*',
'description': 'Allowed CORS origins (* for all, or comma-separated URLs)'
},
{
'key': 'ENABLE_AI_ANALYSIS',
'label': 'Enable AI Analysis',
'type': 'boolean',
'default': 'True',
'description': 'Enable AI-powered market analysis features'
},
]
},
}
@@ -71,7 +71,6 @@ class AICalibrationService:
sell_threshold DECIMAL(10,4) NOT NULL,
min_consensus_abs_override DECIMAL(10,4) NOT NULL,
quality_hold_threshold DECIMAL(10,4) NOT NULL,
sample_count INT NOT NULL DEFAULT 0,
validated_at TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW()
);
@@ -281,9 +280,9 @@ class AICalibrationService:
INSERT INTO qd_ai_calibration
(market, buy_threshold, sell_threshold,
min_consensus_abs_override, quality_hold_threshold,
sample_count, validated_at, created_at)
validated_at, created_at)
VALUES
(%s, %s, %s, %s, %s, %s, NOW(), NOW())
(%s, %s, %s, %s, %s, NOW(), NOW())
""",
(
market,
@@ -291,7 +290,6 @@ class AICalibrationService:
sell_threshold,
min_consensus_abs_override,
quality_hold_threshold,
sample_count,
),
)
db.commit()
@@ -58,16 +58,11 @@ class AnalysisMemory:
decision VARCHAR(10) NOT NULL,
confidence INT DEFAULT 50,
price_at_analysis DECIMAL(24, 8),
entry_price DECIMAL(24, 8),
stop_loss DECIMAL(24, 8),
take_profit DECIMAL(24, 8),
summary TEXT,
reasons JSONB,
risks JSONB,
scores JSONB,
indicators_snapshot JSONB,
raw_result JSONB,
consensus_decision VARCHAR(10),
consensus_score DECIMAL(24, 8),
consensus_abs DECIMAL(24, 8),
agreement_ratio DECIMAL(10, 6),
@@ -102,14 +97,6 @@ class AnalysisMemory:
ALTER TABLE qd_analysis_memory ADD COLUMN raw_result JSONB;
END IF;
-- 添加多周期共识列如果不存在
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_analysis_memory' AND column_name = 'consensus_decision'
) THEN
ALTER TABLE qd_analysis_memory ADD COLUMN consensus_decision VARCHAR(10);
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_analysis_memory' AND column_name = 'consensus_score'
@@ -182,18 +169,13 @@ class AnalysisMemory:
decision = analysis_result.get("decision")
confidence = analysis_result.get("confidence")
price = analysis_result.get("market_data", {}).get("current_price")
entry = analysis_result.get("trading_plan", {}).get("entry_price")
stop = analysis_result.get("trading_plan", {}).get("stop_loss")
take = analysis_result.get("trading_plan", {}).get("take_profit")
summary = analysis_result.get("summary")
reasons = json.dumps(analysis_result.get("reasons", []))
risks = json.dumps(analysis_result.get("risks", []))
scores = json.dumps(analysis_result.get("scores", {}))
indicators = json.dumps(analysis_result.get("indicators", {}))
raw = json.dumps(analysis_result)
consensus = analysis_result.get("consensus") or {}
consensus_decision = consensus.get("consensus_decision")
consensus_score = consensus.get("consensus_score")
consensus_abs = consensus.get("consensus_abs")
agreement_ratio = consensus.get("agreement_ratio")
@@ -202,15 +184,16 @@ class AnalysisMemory:
cur.execute("""
INSERT INTO qd_analysis_memory (
user_id, market, symbol, decision, confidence,
price_at_analysis, entry_price, stop_loss, take_profit,
summary, reasons, risks, scores, indicators_snapshot, raw_result,
consensus_decision, consensus_score, consensus_abs, agreement_ratio, quality_multiplier
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
%s, %s, %s, %s, %s)
price_at_analysis, summary, reasons, scores, indicators_snapshot, raw_result,
consensus_score, consensus_abs, agreement_ratio, quality_multiplier
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
%s, %s, %s, %s)
RETURNING id
""", (user_id, market, symbol, decision, confidence, price, entry, stop, take,
summary, reasons, risks, scores, indicators, raw,
consensus_decision, consensus_score, consensus_abs, agreement_ratio, quality_multiplier))
""", (
user_id, market, symbol, decision, confidence,
price, summary, reasons, scores, indicators, raw,
consensus_score, consensus_abs, agreement_ratio, quality_multiplier,
))
# 使用 lastrowid 属性获取 IDexecute 内部已经处理了 RETURNING
memory_id = cur.lastrowid
@@ -1,12 +1,13 @@
"""
Billing Service - 统一计费服务
管理用户积分消费VIP状态检查计费配置等功能
支持两种计费模
1. 积分消耗模式每次使用功能扣除相应积分
2. VIP免费模式VIP用户在有效期内免费使用
负责用户积分余额功能扣费会员状态与套餐发放
当前计费模型为
1. 是否扣费由 `BILLING_ENABLED` 与各功能 cost 配置决定
2. VIP/会员状态用于会员套餐与权益展示
3. 社区指标的 `vip_free` 逻辑在社区购买流程中单独处理不在这里做全局旁路
计费配置存储在 .env 文件中可通过系统设置界面配置
计费配置存储在 `.env` 文件中可通过系统设置界面配置
"""
import os
import time
@@ -27,9 +28,7 @@ BILLING_CONFIG_PREFIX = 'BILLING_'
DEFAULT_BILLING_CONFIG = {
# 全局开关
'enabled': False, # 是否启用计费
# IMPORTANT: VIP 不再默认免扣积分(VIP 仅对“VIP免费指标”生效)
'vip_bypass': False, # VIP用户是否免费(功能计费层面的旁路,默认关闭)
# 各功能积分消耗(0表示免费)
'cost_ai_analysis': 10, # AI分析 每次消耗积分
'cost_strategy_run': 5, # 策略运行 每次消耗积分(启动时)
@@ -96,7 +95,7 @@ class BillingService:
return config.get('enabled', False)
def get_feature_cost(self, feature: str) -> int:
"""获取功能消耗积分"""
"""获取指定功能的积分消耗,0 表示免费"""
config = self.get_billing_config()
cost_key = f'cost_{feature}'
return config.get(cost_key, 0)
@@ -475,13 +474,10 @@ class BillingService:
# 免费功能
if cost <= 0:
return True, 'free_feature'
# 检查VIP状态
if config.get('vip_bypass', True):
is_vip, _ = self.get_user_vip_status(user_id)
if is_vip:
return True, 'vip_free'
# 说明:这里不再根据 VIP 做全局免扣积分旁路。
# VIP / membership 仅保留为套餐、到期时间和社区 vip_free 指标权益。
# 检查积分余额
credits = self.get_user_credits(user_id)
if credits < cost:
@@ -710,7 +706,7 @@ class BillingService:
return {'items': [], 'total': 0, 'page': 1, 'page_size': page_size, 'total_pages': 0}
def get_user_billing_info(self, user_id: int) -> Dict[str, Any]:
"""获取用户计费信息(供前端显示)"""
"""获取用户计费与会员信息快照(供前端显示)"""
credits = self.get_user_credits(user_id)
is_vip, vip_expires_at = self.get_user_vip_status(user_id)
config = self.get_billing_config()
@@ -720,9 +716,6 @@ 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', 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',
# 功能费用(供前端显示)
'feature_costs': {
'ai_analysis': config.get('cost_ai_analysis', 0),
@@ -20,6 +20,17 @@ from app.services.live_trading.symbols import to_bitget_um_symbol
class BitgetMixClient(BaseRestClient):
_CHANNEL_API_CODE_ORDER_PATHS = {
"/api/v2/mix/order/place-order",
"/api/v2/mix/order/batch-place-order",
"/api/v2/mix/order/modify-order",
"/api/v2/mix/order/place-plan-order",
"/api/v2/mix/order/place-tpsl-order",
"/api/v3/trade/place-order",
"/api/v3/trade/place-batch",
"/api/v3/trade/modify-order",
}
def __init__(
self,
*,
@@ -28,11 +39,13 @@ class BitgetMixClient(BaseRestClient):
passphrase: str,
base_url: str = "https://api.bitget.com",
timeout_sec: float = 15.0,
channel_api_code: str = "qvz9x",
):
super().__init__(base_url=base_url, timeout_sec=timeout_sec)
self.api_key = (api_key or "").strip()
self.secret_key = (secret_key or "").strip()
self.passphrase = (passphrase or "").strip()
self.channel_api_code = (channel_api_code or "").strip()
if not self.api_key or not self.secret_key or not self.passphrase:
raise LiveTradingError("Missing Bitget api_key/secret_key/passphrase")
@@ -172,14 +185,18 @@ class BitgetMixClient(BaseRestClient):
mac = hmac.new(self.secret_key.encode("utf-8"), prehash.encode("utf-8"), hashlib.sha256).digest()
return base64.b64encode(mac).decode("utf-8")
def _headers(self, ts_ms: str, sign: str) -> Dict[str, str]:
return {
def _headers(self, ts_ms: str, sign: str, request_path: str = "") -> Dict[str, str]:
headers = {
"ACCESS-KEY": self.api_key,
"ACCESS-SIGN": sign,
"ACCESS-TIMESTAMP": ts_ms,
"ACCESS-PASSPHRASE": self.passphrase,
"Content-Type": "application/json",
}
clean_path = str(request_path or "").split("?", 1)[0]
if self.channel_api_code and clean_path in self._CHANNEL_API_CODE_ORDER_PATHS:
headers["X-CHANNEL-API-CODE"] = self.channel_api_code
return headers
def _signed_request(
self,
@@ -210,7 +227,7 @@ class BitgetMixClient(BaseRestClient):
path,
params=params,
data=body_str if body_str else None,
headers=self._headers(ts_ms, sign),
headers=self._headers(ts_ms, sign, path),
)
if code >= 400:
raise LiveTradingError(f"Bitget HTTP {code}: {text[:500]}")
@@ -23,6 +23,15 @@ from app.services.live_trading.symbols import to_bitget_um_symbol
class BitgetSpotClient(BaseRestClient):
_CHANNEL_API_CODE_ORDER_PATHS = {
"/api/v2/spot/trade/place-order",
"/api/v2/spot/trade/batch-orders",
"/api/v2/spot/trade/place-plan-order",
"/api/v3/trade/place-order",
"/api/v3/trade/place-batch",
"/api/v3/trade/modify-order",
}
def __init__(
self,
*,
@@ -31,7 +40,7 @@ class BitgetSpotClient(BaseRestClient):
passphrase: str,
base_url: str = "https://api.bitget.com",
timeout_sec: float = 15.0,
channel_api_code: str = "bntva",
channel_api_code: str = "qvz9x",
):
super().__init__(base_url=base_url, timeout_sec=timeout_sec)
self.api_key = (api_key or "").strip()
@@ -150,7 +159,7 @@ class BitgetSpotClient(BaseRestClient):
mac = hmac.new(self.secret_key.encode("utf-8"), prehash.encode("utf-8"), hashlib.sha256).digest()
return base64.b64encode(mac).decode("utf-8")
def _headers(self, ts_ms: str, sign: str) -> Dict[str, str]:
def _headers(self, ts_ms: str, sign: str, request_path: str = "") -> Dict[str, str]:
h = {
"ACCESS-KEY": self.api_key,
"ACCESS-SIGN": sign,
@@ -158,7 +167,8 @@ class BitgetSpotClient(BaseRestClient):
"ACCESS-PASSPHRASE": self.passphrase,
"Content-Type": "application/json",
}
if self.channel_api_code:
clean_path = str(request_path or "").split("?", 1)[0]
if self.channel_api_code and clean_path in self._CHANNEL_API_CODE_ORDER_PATHS:
h["X-CHANNEL-API-CODE"] = self.channel_api_code
return h
@@ -188,7 +198,7 @@ class BitgetSpotClient(BaseRestClient):
path,
params=params,
data=body_str if body_str else None,
headers=self._headers(ts_ms, sign),
headers=self._headers(ts_ms, sign, path),
)
if code >= 400:
raise LiveTradingError(f"BitgetSpot HTTP {code}: {text[:500]}")
@@ -22,6 +22,8 @@ from app.services.live_trading.symbols import to_bybit_symbol
class BybitClient(BaseRestClient):
_DEFAULT_BROKER_REFERER = "Ri001020"
def __init__(
self,
*,
@@ -36,6 +38,7 @@ class BybitClient(BaseRestClient):
self.api_key = (api_key or "").strip()
self.secret_key = (secret_key or "").strip()
self.category = (category or "linear").strip().lower()
self.broker_referer = self._DEFAULT_BROKER_REFERER
if self.category not in ("linear", "spot"):
self.category = "linear"
try:
@@ -155,8 +158,17 @@ class BybitClient(BaseRestClient):
def _sign(self, prehash: str) -> str:
return hmac.new(self.secret_key.encode("utf-8"), prehash.encode("utf-8"), hashlib.sha256).hexdigest()
@staticmethod
def _resolve_position_idx(pos_side: str) -> Optional[int]:
ps = str(pos_side or "").strip().lower()
if ps == "long":
return 1
if ps == "short":
return 2
return None
def _headers(self, ts_ms: str, sign: str) -> Dict[str, str]:
return {
headers = {
"X-BAPI-API-KEY": self.api_key,
"X-BAPI-SIGN": sign,
"X-BAPI-TIMESTAMP": ts_ms,
@@ -164,6 +176,9 @@ class BybitClient(BaseRestClient):
"X-BAPI-SIGN-TYPE": "2",
"Content-Type": "application/json",
}
if self.broker_referer:
headers["Referer"] = self.broker_referer
return headers
def _signed_request(
self,
@@ -277,6 +292,37 @@ class BybitClient(BaseRestClient):
return (Decimal("0"), qty_precision)
return (q, qty_precision)
def _normalize_price(self, *, symbol: str, price: float) -> Tuple[Decimal, Optional[int]]:
p = self._to_dec(price)
if p <= 0:
return (Decimal("0"), None)
sym = to_bybit_symbol(symbol)
try:
info = self.get_instrument_info(category=self.category, symbol=sym) or {}
except Exception:
info = {}
pf = (info.get("priceFilter") if isinstance(info, dict) else None) or {}
tick = self._to_dec((pf or {}).get("tickSize") or "0")
if tick > 0:
p = self._floor_to_step(p, tick)
price_precision = None
if tick > 0:
try:
tick_normalized = tick.normalize()
tick_str = str(tick_normalized)
if "." in tick_str:
price_precision = len(tick_str.split(".")[1])
if price_precision < 0:
price_precision = 0
if price_precision > 18:
price_precision = 18
else:
price_precision = 0
except Exception:
pass
return (p, price_precision)
def place_market_order(
self,
*,
@@ -284,6 +330,7 @@ class BybitClient(BaseRestClient):
side: str,
qty: float,
reduce_only: bool = False,
pos_side: str = "",
client_order_id: Optional[str] = None,
) -> LiveOrderResult:
sym = to_bybit_symbol(symbol)
@@ -300,8 +347,13 @@ class BybitClient(BaseRestClient):
"side": "Buy" if sd == "buy" else "Sell",
"orderType": "Market",
"qty": self._dec_str(q_dec, strict_precision=qty_precision),
"timeInForce": "GTC",
"timeInForce": "IOC",
}
if self.category == "spot":
body["marketUnit"] = "baseCoin"
pos_idx = self._resolve_position_idx(pos_side) if self.category == "linear" else None
if pos_idx is not None:
body["positionIdx"] = pos_idx
if reduce_only and self.category == "linear":
body["reduceOnly"] = True
if client_order_id:
@@ -319,6 +371,7 @@ class BybitClient(BaseRestClient):
qty: float,
price: float,
reduce_only: bool = False,
pos_side: str = "",
client_order_id: Optional[str] = None,
) -> LiveOrderResult:
sym = to_bybit_symbol(symbol)
@@ -326,21 +379,27 @@ class BybitClient(BaseRestClient):
if sd not in ("buy", "sell"):
raise LiveTradingError(f"Invalid side: {side}")
q_req = float(qty or 0.0)
px = float(price or 0.0)
if q_req <= 0 or px <= 0:
px_req = float(price or 0.0)
if q_req <= 0 or px_req <= 0:
raise LiveTradingError("Invalid qty/price")
q_dec, qty_precision = self._normalize_qty(symbol=symbol, qty=q_req)
px_dec, price_precision = self._normalize_price(symbol=symbol, price=px_req)
if float(q_dec or 0) <= 0:
raise LiveTradingError(f"Invalid qty (below step/min): requested={q_req}")
if float(px_dec or 0) <= 0:
raise LiveTradingError(f"Invalid price (below tick/min): requested={px_req}")
body: Dict[str, Any] = {
"category": self.category,
"symbol": sym,
"side": "Buy" if sd == "buy" else "Sell",
"orderType": "Limit",
"qty": self._dec_str(q_dec, strict_precision=qty_precision),
"price": str(px),
"price": self._dec_str(px_dec, strict_precision=price_precision),
"timeInForce": "GTC",
}
pos_idx = self._resolve_position_idx(pos_side) if self.category == "linear" else None
if pos_idx is not None:
body["positionIdx"] = pos_idx
if reduce_only and self.category == "linear":
body["reduceOnly"] = True
if client_order_id:
@@ -404,13 +463,28 @@ class BybitClient(BaseRestClient):
# Extract fee from cumExecFee (Bybit API field for cumulative execution fee)
fee = 0.0
fee_ccy = ""
try:
fee = abs(float(last.get("cumExecFee") or 0.0))
except Exception:
fee = 0.0
# Bybit linear contracts are settled in USDT
if fee > 0:
fee_ccy = "USDT"
fee_detail = last.get("cumFeeDetail") if isinstance(last, dict) else None
if isinstance(fee_detail, dict) and fee_detail:
total_fee = 0.0
fee_keys = []
for k, v in fee_detail.items():
try:
fv = abs(float(v or 0.0))
except Exception:
fv = 0.0
if fv > 0:
total_fee += fv
fee_keys.append(str(k))
fee = total_fee
if len(fee_keys) == 1:
fee_ccy = fee_keys[0]
if fee <= 0:
try:
fee = abs(float(last.get("cumExecFee") or 0.0))
except Exception:
fee = 0.0
if fee > 0 and self.category == "linear":
fee_ccy = "USDT"
if filled > 0 and avg_price > 0:
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
if status.lower() in ("filled", "cancelled", "canceled", "rejected"):
@@ -184,6 +184,7 @@ def place_order_from_signal(
side=side,
qty=qty,
reduce_only=reduce_only,
pos_side=pos_side,
client_order_id=client_order_id,
)
if isinstance(client, CoinbaseExchangeClient):
@@ -84,15 +84,22 @@ def create_client(exchange_config: Dict[str, Any], *, market_type: str = "swap")
if exchange_id == "bitget":
base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.bitget.com"
if mt == "spot":
channel_api_code = _get(exchange_config, "channel_api_code", "channelApiCode") or "bntva"
channel_api_code = _get(exchange_config, "channel_api_code", "channelApiCode") or "qvz9x"
return BitgetSpotClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url, channel_api_code=channel_api_code)
return BitgetMixClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url)
channel_api_code = _get(exchange_config, "channel_api_code", "channelApiCode") or "qvz9x"
return BitgetMixClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url, channel_api_code=channel_api_code)
if exchange_id == "bybit":
base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.bybit.com"
category = "spot" if mt == "spot" else "linear"
recv_window_ms = int(exchange_config.get("recv_window_ms") or exchange_config.get("recvWindow") or 5000)
return BybitClient(api_key=api_key, secret_key=secret_key, base_url=base_url, category=category, recv_window_ms=recv_window_ms)
return BybitClient(
api_key=api_key,
secret_key=secret_key,
base_url=base_url,
category=category,
recv_window_ms=recv_window_ms,
)
if exchange_id in ("coinbaseexchange", "coinbase_exchange"):
base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.exchange.coinbase.com"
@@ -1006,7 +1006,7 @@ class MarketDataCollector:
策略按优先级
1. 结构化API (Finnhub) - 美股首选
2. 搜索引擎 (Bocha/Tavily) - 补充搜索
2. 搜索引擎 (Tavily/Google/Bing/SerpAPI) - 补充搜索
3. 情绪分析 - Finnhub 社交媒体情绪
"""
news_list = []
@@ -1090,7 +1090,7 @@ class MarketDataCollector:
"""
从搜索引擎获取新闻
使用增强的搜索服务 (Bocha/Tavily/SerpAPI)
使用增强的搜索服务 (Tavily/Google/Bing/SerpAPI)
"""
news_list = []
@@ -1386,6 +1386,7 @@ class PendingOrderWorker:
qty=remaining,
price=limit_price,
reduce_only=reduce_only,
pos_side=pos_side,
client_order_id=limit_client_oid,
)
elif isinstance(client, CoinbaseExchangeClient):
@@ -1718,6 +1719,7 @@ class PendingOrderWorker:
side=side,
qty=remaining,
reduce_only=reduce_only,
pos_side=pos_side,
client_order_id=market_client_oid,
)
elif isinstance(client, CoinbaseExchangeClient):
+10 -141
View File
@@ -3,12 +3,11 @@ Search service v2.0 - 增强版搜索服务
整合多个搜索引擎支持 API Key 轮换和故障转移
支持的搜索引擎按优先级
1. Bocha (博查) - 搜索优化
2. Tavily - 专为AI设计免费1000次/
3. SerpAPI - Google/Bing 结果抓取
4. Google CSE - 自定义搜索引擎
5. Bing Search API
6. DuckDuckGo - 免费兜底
1. Tavily - 专为AI设计免费1000次/
2. SerpAPI - Google/Bing 结果抓取
3. Google CSE - 自定义搜索引擎
4. Bing Search API
5. DuckDuckGo - 免费兜底
参考daily_stock_analysis-main/src/search_service.py
"""
@@ -331,130 +330,6 @@ class TavilySearchProvider(BaseSearchProvider):
)
class BochaSearchProvider(BaseSearchProvider):
"""
博查搜索引擎
特点
- 专为AI优化的中文搜索API
- 结果准确摘要完整
- 支持时间范围过滤和AI摘要
文档https://bocha-ai.feishu.cn/wiki/RXEOw02rFiwzGSkd9mUcqoeAnNK
"""
def __init__(self, api_keys: List[str]):
super().__init__(api_keys, "Bocha")
def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
"""执行博查搜索"""
try:
url = "https://api.bochaai.com/v1/web-search"
headers = {
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
}
# 确定时间范围
freshness = "oneWeek"
if days <= 1:
freshness = "oneDay"
elif days <= 7:
freshness = "oneWeek"
elif days <= 30:
freshness = "oneMonth"
else:
freshness = "oneYear"
payload = {
"query": query,
"freshness": freshness,
"summary": True,
"count": min(max_results, 50)
}
response = requests.post(url, headers=headers, json=payload, timeout=15)
if response.status_code != 200:
error_message = response.text
try:
if response.headers.get('content-type', '').startswith('application/json'):
error_data = response.json()
error_message = error_data.get('message', response.text)
except:
pass
if response.status_code == 403:
error_msg = f"余额不足: {error_message}"
elif response.status_code == 401:
error_msg = f"API KEY无效: {error_message}"
elif response.status_code == 429:
error_msg = f"请求频率达到限制: {error_message}"
else:
error_msg = f"HTTP {response.status_code}: {error_message}"
return SearchResponse(
query=query,
results=[],
provider=self.name,
success=False,
error_message=error_msg
)
data = response.json()
if data.get('code') != 200:
return SearchResponse(
query=query,
results=[],
provider=self.name,
success=False,
error_message=data.get('msg') or f"API返回错误码: {data.get('code')}"
)
results = []
web_pages = data.get('data', {}).get('webPages', {})
value_list = web_pages.get('value', [])
for item in value_list[:max_results]:
snippet = item.get('summary') or item.get('snippet', '')
if snippet:
snippet = snippet[:500]
results.append(SearchResult(
title=item.get('name', ''),
snippet=snippet,
url=item.get('url', ''),
source=item.get('siteName') or self._extract_domain(item.get('url', '')),
published_date=item.get('datePublished'),
))
return SearchResponse(
query=query,
results=results,
provider=self.name,
success=True,
)
except requests.exceptions.Timeout:
return SearchResponse(
query=query,
results=[],
provider=self.name,
success=False,
error_message="请求超时"
)
except Exception as e:
return SearchResponse(
query=query,
results=[],
provider=self.name,
success=False,
error_message=str(e)
)
class SerpAPISearchProvider(BaseSearchProvider):
"""
SerpAPI 搜索引擎
@@ -865,38 +740,32 @@ class SearchService:
"""初始化搜索引擎(按优先级排序)"""
from app.config import APIKeys
# 1. Bocha 优先(国内搜索优化
bocha_keys = APIKeys.BOCHA_API_KEYS
if bocha_keys:
self._providers.append(BochaSearchProvider(bocha_keys))
logger.info(f"已配置 Bocha 搜索,共 {len(bocha_keys)} 个 API Key")
# 2. TavilyAI优化搜索)
# 1. TavilyAI优化搜索
tavily_keys = APIKeys.TAVILY_API_KEYS
if tavily_keys:
self._providers.append(TavilySearchProvider(tavily_keys))
logger.info(f"已配置 Tavily 搜索,共 {len(tavily_keys)} 个 API Key")
# 3. SerpAPI
# 2. SerpAPI
serpapi_keys = APIKeys.SERPAPI_KEYS
if serpapi_keys:
self._providers.append(SerpAPISearchProvider(serpapi_keys))
logger.info(f"已配置 SerpAPI 搜索,共 {len(serpapi_keys)} 个 API Key")
# 4. Google CSE
# 3. Google CSE
google_api_key = self._config.get('google', {}).get('api_key')
google_cx = self._config.get('google', {}).get('cx')
if google_api_key and google_cx:
self._providers.append(GoogleSearchProvider(google_api_key, google_cx))
logger.info("已配置 Google CSE 搜索")
# 5. Bing
# 4. Bing
bing_api_key = self._config.get('bing', {}).get('api_key')
if bing_api_key:
self._providers.append(BingSearchProvider(bing_api_key))
logger.info("已配置 Bing 搜索")
# 6. DuckDuckGo(免费兜底)
# 5. DuckDuckGo(免费兜底)
self._providers.append(DuckDuckGoSearchProvider())
logger.info("已配置 DuckDuckGo 搜索(免费兜底)")
@@ -69,7 +69,6 @@ def load_addon_config() -> Dict[str, Any]:
('OPENROUTER_MAX_TOKENS', 'openrouter.max_tokens', 'int'),
('OPENROUTER_TIMEOUT', 'openrouter.timeout', 'int'),
('OPENROUTER_CONNECT_TIMEOUT', 'openrouter.connect_timeout', 'int'),
('AI_MODELS_JSON', 'ai.models', 'json'),
# OpenAI Direct
('OPENAI_API_KEY', 'openai.api_key', 'string'),
@@ -94,11 +93,9 @@ def load_addon_config() -> Dict[str, Any]:
('LLM_PROVIDER', 'llm.provider', 'string'),
# App
('CORS_ORIGINS', 'app.cors_origins', 'string'),
('RATE_LIMIT', 'app.rate_limit', 'int'),
('ENABLE_CACHE', 'app.enable_cache', 'bool'),
('ENABLE_REQUEST_LOG', 'app.enable_request_log', 'bool'),
('ENABLE_AI_ANALYSIS', 'app.enable_ai_analysis', 'bool'),
# Data source common
('DATA_SOURCE_TIMEOUT', 'data_source.timeout', 'int'),
@@ -113,7 +110,6 @@ def load_addon_config() -> Dict[str, Any]:
# CCXT
('CCXT_DEFAULT_EXCHANGE', 'ccxt.default_exchange', 'string'),
('CCXT_TIMEOUT', 'ccxt.timeout', 'int'),
('CCXT_PROXY', 'ccxt.proxy', 'string'),
# Other sources
('YFINANCE_TIMEOUT', 'yfinance.timeout', 'int'),
@@ -131,9 +127,6 @@ def load_addon_config() -> Dict[str, Any]:
# Tavily (AI-optimized search)
('TAVILY_API_KEYS', 'tavily.api_keys', 'string'),
# Bocha (Chinese search optimization)
('BOCHA_API_KEYS', 'bocha.api_keys', 'string'),
# SerpAPI (Google/Bing scraper)
('SERPAPI_KEYS', 'serpapi.api_keys', 'string'),
]
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
# QuantDinger Docker Entrypoint Script
# Checks and validates SECRET_KEY before starting the application
+87 -245
View File
@@ -1,8 +1,13 @@
# QuantDinger local configuration (copy to `.env` and edit)
# `run.py` will load `backend_api_python/.env` automatically if present.
#
# This file is organized as:
# 1) first-time deployment settings at the top
# 2) advanced / rarely changed settings at the bottom
# For Docker image source / ports, use the project-root `.env`.
# =========================
# Auth (local login)
# Auth (required)
# =========================
SECRET_KEY=quantdinger-secret-key-change-me
ADMIN_USER=quantdinger
@@ -10,67 +15,43 @@ ADMIN_PASSWORD=123456
ADMIN_EMAIL=
# =========================
# Demo Mode
# Core app
# =========================
# Set to true to enable read-only mode for public demo.
# Blocks all POST/PUT/DELETE requests except login.
IS_DEMO_MODE=false
# =========================
# Network / App
# =========================
PYTHON_API_HOST=0.0.0.0
PYTHON_API_PORT=5000
PYTHON_API_DEBUG=False
# =========================
# Database Configuration (PostgreSQL)
# =========================
# PostgreSQL connection URL (required for multi-user mode)
# Format: postgresql://user:password@host:port/dbname
# Docker: uses this default, no changes needed
# Local: change 'postgres' to 'localhost' and update password
DATABASE_URL=postgresql://quantdinger:quantdinger123@postgres:5432/quantdinger
FRONTEND_URL=http://localhost:8888
ENABLE_REGISTRATION=true
# =========================
# Pending orders worker (optional)
# AI / LLM (choose one provider)
# =========================
LLM_PROVIDER=openrouter
OPENROUTER_API_KEY=
OPENROUTER_MODEL=openai/gpt-4o
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o
GOOGLE_API_KEY=
GOOGLE_MODEL=gemini-1.5-flash
DEEPSEEK_API_KEY=
DEEPSEEK_MODEL=deepseek-chat
GROK_API_KEY=
GROK_MODEL=grok-beta
# =========================
# Common background jobs
# =========================
# Paper mode default: disabled. If enabled, it will consume `pending_orders` and dispatch signals via webhook.
# Poll and dispatch orders from `pending_orders` (live/signal).
# Local mode default is enabled in code, but you can override here.
ENABLE_PENDING_ORDER_WORKER=true
# =========================
# Portfolio monitor (optional)
# =========================
# Background service that runs scheduled AI analysis on manual positions
# and sends notifications (email/telegram/browser).
# Default: enabled. Set to false to disable.
ENABLE_PORTFOLIO_MONITOR=true
# Reclaim orders stuck in status=processing after worker crashes (seconds).
PENDING_ORDER_STALE_SEC=90
DISABLE_RESTORE_RUNNING_STRATEGIES=false
# =========================
# Live trading order execution settings
# Email / SMTP (optional)
# =========================
# Order execution mode:
# - "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
# Price offset for limit orders in basis points (1 bps = 0.01%)
# Buy orders: price = market_price * (1 - offset)
# Sell orders: price = market_price * (1 + offset)
MAKER_OFFSET_BPS=2
# =========================
# Email / SMTP (公共邮件服务,所有用户共用)
# =========================
# 用户在个人中心配置自己的通知邮箱,SMTP 服务器由管理员统一配置
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
@@ -79,283 +60,144 @@ SMTP_FROM=
SMTP_USE_TLS=true
SMTP_USE_SSL=false
# Phone / SMS (optional; Twilio REST, required if you enable phone channel)
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_FROM_NUMBER=
# Restore strategies with status='running' on backend startup.
# Set to true to disable auto-restore.
DISABLE_RESTORE_RUNNING_STRATEGIES=false
# =========================
# Strategy execution loop (tick interval)
# Proxy (optional)
# =========================
# Default tick interval for strategy monitoring loop (seconds).
# The strategy thread will fetch current price and evaluate triggers once per tick.
STRATEGY_TICK_INTERVAL_SEC=10
# In-memory price cache TTL (seconds). Normally doesn't matter when tick interval is >= TTL.
PRICE_CACHE_TTL_SEC=10
# =========================
# Outbound Proxy (optional, recommended if your network blocks data providers)
# =========================
# If you use a local proxy (common ports: 7890/7891/10808), set PROXY_PORT only.
# Default scheme is socks5h and host is 127.0.0.1.
# For Docker deployment: set PROXY_HOST=host.docker.internal to access host's proxy
PROXY_PORT=
PROXY_HOST=127.0.0.1
PROXY_SCHEME=socks5h
# Most users only need PROXY_URL.
# Example local:
# PROXY_URL=socks5h://127.0.0.1:10808
#
# Example Docker:
# PROXY_URL=socks5h://host.docker.internal:10808
PROXY_URL=
# You can also set standard variables directly (advanced):
# ALL_PROXY=socks5h://127.0.0.1:10808
# HTTP_PROXY=socks5h://127.0.0.1:10808
# HTTPS_PROXY=socks5h://127.0.0.1:10808
# =========================
# Captcha / OAuth (optional)
# =========================
TURNSTILE_SITE_KEY=
TURNSTILE_SECRET_KEY=
# Allow frontend dev server
CORS_ORIGINS=*
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:5000/api/auth/oauth/google/callback
# Request rate limit (per minute)
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_REDIRECT_URI=http://localhost:5000/api/auth/oauth/github/callback
# =========================
# Billing / payments (optional)
# =========================
BILLING_ENABLED=False
USDT_PAY_ENABLED=False
# =========================
# Advanced / rarely changed
# =========================
# The settings below are optional. Most users can leave them unchanged.
# Network / App tuning
PYTHON_API_HOST=0.0.0.0
PYTHON_API_PORT=5000
PYTHON_API_DEBUG=False
RATE_LIMIT=100
ENABLE_CACHE=False
ENABLE_REQUEST_LOG=True
ENABLE_AI_ANALYSIS=True
# =========================
# Agent memory & reflection (optional)
# =========================
# Toggle agent memory usage in multi-agent analysis
ENABLE_AGENT_MEMORY=true
# Strategy / execution tuning
PENDING_ORDER_STALE_SEC=90
ORDER_MODE=market
MAKER_WAIT_SEC=10
MAKER_OFFSET_BPS=2
STRATEGY_TICK_INTERVAL_SEC=10
PRICE_CACHE_TTL_SEC=10
K_LINE_HISTORY_GET_NUMBER=500
# Memory retrieval settings
AGENT_MEMORY_ENABLE_VECTOR=true
AGENT_MEMORY_EMBEDDING_DIM=256
AGENT_MEMORY_TOP_K=5
AGENT_MEMORY_CANDIDATE_LIMIT=500
AGENT_MEMORY_HALF_LIFE_DAYS=30
AGENT_MEMORY_W_SIM=0.75
AGENT_MEMORY_W_RECENCY=0.20
AGENT_MEMORY_W_RETURNS=0.05
# Automated verification loop (replaces cron)
ENABLE_REFLECTION_WORKER=false
REFLECTION_WORKER_INTERVAL_SEC=86400
# =========================
# LLM Provider Selection
# =========================
# Choose your LLM provider: openrouter, openai, google, deepseek, grok
LLM_PROVIDER=openrouter
# =========================
# OpenRouter (Multi-model gateway, recommended)
# =========================
OPENROUTER_API_KEY=
# LLM advanced tuning
OPENROUTER_API_URL=https://openrouter.ai/api/v1/chat/completions
OPENROUTER_MODEL=openai/gpt-4o
OPENROUTER_TEMPERATURE=0.7
OPENROUTER_MAX_TOKENS=4000
OPENROUTER_TIMEOUT=300
OPENROUTER_CONNECT_TIMEOUT=30
# =========================
# OpenAI Direct
# =========================
OPENAI_API_KEY=
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL=gpt-4o
# =========================
# Google Gemini
# =========================
GOOGLE_API_KEY=
GOOGLE_MODEL=gemini-1.5-flash
# =========================
# DeepSeek
# =========================
DEEPSEEK_API_KEY=
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
DEEPSEEK_MODEL=deepseek-chat
# =========================
# xAI Grok
# =========================
GROK_API_KEY=
GROK_BASE_URL=https://api.x.ai/v1
GROK_MODEL=grok-beta
# Optional: override model list shown in UI (JSON object: {"model_id":"Display Name", ...})
AI_MODELS_JSON={}
# =========================
# Data sources (Kline / pricing)
# =========================
# Data sources
DATA_SOURCE_TIMEOUT=30
DATA_SOURCE_RETRY=3
DATA_SOURCE_RETRY_BACKOFF=0.5
# Finnhub (US stocks / forex helpers depending on implementation)
FINNHUB_API_KEY=
FINNHUB_TIMEOUT=10
FINNHUB_RATE_LIMIT=60
# CCXT (crypto via Binance by default)
CCXT_DEFAULT_EXCHANGE=coinbase
CCXT_TIMEOUT=10000
CCXT_PROXY=
# Akshare (optional)
AKSHARE_TIMEOUT=30
# YFinance
YFINANCE_TIMEOUT=30
# Tiingo (optional)
TIINGO_API_KEY=
TIINGO_TIMEOUT=10
# =========================
# Web Search & News (optional)
# =========================
# Search provider priority: tavily > bocha > google > bing > duckduckgo
# AI search / news providers
SEARCH_PROVIDER=google
SEARCH_MAX_RESULTS=10
# Google Custom Search (CSE)
SEARCH_GOOGLE_API_KEY=
SEARCH_GOOGLE_CX=
# Bing Search API
SEARCH_BING_API_KEY=
# Tavily Search API (专为AI设计,推荐!免费1000次/月)
# Get your key at: https://tavily.com/
# 支持多个 key 轮换,用逗号分隔: key1,key2,key3
TAVILY_API_KEYS=
# 博查 Bocha Search API (国内搜索优化)
# Get your key at: https://bochaai.com/
# 支持多个 key 轮换,用逗号分隔: key1,key2,key3
BOCHA_API_KEYS=
# SerpAPI (Google/Bing 结果抓取,免费100次/月)
# Get your key at: https://serpapi.com/
# 支持多个 key 轮换,用逗号分隔: key1,key2,key3
SERPAPI_KEYS=
# Internal API key (optional, if you add internal-service auth later)
INTERNAL_API_KEY=
# SMS / phone notifications
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_FROM_NUMBER=
# =========================
# Registration & Security (注册与安全)
# =========================
# Enable user registration (允许用户注册)
ENABLE_REGISTRATION=true
# Cloudflare Turnstile (人机验证)
# Get your keys at: https://dash.cloudflare.com/?to=/:account/turnstile
TURNSTILE_SITE_KEY=
TURNSTILE_SECRET_KEY=
# Frontend URL (for OAuth redirects)
FRONTEND_URL=http://localhost:8080
# Google OAuth
# Get your credentials at: https://console.cloud.google.com/apis/credentials
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:5000/api/auth/oauth/google/callback
# GitHub OAuth
# Get your credentials at: https://github.com/settings/developers
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_REDIRECT_URI=http://localhost:5000/api/auth/oauth/github/callback
# Security: IP rate limit (防爆破 - IP维度)
# Block IP after N failed attempts within M minutes for X minutes
# Security / verification tuning
SECURITY_IP_MAX_ATTEMPTS=10
SECURITY_IP_WINDOW_MINUTES=5
SECURITY_IP_BLOCK_MINUTES=15
# Security: Account rate limit (防爆破 - 账户维度)
SECURITY_ACCOUNT_MAX_ATTEMPTS=5
SECURITY_ACCOUNT_WINDOW_MINUTES=60
SECURITY_ACCOUNT_BLOCK_MINUTES=30
# Verification code settings (验证码设置)
VERIFICATION_CODE_EXPIRE_MINUTES=10
VERIFICATION_CODE_RATE_LIMIT=60
VERIFICATION_CODE_IP_HOURLY_LIMIT=10
VERIFICATION_CODE_MAX_ATTEMPTS=5
VERIFICATION_CODE_LOCK_MINUTES=30
# =========================
# Billing & Credits (积分计费系统)
# =========================
# Enable billing system (启用计费系统)
BILLING_ENABLED=False
# Legacy: VIP users bypass ALL paid feature credit costs (NOT recommended)
# 建议关闭:VIP 仅用于“VIP免费指标”,其它功能仍扣积分
BILLING_VIP_BYPASS=False
# Credits consumed per feature (各功能消耗积分数)
# Billing / credits
BILLING_COST_AI_ANALYSIS=10
BILLING_COST_STRATEGY_RUN=5
BILLING_COST_BACKTEST=3
BILLING_COST_PORTFOLIO_MONITOR=8
BILLING_COST_POLYMARKET_DEEP_ANALYSIS=15
# Telegram customer service URL for recharge (充值跳转的Telegram链接)
RECHARGE_TELEGRAM_URL=https://t.me/quantdinger
CREDITS_REGISTER_BONUS=100
CREDITS_REFERRAL_BONUS=50
# =========================
# Membership Plans (会员套餐 - Mock支付配置)
# =========================
# Price in USD
# Membership plans
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 payment
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
# Background worker poll interval (seconds) for checking pending USDT orders
USDT_WORKER_POLL_INTERVAL=30
# New user registration bonus credits (新用户注册赠送积分)
CREDITS_REGISTER_BONUS=100
# Referral bonus credits (邀请用户赠送积分,邀请人获得)
CREDITS_REFERRAL_BONUS=50
# History K-Line ticket get number (策略中默认获取历史K线数量)
K_LINE_HISTORY_GET_NUMBER=500
+4 -79
View File
@@ -365,31 +365,6 @@ CREATE TABLE IF NOT EXISTS qd_indicator_codes (
CREATE INDEX IF NOT EXISTS idx_indicator_codes_user_id ON qd_indicator_codes USING btree (user_id);
CREATE INDEX IF NOT EXISTS idx_indicator_review_status ON qd_indicator_codes USING btree (review_status);
-- =============================================================================
-- 8. AI Decisions
-- =============================================================================
CREATE TABLE IF NOT EXISTS qd_ai_decisions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL DEFAULT 1 REFERENCES qd_users(id) ON DELETE CASCADE,
strategy_id INTEGER REFERENCES qd_strategies_trading(id) ON DELETE CASCADE,
decision_data TEXT,
context_data TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_ai_decisions_user_id ON qd_ai_decisions(user_id);
-- =============================================================================
-- 9. Addon Config
-- =============================================================================
CREATE TABLE IF NOT EXISTS qd_addon_config (
config_key VARCHAR(100) PRIMARY KEY,
config_value TEXT,
type VARCHAR(20) DEFAULT 'string'
);
-- =============================================================================
-- 10. Watchlist
-- =============================================================================
@@ -614,56 +589,6 @@ INSERT INTO qd_market_symbols (market, symbol, name, exchange, currency, is_acti
('Futures', 'NQ', 'NASDAQ 100 E-mini', 'CME', 'USD', 1, 1, 91)
ON CONFLICT (market, symbol) DO NOTHING;
-- =============================================================================
-- 18. Agent Memories (AI Learning System)
-- =============================================================================
-- Stores agent decision experiences for RAG-style retrieval during analysis.
-- Each agent (trader, risk_analyst, etc.) shares this table but is identified by agent_name.
CREATE TABLE IF NOT EXISTS qd_agent_memories (
id SERIAL PRIMARY KEY,
agent_name VARCHAR(100) NOT NULL,
situation TEXT NOT NULL,
recommendation TEXT NOT NULL,
result TEXT,
returns REAL,
market VARCHAR(50),
symbol VARCHAR(50),
timeframe VARCHAR(20),
features_json TEXT,
embedding BYTEA,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_agent_memories_agent ON qd_agent_memories(agent_name);
CREATE INDEX IF NOT EXISTS idx_agent_memories_created ON qd_agent_memories(agent_name, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_agent_memories_market ON qd_agent_memories(agent_name, market, symbol);
-- =============================================================================
-- 19. Reflection Records (AI Auto-Verification System)
-- =============================================================================
-- Records analysis predictions for future auto-verification and closed-loop learning.
CREATE TABLE IF NOT EXISTS qd_reflection_records (
id SERIAL PRIMARY KEY,
market VARCHAR(50) NOT NULL,
symbol VARCHAR(50) NOT NULL,
initial_price REAL,
decision VARCHAR(20),
confidence INTEGER,
reasoning TEXT,
analysis_date TIMESTAMP DEFAULT NOW(),
target_check_date TIMESTAMP,
status VARCHAR(20) DEFAULT 'PENDING',
final_price REAL,
actual_return REAL,
check_result TEXT
);
CREATE INDEX IF NOT EXISTS idx_reflection_status ON qd_reflection_records(status, target_check_date);
CREATE INDEX IF NOT EXISTS idx_reflection_market ON qd_reflection_records(market, symbol);
-- =============================================================================
-- 19.5. Analysis Memory (Fast AI Analysis Memory System)
-- =============================================================================
@@ -677,15 +602,15 @@ CREATE TABLE IF NOT EXISTS qd_analysis_memory (
decision VARCHAR(10) NOT NULL,
confidence INT DEFAULT 50,
price_at_analysis DECIMAL(24, 8),
entry_price DECIMAL(24, 8),
stop_loss DECIMAL(24, 8),
take_profit DECIMAL(24, 8),
summary TEXT,
reasons JSONB,
risks JSONB,
scores JSONB,
indicators_snapshot JSONB,
raw_result JSONB, -- Full analysis result for history replay
consensus_score DECIMAL(24, 8),
consensus_abs DECIMAL(24, 8),
agreement_ratio DECIMAL(10, 6),
quality_multiplier DECIMAL(10, 6),
created_at TIMESTAMP DEFAULT NOW(),
validated_at TIMESTAMP,
actual_outcome VARCHAR(20),
-11
View File
@@ -47,14 +47,6 @@ def _apply_proxy_env():
# If user provided explicit proxy URL, honor it.
proxy_url = (os.getenv('PROXY_URL') or '').strip()
# If user only provided port, build a URL (common local proxy setups).
if not proxy_url:
port = (os.getenv('PROXY_PORT') or '').strip()
if port:
host = (os.getenv('PROXY_HOST') or '127.0.0.1').strip()
scheme = (os.getenv('PROXY_SCHEME') or 'socks5h').strip()
proxy_url = f"{scheme}://{host}:{port}"
if not proxy_url:
return
@@ -63,9 +55,6 @@ def _apply_proxy_env():
_set_if_blank('HTTP_PROXY', proxy_url)
_set_if_blank('HTTPS_PROXY', proxy_url)
# CCXT config uses CCXT_PROXY in our codebase.
_set_if_blank('CCXT_PROXY', proxy_url)
_apply_proxy_env()
# Add project root to Python path
+16 -7
View File
@@ -8,19 +8,24 @@
# 3. docker-compose up -d --build
# 4. Open http://localhost:8888
#
# Frontend image: multi-stage build from QuantDinger-Vue-src (context = repo root).
# To refresh static files without Docker: cd QuantDinger-Vue-src && npm run build
# && rm -rf ../frontend/dist && cp -R dist ../frontend/dist
# Frontend image: nginx serving prebuilt files from `frontend/dist`.
# The open-source repo does not require Node.js to build the frontend image.
#
# Note: The container will NOT start if SECRET_KEY is using the default value.
# This is a security measure to prevent insecure deployments.
#
# Docker image sources:
# Default uses official Docker Hub images.
# To switch source globally, set a single `IMAGE_PREFIX` in project-root `.env`.
# Examples: empty (official), `docker.m.daocloud.io/library/`,
# `docker.xuanyuan.me/library/`.
services:
# ========================
# PostgreSQL Database
# ========================
postgres:
image: postgres:16-alpine
image: ${IMAGE_PREFIX:-}postgres:16-alpine
container_name: quantdinger-db
restart: unless-stopped
environment:
@@ -48,6 +53,8 @@ services:
build:
context: ./backend_api_python
dockerfile: Dockerfile
args:
BASE_IMAGE: ${IMAGE_PREFIX:-}python:3.12-slim-bookworm
container_name: quantdinger-backend
restart: unless-stopped
depends_on:
@@ -58,8 +65,8 @@ services:
volumes:
- backend_logs:/app/logs
- backend_data:/app/data
# Mount .env for runtime config
- ./backend_api_python/.env:/app/.env:ro
# Mount .env for runtime config and admin-panel updates
- ./backend_api_python/.env:/app/.env
environment:
- PYTHON_API_HOST=0.0.0.0
- PYTHON_API_PORT=5000
@@ -75,12 +82,14 @@ services:
retries: 3
# ========================
# Frontend (Nginx + Vue build from QuantDinger-Vue-src)
# Frontend (Nginx serving prebuilt dist)
# ========================
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
args:
RUNTIME_IMAGE: ${IMAGE_PREFIX:-}nginx:1.25-alpine
container_name: quantdinger-frontend
restart: unless-stopped
ports:
+450
View File
@@ -0,0 +1,450 @@
# QuantDinger 云服务器部署指南
本文面向生产/准生产环境,使用云服务器 + Docker Compose 部署 QuantDinger,并补充域名、HTTPS、反向代理与前后端分离说明。
## 推荐架构
推荐使用“同域名 + Nginx 反向代理”:
- 用户访问:`https://app.example.com`
- 宿主机 Nginx:监听 `80/443`
- Docker `frontend`:绑定到 `127.0.0.1:8888`
- Docker `backend`:绑定到 `127.0.0.1:5000`
- Docker `postgres`:绑定到 `127.0.0.1:5432`
这样做的好处:
- 外网只暴露 `80/443`
- 前端和 API 走同域名,最省心
- 数据库与后端管理端口不直接暴露到公网
## 1. 服务器准备
建议配置:
- Ubuntu 22.04 / Debian 12
- 2C4G 及以上
- 已开放安全组端口:`22``80``443`
- 已准备域名,例如 `app.example.com`
域名解析:
1. 在 DNS 服务商后台添加一条 `A` 记录
2. 主机记录填 `app`
3. 记录值填云服务器公网 IP
4. 等待解析生效
验证:
```bash
ping app.example.com
```
## 2. 安装 Docker 与 Docker Compose
Ubuntu / Debian 示例:
```bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
docker --version
docker compose version
```
如果你的网络拉取 Docker Hub 较慢,可稍后通过项目根 `.env``IMAGE_PREFIX` 切换镜像源。
## 3. 拉取项目
```bash
git clone https://github.com/brokermr810/QuantDinger.git
cd QuantDinger
```
## 4. 配置后端 `.env`
复制模板:
```bash
cp backend_api_python/env.example backend_api_python/.env
```
生成并写入 `SECRET_KEY`
```bash
./scripts/generate-secret-key.sh
```
至少检查并修改这些配置:
```ini
ADMIN_USER=quantdinger
ADMIN_PASSWORD=your_strong_password
SECRET_KEY=your_generated_secret
```
如果你需要 AI 功能,再补充至少一个模型提供商密钥,例如:
```ini
OPENROUTER_API_KEY=your_key
```
## 5. 配置项目根 `.env`
项目根 `.env` 用于 Docker Compose 的端口和镜像源控制。
推荐生产配置:
```bash
cp .env.example .env
```
编辑为:
```ini
FRONTEND_PORT=127.0.0.1:8888
BACKEND_PORT=127.0.0.1:5000
DB_PORT=127.0.0.1:5432
IMAGE_PREFIX=
```
说明:
- `FRONTEND_PORT=127.0.0.1:8888`:只允许宿主机本地访问,由外层 Nginx 转发
- `BACKEND_PORT=127.0.0.1:5000`:避免后端 API 直接暴露公网
- `DB_PORT=127.0.0.1:5432`:避免数据库端口暴露公网
- `IMAGE_PREFIX=`:空值表示官方 Docker Hub
如果拉镜像失败,可改成:
```ini
IMAGE_PREFIX=docker.m.daocloud.io/library/
```
或:
```ini
IMAGE_PREFIX=docker.xuanyuan.me/library/
```
## 6. 启动容器
```bash
docker-compose up -d --build
docker-compose ps
```
查看日志:
```bash
docker-compose logs -f backend
docker-compose logs -f frontend
```
此时服务通常已在本机监听:
- `127.0.0.1:8888`
- `127.0.0.1:5000`
- `127.0.0.1:5432`
## 7. 安装并配置 Nginx
安装:
```bash
sudo apt update
sudo apt install -y nginx
```
推荐站点配置 `/etc/nginx/sites-available/quantdinger.conf`
```nginx
server {
listen 80;
server_name app.example.com;
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:8888;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
启用站点:
```bash
sudo ln -s /etc/nginx/sites-available/quantdinger.conf /etc/nginx/sites-enabled/quantdinger.conf
sudo nginx -t
sudo systemctl reload nginx
```
如果启用了防火墙:
```bash
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
```
## 8. 配置 HTTPSLet's Encrypt
安装 Certbot
```bash
sudo apt install -y certbot python3-certbot-nginx
```
申请证书:
```bash
sudo certbot --nginx -d app.example.com
```
自动续期测试:
```bash
sudo certbot renew --dry-run
```
完成后访问:
```text
https://app.example.com
```
## 9. 推荐生产模式:同域名部署
这是开源版最推荐的方式。
原因:
- 当前开源前端为预编译 `frontend/dist`
- 前端容器内部已把 `/api/*` 代理到 Docker 网络中的 `backend:5000`
- 用户只需要维护一个域名和一套 HTTPS
拓扑如下:
```text
Browser
-> https://app.example.com
-> Host Nginx :443
-> 127.0.0.1:8888 (frontend container)
-> /api/* 再由 frontend 容器代理到 backend:5000
```
## 10. 高级方案:前后端分离 / 双域名
如果你希望:
- 前端域名:`app.example.com`
- API 域名:`api.example.com`
可以使用双域名方案,但请注意:
1. 需要前端能够把 API 指向 `api.example.com`
2. 后端需要正确处理跨域
3. 这种方式更适合有前端源码控制权或二次开发的部署场景
宿主机 Nginx 可拆成两个站点:
- `app.example.com` -> `127.0.0.1:8888`
- `api.example.com` -> `127.0.0.1:5000`
`api.example.com` 示例:
```nginx
server {
listen 80;
server_name api.example.com;
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
如果你只是想“前后端逻辑分离”,但不想处理跨域,建议仍使用同域名:
- `https://app.example.com/`
- `https://app.example.com/api/`
## 11. 常用运维命令
查看状态:
```bash
docker-compose ps
```
查看日志:
```bash
docker-compose logs -f backend
docker-compose logs -f frontend
docker-compose logs -f postgres
```
更新版本:
```bash
git pull
docker-compose up -d --build
```
重启:
```bash
docker-compose restart backend
docker-compose restart frontend
```
停止:
```bash
docker-compose down
```
## 12. 常见问题排查
### 1. 拉镜像失败
现象:
- `failed to resolve source metadata`
- `registry-1.docker.io`
- `Docker Desktop has no HTTPS proxy`
处理:
```ini
IMAGE_PREFIX=docker.m.daocloud.io/library/
```
然后重新:
```bash
docker-compose up -d --build
```
### 2. 后端日志出现 `exec /usr/local/bin/docker-entrypoint.sh: no such file or directory`
处理:
```bash
docker-compose build --no-cache backend
docker-compose up -d backend
```
### 3. 前端日志出现 `host not found in upstream "backend"`
通常表示后端没先起来。
处理:
```bash
docker-compose ps
docker-compose logs backend --tail=100
docker-compose restart frontend
```
### 4. 前端构建报错 `COPY frontend/dist ... not found`
原因通常是 `.dockerignore``frontend/dist` 排除了,而当前开源版前端正是直接复制这个预编译目录。
检查:
```bash
cat .dockerignore
ls frontend/dist
```
确认 `.dockerignore` 中不要包含:
```text
frontend/dist
```
### 5. 后台保存配置时报 `Read-only file system: '/app/.env'`
原因是 `backend_api_python/.env` 被只读挂载到容器内。
请检查 `docker-compose.yml`,确保不要使用:
```yaml
- ./backend_api_python/.env:/app/.env:ro
```
而应改为可写挂载:
```yaml
- ./backend_api_python/.env:/app/.env
```
然后执行:
```bash
docker-compose up -d backend
```
### 6. Docker 内代理不生效
如果宿主机代理监听在本机 `127.0.0.1:10808`,容器内不能直接写 `127.0.0.1`,因为那会指向容器自己。
Docker 部署请改为:
```ini
PROXY_URL=socks5h://host.docker.internal:10808
```
### 7. 交易所日志出现 `symbol not found`
如果你已经确认代理和外网访问正常,但日志仍出现某些币对不存在,例如:
```text
Symbol 'MATIC/USDT' not found on okx
```
这通常是交易所的符号映射/代币更名问题,不一定是网络故障。
### 8. Nginx 502 / 504
检查:
```bash
docker-compose ps
curl http://127.0.0.1:8888/health
curl http://127.0.0.1:5000/api/health
sudo nginx -t
```
### 9. 数据库不应暴露公网
生产环境建议:
```ini
DB_PORT=127.0.0.1:5432
```
不要开放:
- `5432`
- `5000`
公网只开放:
- `80`
- `443`
+450
View File
@@ -0,0 +1,450 @@
# QuantDinger Cloud Server Deployment Guide
This guide covers production-style deployment on a cloud server with Docker Compose, domain setup, HTTPS, reverse proxy, and frontend/backend separation options.
## Recommended Architecture
Recommended setup: single domain + host Nginx reverse proxy
- Public URL: `https://app.example.com`
- Host Nginx: listens on `80/443`
- Docker `frontend`: binds to `127.0.0.1:8888`
- Docker `backend`: binds to `127.0.0.1:5000`
- Docker `postgres`: binds to `127.0.0.1:5432`
Benefits:
- Only `80/443` are exposed publicly
- Frontend and API stay on the same origin
- Backend and database are not directly exposed to the internet
## 1. Prepare the Server
Recommended:
- Ubuntu 22.04 / Debian 12
- 2 vCPU / 4 GB RAM or higher
- Security group ports open: `22`, `80`, `443`
- A domain such as `app.example.com`
DNS steps:
1. Create an `A` record
2. Host: `app`
3. Value: your server public IP
4. Wait for DNS propagation
Verify:
```bash
ping app.example.com
```
## 2. Install Docker and Docker Compose
Ubuntu / Debian example:
```bash
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
docker --version
docker compose version
```
If Docker Hub is slow or blocked in your network, you can switch image source later with `IMAGE_PREFIX` in the project-root `.env`.
## 3. Clone the Project
```bash
git clone https://github.com/brokermr810/QuantDinger.git
cd QuantDinger
```
## 4. Configure `backend_api_python/.env`
Copy the template:
```bash
cp backend_api_python/env.example backend_api_python/.env
```
Generate and write `SECRET_KEY`:
```bash
./scripts/generate-secret-key.sh
```
At minimum, review these values:
```ini
ADMIN_USER=quantdinger
ADMIN_PASSWORD=your_strong_password
SECRET_KEY=your_generated_secret
```
If you want AI features, add at least one provider key, for example:
```ini
OPENROUTER_API_KEY=your_key
```
## 5. Configure the Project-Root `.env`
The project-root `.env` is used by Docker Compose for ports and image source selection.
Copy the template:
```bash
cp .env.example .env
```
Recommended production values:
```ini
FRONTEND_PORT=127.0.0.1:8888
BACKEND_PORT=127.0.0.1:5000
DB_PORT=127.0.0.1:5432
IMAGE_PREFIX=
```
Explanation:
- `FRONTEND_PORT=127.0.0.1:8888`: only accessible locally, exposed through host Nginx
- `BACKEND_PORT=127.0.0.1:5000`: avoid exposing API directly
- `DB_PORT=127.0.0.1:5432`: avoid exposing PostgreSQL directly
- `IMAGE_PREFIX=`: empty means official Docker Hub
If image pulls fail, try:
```ini
IMAGE_PREFIX=docker.m.daocloud.io/library/
```
or:
```ini
IMAGE_PREFIX=docker.xuanyuan.me/library/
```
## 6. Start the Containers
```bash
docker-compose up -d --build
docker-compose ps
```
Logs:
```bash
docker-compose logs -f backend
docker-compose logs -f frontend
```
At this point, services usually listen on:
- `127.0.0.1:8888`
- `127.0.0.1:5000`
- `127.0.0.1:5432`
## 7. Install and Configure Nginx
Install:
```bash
sudo apt update
sudo apt install -y nginx
```
Recommended site config `/etc/nginx/sites-available/quantdinger.conf`:
```nginx
server {
listen 80;
server_name app.example.com;
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:8888;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Enable it:
```bash
sudo ln -s /etc/nginx/sites-available/quantdinger.conf /etc/nginx/sites-enabled/quantdinger.conf
sudo nginx -t
sudo systemctl reload nginx
```
If using UFW:
```bash
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
```
## 8. Enable HTTPS with Let's Encrypt
Install Certbot:
```bash
sudo apt install -y certbot python3-certbot-nginx
```
Request the certificate:
```bash
sudo certbot --nginx -d app.example.com
```
Test renewal:
```bash
sudo certbot renew --dry-run
```
Then open:
```text
https://app.example.com
```
## 9. Recommended Production Mode: Single Domain
This is the recommended mode for the open-source edition.
Why:
- The open-source frontend is shipped as prebuilt `frontend/dist`
- The frontend container already proxies `/api/*` to `backend:5000` inside Docker
- Only one public domain and one TLS configuration are needed
Topology:
```text
Browser
-> https://app.example.com
-> Host Nginx :443
-> 127.0.0.1:8888 (frontend container)
-> /api/* then proxied by frontend container to backend:5000
```
## 10. Advanced Option: Frontend / Backend Separation
If you want:
- frontend: `app.example.com`
- API: `api.example.com`
you can use a dual-domain setup, but note:
1. the frontend must point API requests to `api.example.com`
2. backend cross-origin handling must be correct
3. this is better suited for deployments where you control frontend source/customization
Host Nginx can expose:
- `app.example.com` -> `127.0.0.1:8888`
- `api.example.com` -> `127.0.0.1:5000`
Example `api.example.com` config:
```nginx
server {
listen 80;
server_name api.example.com;
client_max_body_size 20m;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
If you only want logical separation without cross-origin complexity, keep:
- `https://app.example.com/`
- `https://app.example.com/api/`
## 11. Common Operations
Check status:
```bash
docker-compose ps
```
View logs:
```bash
docker-compose logs -f backend
docker-compose logs -f frontend
docker-compose logs -f postgres
```
Update:
```bash
git pull
docker-compose up -d --build
```
Restart:
```bash
docker-compose restart backend
docker-compose restart frontend
```
Stop:
```bash
docker-compose down
```
## 12. Troubleshooting
### 1. Image pull failures
Symptoms:
- `failed to resolve source metadata`
- `registry-1.docker.io`
- `Docker Desktop has no HTTPS proxy`
Fix:
```ini
IMAGE_PREFIX=docker.m.daocloud.io/library/
```
Then rerun:
```bash
docker-compose up -d --build
```
### 2. Backend logs show `exec /usr/local/bin/docker-entrypoint.sh: no such file or directory`
Fix:
```bash
docker-compose build --no-cache backend
docker-compose up -d backend
```
### 3. Frontend logs show `host not found in upstream "backend"`
This usually means backend failed first.
Fix:
```bash
docker-compose ps
docker-compose logs backend --tail=100
docker-compose restart frontend
```
### 4. Frontend build fails with `COPY frontend/dist ... not found`
This usually means `.dockerignore` excluded `frontend/dist`, while the current open-source frontend image copies that prebuilt directory directly.
Check:
```bash
cat .dockerignore
ls frontend/dist
```
Make sure `.dockerignore` does NOT contain:
```text
frontend/dist
```
### 5. Saving settings fails with `Read-only file system: '/app/.env'`
This means `backend_api_python/.env` is mounted read-only into the container.
In `docker-compose.yml`, avoid:
```yaml
- ./backend_api_python/.env:/app/.env:ro
```
Use a writable mount instead:
```yaml
- ./backend_api_python/.env:/app/.env
```
Then run:
```bash
docker-compose up -d backend
```
### 6. Proxy works on host but not inside Docker
If your proxy listens on host `127.0.0.1:10808`, do not use `127.0.0.1` inside the container, because that points to the container itself.
For Docker deployments, use:
```ini
PROXY_URL=socks5h://host.docker.internal:10808
```
### 7. Exchange logs show `symbol not found`
If proxy/network access is already working but some symbols still fail, for example:
```text
Symbol 'MATIC/USDT' not found on okx
```
this is usually a market-symbol mapping / token-rename issue on the exchange side, not a general network failure.
### 8. Nginx 502 / 504
Check:
```bash
docker-compose ps
curl http://127.0.0.1:8888/health
curl http://127.0.0.1:5000/api/health
sudo nginx -t
```
### 9. PostgreSQL should not be public
Recommended:
```ini
DB_PORT=127.0.0.1:5432
```
Do not expose:
- `5432`
- `5000`
Publicly expose only:
- `80`
- `443`
+74 -13
View File
@@ -68,25 +68,40 @@
git clone https://github.com/brokermr810/QuantDinger.git
cd QuantDinger
# 2. 配置(编辑管理员密码和AI API密钥)
# 2. 复制后端配置
cp backend_api_python/env.example backend_api_python/.env
# 3. 重要:生成并设置安全的 SECRET_KEY
# Linux/Mac:
python3 -c "import secrets; print(secrets.token_hex(32))" | xargs -I {} sed -i 's|SECRET_KEY=.*|SECRET_KEY={}|' backend_api_python/.env
# 或手动编辑 backend_api_python/.env 并替换 SECRET_KEY 值
# Windows PowerShell:
# $key = python -c "import secrets; print(secrets.token_hex(32))"
# (Get-Content backend_api_python\.env) -replace 'SECRET_KEY=.*', "SECRET_KEY=$key" | Set-Content backend_api_python\.env
# 3. 生成并写入安全的 SECRET_KEY
./scripts/generate-secret-key.sh
# 4. 启动!
docker-compose up -d --build
```
> **Linux/macOS**:
> - 复制后端配置:
> `cp backend_api_python/env.example backend_api_python/.env`
> - 如果需要更多高级配置,直接查看这个文件下半部分的 “Advanced / rarely changed”:
> `backend_api_python/env.example`
> - 可选:如果你所在网络拉取 Docker Hub 较慢或不可达,再复制项目根镜像配置:
> `cp .env.example .env`
> - 生成并写入 `SECRET_KEY`
> `./scripts/generate-secret-key.sh`
> - 启动:
> `docker-compose up -d --build`
> **Windows PowerShell**:
> - 复制:`Copy-Item backend_api_python\env.example -Destination backend_api_python\.env`
> - 生成 SECRET_KEY`python -c "import secrets; print(secrets.token_hex(32))"` 然后手动编辑 `.env`
> - 复制后端配置
> `Copy-Item backend_api_python\env.example -Destination backend_api_python\.env`
> - 如果需要更多高级配置,直接查看这个文件下半部分的 “Advanced / rarely changed”:
> `backend_api_python\env.example`
> - 可选:如果你所在网络拉取 Docker Hub 较慢或不可达,再复制项目根镜像配置:
> `Copy-Item .env.example -Destination .env`
> - 生成并写入 `SECRET_KEY`
> `$key = py -c "import secrets; print(secrets.token_hex(32))"`
> `(Get-Content backend_api_python\.env) -replace '^SECRET_KEY=.*$', "SECRET_KEY=$key" | Set-Content backend_api_python\.env -Encoding UTF8`
> - 启动:
> `docker-compose up -d --build`
> **⚠️ 安全提示**:如果 `SECRET_KEY` 使用默认值,容器将**不会启动**。这可以防止不安全的部署。
@@ -159,6 +174,42 @@ FRONTEND_PORT=3000 # 默认:8888
BACKEND_PORT=127.0.0.1:5001 # 默认:5000
```
**可选镜像源** — 默认直连 Docker Hub。如果你所在地区/网络无法拉取基础镜像,可复制 `.env.example``.env`,只改一行:
```ini
IMAGE_PREFIX=docker.m.daocloud.io/library/
```
其他常见选择:
```ini
IMAGE_PREFIX=
IMAGE_PREFIX=docker.xuanyuan.me/library/
```
**Docker 常见问题**
```text
1. 镜像源切换由项目根目录 `.env` 控制,不是 `backend_api_python/.env`
2. 当前开源仓库已内置 `frontend/dist`,前端部署不再依赖 Node.js。
3. Windows 下如果后端日志出现:
exec /usr/local/bin/docker-entrypoint.sh: no such file or directory
通常是镜像层里的 shell/换行符兼容性问题。可执行:
docker-compose build --no-cache backend
4. 如果前端日志出现:
host not found in upstream "backend"
一般是后端先启动失败导致的连锁报错。先修复后端,再执行:
docker-compose restart frontend
5. 如果前端构建报错:
COPY frontend/dist ... not found
请检查 `.dockerignore`,确认没有把 `frontend/dist` 排除在 Docker 构建上下文之外。
6. 如果后台配置页点击保存时报错:
Read-only file system: '/app/.env'
请检查 `docker-compose.yml`,确认 `backend_api_python/.env` 不是以只读方式挂载。
7. Docker 部署下如果需要走本机代理,不要使用 `127.0.0.1`,应改为 `host.docker.internal`
例如:
PROXY_URL=socks5h://host.docker.internal:10808
8. 如果代理修复后仍出现某些交易对 "not found" 日志,通常是交易所符号映射问题
(例如代币更名),不一定是网络故障。
```
</details>
---
@@ -340,6 +391,14 @@ flowchart TB
| **外汇** | MetaTrader 5 (MT5)、OANDA | ✅ 通过MT5 |
| **期货** | 交易所API | ⚡ 数据 + 通知 |
### 交易所返佣入口
如果你本来就准备开通交易所账户,建议直接走返佣链接。很多交易用户本来就会主动找“手续费减免/返佣链接”,而下面这些链接都可以直接享受 20% 手续费减免,不增加任何额外成本。
- **OKX**[开户即享 20% 手续费减免](https://www.promooboost.com/join/QUANTDINGER)
- **Bitget**[开户即享 20% 手续费减免](https://partner.bitget.com/bg/dinger)
- **Bitget(备用链接)**[备用 20% 手续费减免注册链接](https://partner.hdmune.cn/bg/7r4xz8kd)
---
## 🏗️ 架构与配置
@@ -397,7 +456,8 @@ QuantDinger/
<details>
<summary><b>⚙️ 配置参考(.env</b></summary>
使用 `backend_api_python/env.example` 作为模板
使用 `backend_api_python/env.example` 作为精简模板
文件上半部分适合首次部署,下面的 “Advanced / rarely changed” 区域用于可选的高级调优。
| 类别 | 关键变量 |
|----------|-----------|
@@ -408,7 +468,7 @@ QuantDinger/
| **安全** | `TURNSTILE_SITE_KEY``ENABLE_REGISTRATION` |
| **会员** | `MEMBERSHIP_MONTHLY_PRICE_USD``MEMBERSHIP_MONTHLY_CREDITS` |
| **USDT支付** | `USDT_PAY_ENABLED``USDT_TRC20_XPUB``TRONGRID_API_KEY` |
| **代理** | `PROXY_PORT``PROXY_URL` |
| **代理** | `PROXY_URL` |
| **工作器** | `ENABLE_PENDING_ORDER_WORKER``ENABLE_PORTFOLIO_MONITOR` |
</details>
@@ -440,6 +500,7 @@ QuantDinger/
|----------|-------------|
| [更新日志](docs/CHANGELOG.md) | 版本历史和迁移说明 |
| [多用户设置](docs/multi-user-setup.md) | PostgreSQL多用户部署 |
| [云服务器部署](docs/CLOUD_DEPLOYMENT_CN.md) | 云服务器部署、域名、HTTPS 与反向代理 |
### 策略开发
+6 -24
View File
@@ -1,31 +1,13 @@
# QuantDinger Frontend — multi-stage build
# QuantDinger Frontend — static runtime image
# Build context: repository root (see docker-compose.yml)
#
# Stage 1: compile Vue app from QuantDinger-Vue-src
# Stage 2: nginx static + API reverse proxy (nginx.conf)
# The open-source repo ships prebuilt static assets in `frontend/dist`,
# so no Node.js build stage is required.
ARG RUNTIME_IMAGE=nginx:1.25-alpine
FROM ${RUNTIME_IMAGE}
FROM node:18-alpine AS builder
WORKDIR /app
# Layer cache: deps only
COPY QuantDinger-Vue-src/package.json QuantDinger-Vue-src/package-lock.json ./
RUN npm ci
COPY QuantDinger-Vue-src/ ./
# Match local production build (`npm run build` in QuantDinger-Vue-src)
ENV NODE_OPTIONS=--max_old_space_size=4096
RUN npm run build
# --- runtime ---
FROM nginx:1.25-alpine
# curl: docker-compose healthcheck
RUN apk add --no-cache curl
COPY --from=builder /app/dist /usr/share/nginx/html/
COPY frontend/dist/ /usr/share/nginx/html/
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -189,14 +189,14 @@ body.dark .trading-records ::v-deep .ant-pagination .ant-pagination-next:hover .
.user-manage-page .address-text[data-v-49c4f239],.user-manage-page .hash-text[data-v-49c4f239]{color:#1890ff}
.user-manage-page.theme-dark .manage-tabs[data-v-49c4f239] .ant-tabs-tab-active{color:#1890ff}
.user-manage-page .current-credits-info .value[data-v-49c4f239],.user-manage-page .current-vip-info .value[data-v-49c4f239]{color:#1890ff}
.profile-page .page-header .page-title .anticon[data-v-2b760d50]{color:#1890ff}
.profile-page .profile-card .profile-info .info-item .anticon[data-v-2b760d50]{color:#1890ff}
.profile-page.theme-dark .edit-card[data-v-2b760d50] .ant-tabs-tab-active{color:#1890ff}
.profile-page.theme-dark .edit-card[data-v-2b760d50] .ant-input-password:focus,.profile-page.theme-dark .edit-card[data-v-2b760d50] .ant-input-password:hover,.profile-page.theme-dark .edit-card[data-v-2b760d50] .ant-input:focus,.profile-page.theme-dark .edit-card[data-v-2b760d50] .ant-input:hover{border-color:#1890ff}
.profile-page.theme-dark .notification-settings-form[data-v-2b760d50] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .notification-settings-form[data-v-2b760d50] .ant-radio-checked .ant-radio-inner,.profile-page.theme-dark .password-form[data-v-2b760d50] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .password-form[data-v-2b760d50] .ant-radio-checked .ant-radio-inner,.profile-page.theme-dark .profile-form[data-v-2b760d50] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .profile-form[data-v-2b760d50] .ant-radio-checked .ant-radio-inner{background-color:#1890ff;border-color:#1890ff}
.profile-page.theme-dark[data-v-2b760d50] .ant-pagination .ant-pagination-item:hover{border-color:#1890ff}
.profile-page.theme-dark[data-v-2b760d50] .ant-pagination .ant-pagination-item-active{background:#1890ff;border-color:#1890ff}
.profile-page.theme-dark[data-v-2b760d50] .ant-btn.ant-btn-default:hover{border-color:#1890ff;color:#1890ff}
.profile-page .page-header .page-title .anticon[data-v-86a53e9c]{color:#1890ff}
.profile-page .profile-card .profile-info .info-item .anticon[data-v-86a53e9c]{color:#1890ff}
.profile-page.theme-dark .edit-card[data-v-86a53e9c] .ant-tabs-tab-active{color:#1890ff}
.profile-page.theme-dark .edit-card[data-v-86a53e9c] .ant-input-password:focus,.profile-page.theme-dark .edit-card[data-v-86a53e9c] .ant-input-password:hover,.profile-page.theme-dark .edit-card[data-v-86a53e9c] .ant-input:focus,.profile-page.theme-dark .edit-card[data-v-86a53e9c] .ant-input:hover{border-color:#1890ff}
.profile-page.theme-dark .notification-settings-form[data-v-86a53e9c] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .notification-settings-form[data-v-86a53e9c] .ant-radio-checked .ant-radio-inner,.profile-page.theme-dark .password-form[data-v-86a53e9c] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .password-form[data-v-86a53e9c] .ant-radio-checked .ant-radio-inner,.profile-page.theme-dark .profile-form[data-v-86a53e9c] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .profile-form[data-v-86a53e9c] .ant-radio-checked .ant-radio-inner{background-color:#1890ff;border-color:#1890ff}
.profile-page.theme-dark[data-v-86a53e9c] .ant-pagination .ant-pagination-item:hover{border-color:#1890ff}
.profile-page.theme-dark[data-v-86a53e9c] .ant-pagination .ant-pagination-item-active{background:#1890ff;border-color:#1890ff}
.profile-page.theme-dark[data-v-86a53e9c] .ant-btn.ant-btn-default:hover{border-color:#1890ff;color:#1890ff}
.billing-page .plan-card.highlight[data-v-0cdd5f9c]{border:1px solid rgba(24,144,255,.35)}
.usdt-checkout .checkout-body .info-section .addr-box .copy-btn:hover,.usdt-checkout .checkout-body .info-section .amt-box .copy-btn:hover{background:rgba(24,144,255,.08);color:#1890ff}
.theme-dark .usdt-checkout .checkout-body .info-section .addr-box .copy-btn:hover,.theme-dark .usdt-checkout .checkout-body .info-section .amt-box .copy-btn:hover,body.realdark .usdt-checkout .checkout-body .info-section .addr-box .copy-btn:hover,body.realdark .usdt-checkout .checkout-body .info-section .amt-box .copy-btn:hover{background:rgba(24,144,255,.15);color:#40a9ff}
+1 -1
View File
@@ -421,4 +421,4 @@
.brand-text {
font-size: 20px;
}
}</style><script defer="defer" src="/js/chunk-vendors.e3ae0d1f.js" type="module"></script><script defer="defer" src="/js/app.de48b29a.js" type="module"></script><link href="/css/chunk-vendors.b8cb9e53.css" rel="stylesheet"><link href="/css/app.ee3dda40.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.beabf087.js" nomodule></script><script defer="defer" src="/js/app-legacy.a9e4bd9c.js" nomodule></script></head><body><noscript><strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><div class="first-loading-wrp"><h2>Landing</h2><div class="loading-wrp"><div class="pixel-cat-container"><div class="ground"></div><div class="pixel-cat"><div class="cat-head"><div class="cat-ear-left"></div><div class="cat-ear-right"></div><div class="cat-eye-left"></div><div class="cat-eye-right"></div><div class="cat-nose"></div><div class="cat-whiskers"></div></div><div class="cat-body"></div><div class="cat-leg-front-left"></div><div class="cat-leg-front-right"></div><div class="cat-leg-back-left"></div><div class="cat-leg-back-right"></div><div class="cat-tail"></div></div></div></div><div class="brand-text">QuantDinger</div></div></div></body></html>
}</style><script defer="defer" src="/js/chunk-vendors.47fd2294.js" type="module"></script><script defer="defer" src="/js/app.4799d28b.js" type="module"></script><link href="/css/chunk-vendors.b8cb9e53.css" rel="stylesheet"><link href="/css/app.ee3dda40.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.74c62065.js" nomodule></script><script defer="defer" src="/js/app-legacy.73c5f6b0.js" nomodule></script></head><body><noscript><strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><div class="first-loading-wrp"><h2>Landing</h2><div class="loading-wrp"><div class="pixel-cat-container"><div class="ground"></div><div class="pixel-cat"><div class="cat-head"><div class="cat-ear-left"></div><div class="cat-ear-right"></div><div class="cat-eye-left"></div><div class="cat-eye-right"></div><div class="cat-nose"></div><div class="cat-whiskers"></div></div><div class="cat-body"></div><div class="cat-leg-front-left"></div><div class="cat-leg-front-right"></div><div class="cat-leg-back-left"></div><div class="cat-leg-back-right"></div><div class="cat-tail"></div></div></div></div><div class="brand-text">QuantDinger</div></div></div></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
"use strict";(self["webpackChunkvue_antd_pro"]=self["webpackChunkvue_antd_pro"]||[]).push([[24],{20024:function(t,n,r){r.r(n),r.d(n,{changePassword:function(){return p},getGitHubOAuthUrl:function(){return g},getGoogleOAuthUrl:function(){return l},getSecurityConfig:function(){return o},getUserInfo:function(){return c},login:function(){return i},loginWithCode:function(){return s},logout:function(){return a},register:function(){return f},resetPassword:function(){return d},sendVerificationCode:function(){return h}});r(34782),r(27495),r(99449),r(25440),r(11392),r(42762);var u=r(76471);function e(t){var n="/api".trim(),r=t.startsWith("/")?t:"/".concat(t);if(!n)return r;var u=n.replace(/\/+$/,"");return u.endsWith("/api")&&r.startsWith("/api/")?u+r.slice(4):u+r}function o(){return(0,u.Ay)({url:"/api/auth/security-config",method:"get"})}function i(t){return(0,u.Ay)({url:"/api/auth/login",method:"post",data:t})}function a(){return(0,u.Ay)({url:"/api/auth/logout",method:"post"})}function c(){return(0,u.Ay)({url:"/api/auth/info",method:"get"})}function h(t){return(0,u.Ay)({url:"/api/auth/send-code",method:"post",data:t})}function s(t){return(0,u.Ay)({url:"/api/auth/login-code",method:"post",data:t})}function f(t){return(0,u.Ay)({url:"/api/auth/register",method:"post",data:t})}function d(t){return(0,u.Ay)({url:"/api/auth/reset-password",method:"post",data:t})}function p(t){return(0,u.Ay)({url:"/api/auth/change-password",method:"post",data:t})}function l(){return e("/api/auth/oauth/google")}function g(){return e("/api/auth/oauth/github")}}}]);
-1
View File
@@ -1 +0,0 @@
"use strict";(self["webpackChunkvue_antd_pro"]=self["webpackChunkvue_antd_pro"]||[]).push([[24],{20024:function(t,n,r){r.r(n),r.d(n,{changePassword:function(){return p},getGitHubOAuthUrl:function(){return g},getGoogleOAuthUrl:function(){return l},getSecurityConfig:function(){return o},getUserInfo:function(){return c},login:function(){return i},loginWithCode:function(){return s},logout:function(){return a},register:function(){return f},resetPassword:function(){return d},sendVerificationCode:function(){return h}});var u=r(76471);function e(t){var n="/api".trim(),r=t.startsWith("/")?t:"/".concat(t);if(!n)return r;var u=n.replace(/\/+$/,"");return u.endsWith("/api")&&r.startsWith("/api/")?u+r.slice(4):u+r}function o(){return(0,u.Ay)({url:"/api/auth/security-config",method:"get"})}function i(t){return(0,u.Ay)({url:"/api/auth/login",method:"post",data:t})}function a(){return(0,u.Ay)({url:"/api/auth/logout",method:"post"})}function c(){return(0,u.Ay)({url:"/api/auth/info",method:"get"})}function h(t){return(0,u.Ay)({url:"/api/auth/send-code",method:"post",data:t})}function s(t){return(0,u.Ay)({url:"/api/auth/login-code",method:"post",data:t})}function f(t){return(0,u.Ay)({url:"/api/auth/register",method:"post",data:t})}function d(t){return(0,u.Ay)({url:"/api/auth/reset-password",method:"post",data:t})}function p(t){return(0,u.Ay)({url:"/api/auth/change-password",method:"post",data:t})}function l(){return e("/api/auth/oauth/google")}function g(){return e("/api/auth/oauth/github")}}}]);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
"use strict";(self["webpackChunkvue_antd_pro"]=self["webpackChunkvue_antd_pro"]||[]).push([[824],{95824:function(t,n,r){r.r(n),r.d(n,{changePassword:function(){return p},getGitHubOAuthUrl:function(){return g},getGoogleOAuthUrl:function(){return l},getSecurityConfig:function(){return o},getUserInfo:function(){return c},login:function(){return i},loginWithCode:function(){return s},logout:function(){return a},register:function(){return f},resetPassword:function(){return d},sendVerificationCode:function(){return h}});r(34782),r(27495),r(99449),r(25440),r(11392),r(42762);var u=r(75769);function e(t){var n="/api".trim(),r=t.startsWith("/")?t:"/".concat(t);if(!n)return r;var u=n.replace(/\/+$/,"");return u.endsWith("/api")&&r.startsWith("/api/")?u+r.slice(4):u+r}function o(){return(0,u.Ay)({url:"/api/auth/security-config",method:"get"})}function i(t){return(0,u.Ay)({url:"/api/auth/login",method:"post",data:t})}function a(){return(0,u.Ay)({url:"/api/auth/logout",method:"post"})}function c(){return(0,u.Ay)({url:"/api/auth/info",method:"get"})}function h(t){return(0,u.Ay)({url:"/api/auth/send-code",method:"post",data:t})}function s(t){return(0,u.Ay)({url:"/api/auth/login-code",method:"post",data:t})}function f(t){return(0,u.Ay)({url:"/api/auth/register",method:"post",data:t})}function d(t){return(0,u.Ay)({url:"/api/auth/reset-password",method:"post",data:t})}function p(t){return(0,u.Ay)({url:"/api/auth/change-password",method:"post",data:t})}function l(){return e("/api/auth/oauth/google")}function g(){return e("/api/auth/oauth/github")}}}]);
+1
View File
@@ -0,0 +1 @@
"use strict";(self["webpackChunkvue_antd_pro"]=self["webpackChunkvue_antd_pro"]||[]).push([[824],{95824:function(t,n,r){r.r(n),r.d(n,{changePassword:function(){return p},getGitHubOAuthUrl:function(){return g},getGoogleOAuthUrl:function(){return l},getSecurityConfig:function(){return o},getUserInfo:function(){return c},login:function(){return i},loginWithCode:function(){return s},logout:function(){return a},register:function(){return f},resetPassword:function(){return d},sendVerificationCode:function(){return h}});var u=r(75769);function e(t){var n="/api".trim(),r=t.startsWith("/")?t:"/".concat(t);if(!n)return r;var u=n.replace(/\/+$/,"");return u.endsWith("/api")&&r.startsWith("/api/")?u+r.slice(4):u+r}function o(){return(0,u.Ay)({url:"/api/auth/security-config",method:"get"})}function i(t){return(0,u.Ay)({url:"/api/auth/login",method:"post",data:t})}function a(){return(0,u.Ay)({url:"/api/auth/logout",method:"post"})}function c(){return(0,u.Ay)({url:"/api/auth/info",method:"get"})}function h(t){return(0,u.Ay)({url:"/api/auth/send-code",method:"post",data:t})}function s(t){return(0,u.Ay)({url:"/api/auth/login-code",method:"post",data:t})}function f(t){return(0,u.Ay)({url:"/api/auth/register",method:"post",data:t})}function d(t){return(0,u.Ay)({url:"/api/auth/reset-password",method:"post",data:t})}function p(t){return(0,u.Ay)({url:"/api/auth/change-password",method:"post",data:t})}function l(){return e("/api/auth/oauth/google")}function g(){return e("/api/auth/oauth/github")}}}]);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

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