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:
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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)"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
"""应用配置类"""
|
||||
|
||||
|
||||
@@ -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 属性获取 ID(execute 内部已经处理了 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):
|
||||
|
||||
@@ -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. Tavily(AI优化搜索)
|
||||
# 1. Tavily(AI优化搜索)
|
||||
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,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
# QuantDinger Docker Entrypoint Script
|
||||
# Checks and validates SECRET_KEY before starting the application
|
||||
|
||||
|
||||
+87
-245
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user