feat(trading-assistant): refactor strategy creation and enhance script mode functionality

- Removed conditional rendering for the assistant guide bar.
- Simplified strategy overview and strategy list item components.
- Introduced a new modal for selecting strategy mode.
- Enhanced strategy creation modal to support script strategies with a dedicated editor.
- Updated form handling for script strategies, including validation and submission logic.
- Improved user experience with better messaging and streamlined UI components.
- Updated translations for better clarity in Chinese.
This commit is contained in:
dienakdz
2026-04-08 07:27:26 +07:00
parent 2dc29a215e
commit c3cf230104
89 changed files with 4653 additions and 1735 deletions
+2 -2
View File
@@ -19,14 +19,14 @@ ENV/
logs/
*.log
# Database ( docker-compose 中挂载)
# Database (mounted in docker-compose)
*.db
# Environment
.env
.env.local
# Data ( docker-compose 中挂载)
# Data (mounted in docker-compose)
data/memory/*.db
# Git
+2 -2
View File
@@ -30,10 +30,10 @@ wheels/
*.swo
*~
# 日志
# Logs
*.log
# 环境变量
# Environment variables
.env
.env.local
@@ -86,7 +86,7 @@ class DataCache:
if entry.is_expired():
del self._cache[key]
self._misses += 1
logger.debug(f"[缓存] {self.name}:{key} 已过期,删除")
logger.debug(f"[cache] {self.name}:{key} expired and was removed")
return None
# Update access order (LRU)
@@ -94,7 +94,7 @@ class DataCache:
entry.hit_count += 1
self._hits += 1
logger.debug(f"[缓存命中] {self.name}:{key} (年龄: {entry.age():.0f}s/{entry.ttl:.0f}s)")
logger.debug(f"[cache hit] {self.name}:{key} (age: {entry.age():.0f}s/{entry.ttl:.0f}s)")
return entry.data
def set(
@@ -115,7 +115,7 @@ class DataCache:
# Check capacity, perform LRU elimination
while len(self._cache) >= self.max_size:
oldest_key, _ = self._cache.popitem(last=False)
logger.debug(f"[缓存] {self.name} 容量已满,淘汰: {oldest_key}")
logger.debug(f"[cache] {self.name} reached capacity, evicted: {oldest_key}")
actual_ttl = ttl if ttl is not None else self.default_ttl
self._cache[key] = CacheEntry(
@@ -124,14 +124,14 @@ class DataCache:
ttl=actual_ttl
)
logger.debug(f"[缓存更新] {self.name}:{key} TTL={actual_ttl}s")
logger.debug(f"[cache update] {self.name}:{key} TTL={actual_ttl}s")
def delete(self, key: str) -> bool:
"""Delete cache entry"""
with self._lock:
if key in self._cache:
del self._cache[key]
logger.debug(f"[缓存] {self.name}:{key} 已删除")
logger.debug(f"[cache] {self.name}:{key} deleted")
return True
return False
@@ -140,7 +140,7 @@ class DataCache:
with self._lock:
count = len(self._cache)
self._cache.clear()
logger.info(f"[缓存] {self.name} 已清空 {count} 条记录")
logger.info(f"[cache] {self.name} cleared {count} records")
return count
def cleanup_expired(self) -> int:
@@ -154,7 +154,7 @@ class DataCache:
del self._cache[key]
if expired_keys:
logger.debug(f"[缓存] {self.name} 清理 {len(expired_keys)} 条过期记录")
logger.debug(f"[cache] {self.name} cleaned {len(expired_keys)} expired records")
return len(expired_keys)
def stats(self) -> Dict[str, Any]:
@@ -84,11 +84,11 @@ class CircuitBreaker:
# Cooling is completed and enters the half-open state
state['state'] = CircuitState.HALF_OPEN
state['half_open_calls'] = 0
logger.info(f"[熔断器] {source} 冷却完成,进入半开状态")
logger.info(f"[circuit breaker] {source} cooldown finished, entering half-open state")
return True
else:
remaining = self.cooldown_seconds - time_since_failure
logger.debug(f"[熔断器] {source} 处于熔断状态,剩余冷却时间: {remaining:.0f}s")
logger.debug(f"[circuit breaker] {source} is open, remaining cooldown: {remaining:.0f}s")
return False
if state['state'] == CircuitState.HALF_OPEN:
@@ -105,7 +105,7 @@ class CircuitBreaker:
if state['state'] == CircuitState.HALF_OPEN:
# Successful in half-open state, full recovery
logger.info(f"[熔断器] {source} 半开状态请求成功,恢复正常")
logger.info(f"[circuit breaker] {source} request succeeded in half-open state, fully recovered")
# reset state
state['state'] = CircuitState.CLOSED
@@ -126,14 +126,14 @@ class CircuitBreaker:
# Fails in half-open state and continues to fuse
state['state'] = CircuitState.OPEN
state['half_open_calls'] = 0
logger.warning(f"[熔断器] {source} 半开状态请求失败,继续熔断 {self.cooldown_seconds}s")
logger.warning(f"[circuit breaker] {source} request failed in half-open state, staying open for {self.cooldown_seconds}s")
elif state['failures'] >= self.failure_threshold:
# reaches the threshold and enters the circuit breaker
state['state'] = CircuitState.OPEN
logger.warning(f"[熔断器] {source} 连续失败 {state['failures']} 次,进入熔断状态 "
f"(冷却 {self.cooldown_seconds}s)")
logger.warning(f"[circuit breaker] {source} failed {state['failures']} times consecutively and is now open "
f"(cooldown {self.cooldown_seconds}s)")
if error:
logger.warning(f"[熔断器] 最后错误: {error}")
logger.warning(f"[circuit breaker] last error: {error}")
def get_status(self) -> Dict[str, Dict[str, Any]]:
"""Get all data source status"""
@@ -151,10 +151,10 @@ class CircuitBreaker:
if source:
if source in self._states:
del self._states[source]
logger.info(f"[熔断器] 已重置 {source} 的熔断状态")
logger.info(f"[circuit breaker] reset breaker state for {source}")
else:
self._states.clear()
logger.info("[熔断器] 已重置所有数据源的熔断状态")
logger.info("[circuit breaker] reset breaker state for all data sources")
# ============================================
@@ -62,7 +62,7 @@ class DataSourceFactory:
from app.data_sources.futures import FuturesDataSource
return FuturesDataSource()
else:
raise ValueError(f"不支持的市场类型: {market}")
raise ValueError(f"Unsupported market type: {market}")
@classmethod
def get_kline(
@@ -160,7 +160,7 @@ class PolymarketDataSource:
return None
def get_market_history(self, market_id: str, days: int = 30) -> List[Dict]:
"""获取市场历史价格数据"""
"""Get historical market price data."""
# Here you need to implement historical data acquisition logic
# Temporarily returns an empty list
return []
@@ -374,7 +374,7 @@ class PolymarketDataSource:
return []
def _get_cached_markets(self, category: str = None, limit: int = 50) -> Optional[List[Dict]]:
"""从数据库缓存读取市场数据"""
"""Read market data from the database cache."""
try:
with get_db_connection() as db:
cur = db.cursor()
+1 -1
View File
@@ -709,7 +709,7 @@ IMPORTANT: Output Python code directly, without explanations, without descriptio
reference_id=f"ai_code_gen_{user_id}_{int(time.time())}"
)
if not ok:
yield "data: " + json.dumps({"error": f"积分不足: {msg}"}, ensure_ascii=False) + "\n\n"
yield "data: " + json.dumps({"error": f"Insufficient credits: {msg}"}, ensure_ascii=False) + "\n\n"
yield "data: [DONE]\n\n"
return
+5 -5
View File
@@ -1010,7 +1010,7 @@ def get_openrouter_balance():
if not api_key:
return jsonify({
'code': 0,
'msg': 'OpenRouter API Key 未配置',
'msg': 'OpenRouter API key is not configured',
'data': None
})
@@ -1050,27 +1050,27 @@ def get_openrouter_balance():
elif resp.status_code == 401:
return jsonify({
'code': 0,
'msg': 'API Key 无效或已过期',
'msg': 'API key is invalid or expired',
'data': None
})
else:
return jsonify({
'code': 0,
'msg': f'查询失败: HTTP {resp.status_code}',
'msg': f'Query failed: HTTP {resp.status_code}',
'data': None
})
except requests.exceptions.Timeout:
return jsonify({
'code': 0,
'msg': '请求超时,请检查网络连接',
'msg': 'Request timed out. Please check the network connection.',
'data': None
})
except Exception as e:
logger.error(f"Get OpenRouter balance failed: {e}")
return jsonify({
'code': 0,
'msg': f'查询失败: {str(e)}',
'msg': f'Query failed: {str(e)}',
'data': None
})
@@ -84,7 +84,7 @@ class AnalysisMemory:
cur.execute("""
DO $$
BEGIN
-- 添加 user_id 如果不存在
-- Add the user_id column if it does not exist
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_analysis_memory' AND column_name = 'user_id'
@@ -92,7 +92,7 @@ class AnalysisMemory:
ALTER TABLE qd_analysis_memory ADD COLUMN user_id INT;
END IF;
-- 添加 raw_result 如果不存在
-- Add the raw_result column if it does not exist
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_analysis_memory' AND column_name = 'raw_result'
+3 -3
View File
@@ -3378,7 +3378,7 @@ import pandas as pd
else:
# SL not strict enough, liquidation triggered
logger.warning(f"Long liquidation! entry={entry_price:.2f}, low={low:.2f}, "
f"爆仓线={liquidation_price:.2f}, 止损价={stop_loss_price:.2f}")
f"liq_price={liquidation_price:.2f}, stop_loss_price={stop_loss_price:.2f}")
is_liquidated = True
liquidation_loss = self._liquidation_loss(capital)
capital = 0
@@ -3402,7 +3402,7 @@ import pandas as pd
stop_loss_price = close_short_price_arr[i] if has_stop_loss else 0
logger.warning(f"[candle {i}] Short hit liquidation! entry={entry_price:.2f}, high={high:.2f}, liq_price={liquidation_price:.2f}, "
f"止损信号={close_short_arr[i]}, 止损价={stop_loss_price:.4f}, 时间={timestamp}")
f"stop_loss_signal={close_short_arr[i]}, stop_loss_price={stop_loss_price:.4f}, time={timestamp}")
# Determine SL or liquidation first
if has_stop_loss and stop_loss_price < liquidation_price:
@@ -3425,7 +3425,7 @@ import pandas as pd
else:
# SL not strict enough, liquidation triggered
logger.warning(f"Short liquidation! entry={entry_price:.2f}, high={high:.2f}, "
f"爆仓线={liquidation_price:.2f}, 止损价={stop_loss_price:.2f}")
f"liq_price={liquidation_price:.2f}, stop_loss_price={stop_loss_price:.2f}")
is_liquidated = True
liquidation_loss = self._liquidation_loss(capital)
capital = 0
@@ -155,7 +155,7 @@ class CommunityService:
return {'items': [], 'total': 0, 'page': 1, 'page_size': page_size, 'total_pages': 0}
def get_indicator_detail(self, indicator_id: int, user_id: int = None) -> Optional[Dict[str, Any]]:
"""获取指标详情"""
"""Get indicator details."""
try:
with get_db_connection() as db:
cur = db.cursor()
@@ -235,8 +235,8 @@ class CommunityService:
def purchase_indicator(self, buyer_id: int, indicator_id: int) -> Tuple[bool, str, Dict[str, Any]]:
"""
购买指标
Purchase an indicator.
Returns:
(success, message, data)
"""
@@ -367,7 +367,7 @@ class CommunityService:
return False, f'error: {str(e)}', {}
def get_my_purchases(self, user_id: int, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""获取用户购买的指标列表"""
"""Get the list of indicators purchased by the user."""
offset = (page - 1) * page_size
try:
@@ -433,7 +433,7 @@ class CommunityService:
# ==========================================
def get_comments(self, indicator_id: int, page: int = 1, page_size: int = 20) -> Dict[str, Any]:
"""获取指标评论列表"""
"""Get the list of indicator comments."""
offset = (page - 1) * page_size
try:
@@ -495,7 +495,9 @@ class CommunityService:
content: str
) -> Tuple[bool, str, Dict[str, Any]]:
"""
添加评论只有购买过的用户可以评论且只能评论一次
Add a comment.
Only users who purchased the indicator can comment, and only once.
"""
try:
# Verify score range
@@ -577,7 +579,9 @@ class CommunityService:
content: str
) -> Tuple[bool, str, Dict[str, Any]]:
"""
更新评论只能修改自己的评论
Update a comment.
Users can only edit their own comments.
"""
try:
rating = max(1, min(5, int(rating)))
@@ -628,7 +632,7 @@ class CommunityService:
return False, f'error: {str(e)}', {}
def get_user_comment(self, user_id: int, indicator_id: int) -> Optional[Dict[str, Any]]:
"""获取用户对某个指标的评论"""
"""Get the user's comment for a specific indicator."""
try:
with get_db_connection() as db:
cur = db.cursor()
@@ -665,7 +669,7 @@ class CommunityService:
page_size: int = 20,
review_status: str = 'pending' # 'pending' / 'approved' / 'rejected' / 'all'
) -> Dict[str, Any]:
"""获取待审核的指标列表(管理员用)"""
"""Get the list of indicators pending review for admins."""
offset = (page - 1) * page_size
try:
@@ -753,7 +757,7 @@ class CommunityService:
action: str, # 'approve' / 'reject'
note: str = ''
) -> Tuple[bool, str]:
"""审核指标"""
"""Review an indicator."""
try:
new_status = 'approved' if action == 'approve' else 'rejected'
note = (note or '').strip()[:500]
@@ -790,7 +794,7 @@ class CommunityService:
return False, f'error: {str(e)}'
def unpublish_indicator(self, admin_id: int, indicator_id: int, note: str = '') -> Tuple[bool, str]:
"""下架指标(取消发布)"""
"""Unpublish an indicator."""
try:
note = (note or '').strip()[:500]
@@ -826,7 +830,7 @@ class CommunityService:
return False, f'error: {str(e)}'
def admin_delete_indicator(self, admin_id: int, indicator_id: int) -> Tuple[bool, str]:
"""管理员删除指标"""
"""Delete an indicator as an admin."""
try:
with get_db_connection() as db:
cur = db.cursor()
@@ -889,11 +893,11 @@ class CommunityService:
def get_indicator_performance(self, indicator_id: int) -> Dict[str, Any]:
"""
获取指标的实盘表现统计
Get live performance statistics for an indicator.
数据来源
1. qd_backtest_runs - 回测记录result_json 内含 totalReturn / winRate
2. qd_strategy_trades + qd_strategies_trading - 真实实盘交易记录
Data sources:
1. qd_backtest_runs - Backtest records (result_json contains totalReturn, winRate, etc.)
2. qd_strategy_trades + qd_strategies_trading - Real live trading records
"""
default_result = {
'strategy_count': 0,
@@ -1032,7 +1036,7 @@ _community_service = None
def get_community_service() -> CommunityService:
"""获取社区服务单例"""
"""Get the community service singleton."""
global _community_service
if _community_service is None:
_community_service = CommunityService()
@@ -272,13 +272,13 @@ class IndicatorCaller:
def get_indicator_params(indicator_id: int) -> List[Dict[str, Any]]:
"""
获取指标的参数声明供API调用
Get the indicator parameter declarations for API usage
Args:
indicator_id: 指标ID
indicator_id: Indicator ID
Returns:
参数声明列表
A list of parameter declarations
"""
try:
with get_db_connection() as db:
@@ -722,8 +722,8 @@ class OkxClient(BaseRestClient):
"""
mt = (market_type or "swap").strip().lower()
inst_id = to_okx_spot_inst_id(symbol) if mt == "spot" else to_okx_swap_inst_id(symbol)
# IMPORTANT: For OKX SWAP, fillSz/accFillSz are in "contracts" (张数), not base-asset quantity.
# Our system standardizes on base-asset quantity everywhere ("币数"), so we convert using ctVal.
# IMPORTANT: For OKX SWAP, fillSz/accFillSz are in contract counts, not base-asset quantity.
# Our system standardizes on base-asset quantity everywhere, so we convert using ctVal.
ct_val = Decimal("0")
if mt != "spot":
try:
+3 -3
View File
@@ -212,11 +212,11 @@ class LLMService:
if "openrouter" in (base_url or "").lower():
from app.config.api_keys import APIKeys
if not APIKeys.OPENROUTER_API_KEY:
error_msg += ". OPENROUTER_API_KEY 未配置,请在 backend_api_python/.env 中设置"
error_msg += ". OPENROUTER_API_KEY is not configured. Set it in backend_api_python/.env"
elif response.status_code == 403:
error_msg += ". 可能原因:API 密钥无效/过期、余额不足、或无模型权限。请检查 https://openrouter.ai/keys"
error_msg += ". Possible causes: API key is invalid or expired, account balance is insufficient, or the model is not permitted. Check https://openrouter.ai/keys"
elif response.status_code == 404:
error_msg += ". 可能原因:模型不可用或账户隐私/数据策略限制。请检查 https://openrouter.ai/settings/privacy"
error_msg += ". Possible causes: the model is unavailable or blocked by account privacy/data-policy settings. Check https://openrouter.ai/settings/privacy"
raise ValueError(error_msg)
@@ -534,7 +534,7 @@ class MarketDataCollector:
def _calc_macd(self, closes: List[float]) -> Dict[str, float]:
"""
MACD(12,26,9)DIF = EMA12(close) EMA26(close)DEA = EMA9(DIF) = DIF DEA
MACD(12,26,9): DIF = EMA12(close) - EMA26(close), DEA = EMA9(DIF), histogram = DIF - DEA.
Each EMA uses SMA seeds; DIF is defined from the 26th K onwards, and EMA9 is calculated for the DIF subsequence of the signal line pair.
"""
n = len(closes)
@@ -866,32 +866,32 @@ class MarketDataCollector:
crypto_info = {
'BTC': {
'name': 'Bitcoin',
'description': '比特币,数字黄金,市值第一的加密货币,作为价值存储和避险资产',
'description': 'Bitcoin, the leading cryptocurrency by market cap, often viewed as digital gold and a store-of-value asset.',
'category': 'Store of Value',
},
'ETH': {
'name': 'Ethereum',
'description': '以太坊,智能合约平台,DeFi和NFT生态的基础设施',
'description': 'Ethereum, a smart-contract platform and core infrastructure for the DeFi and NFT ecosystem.',
'category': 'Smart Contract Platform',
},
'BNB': {
'name': 'Binance Coin',
'description': '币安币,全球最大交易所的平台代币',
'description': 'Binance Coin, the platform token of one of the world\'s largest exchanges.',
'category': 'Exchange Token',
},
'SOL': {
'name': 'Solana',
'description': '高性能公链,主打高TPS和低Gas费',
'description': 'A high-performance public chain focused on high throughput and low transaction fees.',
'category': 'Smart Contract Platform',
},
'XRP': {
'name': 'Ripple',
'description': '瑞波币,专注跨境支付解决方案',
'description': 'Ripple, focused on cross-border payment solutions.',
'category': 'Payment',
},
'DOGE': {
'name': 'Dogecoin',
'description': '狗狗币,Meme币代表,社区驱动',
'description': 'Dogecoin, a community-driven meme coin.',
'category': 'Meme',
},
}
@@ -905,7 +905,7 @@ class MarketDataCollector:
return {
'name': base,
'description': f'{base} 是一种加密货币',
'description': f'{base} is a cryptocurrency.',
'category': 'Unknown',
}
@@ -960,7 +960,7 @@ class MarketDataCollector:
if cached_sentiment.get('vix'):
vix = cached_sentiment['vix']
result['VIX'] = {
'name': 'VIX恐慌指数',
'name': 'VIX Fear Index',
'description': vix.get('interpretation', ''),
'price': vix.get('value', 0),
'change': vix.get('change', 0),
@@ -971,7 +971,7 @@ class MarketDataCollector:
if cached_sentiment.get('dxy'):
dxy = cached_sentiment['dxy']
result['DXY'] = {
'name': '美元指数',
'name': 'US Dollar Index',
'description': dxy.get('interpretation', ''),
'price': dxy.get('value', 0),
'change': dxy.get('change', 0),
@@ -982,7 +982,7 @@ class MarketDataCollector:
if cached_sentiment.get('yield_curve'):
yc = cached_sentiment['yield_curve']
result['TNX'] = {
'name': '美债10年收益率',
'name': 'US 10Y Treasury Yield',
'description': yc.get('interpretation', ''),
'price': yc.get('yield_10y', 0),
'change': yc.get('change', 0),
@@ -994,7 +994,7 @@ class MarketDataCollector:
if cached_sentiment.get('fear_greed'):
fg = cached_sentiment['fear_greed']
result['FEAR_GREED'] = {
'name': '恐惧贪婪指数',
'name': 'Fear & Greed Index',
'description': fg.get('classification', 'Neutral'),
'price': fg.get('value', 50),
'change': 0,
@@ -1024,7 +1024,7 @@ class MarketDataCollector:
# Convert to unified format
if key == 'VIX':
result[key] = {
'name': 'VIX恐慌指数',
'name': 'VIX Fear Index',
'description': data.get('interpretation', ''),
'price': data.get('value', 0),
'change': data.get('change', 0),
@@ -1033,7 +1033,7 @@ class MarketDataCollector:
}
elif key == 'DXY':
result[key] = {
'name': '美元指数',
'name': 'US Dollar Index',
'description': data.get('interpretation', ''),
'price': data.get('value', 0),
'change': data.get('change', 0),
@@ -1042,7 +1042,7 @@ class MarketDataCollector:
}
elif key == 'TNX':
result[key] = {
'name': '美债10年收益率',
'name': 'US 10Y Treasury Yield',
'description': data.get('interpretation', ''),
'price': data.get('yield_10y', 0),
'change': data.get('change', 0),
@@ -1052,7 +1052,7 @@ class MarketDataCollector:
}
elif key == 'FEAR_GREED':
result[key] = {
'name': '恐惧贪婪指数',
'name': 'Fear & Greed Index',
'description': data.get('classification', 'Neutral'),
'price': data.get('value', 50),
'change': 0,
@@ -1122,7 +1122,7 @@ class MarketDataCollector:
"url": item.get('url', ''),
"sentiment": item.get('sentiment', 'neutral'),
})
logger.info(f"Finnhub 新闻获取成功: {len(news_list)} ")
logger.info(f"Finnhub news fetched successfully: {len(news_list)} items")
except Exception as e:
logger.debug(f"Finnhub news fetch failed: {e}")
@@ -1199,13 +1199,13 @@ class MarketDataCollector:
"datetime": result.published_date or datetime.now().strftime('%Y-%m-%d'),
"headline": result.title,
"summary": result.snippet[:200] if result.snippet else '',
"source": f"搜索:{result.source}",
"source": f"Search:{result.source}",
"url": result.url,
"sentiment": result.sentiment,
})
logger.info(f"搜索引擎新闻补充: {len(news_list)} 条 (来源: {response.provider})")
logger.info(f"Search-engine news supplement: {len(news_list)} items (provider: {response.provider})")
except Exception as e:
logger.debug(f"搜索引擎新闻获取失败: {e}")
logger.debug(f"Search-engine news fetch failed: {e}")
return news_list
@@ -1260,7 +1260,7 @@ class MarketDataCollector:
"datetime": result.published_date or datetime.now().strftime('%Y-%m-%d %H:%M'),
"headline": result.title,
"summary": result.snippet[:300] if result.snippet else '',
"source": f"全球事件:{result.source}",
"source": f"Global event:{result.source}",
"url": result.url,
"sentiment": "negative" if any(kw in text for kw in ["war", "conflict", "attack", "战争", "冲突", "袭击"]) else "neutral",
"is_global_event": True # Flag as global event
+22 -22
View File
@@ -9,7 +9,7 @@ Supported search engines (in order of priority):
4. Bing Search API
5. DuckDuckGo Get the scoop for free
参考daily_stock_analysis-main/src/search_service.py
Reference: daily_stock_analysis-main/src/search_service.py
"""
import requests
import json
@@ -72,9 +72,9 @@ class SearchResponse:
def to_context(self, max_results: int = 5) -> str:
"""Transform search results into context that can be used for AI analysis"""
if not self.success or not self.results:
return f"搜索 '{self.query}' 未找到相关结果。"
return f"No relevant results were found for '{self.query}'."
lines = [f"{self.query} 搜索结果】(来源:{self.provider}"]
lines = [f"[Search results for {self.query}] (source: {self.provider})"]
for i, result in enumerate(self.results[:max_results], 1):
lines.append(f"\n{i}. {result.to_text()}")
@@ -128,7 +128,7 @@ class BaseSearchProvider(ABC):
return key
# There is a problem with all keys, reset the error count and return the first one
logger.warning(f"[{self._name}] 所有 API Key 都有错误记录,重置错误计数")
logger.warning(f"[{self._name}] all API keys have recorded errors, resetting error counters")
self._key_errors = {key: 0 for key in self._api_keys}
return self._api_keys[0] if self._api_keys else None
@@ -142,7 +142,7 @@ class BaseSearchProvider(ABC):
def _record_error(self, key: str) -> None:
"""Log errors"""
self._key_errors[key] = self._key_errors.get(key, 0) + 1
logger.warning(f"[{self._name}] API Key {key[:8]}... 错误计数: {self._key_errors[key]}")
logger.warning(f"[{self._name}] API key {key[:8]}... error count: {self._key_errors[key]}")
@abstractmethod
def _do_search(self, query: str, api_key: str, max_results: int, days: int = 7) -> SearchResponse:
@@ -168,7 +168,7 @@ class BaseSearchProvider(ABC):
results=[],
provider=self._name,
success=False,
error_message=f"{self._name} 未配置 API Key"
error_message=f"{self._name} API key is not configured"
)
start_time = time.time()
@@ -178,7 +178,7 @@ class BaseSearchProvider(ABC):
if response.success:
self._record_success(api_key)
logger.info(f"[{self._name}] 搜索 '{query}' 成功,返回 {len(response.results)} 条结果,耗时 {response.search_time:.2f}s")
logger.info(f"[{self._name}] search '{query}' succeeded, returned {len(response.results)} results in {response.search_time:.2f}s")
else:
self._record_error(api_key)
@@ -187,7 +187,7 @@ class BaseSearchProvider(ABC):
except Exception as e:
self._record_error(api_key)
elapsed = time.time() - start_time
logger.error(f"[{self._name}] 搜索 '{query}' 失败: {e}")
logger.error(f"[{self._name}] search '{query}' failed: {e}")
return SearchResponse(
query=query,
results=[],
@@ -203,9 +203,9 @@ class BaseSearchProvider(ABC):
try:
parsed = urlparse(url)
domain = parsed.netloc.replace('www.', '')
return domain or '未知来源'
return domain or 'Unknown source'
except:
return '未知来源'
return 'Unknown source'
class TavilySearchProvider(BaseSearchProvider):
@@ -265,7 +265,7 @@ class TavilySearchProvider(BaseSearchProvider):
except Exception as e:
error_msg = str(e)
if 'rate limit' in error_msg.lower() or 'quota' in error_msg.lower():
error_msg = f"API 配额已用尽: {error_msg}"
error_msg = f"API quota exhausted: {error_msg}"
return SearchResponse(
query=query,
@@ -483,7 +483,7 @@ class GoogleSearchProvider(BaseSearchProvider):
results=[],
provider=self.name,
success=False,
error_message="Google Search 未配置 CX"
error_message="Google Search CX is not configured"
)
try:
@@ -515,7 +515,7 @@ class GoogleSearchProvider(BaseSearchProvider):
results=[],
provider=self.name,
success=False,
error_message="Google API 配额已用尽"
error_message="Google API quota exhausted"
)
response.raise_for_status()
@@ -744,33 +744,33 @@ class SearchService:
tavily_keys = APIKeys.TAVILY_API_KEYS
if tavily_keys:
self._providers.append(TavilySearchProvider(tavily_keys))
logger.info(f"已配置 Tavily 搜索,共 {len(tavily_keys)} API Key")
logger.info(f"Configured Tavily search with {len(tavily_keys)} API keys")
# 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")
logger.info(f"Configured SerpAPI search with {len(serpapi_keys)} API keys")
# 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 搜索")
logger.info("Configured Google CSE search")
# 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 搜索")
logger.info("Configured Bing search")
# 5. DuckDuckGo (Free Tips)
self._providers.append(DuckDuckGoSearchProvider())
logger.info("已配置 DuckDuckGo 搜索(免费兜底)")
logger.info("Configured DuckDuckGo search as the free fallback")
if len(self._providers) == 1:
logger.warning("仅有 DuckDuckGo 可用,建议配置更多搜索引擎 API Key")
logger.warning("Only DuckDuckGo is available. Configure more search-engine API keys for better coverage.")
@property
def is_available(self) -> bool:
@@ -826,7 +826,7 @@ class SearchService:
if response.success and response.results:
return response
else:
logger.warning(f"{provider.name} 搜索失败: {response.error_message},尝试下一个引擎")
logger.warning(f"{provider.name} search failed: {response.error_message}. Trying the next engine.")
# all engines fail
return SearchResponse(
@@ -834,7 +834,7 @@ class SearchService:
results=[],
provider="None",
success=False,
error_message="所有搜索引擎都不可用或搜索失败"
error_message="All search engines are unavailable or failed"
)
def search_stock_news(
@@ -875,7 +875,7 @@ class SearchService:
else:
query = f"{stock_name} {stock_code} latest news"
logger.info(f"搜索股票新闻: {stock_name}({stock_code}), market={market}, days={search_days}")
logger.info(f"Searching stock news: {stock_name}({stock_code}), market={market}, days={search_days}")
return self.search_with_fallback(query, max_results, search_days)
@@ -501,15 +501,15 @@ class SignalNotifier:
"""
Generic webhook delivery.
用户在个人中心配置
- webhook_url: Webhook 地址
- webhook_token: Bearer Token可选
User configuration in the profile center:
- webhook_url: webhook URL
- webhook_token: optional Bearer token
支持功能
- 自定义 headers: notification_config.targets.webhook_headers
Supported features:
- Custom headers: notification_config.targets.webhook_headers
- Bearer Token: notification_config.targets.webhook_token
- 签名验证: notification_config.targets.webhook_signing_secret
- 自动重试: 429/5xx 时重试一次
- Signature verification: notification_config.targets.webhook_signing_secret
- Automatic retry: retry once on 429/5xx
"""
if not url:
return False, "missing_webhook_url"
@@ -933,6 +933,8 @@ class StrategyService:
trading_config = payload.get('trading_config') if payload.get('trading_config') is not None else (existing.get('trading_config') or {})
exchange_config = payload.get('exchange_config') if payload.get('exchange_config') is not None else (existing.get('exchange_config') or {})
ai_model_config = payload.get('ai_model_config') if payload.get('ai_model_config') is not None else (existing.get('ai_model_config') or {})
strategy_mode = payload.get('strategy_mode') if payload.get('strategy_mode') is not None else (existing.get('strategy_mode') or 'signal')
strategy_code = payload.get('strategy_code') if payload.get('strategy_code') is not None else (existing.get('strategy_code') or '')
# When credential_id is present, strip raw API keys to avoid
# storing secrets in the strategy record — they live in qd_exchange_credentials.
@@ -964,6 +966,8 @@ class StrategyService:
"""
UPDATE qd_strategies_trading
SET strategy_name = ?,
strategy_mode = ?,
strategy_code = ?,
market_category = ?,
execution_mode = ?,
notification_config = ?,
@@ -981,6 +985,8 @@ class StrategyService:
""",
(
name,
strategy_mode,
strategy_code,
market_category,
execution_mode,
self._dump_json_or_encrypt(notification_config, encrypt=False),
@@ -88,9 +88,10 @@ class TradingExecutor:
def _normalize_trade_symbol(self, exchange: Any, symbol: str, market_type: str, exchange_id: str) -> str:
"""
将数据库/配置里的 symbol 规范化为交易所合约可用的 CCXT symbol
Normalize symbols from the database/config into the CCXT symbol format accepted by the exchange.
典型场景OKX 永续统一符号通常是 `BNB/USDT:USDT`但前端/数据库可能传 `BNB/USDT`
Typical case: the normalized OKX perpetual symbol is usually `BNB/USDT:USDT`,
while the frontend or database may pass `BNB/USDT`.
"""
try:
# New system: only supports swap (perpetual contract) / spot (spot)
@@ -131,7 +132,7 @@ class TradingExecutor:
return symbol
def _log_resource_status(self, prefix: str = ""):
"""调试:记录线程/内存使用,便于定位 can't start new thread 根因"""
"""Debug helper: record thread and memory usage to diagnose the root cause of 'can't start new thread'."""
try:
import psutil # Use more precise metrics if installed
p = psutil.Process()
@@ -415,7 +416,7 @@ class TradingExecutor:
thread.start()
except Exception as e:
# Capture exceptions such as can't start new thread and record resource status
self._log_resource_status(prefix="启动异常")
self._log_resource_status(prefix="startup exception")
raise e
self.running_strategies[strategy_id] = thread
@@ -631,15 +632,15 @@ class TradingExecutor:
# Even in signal mode, it is necessary to check and clean up the situation when the user manually closes the position on the exchange but the database record is still there.
# This can prevent the strategy from thinking that there are still positions and being unable to execute a new opening signal.
try:
logger.info(f"策略 {strategy_id} 启动时检查持仓同步...")
logger.info(f"Checking position synchronization during startup for strategy {strategy_id}...")
# Call position synchronization logic (check even in signal mode)
from app import get_pending_order_worker
worker = get_pending_order_worker()
if worker and hasattr(worker, '_sync_positions_best_effort'):
worker._sync_positions_best_effort(target_strategy_id=strategy_id)
logger.info(f"策略 {strategy_id} 启动时持仓同步完成")
logger.info(f"Position synchronization completed during startup for strategy {strategy_id}")
except Exception as e:
logger.warning(f"策略 {strategy_id} 启动时持仓同步失败(不影响启动): {e}")
logger.warning(f"Position synchronization failed during startup for strategy {strategy_id} (startup continues): {e}")
# Get the current highest position price (read from local database)
current_pos_list = self._get_current_positions(strategy_id, symbol)
@@ -660,7 +661,7 @@ class TradingExecutor:
# Key diagnostic log: Confirm whether the indicator has obtained the position status
logger.info(
f"策略 {strategy_id} 指标注入持仓状态: count={len(current_pos_list)}, "
f"Injected position state for strategy {strategy_id}: count={len(current_pos_list)}, "
f"position={initial_position}, entry_price={initial_avg_entry_price}, highest={initial_highest}"
)
@@ -1005,7 +1006,7 @@ class TradingExecutor:
market=market_type or 'Crypto',
symbol=symbol,
signal_type=signal_type,
signal_detail=f"策略: {strategy_name}\n信号: {signal_type}\n价格: {execute_price:.4f}"
signal_detail=f"Strategy: {strategy_name}\nSignal: {signal_type}\nPrice: {execute_price:.4f}"
)
except Exception as link_e:
logger.warning(f"Strategy signal linkage notification failed: {link_e}")
@@ -1095,8 +1096,8 @@ class TradingExecutor:
def _is_strategy_running(self, strategy_id: int) -> bool:
"""
检查策略是否在运行
同时检查数据库状态和线程状态避免重启后状态不一致
Check whether the strategy is running.
This checks both the database state and the thread state to avoid mismatches after restart.
"""
try:
# 1. Check database status
@@ -1145,7 +1146,7 @@ class TradingExecutor:
leverage: float = None,
strategy_id: int = None
) -> Optional[ccxt.Exchange]:
"""(Mock) 信号模式不需要真实交易所连接"""
"""(Mock) Signal mode does not require a real exchange connection."""
return None
def _fetch_latest_kline(self, symbol: str, timeframe: str, limit: int = 500, market_category: str = 'Crypto') -> List[Dict[str, Any]]:
@@ -1872,7 +1873,7 @@ class TradingExecutor:
allowed_modules = ['numpy', 'pandas', 'math', 'json', 'time']
if name in allowed_modules or name.split('.')[0] in allowed_modules:
return builtins.__import__(name, *args, **kwargs)
raise ImportError(f"不允许导入模块: {name}")
raise ImportError(f"Importing modules is not allowed: {name}")
safe_builtins = {k: getattr(builtins, k) for k in dir(builtins)
if not k.startswith('_') and k not in [
@@ -1961,7 +1962,7 @@ class TradingExecutor:
return []
def _execute_trading_logic(self, *args, **kwargs):
"""已废弃"""
"""Deprecated."""
pass
def _execute_signal(
@@ -2014,8 +2015,8 @@ class TradingExecutor:
# Best-effort persist a browser notification so UI can show "HOLD due to AI filter".
reason = (ai_info or {}).get("reason") or "ai_filter_rejected"
ai_decision = (ai_info or {}).get("ai_decision") or ""
title = f"AI过滤拦截开仓 | {symbol}"
msg = f"策略信号={sig}AI决策={ai_decision or 'UNKNOWN'},原因={reason};已HOLD(不下单)"
title = f"AI filter blocked opening a position | {symbol}"
msg = f"Strategy signal={sig}, AI decision={ai_decision or 'UNKNOWN'}, reason={reason}; action forced to HOLD (no order placed)"
self._persist_browser_notification(
strategy_id=strategy_id,
symbol=symbol,
@@ -2369,7 +2370,7 @@ class TradingExecutor:
payload: Optional[Dict[str, Any]] = None,
user_id: int = None,
) -> None:
"""Best-effort persist notification row for the frontend '通知' panel (browser channel)."""
"""Best-effort persistence of a notification row for the frontend notifications panel (browser channel)."""
try:
now = int(time.time())
# Get user_id from strategy if not provided
@@ -2776,7 +2777,7 @@ class TradingExecutor:
highest_price: float = 0.0,
lowest_price: float = 0.0,
):
"""更新持仓状态"""
"""Update position state."""
try:
# Get user_id from strategy
user_id = 1
@@ -2811,7 +2812,7 @@ class TradingExecutor:
logger.error(f"Failed to update position: {e}")
def _close_position(self, strategy_id: int, symbol: str, side: str):
"""平仓:删除持仓记录"""
"""Close a position by deleting the position record."""
try:
with get_db_connection() as db:
cursor = db.cursor()
@@ -2861,7 +2862,7 @@ class TradingExecutor:
return []
def _should_rebalance(self, strategy_id: int, rebalance_frequency: str) -> bool:
"""检查是否应该调仓"""
"""Check whether rebalancing should run."""
try:
with get_db_connection() as db:
cursor = db.cursor()
@@ -2892,7 +2893,7 @@ class TradingExecutor:
return True
def _update_last_rebalance(self, strategy_id: int):
"""更新上次调仓时间"""
"""Update the last rebalance timestamp."""
try:
with get_db_connection() as db:
cursor = db.cursor()
@@ -2920,7 +2921,7 @@ class TradingExecutor:
timeframe: str
) -> Optional[Dict[str, Any]]:
"""
执行截面策略指标返回所有标的的评分和排序
Execute cross-sectional strategy indicators and return scores and ranking for all instruments.
"""
try:
# Get K-line data of all targets
@@ -2989,7 +2990,7 @@ class TradingExecutor:
trading_config: Dict[str, Any]
) -> List[Dict[str, Any]]:
"""
根据排序结果生成截面策略信号
Generate cross-sectional strategy signals from the ranking results.
"""
portfolio_size = trading_config.get('portfolio_size', 10)
long_ratio = float(trading_config.get('long_ratio', 0.5))
@@ -3080,7 +3081,7 @@ class TradingExecutor:
indicator_id: Optional[int]
):
"""
截面策略执行循环
Cross-sectional strategy execution loop.
"""
logger.info(f"Starting cross-sectional strategy loop for strategy {strategy_id}")
@@ -3188,4 +3189,4 @@ class TradingExecutor:
except Exception as e:
logger.error(f"Cross-sectional strategy loop error: {e}")
logger.error(traceback.format_exc())
time.sleep(5) # Wait before retrying
time.sleep(5) # Wait before retrying
+11 -11
View File
@@ -50,7 +50,7 @@ def timeout_context(seconds: int):
return
def timeout_handler(signum, frame):
raise TimeoutError(f"代码执行超时(超过{seconds}秒)")
raise TimeoutError(f"Code execution timed out after {seconds} seconds")
try:
# Set up signal handler
@@ -129,7 +129,7 @@ def safe_exec_code(
}
except MemoryError as e:
error_msg = f"代码执行内存不足(超过{max_memory_mb}MB限制)"
error_msg = f"Code execution exceeded the memory limit of {max_memory_mb}MB"
logger.error(f"Code execution out of memory (limit={max_memory_mb}MB)")
return {
'success': False,
@@ -145,7 +145,7 @@ def safe_exec_code(
'result': None
}
except Exception as e:
error_msg = f"代码执行错误: {str(e)}\n{traceback.format_exc()}"
error_msg = f"Code execution error: {str(e)}\n{traceback.format_exc()}"
logger.error(f"Code execution error: {str(e)}")
logger.error(traceback.format_exc())
return {
@@ -237,7 +237,7 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]:
# Check your code for dangerous patterns
for pattern in dangerous_patterns:
if re.search(pattern, code):
return False, f"检测到危险代码模式: {pattern}"
return False, f"Dangerous code pattern detected: {pattern}"
# Try parsing the AST, checking for dangerous nodes
try:
@@ -267,13 +267,13 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]:
if isinstance(node.func, ast.Name):
func_name = node.func.id
if func_name in dangerous_functions:
return False, f"检测到危险函数调用: {func_name}()"
return False, f"Dangerous function call detected: {func_name}()"
# Check if there are calls to os.system etc.
if isinstance(node.func, ast.Attribute):
if isinstance(node.func.value, ast.Name):
if node.func.value.id in dangerous_modules:
return False, f"检测到危险模块调用: {node.func.value.id}.{node.func.attr}"
return False, f"Dangerous module call detected: {node.func.value.id}.{node.func.attr}"
# Check if there are bypass methods such as getattr(builtins, '__import__')
if isinstance(node.func, ast.Name) and node.func.id == 'getattr':
@@ -281,15 +281,15 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]:
if len(node.args) >= 2:
if isinstance(node.args[0], ast.Name) and node.args[0].id in ['builtins', '__builtins__']:
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}')"
return False, f"Detected an attempt to bypass restrictions via getattr: getattr({node.args[0].id}, '{node.args[1].value}')"
# Check the import statement: the use of import is prohibited in user scripts (security dependencies are uniformly injected by the platform)
for node in ast.walk(tree):
if isinstance(node, ast.Import):
return False, "不允许在脚本中使用 import 语句,请直接使用平台提供的 pd/np 等对象"
return False, "Import statements are not allowed in scripts. Use the platform-provided objects such as pd and np directly."
if isinstance(node, ast.ImportFrom):
return False, "不允许在脚本中使用 import 语句,请直接使用平台提供的 pd/np 等对象"
return False, "Import statements are not allowed in scripts. Use the platform-provided objects such as pd and np directly."
# Check if there is an attempt to access __builtins__
for node in ast.walk(tree):
@@ -298,10 +298,10 @@ def validate_code_safety(code: str) -> Tuple[bool, Optional[str]]:
if node.attr in ['__builtins__', '__import__', '__class__', '__bases__', '__subclasses__', '__mro__']:
# Check if used in dangerous context
if isinstance(node.value, ast.Name) and node.value.id in ['builtins', '__builtins__']:
return False, f"检测到访问危险属性: {node.value.id}.{node.attr}"
return False, f"Dangerous attribute access detected: {node.value.id}.{node.attr}"
except SyntaxError as e:
return False, f"代码语法错误: {str(e)}"
return False, f"Code syntax error: {str(e)}"
except Exception as e:
# If AST parsing fails, log a warning but allow to continue (possibly incomplete code)
logger.warning(f"AST parse failed; skipping safety checks: {str(e)}")
+1 -1
View File
@@ -88,7 +88,7 @@ GITHUB_REDIRECT_URI=http://localhost:5000/api/auth/oauth/github/callback
# =========================
BILLING_ENABLED=false
# 积分单价
# Credit unit cost
BILLING_COST_AI_ANALYSIS=10
BILLING_COST_AI_CODE_GEN=30
@@ -1,19 +1,19 @@
"""
回填历史 qd_strategy_trades price/amount/value 0 的记录
Backfill historical qd_strategy_trades rows whose price/amount/value are 0.
背景
- 某些交易所/订单类型下执行器回报里 filled_price/filled_amount 可能为 0
但交易所实际已成交导致交易纪律/交易记录显示为 0
- 我们现在在 OrderProcessor 中增加了fetch_order/fetch_my_trades 回补逻辑避免新数据再出现该问题
- 对历史脏数据可用 qd_pending_orders 中的 executed_at/filled_price/filled_amount/fee 做近似匹配回填
Background:
- For some exchanges and order types, the executor may report filled_price/filled_amount as 0
even though the exchange has actually filled the order, causing trade records to show 0.
- We have now added fetch_order/fetch_my_trades backfill logic in OrderProcessor to avoid this for new data.
- For historical dirty data, use executed_at/filled_price/filled_amount/fee in qd_pending_orders for approximate backfilling.
使用
Usage:
python backend_api_python/scripts/backfill_zero_trades.py --strategy-id 43 --since 2025-12-24 --until 2025-12-25
python backend_api_python/scripts/backfill_zero_trades.py --strategy-id 43 --since 2025-12-24 --until 2025-12-25 --apply
注意
- 该脚本按 (strategy_id, symbol, type) + 时间窗口(默认 ±600s) 匹配 qd_pending_orders
- 若同一条 trade 匹配到多个候选订单将选择 executed_at 最接近的那条若仍不唯一会跳过
Notes:
- The script matches qd_pending_orders by (strategy_id, symbol, type) plus a time window of ±600s by default.
- If one trade matches multiple candidate orders, it chooses the one with the closest executed_at; if still ambiguous, it skips the row.
"""
from __future__ import annotations
@@ -28,15 +28,15 @@ from app.utils.db import get_db_connection
def _parse_date_to_ts(s: str) -> int:
s = (s or "").strip()
# 支持 YYYY-MM-DD YYYY/MM/DD
# Support YYYY-MM-DD or YYYY/MM/DD
for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S"):
try:
dt = datetime.strptime(s, fmt)
# 服务器通常用本地时间写入 int(time.time());这里按本地时间解析
# The server usually writes int(time.time()) in local time; parse it as local time here
return int(dt.replace(tzinfo=None).timestamp())
except Exception:
pass
raise ValueError(f"无法解析日期: {s}")
raise ValueError(f"Unable to parse date: {s}")
def _fetch_bad_trades(strategy_id: int, since_ts: int, until_ts: int, limit: int) -> List[Dict[str, Any]]:
@@ -95,7 +95,7 @@ def _find_best_order_match(
cursor.close()
if not cand:
return None
# 若最接近的有并列(比如 executed_at 相同),认为不唯一,跳过以免误回填
# If the closest candidates are tied, such as identical executed_at, treat it as ambiguous and skip it
if len(cand) >= 2 and abs(int(cand[0]["executed_at"]) - trade_ts) == abs(int(cand[1]["executed_at"]) - trade_ts):
return None
return cand[0]
@@ -128,11 +128,11 @@ def _update_trade(
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--strategy-id", type=int, required=True)
ap.add_argument("--since", type=str, required=True, help="YYYY-MM-DD YYYY/MM/DD")
ap.add_argument("--until", type=str, required=True, help="YYYY-MM-DD YYYY/MM/DD")
ap.add_argument("--window-sec", type=int, default=600, help="匹配窗口,默认±600秒")
ap.add_argument("--limit", type=int, default=500, help="最多处理多少条 trade")
ap.add_argument("--apply", action="store_true", help="真正写库;默认 dry-run 仅打印")
ap.add_argument("--since", type=str, required=True, help="YYYY-MM-DD or YYYY/MM/DD")
ap.add_argument("--until", type=str, required=True, help="YYYY-MM-DD or YYYY/MM/DD")
ap.add_argument("--window-sec", type=int, default=600, help="Match window, default ±600 seconds")
ap.add_argument("--limit", type=int, default=500, help="Maximum number of trades to process")
ap.add_argument("--apply", action="store_true", help="Actually write to the database; default is dry-run output only")
args = ap.parse_args()
since_ts = _parse_date_to_ts(args.since)
@@ -2,8 +2,8 @@ import sys
import os
from dotenv import load_dotenv
# 添加后端目录到 Python 路径(使得可以 import app.*
# 由于 app 包位于 backend_api_python/app 下,而脚本位于 backend_api_python/scripts
# Add the backend directory to the Python path so app.* can be imported
# The app package lives under backend_api_python/app while this script is under backend_api_python/scripts
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from app.services.reflection import ReflectionService
@@ -19,8 +19,8 @@ def main():
load_dotenv(backend_env_path, override=False)
"""
运行自动反思验证任务
建议通过 cron 定时任务调度器 每天运行一次
Run the automated reflection verification task.
It is intended to run once per day via cron or another task scheduler.
"""
print("Running Automated Reflection Verification Task...")
service = ReflectionService()
+10 -11
View File
@@ -1,26 +1,25 @@
#!/bin/bash
# QuantDinger Python API 启动脚本
# QuantDinger Python API startup script
# 激活虚拟环境(如果使用虚拟环境)
# Activate the virtual environment if one is used
# source venv/bin/activate
# 检查依赖是否安装
# Check whether dependencies are installed
if ! python -c "import flask" 2>/dev/null; then
echo "正在安装依赖..."
echo "Installing dependencies..."
pip install -r requirements.txt
fi
# 启动服务
echo "启动 QuantDinger Python API 服务..."
echo "服务地址: http://0.0.0.0:5000"
# Start the service
echo "Starting QuantDinger Python API service..."
echo "Service address: http://0.0.0.0:5000"
# 创建日志目录
# Create the log directory
mkdir -p logs
# 开发环境(使用新的入口文件)
# Development environment (uses the new entry file)
python run.py
# 生产环境(使用 gunicorn
# Production environment (uses gunicorn)
# gunicorn -w 4 -b 0.0.0.0:5000 --timeout 120 --access-logfile logs/access.log --error-logfile logs/error.log "run:create_app()"