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
+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