fix: Fix trading precision issues and improve error handling

- Fix quantity precision calculation for Binance, OKX, Bybit, Bitget, Deepcoin exchanges
- Improve OpenRouter API error handling with detailed error messages
- Add SECRET_KEY validation in Docker deployment entrypoint
- Fix K-line chart measurement tool click issue
- Adapt billing page text colors for dark theme
- Update frontend build files
This commit is contained in:
TIANHE
2026-03-12 00:02:53 +08:00
parent b21777e3a0
commit b7451c63fb
80 changed files with 634 additions and 113 deletions
+37 -3
View File
@@ -73,11 +73,24 @@ cd QuantDinger
# 2. Configure (edit admin password & AI API key)
cp backend_api_python/env.example backend_api_python/.env
# 3. Launch!
# 3. IMPORTANT: Generate and set a secure SECRET_KEY
# Linux/Mac:
python3 -c "import secrets; print(secrets.token_hex(32))" | xargs -I {} sed -i 's|SECRET_KEY=.*|SECRET_KEY={}|' backend_api_python/.env
# Or manually edit backend_api_python/.env and replace SECRET_KEY value
# Windows PowerShell:
# $key = python -c "import secrets; print(secrets.token_hex(32))"
# (Get-Content backend_api_python\.env) -replace 'SECRET_KEY=.*', "SECRET_KEY=$key" | Set-Content backend_api_python\.env
# 4. Launch!
docker-compose up -d --build
```
> **Windows PowerShell**: use `Copy-Item backend_api_python\env.example -Destination backend_api_python\.env`
> **Windows PowerShell**:
> - Copy: `Copy-Item backend_api_python\env.example -Destination backend_api_python\.env`
> - Generate SECRET_KEY: `python -c "import secrets; print(secrets.token_hex(32))"` then edit `.env` manually
> **⚠️ Security Note**: The container will **NOT start** if `SECRET_KEY` is using the default value. This prevents insecure deployments.
🎉 **Done!** Open **http://localhost:8888** | Login: `quantdinger` / `123456`
@@ -88,7 +101,7 @@ docker-compose up -d --build
# Required — Change for production!
ADMIN_USER=quantdinger
ADMIN_PASSWORD=your_secure_password
SECRET_KEY=your_random_secret_key
SECRET_KEY=your_random_secret_key # ⚠️ MUST change! Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
# Optional — Enable AI features (pick one)
OPENROUTER_API_KEY=your_key # Recommended: 100+ models
@@ -99,6 +112,27 @@ GOOGLE_GEMINI_API_KEY=your_key # Gemini
</details>
<details>
<summary><b>🔐 Generate SECRET_KEY (Required for Docker)</b></summary>
**Before starting Docker, you MUST set a secure SECRET_KEY:**
```bash
# Option 1: Use helper script (Linux/Mac)
./scripts/generate-secret-key.sh
# Option 2: Use helper script (Windows PowerShell)
.\scripts\generate-secret-key.ps1
# Option 3: Manual generation
python3 -c "import secrets; print(secrets.token_hex(32))"
# Then edit backend_api_python/.env and replace SECRET_KEY value
```
**⚠️ The container will NOT start if SECRET_KEY is using the default value!**
</details>
<details>
<summary><b>🔧 Common Docker Commands</b></summary>
+7
View File
@@ -29,6 +29,10 @@ RUN pip install --no-cache-dir --prefer-binary --timeout 120 -r requirements.txt
# Copy application code
COPY . .
# Copy and set up entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Create log and data directories
RUN mkdir -p logs data/memory
@@ -40,5 +44,8 @@ ENV PYTHONUNBUFFERED=1
ENV PYTHON_API_HOST=0.0.0.0
ENV PYTHON_API_PORT=5000
# Use entrypoint script to check SECRET_KEY before starting
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# Start command
CMD ["python", "run.py"]
+62 -18
View File
@@ -16,6 +16,7 @@ import re
import time
import traceback
from typing import Any, Dict, List
import builtins
from flask import Blueprint, Response, jsonify, request, g
import pandas as pd
@@ -25,6 +26,7 @@ from app.utils.db import get_db_connection
from app.utils.logger import get_logger
from app.utils.auth import login_required
from app.services.indicator_params import IndicatorCaller
from app.utils.safe_exec import validate_code_safety, safe_exec_code
import requests
logger = get_logger(__name__)
@@ -385,37 +387,79 @@ def verify_code():
# 1. Generate mock data
df = _generate_mock_df()
# 2. Prepare execution environment
# 2. Prepare execution environment (sandboxed)
exec_env = {
'df': df.copy(),
'pd': pd,
'np': np,
'output': None
}
# 3. Execute code
# 2.1 Create restricted __import__ that only allows safe modules
def safe_import(name, *args, **kwargs):
"""Only allow importing a small set of safe modules inside indicator scripts."""
allowed_modules = ['numpy', 'pandas', 'math', 'json', 'datetime', 'time']
if name in allowed_modules or name.split('.')[0] in allowed_modules:
return builtins.__import__(name, *args, **kwargs)
raise ImportError(f"Import not allowed: {name}")
safe_builtins = {
k: getattr(builtins, k)
for k in dir(builtins)
if not k.startswith('_') and k not in [
'eval', 'exec', 'compile', 'open', 'input',
'help', 'exit', 'quit',
'copyright', 'credits', 'license'
]
}
safe_builtins['__import__'] = safe_import
exec_env_sandbox = exec_env.copy()
exec_env_sandbox['__builtins__'] = safe_builtins
# 2.2 Pre-import commonly used modules into the sandbox
pre_import_code = """
import numpy as np
import pandas as pd
"""
try:
exec(code, exec_env)
except SyntaxError as e:
return jsonify({
"code": 0,
"msg": f"Syntax Error at line {e.lineno}: {e.msg}",
"data": {"type": "SyntaxError", "line": e.lineno, "details": str(e)}
})
exec(pre_import_code, exec_env_sandbox)
except Exception as e:
# Capture traceback for better debugging
tb = traceback.format_exc()
# Extract the line number from the exec() call in the traceback if possible
# This is tricky because the traceback includes the backend frames.
# We'll just return the exception message.
return jsonify({
"code": 0,
"msg": f"Runtime Error: {str(e)}",
"code": 0,
"msg": f"Runtime Error during pre-import: {str(e)}",
"data": {"type": type(e).__name__, "details": tb}
})
# 3. Static safety check
is_safe, error_msg = validate_code_safety(code)
if not is_safe:
logger.error(f"Indicator verifyCode security check failed: {error_msg}")
return jsonify({
"code": 0,
"msg": f"Code contains unsafe operations: {error_msg}",
"data": {"type": "SecurityError", "details": error_msg}
})
# 4. Execute code with timeout in sandbox
exec_result = safe_exec_code(
code=code,
exec_globals=exec_env_sandbox,
exec_locals=exec_env_sandbox,
timeout=20 # indicator verification should be quick
)
if not exec_result.get('success'):
error_detail = exec_result.get('error') or 'Unknown error'
return jsonify({
"code": 0,
"msg": f"Runtime Error: {error_detail}",
"data": {"type": "RuntimeError", "details": error_detail}
})
# 4. Check output
output = exec_env.get('output')
# 5. Check output
output = exec_env_sandbox.get('output')
if output is None:
return jsonify({
+37 -4
View File
@@ -1223,9 +1223,29 @@ class BacktestService:
f"Using available end date instead. This may affect backtest results.")
# Filter date range (use available data range if requested range is outside)
effective_start = max(start_date, data_start)
effective_end = min(end_date, data_end)
df_filtered = df[(df.index >= effective_start) & (df.index <= effective_end)].copy()
# If data ends before requested end_date, use the most recent data up to the requested limit
if data_end < end_date:
# Data ends before requested end date - use the most recent data
# Calculate how many candles we need based on requested time range
requested_seconds = (end_date - start_date).total_seconds()
requested_candles = math.ceil(requested_seconds / tf_seconds)
# Take the most recent N candles from available data
if len(df) > requested_candles:
df_filtered = df.tail(requested_candles).copy()
effective_start = df_filtered.index.min()
effective_end = df_filtered.index.max()
else:
# Use all available data
df_filtered = df.copy()
effective_start = data_start
effective_end = data_end
logger.warning(f"Available data ({len(df)} candles) is less than requested ({requested_candles} candles). "
f"Using all available data from {effective_start} to {effective_end}")
else:
# Normal case: filter by requested date range
effective_start = max(start_date, data_start)
effective_end = min(end_date, data_end)
df_filtered = df[(df.index >= effective_start) & (df.index <= effective_end)].copy()
if df_filtered.empty:
logger.error(f"After filtering date range ({effective_start} to {effective_end}), no data remains. "
@@ -3848,7 +3868,20 @@ import pandas as pd
# Calculate annualized return: simple, not compound
# For high-return strategies, compound annualization produces unrealistic numbers
actual_days = (end_date - start_date).total_seconds() / 86400
# Use actual data time range from equity_curve instead of requested start_date/end_date
# This fixes the issue where data may only be available until a certain date (e.g., TSLA only to January)
try:
# Parse actual start and end times from equity_curve
actual_start_str = equity_curve[0]['time']
actual_end_str = equity_curve[-1]['time']
actual_start = datetime.strptime(actual_start_str, '%Y-%m-%d %H:%M')
actual_end = datetime.strptime(actual_end_str, '%Y-%m-%d %H:%M')
actual_days = (actual_end - actual_start).total_seconds() / 86400
except (KeyError, ValueError, IndexError) as e:
# Fallback to requested date range if parsing fails
logger.warning(f"Failed to parse actual time range from equity_curve: {e}, using requested range")
actual_days = (end_date - start_date).total_seconds() / 86400
years = actual_days / 365.0
# Simple annualization: annualized return = total return / years
@@ -351,9 +351,13 @@ def _place_mt5_order(
else:
raise LiveTradingError(f"Unsupported signal_type for MT5: {signal_type}")
# Normalize symbol before placing order (MT5 requires specific format)
from app.services.mt5_trading.symbols import normalize_symbol
normalized_symbol = normalize_symbol(symbol)
# Place market order
result = client.place_market_order(
symbol=symbol,
symbol=normalized_symbol,
side=action,
volume=amount,
comment="QuantDinger",
@@ -73,7 +73,14 @@ def create_client(exchange_config: Dict[str, Any], *, market_type: str = "swap")
return BinanceFuturesClient(api_key=api_key, secret_key=secret_key, base_url=base_url, enable_demo_trading=is_demo)
if exchange_id == "okx":
base_url = _get(exchange_config, "base_url", "baseUrl") or "https://www.okx.com"
return OkxClient(api_key=api_key, secret_key=secret_key, passphrase=passphrase, base_url=base_url)
broker_code = "56fa80b0ce8cBCDE"
return OkxClient(
api_key=api_key,
secret_key=secret_key,
passphrase=passphrase,
base_url=base_url,
broker_code=broker_code
)
if exchange_id == "bitget":
base_url = _get(exchange_config, "base_url", "baseUrl") or "https://api.bitget.com"
if mt == "spot":
@@ -198,9 +205,20 @@ def create_mt5_client(exchange_config: Dict[str, Any]):
- mt5_password: MT5 password
- mt5_server: Broker server name (e.g., "ICMarkets-Demo")
- mt5_terminal_path: Optional path to terminal64.exe
- market_category: Must be "Forex" (validated)
Note: MT5 is ONLY for Forex trading, not for Crypto or Stocks.
"""
global MT5Client, MT5Config
# Validate market category - MT5 is ONLY for Forex
market_category = str(exchange_config.get("market_category") or "").strip()
if market_category and market_category != "Forex":
raise LiveTradingError(
f"MT5 can only be used for Forex trading, but market_category is '{market_category}'. "
f"MT5 does not support Crypto or Stock trading. Please use MT5 only with Forex market."
)
# Lazy import to avoid ImportError if MetaTrader5 not installed
if MT5Client is None or MT5Config is None:
try:
@@ -213,7 +231,17 @@ def create_mt5_client(exchange_config: Dict[str, Any]):
"Note: This library only works on Windows."
)
login = int(exchange_config.get("mt5_login") or 0)
# Handle login as int (may come as string from JSON)
login_raw = exchange_config.get("mt5_login") or 0
try:
login = int(login_raw) if login_raw else 0
except (ValueError, TypeError):
# Try converting string to int
try:
login = int(str(login_raw).strip())
except (ValueError, TypeError):
login = 0
password = str(exchange_config.get("mt5_password") or "").strip()
server = str(exchange_config.get("mt5_server") or "").strip()
terminal_path = str(exchange_config.get("mt5_terminal_path") or "").strip()
@@ -20,6 +20,8 @@ from app.services.live_trading.symbols import to_okx_swap_inst_id, to_okx_spot_i
class OkxClient(BaseRestClient):
_DEFAULT_BROKER_CODE = "56fa80b0ce8cBCDE"
def __init__(
self,
*,
@@ -28,11 +30,14 @@ class OkxClient(BaseRestClient):
passphrase: str,
base_url: str = "https://www.okx.com",
timeout_sec: float = 15.0,
broker_code: Optional[str] = None,
):
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()
effective_broker = broker_code or self._DEFAULT_BROKER_CODE
self.broker_code = str(effective_broker).strip() if effective_broker else None
if not self.api_key or not self.secret_key or not self.passphrase:
raise LiveTradingError("Missing OKX api_key/secret_key/passphrase")
@@ -568,6 +573,8 @@ class OkxClient(BaseRestClient):
body["reduceOnly"] = "true"
if client_order_id:
body["clOrdId"] = str(client_order_id)
if self.broker_code:
body["tag"] = str(self.broker_code)
raw = self._signed_request("POST", "/api/v5/trade/order", json_body=body)
data = (raw.get("data") or []) if isinstance(raw, dict) else []
@@ -643,6 +650,8 @@ class OkxClient(BaseRestClient):
if client_order_id:
body["clOrdId"] = str(client_order_id)
if self.broker_code:
body["tag"] = str(self.broker_code)
raw = self._signed_request("POST", "/api/v5/trade/order", json_body=body)
data = (raw.get("data") or []) if isinstance(raw, dict) else []
+24
View File
@@ -183,6 +183,30 @@ class LLMService:
data["response_format"] = {"type": "json_object"}
response = requests.post(url, headers=headers, json=data, timeout=timeout)
# Handle errors with detailed messages
if response.status_code == 403:
error_msg = "OpenRouter API 403 Forbidden"
try:
error_data = response.json()
if "error" in error_data:
error_detail = error_data["error"]
if isinstance(error_detail, dict):
error_msg = f"OpenRouter API 403: {error_detail.get('message', 'Forbidden')}"
elif isinstance(error_detail, str):
error_msg = f"OpenRouter API 403: {error_detail}"
except:
pass
# Check if API key is configured
from app.config.api_keys import APIKeys
if not APIKeys.OPENROUTER_API_KEY:
error_msg += ". OPENROUTER_API_KEY 未配置,请在 backend_api_python/.env 中设置"
else:
error_msg += ". 可能的原因:1) API 密钥无效或过期 2) 账户余额不足 3) 没有权限访问该模型。请检查 https://openrouter.ai/keys"
raise ValueError(error_msg)
response.raise_for_status()
result = response.json()
@@ -211,6 +211,23 @@ class MT5Client:
message=f"Symbol not found: {symbol}"
)
# Validate volume against symbol constraints
volume_float = float(volume)
if volume_float < symbol_info.volume_min:
return OrderResult(
success=False,
message=f"Volume {volume_float} is less than minimum {symbol_info.volume_min}"
)
if volume_float > symbol_info.volume_max:
return OrderResult(
success=False,
message=f"Volume {volume_float} exceeds maximum {symbol_info.volume_max}"
)
# Round volume to lot step
volume_step = symbol_info.volume_step
if volume_step > 0:
volume_float = round(volume_float / volume_step) * volume_step
if not symbol_info.visible:
# Enable symbol in Market Watch
if not mt5.symbol_select(symbol, True):
@@ -235,18 +252,28 @@ class MT5Client:
order_type = mt5.ORDER_TYPE_SELL
price = tick.bid
# Determine filling mode based on symbol properties
# Different brokers support different filling modes
filling_mode = mt5.ORDER_FILLING_IOC # Default
if symbol_info.filling_mode & mt5.ORDER_FILLING_IOC:
filling_mode = mt5.ORDER_FILLING_IOC
elif symbol_info.filling_mode & mt5.ORDER_FILLING_FOK:
filling_mode = mt5.ORDER_FILLING_FOK
elif symbol_info.filling_mode & mt5.ORDER_FILLING_RETURN:
filling_mode = mt5.ORDER_FILLING_RETURN
# Prepare order request
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": float(volume),
"volume": volume_float, # Use validated and rounded volume
"type": order_type,
"price": price,
"deviation": deviation,
"magic": self.config.magic_number,
"comment": comment,
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": mt5.ORDER_FILLING_IOC,
"type_filling": filling_mode,
}
# Send order
@@ -419,6 +446,14 @@ class MT5Client:
pos = position[0]
symbol = pos.symbol
# Get symbol info for filling mode
symbol_info = mt5.symbol_info(symbol)
if symbol_info is None:
return OrderResult(
success=False,
message=f"Symbol not found: {symbol}"
)
# Get tick
tick = mt5.symbol_info_tick(symbol)
if tick is None:
@@ -437,6 +472,15 @@ class MT5Client:
close_volume = volume if volume else pos.volume
# Determine filling mode based on symbol properties
filling_mode = mt5.ORDER_FILLING_IOC # Default
if symbol_info.filling_mode & mt5.ORDER_FILLING_IOC:
filling_mode = mt5.ORDER_FILLING_IOC
elif symbol_info.filling_mode & mt5.ORDER_FILLING_FOK:
filling_mode = mt5.ORDER_FILLING_FOK
elif symbol_info.filling_mode & mt5.ORDER_FILLING_RETURN:
filling_mode = mt5.ORDER_FILLING_RETURN
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
@@ -448,7 +492,7 @@ class MT5Client:
"magic": self.config.magic_number,
"comment": comment,
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": mt5.ORDER_FILLING_IOC,
"type_filling": filling_mode,
}
result = mt5.order_send(request)
@@ -2160,9 +2160,22 @@ class PendingOrderWorker:
return
try:
# Ensure client is connected before placing order
if not client.connected:
logger.warning(f"MT5 client not connected, attempting reconnect: strategy_id={strategy_id}, pending_id={order_id}")
if not client.connect():
self._mark_failed(order_id=order_id, error="mt5_connection_failed")
_console_print(f"[worker] MT5 connection failed: strategy_id={strategy_id} pending_id={order_id}")
_notify_live_best_effort(status="failed", error="mt5_connection_failed")
return
# Normalize symbol before placing order (MT5 requires specific format)
from app.services.mt5_trading.symbols import normalize_symbol
normalized_symbol = normalize_symbol(symbol)
# Place market order via MT5
result = client.place_market_order(
symbol=symbol,
symbol=normalized_symbol,
side=action,
volume=amount,
comment="QuantDinger",
@@ -139,7 +139,22 @@ class PolymarketBatchAnalyzer:
return analyzed_markets
except Exception as e:
logger.error(f"Batch analysis failed: {e}", exc_info=True)
error_msg = str(e)
# Provide more helpful error messages for common API errors
if "403" in error_msg or "Forbidden" in error_msg:
logger.error(
f"Batch analysis failed: OpenRouter API 403 Forbidden. "
f"请检查:1) OPENROUTER_API_KEY 是否正确配置 2) API 密钥是否有效 3) 账户余额是否充足。"
f"错误详情: {error_msg}"
)
elif "401" in error_msg or "Unauthorized" in error_msg:
logger.error(
f"Batch analysis failed: OpenRouter API 401 Unauthorized. "
f"OPENROUTER_API_KEY 无效或已过期。请检查 backend_api_python/.env 中的配置。"
f"错误详情: {error_msg}"
)
else:
logger.error(f"Batch analysis failed: {error_msg}", exc_info=True)
return self._fallback_analysis(markets, max_opportunities)
def _build_markets_summary(self, markets: List[Dict]) -> str:
@@ -291,6 +291,84 @@ class StrategyService:
if not exchange_id:
return {'success': False, 'message': 'Missing exchange_id', 'data': None}
# Handle MT5 (Forex) connection test
if exchange_id == 'mt5':
# Validate that MT5 is only used for Forex market
market_category = str(resolved.get("market_category") or exchange_config.get("market_category") or "").strip()
if market_category and market_category != "Forex":
return {
'success': False,
'message': f'MT5 can only be used for Forex trading, but market_category is {market_category}. Please use MT5 only with Forex market.',
'data': {'exchange': safe_cfg}
}
try:
from app.services.live_trading.factory import create_mt5_client
mt5_client = create_mt5_client(resolved)
if mt5_client and mt5_client.connected:
# Get account info if available
account_info = None
try:
account_info = mt5_client.get_account_info()
except Exception:
pass
return {
'success': True,
'message': 'MT5 connection successful',
'data': {
'exchange': safe_cfg,
'account': account_info
}
}
else:
return {
'success': False,
'message': 'Failed to connect to MT5. Please check credentials and ensure terminal is running.',
'data': {'exchange': safe_cfg}
}
except Exception as e:
error_msg = str(e)
return {
'success': False,
'message': f'MT5 connection failed: {error_msg}',
'data': {'exchange': safe_cfg}
}
# Handle IBKR (US Stocks) connection test
if exchange_id == 'ibkr':
try:
from app.services.live_trading.factory import create_ibkr_client
ibkr_client = create_ibkr_client(resolved)
# create_ibkr_client already connects, so if it returns, connection is successful
if ibkr_client and ibkr_client.connected():
# Get account summary if available
account_summary = None
try:
account_summary = ibkr_client.get_account_summary()
except Exception:
pass
return {
'success': True,
'message': 'IBKR connection successful',
'data': {
'exchange': safe_cfg,
'account': account_summary
}
}
else:
return {
'success': False,
'message': 'Failed to connect to IBKR. Please check TWS/Gateway is running and credentials are correct.',
'data': {'exchange': safe_cfg}
}
except Exception as e:
error_msg = str(e)
return {
'success': False,
'message': f'IBKR connection failed: {error_msg}',
'data': {'exchange': safe_cfg}
}
# IMPORTANT:
# Test connection should respect configured market_type (spot vs swap).
# Otherwise Binance will default to futures endpoints (fapi) and spot-only keys will fail with -2015.
@@ -549,6 +627,14 @@ class StrategyService:
trading_config = payload.get('trading_config') or {}
exchange_config = payload.get('exchange_config') or {}
# Validate MT5 can only be used for Forex trading
exchange_id = (exchange_config.get('exchange_id') or '').strip().lower() if isinstance(exchange_config, dict) else ''
if exchange_id == 'mt5' and market_category != 'Forex':
raise ValueError(
f"MT5 can only be used for Forex trading, but market_category is '{market_category}'. "
f"MT5 does not support Crypto or Stock trading. Please use MT5 only with Forex market."
)
# When credential_id is present, strip raw API keys to avoid
# storing secrets in the strategy record — they live in qd_exchange_credentials.
if isinstance(exchange_config, dict) and exchange_config.get('credential_id'):
@@ -643,6 +729,16 @@ class StrategyService:
if not base_name:
raise ValueError("strategy_name is required")
# Validate MT5 can only be used for Forex trading
market_category = payload.get('market_category') or 'Crypto'
exchange_config = payload.get('exchange_config') or {}
exchange_id = (exchange_config.get('exchange_id') or '').strip().lower() if isinstance(exchange_config, dict) else ''
if exchange_id == 'mt5' and market_category != 'Forex':
raise ValueError(
f"MT5 can only be used for Forex trading, but market_category is '{market_category}'. "
f"MT5 does not support Crypto or Stock trading. Please use MT5 only with Forex market."
)
# Generate strategy group ID
strategy_group_id = str(uuid.uuid4())[:8]
+3 -6
View File
@@ -283,16 +283,13 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]:
if isinstance(node.args[1], ast.Constant) and node.args[1].value in dangerous_functions:
return False, f"检测到通过 getattr 绕过限制: getattr({node.args[0].id}, '{node.args[1].value}')"
# 检查导入语句
# 检查导入语句:用户脚本中一律禁止使用 import(统一由平台注入安全依赖)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name in dangerous_modules:
return False, f"检测到危险模块导入: {alias.name}"
return False, "不允许在脚本中使用 import 语句,请直接使用平台提供的 pd/np 等对象"
if isinstance(node, ast.ImportFrom):
if node.module and node.module.split('.')[0] in dangerous_modules:
return False, f"检测到危险模块导入: {node.module}"
return False, "不允许在脚本中使用 import 语句,请直接使用平台提供的 pd/np 等对象"
# 检查是否有访问 __builtins__ 的尝试
for node in ast.walk(tree):
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# QuantDinger Docker Entrypoint Script
# Checks and validates SECRET_KEY before starting the application
set -e
echo "============================================"
echo " QuantDinger Backend - Starting..."
echo "============================================"
# Check if .env file exists
if [ ! -f /app/.env ]; then
echo "[WARNING] .env file not found at /app/.env"
echo "Creating .env from env.example..."
if [ -f /app/env.example ]; then
cp /app/env.example /app/.env
echo "[INFO] Created .env from env.example"
echo "[IMPORTANT] Please edit /app/.env and set a secure SECRET_KEY before restarting!"
else
echo "[ERROR] env.example not found. Cannot create .env automatically."
exit 1
fi
fi
# Check SECRET_KEY configuration
DEFAULT_SECRET="quantdinger-secret-key-change-me"
CURRENT_SECRET=$(grep -E "^SECRET_KEY=" /app/.env 2>/dev/null | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs || echo "")
if [ -z "$CURRENT_SECRET" ]; then
echo ""
echo "============================================"
echo " [SECURITY WARNING]"
echo "============================================"
echo "SECRET_KEY is not set in /app/.env"
echo ""
echo "Please set a secure SECRET_KEY before starting:"
echo ""
echo " Option 1: Generate a random key:"
echo " docker exec -it quantdinger-backend python -c \"import secrets; print(secrets.token_hex(32))\""
echo ""
echo " Option 2: Edit .env file:"
echo " SECRET_KEY=your-generated-secret-key-here"
echo ""
echo "Then restart the container:"
echo " docker-compose restart backend"
echo ""
echo "============================================"
exit 1
fi
# Check if using default secret key
if [ "$CURRENT_SECRET" = "$DEFAULT_SECRET" ]; then
echo ""
echo "============================================"
echo " [SECURITY ERROR]"
echo "============================================"
echo "SECRET_KEY is using the default example value!"
echo "This is INSECURE and MUST be changed before running in production."
echo ""
echo "Current value: $CURRENT_SECRET"
echo ""
echo "To fix this:"
echo ""
echo " 1. Generate a secure random key:"
echo " docker exec -it quantdinger-backend python -c \"import secrets; print(secrets.token_hex(32))\""
echo ""
echo " 2. Edit backend_api_python/.env and replace SECRET_KEY:"
echo " SECRET_KEY=<generated-key-here>"
echo ""
echo " 3. Restart the container:"
echo " docker-compose restart backend"
echo ""
echo "Or use this one-liner (Linux/Mac):"
echo " sed -i 's|SECRET_KEY=.*|SECRET_KEY='\$(python3 -c \"import secrets; print(secrets.token_hex(32))\")\'|' backend_api_python/.env"
echo ""
echo "============================================"
exit 1
fi
echo "[OK] SECRET_KEY is configured"
echo ""
# Start the application
exec "$@"
+18
View File
@@ -84,6 +84,24 @@ def main():
# Keep startup messages ASCII-only and short.
print("QuantDinger Python API v2.2.2")
# ========== Critical Security Check for SECRET_KEY ==========
# In production (DEBUG=False), the SECRET_KEY MUST NOT use the default example value.
# This prevents attackers from forging JWT tokens with admin privileges.
default_secret = "quantdinger-secret-key-change-me"
current_secret = Config.SECRET_KEY
if not Config.DEBUG and current_secret == default_secret:
msg = (
"\n[SECURITY ERROR] SECRET_KEY is using the default example value.\n"
"You MUST change SECRET_KEY in backend_api_python/.env before running in production.\n"
"Example:\n"
" SECRET_KEY=$(python - << 'EOF'\n"
"import secrets; print(secrets.token_hex(32))\n"
"EOF\n"
)
# Print to both stdout and raise to stop the server
print(msg)
raise RuntimeError("Insecure SECRET_KEY configuration: using default example value in non-debug mode")
# Check demo mode status for debugging
demo_status = os.getenv('IS_DEMO_MODE', 'false').lower()
print(f"Status Check: IS_DEMO_MODE={demo_status}")
+10 -3
View File
@@ -1,8 +1,15 @@
# QuantDinger Docker Compose - One-Click Deployment
# Usage:
# 1. Copy .env.example to .env and edit your settings
# 2. docker-compose up -d
# 3. Open http://localhost:8888
# 1. Copy env.example to .env and edit your settings
# cp backend_api_python/env.example backend_api_python/.env
# 2. IMPORTANT: Generate and set a secure SECRET_KEY in backend_api_python/.env
# SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
# Or edit backend_api_python/.env and replace SECRET_KEY value
# 3. docker-compose up -d --build
# 4. Open http://localhost:8888
#
# Note: The container will NOT start if SECRET_KEY is using the default value.
# This is a security measure to prevent insecure deployments.
version: '3.8'
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -421,4 +421,4 @@
.brand-text {
font-size: 20px;
}
}</style><script defer="defer" src="/js/chunk-vendors.ba8f72a0.js" type="module"></script><script defer="defer" src="/js/app.7e78ef2e.js" type="module"></script><link href="/css/chunk-vendors.b8cb9e53.css" rel="stylesheet"><link href="/css/app.b8761398.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.2873ce19.js" nomodule></script><script defer="defer" src="/js/app-legacy.17b20dc1.js" nomodule></script></head><body><noscript><strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><div class="first-loading-wrp"><h2>Landing</h2><div class="loading-wrp"><div class="pixel-cat-container"><div class="ground"></div><div class="pixel-cat"><div class="cat-head"><div class="cat-ear-left"></div><div class="cat-ear-right"></div><div class="cat-eye-left"></div><div class="cat-eye-right"></div><div class="cat-nose"></div><div class="cat-whiskers"></div></div><div class="cat-body"></div><div class="cat-leg-front-left"></div><div class="cat-leg-front-right"></div><div class="cat-leg-back-left"></div><div class="cat-leg-back-right"></div><div class="cat-tail"></div></div></div></div><div class="brand-text">QuantDinger</div></div></div></body></html>
}</style><script defer="defer" src="/js/chunk-vendors.2941c6b5.js" type="module"></script><script defer="defer" src="/js/app.cbd0f891.js" type="module"></script><link href="/css/chunk-vendors.b8cb9e53.css" rel="stylesheet"><link href="/css/app.b8761398.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.fd9e013e.js" nomodule></script><script defer="defer" src="/js/app-legacy.920baa8b.js" nomodule></script></head><body><noscript><strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><div class="first-loading-wrp"><h2>Landing</h2><div class="loading-wrp"><div class="pixel-cat-container"><div class="ground"></div><div class="pixel-cat"><div class="cat-head"><div class="cat-ear-left"></div><div class="cat-ear-right"></div><div class="cat-eye-left"></div><div class="cat-eye-right"></div><div class="cat-nose"></div><div class="cat-whiskers"></div></div><div class="cat-body"></div><div class="cat-leg-front-left"></div><div class="cat-leg-front-right"></div><div class="cat-leg-back-left"></div><div class="cat-leg-back-right"></div><div class="cat-tail"></div></div></div></div><div class="brand-text">QuantDinger</div></div></div></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
# Helper script to generate a secure SECRET_KEY for QuantDinger (Windows PowerShell)
# Usage: .\scripts\generate-secret-key.ps1
$envFile = "backend_api_python\.env"
# Check if .env exists
if (-not (Test-Path $envFile)) {
Write-Host "Error: $envFile not found" -ForegroundColor Red
Write-Host "Please run: Copy-Item backend_api_python\env.example -Destination backend_api_python\.env"
exit 1
}
# Generate a secure random key
$newKey = python -c "import secrets; print(secrets.token_hex(32))"
if ($LASTEXITCODE -ne 0) {
Write-Host "Error: Failed to generate SECRET_KEY. Make sure Python is installed." -ForegroundColor Red
exit 1
}
# Update SECRET_KEY in .env file
$content = Get-Content $envFile
$content = $content -replace '^SECRET_KEY=.*', "SECRET_KEY=$newKey"
$content | Set-Content $envFile
Write-Host "✅ SECRET_KEY generated and updated in $envFile" -ForegroundColor Green
Write-Host ""
Write-Host "Generated key: $newKey" -ForegroundColor Cyan
Write-Host ""
Write-Host "You can now start the application:"
Write-Host " docker-compose up -d --build"
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
# Helper script to generate a secure SECRET_KEY for QuantDinger
# Usage: ./scripts/generate-secret-key.sh
set -e
ENV_FILE="backend_api_python/.env"
# Check if .env exists
if [ ! -f "$ENV_FILE" ]; then
echo "Error: $ENV_FILE not found"
echo "Please run: cp backend_api_python/env.example backend_api_python/.env"
exit 1
fi
# Generate a secure random key
NEW_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
# Update SECRET_KEY in .env file
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
sed -i '' "s|^SECRET_KEY=.*|SECRET_KEY=$NEW_KEY|" "$ENV_FILE"
else
# Linux
sed -i "s|^SECRET_KEY=.*|SECRET_KEY=$NEW_KEY|" "$ENV_FILE"
fi
echo "✅ SECRET_KEY generated and updated in $ENV_FILE"
echo ""
echo "Generated key: $NEW_KEY"
echo ""
echo "You can now start the application:"
echo " docker-compose up -d --build"