V2.2.1: Membership & Billing, USDT TRC20 payment, VIP free indicators, AI Trading Radar, simplified strategy creation, system settings simplification, bug fixes and UI improvements
This commit is contained in:
@@ -24,6 +24,7 @@ def register_routes(app: Flask):
|
||||
from app.routes.global_market import global_market_bp
|
||||
from app.routes.community import community_bp
|
||||
from app.routes.fast_analysis import fast_analysis_bp
|
||||
from app.routes.billing import billing_bp
|
||||
|
||||
app.register_blueprint(health_bp)
|
||||
app.register_blueprint(auth_bp, url_prefix='/api/auth') # Auth routes
|
||||
@@ -42,4 +43,5 @@ def register_routes(app: Flask):
|
||||
app.register_blueprint(mt5_bp, url_prefix='/api/mt5')
|
||||
app.register_blueprint(global_market_bp, url_prefix='/api/global-market')
|
||||
app.register_blueprint(community_bp, url_prefix='/api/community')
|
||||
app.register_blueprint(fast_analysis_bp, url_prefix='/api/fast-analysis')
|
||||
app.register_blueprint(fast_analysis_bp, url_prefix='/api/fast-analysis')
|
||||
app.register_blueprint(billing_bp, url_prefix='/api/billing')
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Billing APIs - 会员购买/套餐配置(Mock支付)
|
||||
|
||||
当前版本先实现“快速商业闭环”的最小可用:
|
||||
- 从系统设置(.env)读取 3 档会员(包月/包年/永久)金额与赠送积分配置
|
||||
- 用户在前端购买后立即开通/发放积分(后续可替换为真实支付网关)
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify, request, g
|
||||
|
||||
from app.utils.auth import login_required
|
||||
from app.utils.logger import get_logger
|
||||
from app.services.billing_service import get_billing_service
|
||||
from app.services.usdt_payment_service import get_usdt_payment_service
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
billing_bp = Blueprint("billing", __name__)
|
||||
|
||||
|
||||
@billing_bp.route("/plans", methods=["GET"])
|
||||
@login_required
|
||||
def get_membership_plans():
|
||||
"""Get membership plan configuration + current user's billing snapshot."""
|
||||
try:
|
||||
user_id = getattr(g, "user_id", None)
|
||||
svc = get_billing_service()
|
||||
plans = svc.get_membership_plans()
|
||||
billing_info = svc.get_user_billing_info(user_id) if user_id else {}
|
||||
return jsonify({"code": 1, "msg": "success", "data": {"plans": plans, "billing": billing_info}})
|
||||
except Exception as e:
|
||||
logger.error(f"get_membership_plans failed: {e}", exc_info=True)
|
||||
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
|
||||
|
||||
|
||||
@billing_bp.route("/purchase", methods=["POST"])
|
||||
@login_required
|
||||
def purchase_membership():
|
||||
"""
|
||||
Purchase membership (mock: immediate activation).
|
||||
|
||||
Body:
|
||||
{ plan: "monthly" | "yearly" | "lifetime" }
|
||||
"""
|
||||
try:
|
||||
user_id = getattr(g, "user_id", None)
|
||||
data = request.get_json() or {}
|
||||
plan = (data.get("plan") or "").strip().lower()
|
||||
if not plan:
|
||||
return jsonify({"code": 0, "msg": "missing_plan", "data": None}), 400
|
||||
|
||||
success, msg, out = get_billing_service().purchase_membership(user_id, plan)
|
||||
if success:
|
||||
return jsonify({"code": 1, "msg": msg, "data": out})
|
||||
return jsonify({"code": 0, "msg": msg, "data": out}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"purchase_membership failed: {e}", exc_info=True)
|
||||
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
|
||||
|
||||
|
||||
# =========================
|
||||
# USDT Pay (方案B)
|
||||
# =========================
|
||||
|
||||
|
||||
@billing_bp.route("/usdt/create", methods=["POST"])
|
||||
@login_required
|
||||
def usdt_create_order():
|
||||
"""
|
||||
Create USDT order for membership plan (per-order address).
|
||||
|
||||
Body:
|
||||
{ plan: "monthly"|"yearly"|"lifetime" }
|
||||
"""
|
||||
try:
|
||||
user_id = getattr(g, "user_id", None)
|
||||
data = request.get_json() or {}
|
||||
plan = (data.get("plan") or "").strip().lower()
|
||||
if not plan:
|
||||
return jsonify({"code": 0, "msg": "missing_plan", "data": None}), 400
|
||||
|
||||
ok, msg, out = get_usdt_payment_service().create_order(user_id, plan)
|
||||
if ok:
|
||||
return jsonify({"code": 1, "msg": "success", "data": out})
|
||||
return jsonify({"code": 0, "msg": msg, "data": out}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"usdt_create_order failed: {e}", exc_info=True)
|
||||
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
|
||||
|
||||
|
||||
@billing_bp.route("/usdt/order/<int:order_id>", methods=["GET"])
|
||||
@login_required
|
||||
def usdt_get_order(order_id: int):
|
||||
"""Get my USDT order; refresh chain status by default."""
|
||||
try:
|
||||
user_id = getattr(g, "user_id", None)
|
||||
refresh = str(request.args.get("refresh", "1")).lower() in ("1", "true", "yes")
|
||||
ok, msg, out = get_usdt_payment_service().get_order(user_id, order_id, refresh=refresh)
|
||||
if ok:
|
||||
return jsonify({"code": 1, "msg": "success", "data": out})
|
||||
return jsonify({"code": 0, "msg": msg, "data": out}), 404
|
||||
except Exception as e:
|
||||
logger.error(f"usdt_get_order failed: {e}", exc_info=True)
|
||||
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
|
||||
|
||||
@@ -23,7 +23,7 @@ def analyze():
|
||||
|
||||
POST /api/fast-analysis/analyze
|
||||
Body: {
|
||||
"market": "Crypto" | "USStock" | "AShare" | "Forex" | ...,
|
||||
"market": "Crypto" | "USStock" | "Forex" | ...,
|
||||
"symbol": "BTC/USDT" | "AAPL" | ...,
|
||||
"language": "zh-CN" | "en-US" (optional),
|
||||
"model": "openai/gpt-4o" (optional),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Global Market Dashboard APIs.
|
||||
|
||||
Provides aggregated global market data including:
|
||||
- Major indices (US, China, Hong Kong, Europe, Japan)
|
||||
- Major indices (US, Europe, Japan, Korea, Australia, India)
|
||||
- Forex pairs
|
||||
- Crypto prices
|
||||
- Market heatmap data (crypto, stocks, forex)
|
||||
@@ -53,7 +53,7 @@ CACHE_TTL = {
|
||||
"market_news": 180, # 3分钟 - 新闻
|
||||
"economic_calendar": 3600, # 1小时 - 日历事件
|
||||
"market_sentiment": 21600, # 6小时 - 宏观情绪变化缓慢
|
||||
"trading_opportunities": 60, # 1分钟 - 交易机会需要较新
|
||||
"trading_opportunities": 3600, # 1小时 - 每小时更新一次
|
||||
}
|
||||
|
||||
|
||||
@@ -257,12 +257,6 @@ def _fetch_stock_indices() -> List[Dict[str, Any]]:
|
||||
{"symbol": "^GSPC", "name_cn": "标普500", "name_en": "S&P 500", "region": "US", "flag": "🇺🇸", "lat": 40.7, "lng": -74.0},
|
||||
{"symbol": "^DJI", "name_cn": "道琼斯", "name_en": "Dow Jones", "region": "US", "flag": "🇺🇸", "lat": 38.5, "lng": -77.0},
|
||||
{"symbol": "^IXIC", "name_cn": "纳斯达克", "name_en": "NASDAQ", "region": "US", "flag": "🇺🇸", "lat": 37.5, "lng": -122.4},
|
||||
# China Markets - 坐标错开
|
||||
{"symbol": "000001.SS", "name_cn": "上证指数", "name_en": "SSE Composite", "region": "CN", "flag": "🇨🇳", "lat": 31.2, "lng": 121.5},
|
||||
{"symbol": "399001.SZ", "name_cn": "深证成指", "name_en": "SZSE Component", "region": "CN", "flag": "🇨🇳", "lat": 22.5, "lng": 114.1},
|
||||
{"symbol": "399006.SZ", "name_cn": "创业板指", "name_en": "ChiNext", "region": "CN", "flag": "🇨🇳", "lat": 25.0, "lng": 117.0},
|
||||
# Hong Kong - 只保留恒生指数
|
||||
{"symbol": "^HSI", "name_cn": "恒生指数", "name_en": "Hang Seng", "region": "HK", "flag": "🇭🇰", "lat": 22.3, "lng": 114.2},
|
||||
# Europe
|
||||
{"symbol": "^GDAXI", "name_cn": "德国DAX", "name_en": "DAX", "region": "EU", "flag": "🇩🇪", "lat": 50.1109, "lng": 8.6821},
|
||||
{"symbol": "^FTSE", "name_cn": "英国富时100", "name_en": "FTSE 100", "region": "EU", "flag": "🇬🇧", "lat": 51.5074, "lng": -0.1278},
|
||||
@@ -977,12 +971,12 @@ def _fetch_financial_news(lang: str = "all") -> Dict[str, List[Dict[str, Any]]]:
|
||||
|
||||
# Chinese news queries
|
||||
cn_queries = [
|
||||
"A股市场最新消息",
|
||||
"加密货币新闻",
|
||||
"美联储利率",
|
||||
"中国经济数据",
|
||||
"港股市场动态",
|
||||
"美股市场最新消息",
|
||||
"外汇市场分析",
|
||||
"全球经济数据",
|
||||
"期货市场动态",
|
||||
]
|
||||
|
||||
# English news queries
|
||||
@@ -1106,42 +1100,6 @@ def _get_economic_calendar() -> List[Dict[str, Any]]:
|
||||
"impact_desc": "加息利空欧股,利多欧元",
|
||||
"impact_desc_en": "Rate hike: bearish EU stocks, bullish EUR"
|
||||
},
|
||||
{
|
||||
"name": "中国GDP年率",
|
||||
"name_en": "China GDP y/y",
|
||||
"country": "CN",
|
||||
"importance": "high",
|
||||
"forecast": "5.2%",
|
||||
"previous": "5.0%",
|
||||
"impact_if_above": "bullish",
|
||||
"impact_if_below": "bearish",
|
||||
"impact_desc": "GDP高于预期利多A股和港股",
|
||||
"impact_desc_en": "Above forecast: bullish A-shares and HK stocks"
|
||||
},
|
||||
{
|
||||
"name": "中国CPI年率",
|
||||
"name_en": "China CPI y/y",
|
||||
"country": "CN",
|
||||
"importance": "medium",
|
||||
"forecast": "0.3%",
|
||||
"previous": "0.1%",
|
||||
"impact_if_above": "neutral",
|
||||
"impact_if_below": "bearish",
|
||||
"impact_desc": "通胀过低反映需求不足,利空股市",
|
||||
"impact_desc_en": "Low inflation reflects weak demand, bearish stocks"
|
||||
},
|
||||
{
|
||||
"name": "中国PMI",
|
||||
"name_en": "China Manufacturing PMI",
|
||||
"country": "CN",
|
||||
"importance": "medium",
|
||||
"forecast": "50.2",
|
||||
"previous": "49.8",
|
||||
"impact_if_above": "bullish",
|
||||
"impact_if_below": "bearish",
|
||||
"impact_desc": "PMI>50表示扩张,利多A股和大宗商品",
|
||||
"impact_desc_en": "PMI>50 = expansion, bullish A-shares and commodities"
|
||||
},
|
||||
{
|
||||
"name": "日本央行利率决议",
|
||||
"name_en": "BoJ Interest Rate Decision",
|
||||
@@ -1634,80 +1592,265 @@ def market_sentiment():
|
||||
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
|
||||
|
||||
|
||||
def _fetch_stock_opportunity_prices() -> List[Dict[str, Any]]:
|
||||
"""Fetch popular US stock prices for opportunity scanning."""
|
||||
stocks = [
|
||||
{"symbol": "AAPL", "name": "Apple"},
|
||||
{"symbol": "MSFT", "name": "Microsoft"},
|
||||
{"symbol": "GOOGL", "name": "Alphabet"},
|
||||
{"symbol": "AMZN", "name": "Amazon"},
|
||||
{"symbol": "TSLA", "name": "Tesla"},
|
||||
{"symbol": "NVDA", "name": "NVIDIA"},
|
||||
{"symbol": "META", "name": "Meta"},
|
||||
{"symbol": "NFLX", "name": "Netflix"},
|
||||
{"symbol": "AMD", "name": "AMD"},
|
||||
{"symbol": "CRM", "name": "Salesforce"},
|
||||
{"symbol": "COIN", "name": "Coinbase"},
|
||||
{"symbol": "BABA", "name": "Alibaba"},
|
||||
{"symbol": "NIO", "name": "NIO"},
|
||||
{"symbol": "PLTR", "name": "Palantir"},
|
||||
{"symbol": "INTC", "name": "Intel"},
|
||||
]
|
||||
|
||||
try:
|
||||
import yfinance as yf
|
||||
|
||||
symbols = [s["symbol"] for s in stocks]
|
||||
tickers = yf.Tickers(" ".join(symbols))
|
||||
|
||||
result = []
|
||||
for stock in stocks:
|
||||
try:
|
||||
ticker = tickers.tickers.get(stock["symbol"])
|
||||
if ticker:
|
||||
hist = ticker.history(period="2d")
|
||||
if len(hist) >= 2:
|
||||
prev_close = float(hist["Close"].iloc[-2])
|
||||
current = float(hist["Close"].iloc[-1])
|
||||
change = ((current - prev_close) / prev_close) * 100
|
||||
elif len(hist) == 1:
|
||||
current = float(hist["Close"].iloc[-1])
|
||||
change = 0
|
||||
else:
|
||||
continue
|
||||
|
||||
result.append({
|
||||
"symbol": stock["symbol"],
|
||||
"name": stock["name"],
|
||||
"price": round(current, 2),
|
||||
"change": round(change, 2)
|
||||
})
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to fetch stock {stock['symbol']}: {e}")
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch stock opportunity prices: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def _analyze_opportunities_crypto(opportunities: list):
|
||||
"""Scan crypto market for trading opportunities."""
|
||||
crypto_data = _get_cached("crypto_prices")
|
||||
if not crypto_data:
|
||||
crypto_data = _fetch_crypto_prices()
|
||||
if crypto_data:
|
||||
_set_cached("crypto_prices", crypto_data)
|
||||
|
||||
for coin in (crypto_data or [])[:20]:
|
||||
change = _safe_float(coin.get("change_24h", 0))
|
||||
change_7d = _safe_float(coin.get("change_7d", 0))
|
||||
symbol = coin.get("symbol", "")
|
||||
name = coin.get("name", "")
|
||||
price = _safe_float(coin.get("price", 0))
|
||||
|
||||
signal = None
|
||||
strength = "medium"
|
||||
reason = ""
|
||||
impact = "neutral"
|
||||
|
||||
if change > 15:
|
||||
signal = "overbought"
|
||||
strength = "strong"
|
||||
reason = f"24h涨幅{change:.1f}%,7日涨幅{change_7d:.1f}%,短期超买风险"
|
||||
impact = "bearish"
|
||||
elif change > 8:
|
||||
signal = "bullish_momentum"
|
||||
strength = "medium"
|
||||
reason = f"24h涨幅{change:.1f}%,上涨动能强劲"
|
||||
impact = "bullish"
|
||||
elif change < -15:
|
||||
signal = "oversold"
|
||||
strength = "strong"
|
||||
reason = f"24h跌幅{abs(change):.1f}%,可能超卖反弹"
|
||||
impact = "bullish"
|
||||
elif change < -8:
|
||||
signal = "bearish_momentum"
|
||||
strength = "medium"
|
||||
reason = f"24h跌幅{abs(change):.1f}%,下跌趋势明显"
|
||||
impact = "bearish"
|
||||
|
||||
if signal:
|
||||
opportunities.append({
|
||||
"symbol": symbol,
|
||||
"name": name,
|
||||
"price": price,
|
||||
"change_24h": change,
|
||||
"change_7d": change_7d,
|
||||
"signal": signal,
|
||||
"strength": strength,
|
||||
"reason": reason,
|
||||
"impact": impact,
|
||||
"market": "Crypto",
|
||||
"timestamp": int(time.time())
|
||||
})
|
||||
|
||||
|
||||
def _analyze_opportunities_stocks(opportunities: list):
|
||||
"""Scan US stocks for trading opportunities."""
|
||||
stock_data = _get_cached("stock_opportunity_prices")
|
||||
if not stock_data:
|
||||
stock_data = _fetch_stock_opportunity_prices()
|
||||
if stock_data:
|
||||
_set_cached("stock_opportunity_prices", stock_data, 3600)
|
||||
|
||||
for stock in (stock_data or []):
|
||||
change = _safe_float(stock.get("change", 0))
|
||||
symbol = stock.get("symbol", "")
|
||||
name = stock.get("name", "")
|
||||
price = _safe_float(stock.get("price", 0))
|
||||
|
||||
signal = None
|
||||
strength = "medium"
|
||||
reason = ""
|
||||
impact = "neutral"
|
||||
|
||||
# US stocks: smaller thresholds than crypto
|
||||
if change > 5:
|
||||
signal = "overbought"
|
||||
strength = "strong"
|
||||
reason = f"日涨幅{change:.1f}%,短期涨幅较大,注意回调风险"
|
||||
impact = "bearish"
|
||||
elif change > 3:
|
||||
signal = "bullish_momentum"
|
||||
strength = "medium"
|
||||
reason = f"日涨幅{change:.1f}%,上涨动能强劲"
|
||||
impact = "bullish"
|
||||
elif change < -5:
|
||||
signal = "oversold"
|
||||
strength = "strong"
|
||||
reason = f"日跌幅{abs(change):.1f}%,可能超卖反弹"
|
||||
impact = "bullish"
|
||||
elif change < -3:
|
||||
signal = "bearish_momentum"
|
||||
strength = "medium"
|
||||
reason = f"日跌幅{abs(change):.1f}%,下跌趋势明显"
|
||||
impact = "bearish"
|
||||
|
||||
if signal:
|
||||
opportunities.append({
|
||||
"symbol": symbol,
|
||||
"name": name,
|
||||
"price": price,
|
||||
"change_24h": change,
|
||||
"signal": signal,
|
||||
"strength": strength,
|
||||
"reason": reason,
|
||||
"impact": impact,
|
||||
"market": "USStock",
|
||||
"timestamp": int(time.time())
|
||||
})
|
||||
|
||||
|
||||
def _analyze_opportunities_forex(opportunities: list):
|
||||
"""Scan forex pairs for trading opportunities."""
|
||||
forex_data = _get_cached("forex_pairs")
|
||||
if not forex_data:
|
||||
forex_data = _fetch_forex_pairs()
|
||||
if forex_data:
|
||||
_set_cached("forex_pairs", forex_data, 3600)
|
||||
|
||||
for pair in (forex_data or []):
|
||||
change = _safe_float(pair.get("change", 0))
|
||||
symbol = pair.get("symbol", pair.get("name", ""))
|
||||
name = pair.get("name_cn", pair.get("name", ""))
|
||||
price = _safe_float(pair.get("price", 0))
|
||||
|
||||
signal = None
|
||||
strength = "medium"
|
||||
reason = ""
|
||||
impact = "neutral"
|
||||
|
||||
# Forex: even smaller thresholds
|
||||
if change > 1.5:
|
||||
signal = "overbought"
|
||||
strength = "strong"
|
||||
reason = f"日涨幅{change:.2f}%,汇率波动剧烈,注意回调"
|
||||
impact = "bearish"
|
||||
elif change > 0.8:
|
||||
signal = "bullish_momentum"
|
||||
strength = "medium"
|
||||
reason = f"日涨幅{change:.2f}%,上涨动能较强"
|
||||
impact = "bullish"
|
||||
elif change < -1.5:
|
||||
signal = "oversold"
|
||||
strength = "strong"
|
||||
reason = f"日跌幅{abs(change):.2f}%,汇率波动剧烈,可能反弹"
|
||||
impact = "bullish"
|
||||
elif change < -0.8:
|
||||
signal = "bearish_momentum"
|
||||
strength = "medium"
|
||||
reason = f"日跌幅{abs(change):.2f}%,下跌趋势明显"
|
||||
impact = "bearish"
|
||||
|
||||
if signal:
|
||||
opportunities.append({
|
||||
"symbol": symbol,
|
||||
"name": name,
|
||||
"price": price,
|
||||
"change_24h": change,
|
||||
"signal": signal,
|
||||
"strength": strength,
|
||||
"reason": reason,
|
||||
"impact": impact,
|
||||
"market": "Forex",
|
||||
"timestamp": int(time.time())
|
||||
})
|
||||
|
||||
|
||||
@global_market_bp.route("/opportunities", methods=["GET"])
|
||||
@login_required
|
||||
def trading_opportunities():
|
||||
"""
|
||||
Scan for trading opportunities based on technical indicators.
|
||||
Scan for trading opportunities across Crypto, US Stocks, and Forex.
|
||||
Cached for 1 hour. Pass ?force=true to skip cache.
|
||||
"""
|
||||
try:
|
||||
cached = _get_cached("trading_opportunities", 60)
|
||||
if cached:
|
||||
return jsonify({"code": 1, "msg": "success", "data": cached})
|
||||
|
||||
force = request.args.get("force", "").lower() in ("true", "1")
|
||||
|
||||
if not force:
|
||||
cached = _get_cached("trading_opportunities")
|
||||
if cached:
|
||||
return jsonify({"code": 1, "msg": "success", "data": cached})
|
||||
|
||||
opportunities = []
|
||||
|
||||
# Get crypto data
|
||||
crypto_data = _get_cached("crypto_prices")
|
||||
if not crypto_data:
|
||||
crypto_data = _fetch_crypto_prices()
|
||||
|
||||
# Analyze crypto for opportunities
|
||||
for coin in crypto_data[:15]:
|
||||
change = coin.get("change_24h", 0)
|
||||
change_7d = coin.get("change_7d", 0)
|
||||
symbol = coin.get("symbol", "")
|
||||
name = coin.get("name", "")
|
||||
price = coin.get("price", 0)
|
||||
|
||||
signal = None
|
||||
strength = "medium"
|
||||
reason = ""
|
||||
impact = "neutral"
|
||||
|
||||
if change > 15:
|
||||
signal = "overbought"
|
||||
strength = "strong"
|
||||
reason = f"24h涨幅{change:.1f}%,7日涨幅{change_7d:.1f}%,短期超买风险"
|
||||
impact = "bearish"
|
||||
elif change > 8:
|
||||
signal = "bullish_momentum"
|
||||
strength = "medium"
|
||||
reason = f"24h涨幅{change:.1f}%,上涨动能强劲"
|
||||
impact = "bullish"
|
||||
elif change < -15:
|
||||
signal = "oversold"
|
||||
strength = "strong"
|
||||
reason = f"24h跌幅{abs(change):.1f}%,可能超卖反弹"
|
||||
impact = "bullish"
|
||||
elif change < -8:
|
||||
signal = "bearish_momentum"
|
||||
strength = "medium"
|
||||
reason = f"24h跌幅{abs(change):.1f}%,下跌趋势明显"
|
||||
impact = "bearish"
|
||||
|
||||
if signal:
|
||||
opportunities.append({
|
||||
"symbol": symbol,
|
||||
"name": name,
|
||||
"price": price,
|
||||
"change_24h": change,
|
||||
"change_7d": change_7d,
|
||||
"signal": signal,
|
||||
"strength": strength,
|
||||
"reason": reason,
|
||||
"impact": impact,
|
||||
"market": "crypto",
|
||||
"timestamp": int(time.time())
|
||||
})
|
||||
|
||||
# Sort by absolute change
|
||||
|
||||
# 1) Crypto
|
||||
_analyze_opportunities_crypto(opportunities)
|
||||
|
||||
# 2) US Stocks
|
||||
_analyze_opportunities_stocks(opportunities)
|
||||
|
||||
# 3) Forex
|
||||
_analyze_opportunities_forex(opportunities)
|
||||
|
||||
# Sort by absolute change descending
|
||||
opportunities.sort(key=lambda x: abs(x.get("change_24h", 0)), reverse=True)
|
||||
|
||||
_set_cached("trading_opportunities", opportunities, 60)
|
||||
|
||||
|
||||
_set_cached("trading_opportunities", opportunities, 3600)
|
||||
|
||||
return jsonify({"code": 1, "msg": "success", "data": opportunities})
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"trading_opportunities failed: {e}", exc_info=True)
|
||||
return jsonify({"code": 0, "msg": str(e), "data": None}), 500
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Interactive Brokers API Routes
|
||||
|
||||
Standalone API endpoints for US and Hong Kong stock trading.
|
||||
Standalone API endpoints for US stock trading.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
@@ -235,7 +235,7 @@ def place_order():
|
||||
"symbol": "AAPL", // Required, symbol code
|
||||
"side": "buy", // Required, buy or sell
|
||||
"quantity": 10, // Required, number of shares
|
||||
"marketType": "USStock", // Optional, USStock or HShare, default USStock
|
||||
"marketType": "USStock", // Optional, default USStock
|
||||
"orderType": "market", // Optional, market or limit, default market
|
||||
"price": 150.00 // Required for limit orders
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ def _row_to_indicator(row: Dict[str, Any], user_id: int) -> Dict[str, Any]:
|
||||
"publish_to_community": row.get("publish_to_community") if row.get("publish_to_community") is not None else 0,
|
||||
"pricing_type": row.get("pricing_type") or "free",
|
||||
"price": row.get("price") if row.get("price") is not None else 0,
|
||||
# VIP-free indicator flag (community publishing)
|
||||
"vip_free": 1 if (row.get("vip_free") or 0) else 0,
|
||||
# Local mode: encryption is not supported; keep field for frontend compatibility (always 0).
|
||||
"is_encrypted": 0,
|
||||
"preview_image": row.get("preview_image") or "",
|
||||
@@ -131,12 +133,17 @@ def get_indicators():
|
||||
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
# Best-effort schema upgrade for VIP-free indicators
|
||||
try:
|
||||
cur.execute("ALTER TABLE qd_indicator_codes ADD COLUMN IF NOT EXISTS vip_free BOOLEAN DEFAULT FALSE")
|
||||
except Exception:
|
||||
pass
|
||||
# Get user's own indicators (both purchased and custom).
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
id, user_id, is_buy, end_time, name, code, description,
|
||||
publish_to_community, pricing_type, price, is_encrypted, preview_image,
|
||||
publish_to_community, pricing_type, price, is_encrypted, preview_image, vip_free,
|
||||
createtime, updatetime, created_at, updated_at
|
||||
FROM qd_indicator_codes
|
||||
WHERE user_id = ?
|
||||
@@ -178,6 +185,7 @@ def save_indicator():
|
||||
description = (data.get("description") or "").strip()
|
||||
publish_to_community = 1 if data.get("publishToCommunity") or data.get("publish_to_community") else 0
|
||||
pricing_type = (data.get("pricingType") or data.get("pricing_type") or "free").strip() or "free"
|
||||
vip_free = 1 if (data.get("vipFree") or data.get("vip_free")) else 0
|
||||
try:
|
||||
price = float(data.get("price") or 0)
|
||||
except Exception:
|
||||
@@ -206,6 +214,11 @@ def save_indicator():
|
||||
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
# Best-effort schema upgrade for VIP-free indicators
|
||||
try:
|
||||
cur.execute("ALTER TABLE qd_indicator_codes ADD COLUMN IF NOT EXISTS vip_free BOOLEAN DEFAULT FALSE")
|
||||
except Exception:
|
||||
pass
|
||||
if indicator_id and indicator_id > 0:
|
||||
# 检查是否从未发布改为发布,需要设置审核状态
|
||||
if publish_to_community:
|
||||
@@ -224,11 +237,12 @@ def save_indicator():
|
||||
UPDATE qd_indicator_codes
|
||||
SET name = ?, code = ?, description = ?,
|
||||
publish_to_community = ?, pricing_type = ?, price = ?, preview_image = ?,
|
||||
vip_free = ?,
|
||||
review_status = ?, review_note = '', reviewed_at = NOW(), reviewed_by = ?,
|
||||
updatetime = ?, updated_at = NOW()
|
||||
WHERE id = ? AND user_id = ? AND (is_buy IS NULL OR is_buy = 0)
|
||||
""",
|
||||
(name, code, description, publish_to_community, pricing_type, price, preview_image,
|
||||
(name, code, description, publish_to_community, pricing_type, price, preview_image, vip_free,
|
||||
new_review_status, user_id if is_admin else None, now, indicator_id, user_id),
|
||||
)
|
||||
else:
|
||||
@@ -238,10 +252,11 @@ def save_indicator():
|
||||
UPDATE qd_indicator_codes
|
||||
SET name = ?, code = ?, description = ?,
|
||||
publish_to_community = ?, pricing_type = ?, price = ?, preview_image = ?,
|
||||
vip_free = ?,
|
||||
updatetime = ?, updated_at = NOW()
|
||||
WHERE id = ? AND user_id = ? AND (is_buy IS NULL OR is_buy = 0)
|
||||
""",
|
||||
(name, code, description, publish_to_community, pricing_type, price, preview_image, now, indicator_id, user_id),
|
||||
(name, code, description, publish_to_community, pricing_type, price, preview_image, vip_free, now, indicator_id, user_id),
|
||||
)
|
||||
else:
|
||||
# 取消发布,清除审核状态
|
||||
@@ -250,6 +265,7 @@ def save_indicator():
|
||||
UPDATE qd_indicator_codes
|
||||
SET name = ?, code = ?, description = ?,
|
||||
publish_to_community = ?, pricing_type = ?, price = ?, preview_image = ?,
|
||||
vip_free = 0,
|
||||
review_status = NULL, review_note = '', reviewed_at = NULL, reviewed_by = NULL,
|
||||
updatetime = ?, updated_at = NOW()
|
||||
WHERE id = ? AND user_id = ? AND (is_buy IS NULL OR is_buy = 0)
|
||||
@@ -265,11 +281,11 @@ def save_indicator():
|
||||
"""
|
||||
INSERT INTO qd_indicator_codes
|
||||
(user_id, is_buy, end_time, name, code, description,
|
||||
publish_to_community, pricing_type, price, preview_image, review_status,
|
||||
publish_to_community, pricing_type, price, preview_image, vip_free, review_status,
|
||||
createtime, updatetime, created_at, updated_at)
|
||||
VALUES (?, 0, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
VALUES (?, 0, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
""",
|
||||
(user_id, name, code, description, publish_to_community, pricing_type, price, preview_image, review_status, now, now),
|
||||
(user_id, name, code, description, publish_to_community, pricing_type, price, preview_image, vip_free, review_status, now, now),
|
||||
)
|
||||
indicator_id = int(cur.lastrowid or 0)
|
||||
db.commit()
|
||||
|
||||
@@ -20,7 +20,7 @@ def get_kline():
|
||||
获取K线数据
|
||||
|
||||
参数:
|
||||
market: 市场类型 (Crypto, USStock, AShare, HShare, Forex, Futures)
|
||||
market: 市场类型 (Crypto, USStock, Forex, Futures)
|
||||
symbol: 交易对/股票代码
|
||||
timeframe: 时间周期 (1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W)
|
||||
limit: 数据条数 (默认300)
|
||||
|
||||
@@ -83,7 +83,7 @@ def get_public_config():
|
||||
@market_bp.route('/types', methods=['GET'])
|
||||
def get_market_types():
|
||||
"""Return supported market types for the add-watchlist modal."""
|
||||
desired_order = ['USStock', 'Crypto', 'Forex', 'Futures', 'HShare', 'AShare']
|
||||
desired_order = ['USStock', 'Crypto', 'Forex', 'Futures']
|
||||
order_rank = {v: i for i, v in enumerate(desired_order)}
|
||||
|
||||
def _normalize_item(x):
|
||||
@@ -495,22 +495,11 @@ def get_stock_name():
|
||||
stock_name = symbol # 默认使用代码
|
||||
|
||||
try:
|
||||
if market in ['USStock', 'AShare', 'HShare']:
|
||||
if market == 'USStock':
|
||||
# 对于股票,尝试获取基本信息
|
||||
import yfinance as yf
|
||||
|
||||
# 转换symbol格式
|
||||
if market == 'USStock':
|
||||
yf_symbol = symbol
|
||||
elif market == 'AShare':
|
||||
yf_symbol = symbol + '.SS' if symbol.startswith('6') else symbol + '.SZ'
|
||||
elif market == 'HShare':
|
||||
# 港股需要补齐4位数字并添加.HK
|
||||
hk_code = symbol.zfill(4)
|
||||
yf_symbol = hk_code + '.HK'
|
||||
else:
|
||||
yf_symbol = symbol
|
||||
|
||||
yf_symbol = symbol
|
||||
ticker = yf.Ticker(yf_symbol)
|
||||
info = ticker.info
|
||||
|
||||
|
||||
@@ -18,42 +18,19 @@ settings_bp = Blueprint('settings', __name__)
|
||||
ENV_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '.env')
|
||||
|
||||
# 配置项定义(分组)- 按功能模块划分,每个配置项包含描述
|
||||
# ---------------------------------------------------------------
|
||||
# 精简原则:
|
||||
# - 部署级配置(host/port/debug)不在 UI 暴露,用户通过 .env 或 docker-compose 设置
|
||||
# - 内部调优参数(超时/重试/tick间隔/向量维度等)使用默认值即可,不暴露给普通用户
|
||||
# - 只保留用户真正需要配置的功能开关和 API Key
|
||||
# ---------------------------------------------------------------
|
||||
CONFIG_SCHEMA = {
|
||||
# ==================== 1. 服务配置 ====================
|
||||
'server': {
|
||||
'title': 'Server Configuration',
|
||||
'icon': 'cloud-server',
|
||||
'order': 1,
|
||||
'items': [
|
||||
{
|
||||
'key': 'PYTHON_API_HOST',
|
||||
'label': 'Listen Address',
|
||||
'type': 'text',
|
||||
'default': '0.0.0.0',
|
||||
'description': 'Server listen address. 0.0.0.0 allows external access, 127.0.0.1 for local only'
|
||||
},
|
||||
{
|
||||
'key': 'PYTHON_API_PORT',
|
||||
'label': 'Port',
|
||||
'type': 'number',
|
||||
'default': '5000',
|
||||
'description': 'Server listen port, default 5000'
|
||||
},
|
||||
{
|
||||
'key': 'PYTHON_API_DEBUG',
|
||||
'label': 'Debug Mode',
|
||||
'type': 'boolean',
|
||||
'default': 'False',
|
||||
'description': 'Enable debug mode for development. Disable in production'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 2. 安全认证 ====================
|
||||
# ==================== 1. 安全认证 ====================
|
||||
'auth': {
|
||||
'title': 'Security & Authentication',
|
||||
'icon': 'lock',
|
||||
'order': 2,
|
||||
'order': 1,
|
||||
'items': [
|
||||
{
|
||||
'key': 'SECRET_KEY',
|
||||
@@ -86,11 +63,11 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 3. AI/LLM 配置 ====================
|
||||
# ==================== 2. AI/LLM 配置 ====================
|
||||
'ai': {
|
||||
'title': 'AI / LLM Configuration',
|
||||
'icon': 'robot',
|
||||
'order': 3,
|
||||
'order': 2,
|
||||
'items': [
|
||||
{
|
||||
'key': 'LLM_PROVIDER',
|
||||
@@ -235,13 +212,6 @@ CONFIG_SCHEMA = {
|
||||
'default': '0.7',
|
||||
'description': 'Model creativity (0-1). Lower = more deterministic'
|
||||
},
|
||||
{
|
||||
'key': 'OPENROUTER_TIMEOUT',
|
||||
'label': 'Request Timeout (sec)',
|
||||
'type': 'number',
|
||||
'default': '300',
|
||||
'description': 'API request timeout in seconds'
|
||||
},
|
||||
{
|
||||
'key': 'AI_MODELS_JSON',
|
||||
'label': 'Custom Models (JSON)',
|
||||
@@ -253,33 +223,19 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 4. 实盘交易 ====================
|
||||
# ==================== 3. 实盘交易 ====================
|
||||
'trading': {
|
||||
'title': 'Live Trading',
|
||||
'icon': 'stock',
|
||||
'order': 4,
|
||||
'order': 3,
|
||||
'items': [
|
||||
{
|
||||
'key': 'ENABLE_PENDING_ORDER_WORKER',
|
||||
'label': 'Enable Order Worker',
|
||||
'type': 'boolean',
|
||||
'default': 'True',
|
||||
'description': 'Enable background order processing worker for live trading'
|
||||
},
|
||||
{
|
||||
'key': 'PENDING_ORDER_STALE_SEC',
|
||||
'label': 'Order Stale Timeout (sec)',
|
||||
'type': 'number',
|
||||
'default': '90',
|
||||
'description': 'Mark pending order as stale after this many seconds'
|
||||
},
|
||||
{
|
||||
'key': 'ORDER_MODE',
|
||||
'label': 'Order Execution Mode',
|
||||
'type': 'select',
|
||||
'options': ['maker', 'market'],
|
||||
'default': 'maker',
|
||||
'description': 'maker: Limit order first (lower fees), market: Market order (instant fill)'
|
||||
'options': ['market', 'maker'],
|
||||
'default': 'market',
|
||||
'description': 'market: Market order (instant fill, recommended), maker: Limit order first (lower fees but may not fill)'
|
||||
},
|
||||
{
|
||||
'key': 'MAKER_WAIT_SEC',
|
||||
@@ -288,95 +244,30 @@ CONFIG_SCHEMA = {
|
||||
'default': '10',
|
||||
'description': 'Wait time for limit order fill before switching to market order'
|
||||
},
|
||||
{
|
||||
'key': 'MAKER_OFFSET_BPS',
|
||||
'label': 'Limit Order Offset (bps)',
|
||||
'type': 'number',
|
||||
'default': '2',
|
||||
'description': 'Price offset in basis points (1bps=0.01%). Buy: price*(1-offset), Sell: price*(1+offset)'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 5. 策略执行 ====================
|
||||
'strategy': {
|
||||
'title': 'Strategy Execution',
|
||||
'icon': 'fund',
|
||||
'order': 5,
|
||||
'items': [
|
||||
{
|
||||
'key': 'DISABLE_RESTORE_RUNNING_STRATEGIES',
|
||||
'label': 'Disable Auto Restore',
|
||||
'type': 'boolean',
|
||||
'default': 'False',
|
||||
'description': 'Disable automatic restore of running strategies on server restart'
|
||||
},
|
||||
{
|
||||
'key': 'STRATEGY_TICK_INTERVAL_SEC',
|
||||
'label': 'Tick Interval (sec)',
|
||||
'type': 'number',
|
||||
'default': '10',
|
||||
'description': 'Strategy main loop tick interval in seconds'
|
||||
},
|
||||
{
|
||||
'key': 'PRICE_CACHE_TTL_SEC',
|
||||
'label': 'Price Cache TTL (sec)',
|
||||
'type': 'number',
|
||||
'default': '10',
|
||||
'description': 'Time-to-live for cached price data in seconds'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 6. 数据源配置 ====================
|
||||
# ==================== 4. 数据源配置 ====================
|
||||
'data_source': {
|
||||
'title': 'Data Sources',
|
||||
'icon': 'database',
|
||||
'order': 6,
|
||||
'order': 4,
|
||||
'items': [
|
||||
{
|
||||
'key': 'DATA_SOURCE_TIMEOUT',
|
||||
'label': 'Default Timeout (sec)',
|
||||
'type': 'number',
|
||||
'default': '30',
|
||||
'description': 'Default timeout for all data source requests'
|
||||
},
|
||||
{
|
||||
'key': 'DATA_SOURCE_RETRY',
|
||||
'label': 'Retry Count',
|
||||
'type': 'number',
|
||||
'default': '3',
|
||||
'description': 'Number of retry attempts on data source failure'
|
||||
},
|
||||
{
|
||||
'key': 'DATA_SOURCE_RETRY_BACKOFF',
|
||||
'label': 'Retry Backoff (sec)',
|
||||
'type': 'number',
|
||||
'default': '0.5',
|
||||
'description': 'Backoff time between retry attempts'
|
||||
},
|
||||
{
|
||||
'key': 'CCXT_DEFAULT_EXCHANGE',
|
||||
'label': 'CCXT Default Exchange',
|
||||
'label': 'Default Crypto Exchange',
|
||||
'type': 'text',
|
||||
'default': 'coinbase',
|
||||
'link': 'https://github.com/ccxt/ccxt#supported-cryptocurrency-exchange-markets',
|
||||
'link_text': 'settings.link.supportedExchanges',
|
||||
'description': 'Default exchange for CCXT crypto data (binance, coinbase, okx, etc.)'
|
||||
},
|
||||
{
|
||||
'key': 'CCXT_TIMEOUT',
|
||||
'label': 'CCXT Timeout (ms)',
|
||||
'type': 'number',
|
||||
'default': '10000',
|
||||
'description': 'CCXT request timeout in milliseconds'
|
||||
'description': 'Default exchange for crypto data (binance, coinbase, okx, etc.)'
|
||||
},
|
||||
{
|
||||
'key': 'CCXT_PROXY',
|
||||
'label': 'CCXT Proxy',
|
||||
'label': 'Crypto Data Proxy',
|
||||
'type': 'text',
|
||||
'required': False,
|
||||
'description': 'Proxy URL for CCXT requests (e.g. socks5h://127.0.0.1:1080)'
|
||||
'description': 'Proxy URL for crypto data requests (e.g. socks5h://127.0.0.1:1080)'
|
||||
},
|
||||
{
|
||||
'key': 'FINNHUB_API_KEY',
|
||||
@@ -387,20 +278,6 @@ CONFIG_SCHEMA = {
|
||||
'link_text': 'settings.link.freeRegister',
|
||||
'description': 'Finnhub API key for US stock data (free tier available)'
|
||||
},
|
||||
{
|
||||
'key': 'FINNHUB_TIMEOUT',
|
||||
'label': 'Finnhub Timeout (sec)',
|
||||
'type': 'number',
|
||||
'default': '10',
|
||||
'description': 'Finnhub API request timeout'
|
||||
},
|
||||
{
|
||||
'key': 'FINNHUB_RATE_LIMIT',
|
||||
'label': 'Finnhub Rate Limit',
|
||||
'type': 'number',
|
||||
'default': '60',
|
||||
'description': 'Finnhub API rate limit (requests per minute)'
|
||||
},
|
||||
{
|
||||
'key': 'TIINGO_API_KEY',
|
||||
'label': 'Tiingo API Key',
|
||||
@@ -408,37 +285,16 @@ CONFIG_SCHEMA = {
|
||||
'required': False,
|
||||
'link': 'https://www.tiingo.com/account/api/token',
|
||||
'link_text': 'settings.link.getToken',
|
||||
'description': 'Tiingo API key for Forex/Metals data (free tier does not support 1-minute data)'
|
||||
},
|
||||
{
|
||||
'key': 'TIINGO_TIMEOUT',
|
||||
'label': 'Tiingo Timeout (sec)',
|
||||
'type': 'number',
|
||||
'default': '10',
|
||||
'description': 'Tiingo API request timeout'
|
||||
},
|
||||
{
|
||||
'key': 'AKSHARE_TIMEOUT',
|
||||
'label': 'Akshare Timeout (sec)',
|
||||
'type': 'number',
|
||||
'default': '30',
|
||||
'description': 'Akshare API timeout for China A-share data'
|
||||
},
|
||||
{
|
||||
'key': 'YFINANCE_TIMEOUT',
|
||||
'label': 'YFinance Timeout (sec)',
|
||||
'type': 'number',
|
||||
'default': '30',
|
||||
'description': 'Yahoo Finance API timeout'
|
||||
'description': 'Tiingo API key for Forex/Metals data'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 7. 邮件配置 (公共 SMTP) ====================
|
||||
# ==================== 5. 邮件配置 ====================
|
||||
'email': {
|
||||
'title': 'Email (SMTP)',
|
||||
'icon': 'mail',
|
||||
'order': 7,
|
||||
'order': 5,
|
||||
'items': [
|
||||
{
|
||||
'key': 'SMTP_HOST',
|
||||
@@ -452,7 +308,7 @@ CONFIG_SCHEMA = {
|
||||
'label': 'SMTP Port',
|
||||
'type': 'number',
|
||||
'default': '587',
|
||||
'description': 'SMTP port (587 for TLS, 465 for SSL, 25 for plain)'
|
||||
'description': 'SMTP port (587 for TLS, 465 for SSL)'
|
||||
},
|
||||
{
|
||||
'key': 'SMTP_USER',
|
||||
@@ -492,11 +348,11 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 9. 短信配置 ====================
|
||||
# ==================== 6. 短信配置 ====================
|
||||
'sms': {
|
||||
'title': 'SMS (Twilio)',
|
||||
'icon': 'phone',
|
||||
'order': 8,
|
||||
'order': 6,
|
||||
'items': [
|
||||
{
|
||||
'key': 'TWILIO_ACCOUNT_SID',
|
||||
@@ -524,11 +380,11 @@ CONFIG_SCHEMA = {
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 9. AI Agent 配置 ====================
|
||||
# ==================== 7. AI Agent ====================
|
||||
'agent': {
|
||||
'title': 'AI Agent',
|
||||
'icon': 'experiment',
|
||||
'order': 9,
|
||||
'order': 7,
|
||||
'items': [
|
||||
{
|
||||
'key': 'ENABLE_AGENT_MEMORY',
|
||||
@@ -537,62 +393,6 @@ CONFIG_SCHEMA = {
|
||||
'default': 'True',
|
||||
'description': 'Enable AI agent memory for learning from past trades'
|
||||
},
|
||||
{
|
||||
'key': 'AGENT_MEMORY_ENABLE_VECTOR',
|
||||
'label': 'Enable Vector Search',
|
||||
'type': 'boolean',
|
||||
'default': 'True',
|
||||
'description': 'Enable local vector similarity search for memory retrieval'
|
||||
},
|
||||
{
|
||||
'key': 'AGENT_MEMORY_EMBEDDING_DIM',
|
||||
'label': 'Embedding Dimension',
|
||||
'type': 'number',
|
||||
'default': '256',
|
||||
'description': 'Vector embedding dimension for memory storage'
|
||||
},
|
||||
{
|
||||
'key': 'AGENT_MEMORY_TOP_K',
|
||||
'label': 'Retrieval Top-K',
|
||||
'type': 'number',
|
||||
'default': '5',
|
||||
'description': 'Number of similar memories to retrieve'
|
||||
},
|
||||
{
|
||||
'key': 'AGENT_MEMORY_CANDIDATE_LIMIT',
|
||||
'label': 'Candidate Limit',
|
||||
'type': 'number',
|
||||
'default': '500',
|
||||
'description': 'Maximum candidates for similarity search'
|
||||
},
|
||||
{
|
||||
'key': 'AGENT_MEMORY_HALF_LIFE_DAYS',
|
||||
'label': 'Recency Half-life (days)',
|
||||
'type': 'number',
|
||||
'default': '30',
|
||||
'description': 'Time decay half-life for memory recency scoring'
|
||||
},
|
||||
{
|
||||
'key': 'AGENT_MEMORY_W_SIM',
|
||||
'label': 'Similarity Weight',
|
||||
'type': 'number',
|
||||
'default': '0.75',
|
||||
'description': 'Weight for similarity score in memory ranking (0-1)'
|
||||
},
|
||||
{
|
||||
'key': 'AGENT_MEMORY_W_RECENCY',
|
||||
'label': 'Recency Weight',
|
||||
'type': 'number',
|
||||
'default': '0.20',
|
||||
'description': 'Weight for recency score in memory ranking (0-1)'
|
||||
},
|
||||
{
|
||||
'key': 'AGENT_MEMORY_W_RETURNS',
|
||||
'label': 'Returns Weight',
|
||||
'type': 'number',
|
||||
'default': '0.05',
|
||||
'description': 'Weight for returns score in memory ranking (0-1)'
|
||||
},
|
||||
{
|
||||
'key': 'ENABLE_REFLECTION_WORKER',
|
||||
'label': 'Enable Auto Reflection',
|
||||
@@ -600,59 +400,30 @@ CONFIG_SCHEMA = {
|
||||
'default': 'False',
|
||||
'description': 'Enable background worker for automatic trade reflection'
|
||||
},
|
||||
{
|
||||
'key': 'REFLECTION_WORKER_INTERVAL_SEC',
|
||||
'label': 'Reflection Interval (sec)',
|
||||
'type': 'number',
|
||||
'default': '86400',
|
||||
'description': 'Interval between automatic reflection runs (default: 24h)'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 10. 网络代理 ====================
|
||||
# ==================== 8. 网络代理 ====================
|
||||
'network': {
|
||||
'title': 'Network & Proxy',
|
||||
'icon': 'global',
|
||||
'order': 10,
|
||||
'order': 8,
|
||||
'items': [
|
||||
{
|
||||
'key': 'PROXY_HOST',
|
||||
'label': 'Proxy Host',
|
||||
'type': 'text',
|
||||
'default': '127.0.0.1',
|
||||
'description': 'Proxy server hostname or IP'
|
||||
},
|
||||
{
|
||||
'key': 'PROXY_PORT',
|
||||
'label': 'Proxy Port',
|
||||
'type': 'text',
|
||||
'required': False,
|
||||
'description': 'Proxy server port (leave empty to disable proxy)'
|
||||
},
|
||||
{
|
||||
'key': 'PROXY_SCHEME',
|
||||
'label': 'Proxy Protocol',
|
||||
'type': 'select',
|
||||
'options': ['socks5h', 'socks5', 'http', 'https'],
|
||||
'default': 'socks5h',
|
||||
'description': 'Proxy protocol type. socks5h: SOCKS5 with DNS resolution'
|
||||
},
|
||||
{
|
||||
'key': 'PROXY_URL',
|
||||
'label': 'Full Proxy URL',
|
||||
'label': 'Proxy URL',
|
||||
'type': 'text',
|
||||
'required': False,
|
||||
'description': 'Complete proxy URL (overrides above settings if set)'
|
||||
'description': 'Global proxy URL (e.g. socks5h://127.0.0.1:1080 or http://proxy:8080)'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 11. 搜索配置 ====================
|
||||
# ==================== 9. 搜索配置 ====================
|
||||
'search': {
|
||||
'title': 'Web Search',
|
||||
'icon': 'search',
|
||||
'order': 11,
|
||||
'order': 9,
|
||||
'items': [
|
||||
{
|
||||
'key': 'SEARCH_PROVIDER',
|
||||
@@ -660,16 +431,8 @@ CONFIG_SCHEMA = {
|
||||
'type': 'select',
|
||||
'options': ['bocha', 'tavily', 'google', 'bing', 'none'],
|
||||
'default': 'bocha',
|
||||
'description': 'Web search provider for AI research features. Bocha recommended for A-share news'
|
||||
'description': 'Web search provider for AI research features'
|
||||
},
|
||||
{
|
||||
'key': 'SEARCH_MAX_RESULTS',
|
||||
'label': 'Max Results',
|
||||
'type': 'number',
|
||||
'default': '10',
|
||||
'description': 'Maximum search results to return'
|
||||
},
|
||||
# Tavily Search API
|
||||
{
|
||||
'key': 'TAVILY_API_KEYS',
|
||||
'label': 'Tavily API Keys',
|
||||
@@ -677,9 +440,8 @@ CONFIG_SCHEMA = {
|
||||
'required': False,
|
||||
'link': 'https://tavily.com/',
|
||||
'link_text': 'settings.link.getApiKey',
|
||||
'description': 'Tavily Search API keys, comma-separated for rotation. Free 1000 requests/month'
|
||||
'description': 'Tavily Search API keys (comma-separated). Free 1000 req/month'
|
||||
},
|
||||
# Bocha Search API
|
||||
{
|
||||
'key': 'BOCHA_API_KEYS',
|
||||
'label': 'Bocha API Keys',
|
||||
@@ -687,60 +449,16 @@ CONFIG_SCHEMA = {
|
||||
'required': False,
|
||||
'link': 'https://bochaai.com/',
|
||||
'link_text': 'settings.link.getApiKey',
|
||||
'description': 'Bocha Search API keys, comma-separated for rotation. Best for A-share news'
|
||||
},
|
||||
# SerpAPI
|
||||
{
|
||||
'key': 'SERPAPI_KEYS',
|
||||
'label': 'SerpAPI Keys',
|
||||
'type': 'password',
|
||||
'required': False,
|
||||
'link': 'https://serpapi.com/',
|
||||
'link_text': 'settings.link.getApiKey',
|
||||
'description': 'SerpAPI keys for Google/Bing search, comma-separated for rotation'
|
||||
},
|
||||
{
|
||||
'key': 'SEARCH_GOOGLE_API_KEY',
|
||||
'label': 'Google API Key',
|
||||
'type': 'password',
|
||||
'required': False,
|
||||
'link': 'https://developers.google.com/custom-search/v1/introduction',
|
||||
'link_text': 'settings.link.applyApi',
|
||||
'description': 'Google Custom Search JSON API key'
|
||||
},
|
||||
{
|
||||
'key': 'SEARCH_GOOGLE_CX',
|
||||
'label': 'Google Search Engine ID',
|
||||
'type': 'text',
|
||||
'required': False,
|
||||
'link': 'https://programmablesearchengine.google.com/controlpanel/all',
|
||||
'link_text': 'settings.link.createSearchEngine',
|
||||
'description': 'Google Programmable Search Engine ID (CX)'
|
||||
},
|
||||
{
|
||||
'key': 'SEARCH_BING_API_KEY',
|
||||
'label': 'Bing API Key',
|
||||
'type': 'password',
|
||||
'required': False,
|
||||
'link': 'https://www.microsoft.com/en-us/bing/apis/bing-web-search-api',
|
||||
'link_text': 'settings.link.applyApi',
|
||||
'description': 'Microsoft Bing Web Search API key'
|
||||
},
|
||||
{
|
||||
'key': 'INTERNAL_API_KEY',
|
||||
'label': 'Internal API Key',
|
||||
'type': 'password',
|
||||
'required': False,
|
||||
'description': 'Internal API authentication key for service-to-service calls'
|
||||
'description': 'Bocha Search API keys (comma-separated)'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 12. 注册与安全 ====================
|
||||
# ==================== 10. 注册与 OAuth ====================
|
||||
'security': {
|
||||
'title': 'Registration & Security',
|
||||
'title': 'Registration & OAuth',
|
||||
'icon': 'safety',
|
||||
'order': 12,
|
||||
'order': 10,
|
||||
'items': [
|
||||
{
|
||||
'key': 'ENABLE_REGISTRATION',
|
||||
@@ -749,6 +467,13 @@ CONFIG_SCHEMA = {
|
||||
'default': 'True',
|
||||
'description': 'Allow new users to register accounts'
|
||||
},
|
||||
{
|
||||
'key': 'FRONTEND_URL',
|
||||
'label': 'Frontend URL',
|
||||
'type': 'text',
|
||||
'default': 'http://localhost:8080',
|
||||
'description': 'Frontend URL for OAuth redirects'
|
||||
},
|
||||
{
|
||||
'key': 'TURNSTILE_SITE_KEY',
|
||||
'label': 'Turnstile Site Key',
|
||||
@@ -756,7 +481,7 @@ CONFIG_SCHEMA = {
|
||||
'required': False,
|
||||
'link': 'https://dash.cloudflare.com/?to=/:account/turnstile',
|
||||
'link_text': 'settings.link.getTurnstileKey',
|
||||
'description': 'Cloudflare Turnstile site key for CAPTCHA verification'
|
||||
'description': 'Cloudflare Turnstile site key for CAPTCHA'
|
||||
},
|
||||
{
|
||||
'key': 'TURNSTILE_SECRET_KEY',
|
||||
@@ -765,193 +490,198 @@ CONFIG_SCHEMA = {
|
||||
'required': False,
|
||||
'description': 'Cloudflare Turnstile secret key'
|
||||
},
|
||||
{
|
||||
'key': 'FRONTEND_URL',
|
||||
'label': 'Frontend URL',
|
||||
'type': 'text',
|
||||
'default': 'http://localhost:8080',
|
||||
'description': 'Frontend URL for OAuth redirects'
|
||||
},
|
||||
{
|
||||
'key': 'GOOGLE_CLIENT_ID',
|
||||
'label': 'Google Client ID',
|
||||
'label': 'Google OAuth Client ID',
|
||||
'type': 'text',
|
||||
'required': False,
|
||||
'link': 'https://console.cloud.google.com/apis/credentials',
|
||||
'link_text': 'settings.link.getGoogleCredentials',
|
||||
'description': 'Google OAuth Client ID'
|
||||
'description': 'Google OAuth Client ID for Google login'
|
||||
},
|
||||
{
|
||||
'key': 'GOOGLE_CLIENT_SECRET',
|
||||
'label': 'Google Client Secret',
|
||||
'label': 'Google OAuth Secret',
|
||||
'type': 'password',
|
||||
'required': False,
|
||||
'description': 'Google OAuth Client Secret'
|
||||
},
|
||||
{
|
||||
'key': 'GOOGLE_REDIRECT_URI',
|
||||
'label': 'Google Redirect URI',
|
||||
'type': 'text',
|
||||
'default': 'http://localhost:5000/api/auth/oauth/google/callback',
|
||||
'description': 'Google OAuth callback URL'
|
||||
},
|
||||
{
|
||||
'key': 'GITHUB_CLIENT_ID',
|
||||
'label': 'GitHub Client ID',
|
||||
'label': 'GitHub OAuth Client ID',
|
||||
'type': 'text',
|
||||
'required': False,
|
||||
'link': 'https://github.com/settings/developers',
|
||||
'link_text': 'settings.link.getGithubCredentials',
|
||||
'description': 'GitHub OAuth Client ID'
|
||||
'description': 'GitHub OAuth Client ID for GitHub login'
|
||||
},
|
||||
{
|
||||
'key': 'GITHUB_CLIENT_SECRET',
|
||||
'label': 'GitHub Client Secret',
|
||||
'label': 'GitHub OAuth Secret',
|
||||
'type': 'password',
|
||||
'required': False,
|
||||
'description': 'GitHub OAuth Client Secret'
|
||||
},
|
||||
{
|
||||
'key': 'GITHUB_REDIRECT_URI',
|
||||
'label': 'GitHub Redirect URI',
|
||||
'type': 'text',
|
||||
'default': 'http://localhost:5000/api/auth/oauth/github/callback',
|
||||
'description': 'GitHub OAuth callback URL'
|
||||
},
|
||||
{
|
||||
'key': 'SECURITY_IP_MAX_ATTEMPTS',
|
||||
'label': 'IP Max Failed Attempts',
|
||||
'type': 'number',
|
||||
'default': '10',
|
||||
'description': 'Block IP after this many failed login attempts'
|
||||
},
|
||||
{
|
||||
'key': 'SECURITY_IP_WINDOW_MINUTES',
|
||||
'label': 'IP Window (minutes)',
|
||||
'type': 'number',
|
||||
'default': '5',
|
||||
'description': 'Time window for counting IP failed attempts'
|
||||
},
|
||||
{
|
||||
'key': 'SECURITY_IP_BLOCK_MINUTES',
|
||||
'label': 'IP Block Duration (minutes)',
|
||||
'type': 'number',
|
||||
'default': '15',
|
||||
'description': 'How long to block IP after exceeding limit'
|
||||
},
|
||||
{
|
||||
'key': 'SECURITY_ACCOUNT_MAX_ATTEMPTS',
|
||||
'label': 'Account Max Failed Attempts',
|
||||
'type': 'number',
|
||||
'default': '5',
|
||||
'description': 'Lock account after this many failed login attempts'
|
||||
},
|
||||
{
|
||||
'key': 'SECURITY_ACCOUNT_WINDOW_MINUTES',
|
||||
'label': 'Account Window (minutes)',
|
||||
'type': 'number',
|
||||
'default': '60',
|
||||
'description': 'Time window for counting account failed attempts'
|
||||
},
|
||||
{
|
||||
'key': 'SECURITY_ACCOUNT_BLOCK_MINUTES',
|
||||
'label': 'Account Block Duration (minutes)',
|
||||
'type': 'number',
|
||||
'default': '30',
|
||||
'description': 'How long to lock account after exceeding limit'
|
||||
},
|
||||
{
|
||||
'key': 'VERIFICATION_CODE_EXPIRE_MINUTES',
|
||||
'label': 'Verification Code Expiry (minutes)',
|
||||
'type': 'number',
|
||||
'default': '10',
|
||||
'description': 'Email verification code validity period'
|
||||
},
|
||||
{
|
||||
'key': 'VERIFICATION_CODE_RATE_LIMIT',
|
||||
'label': 'Code Rate Limit (seconds)',
|
||||
'type': 'number',
|
||||
'default': '60',
|
||||
'description': 'Minimum time between verification code requests per email'
|
||||
},
|
||||
{
|
||||
'key': 'VERIFICATION_CODE_IP_HOURLY_LIMIT',
|
||||
'label': 'Code Hourly Limit per IP',
|
||||
'type': 'number',
|
||||
'default': '10',
|
||||
'description': 'Maximum verification codes per IP per hour'
|
||||
},
|
||||
{
|
||||
'key': 'VERIFICATION_CODE_MAX_ATTEMPTS',
|
||||
'label': 'Code Max Attempts',
|
||||
'type': 'number',
|
||||
'default': '5',
|
||||
'description': 'Maximum attempts to verify a code before lockout'
|
||||
},
|
||||
{
|
||||
'key': 'VERIFICATION_CODE_LOCK_MINUTES',
|
||||
'label': 'Code Lock Minutes',
|
||||
'type': 'number',
|
||||
'default': '30',
|
||||
'description': 'Lockout duration after exceeding max attempts'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 13. 计费配置 ====================
|
||||
# ==================== 11. 计费配置 ====================
|
||||
'billing': {
|
||||
'title': 'Billing & Credits',
|
||||
'icon': 'dollar',
|
||||
'order': 13,
|
||||
'order': 11,
|
||||
'items': [
|
||||
{
|
||||
'key': 'BILLING_ENABLED',
|
||||
'label': 'Enable Billing',
|
||||
'type': 'boolean',
|
||||
'default': 'False',
|
||||
'description': 'Enable billing system. When enabled, users need credits to use certain features'
|
||||
'description': 'Enable billing system. Users need credits to use certain features'
|
||||
},
|
||||
{
|
||||
'key': 'BILLING_VIP_BYPASS',
|
||||
'label': 'VIP Free',
|
||||
'label': 'VIP Bypass (Legacy)',
|
||||
'type': 'boolean',
|
||||
'default': 'True',
|
||||
'description': 'VIP users can use all paid features for free during VIP period'
|
||||
'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) =====
|
||||
{
|
||||
'key': 'MEMBERSHIP_MONTHLY_PRICE_USD',
|
||||
'label': 'Monthly Membership Price (USD)',
|
||||
'type': 'number',
|
||||
'default': '19.9',
|
||||
'description': 'Monthly membership price in USD (mock payment in current version)'
|
||||
},
|
||||
{
|
||||
'key': 'MEMBERSHIP_MONTHLY_CREDITS',
|
||||
'label': 'Monthly Membership Bonus Credits',
|
||||
'type': 'number',
|
||||
'default': '500',
|
||||
'description': 'Credits granted immediately after purchasing monthly membership'
|
||||
},
|
||||
{
|
||||
'key': 'MEMBERSHIP_YEARLY_PRICE_USD',
|
||||
'label': 'Yearly Membership Price (USD)',
|
||||
'type': 'number',
|
||||
'default': '199',
|
||||
'description': 'Yearly membership price in USD (mock payment in current version)'
|
||||
},
|
||||
{
|
||||
'key': 'MEMBERSHIP_YEARLY_CREDITS',
|
||||
'label': 'Yearly Membership Bonus Credits',
|
||||
'type': 'number',
|
||||
'default': '8000',
|
||||
'description': 'Credits granted immediately after purchasing yearly membership'
|
||||
},
|
||||
{
|
||||
'key': 'MEMBERSHIP_LIFETIME_PRICE_USD',
|
||||
'label': 'Lifetime Membership Price (USD)',
|
||||
'type': 'number',
|
||||
'default': '499',
|
||||
'description': 'Lifetime membership price in USD (mock payment in current version)'
|
||||
},
|
||||
{
|
||||
'key': 'MEMBERSHIP_LIFETIME_MONTHLY_CREDITS',
|
||||
'label': 'Lifetime Membership Monthly Credits',
|
||||
'type': 'number',
|
||||
'default': '800',
|
||||
'description': 'Credits granted every 30 days for lifetime members'
|
||||
},
|
||||
|
||||
# ===== USDT Pay (方案B:每单独立地址) =====
|
||||
{
|
||||
'key': 'USDT_PAY_ENABLED',
|
||||
'label': 'Enable USDT Pay',
|
||||
'type': 'boolean',
|
||||
'default': 'False',
|
||||
'description': 'Enable USDT scan-to-pay flow (per-order unique address)'
|
||||
},
|
||||
{
|
||||
'key': 'USDT_PAY_CHAIN',
|
||||
'label': 'USDT Chain',
|
||||
'type': 'select',
|
||||
'default': 'TRC20',
|
||||
'options': ['TRC20'],
|
||||
'description': 'Currently only TRC20 is supported'
|
||||
},
|
||||
{
|
||||
'key': 'USDT_TRC20_XPUB',
|
||||
'label': 'TRC20 XPUB (Watch-only)',
|
||||
'type': 'password',
|
||||
'required': False,
|
||||
'description': 'Watch-only xpub used to derive per-order deposit addresses. Do NOT paste private key.'
|
||||
},
|
||||
{
|
||||
'key': 'USDT_TRC20_CONTRACT',
|
||||
'label': 'USDT TRC20 Contract',
|
||||
'type': 'text',
|
||||
'default': 'TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj',
|
||||
'description': 'USDT contract address on TRON'
|
||||
},
|
||||
{
|
||||
'key': 'TRONGRID_BASE_URL',
|
||||
'label': 'TronGrid Base URL',
|
||||
'type': 'text',
|
||||
'default': 'https://api.trongrid.io',
|
||||
'description': 'TronGrid API base URL'
|
||||
},
|
||||
{
|
||||
'key': 'TRONGRID_API_KEY',
|
||||
'label': 'TronGrid API Key',
|
||||
'type': 'password',
|
||||
'required': False,
|
||||
'description': 'Optional TronGrid API key for higher rate limits'
|
||||
},
|
||||
{
|
||||
'key': 'USDT_PAY_CONFIRM_SECONDS',
|
||||
'label': 'Confirm Delay (sec)',
|
||||
'type': 'number',
|
||||
'default': '30',
|
||||
'description': 'Delay before marking a paid transaction as confirmed (TRC20)'
|
||||
},
|
||||
{
|
||||
'key': 'USDT_PAY_EXPIRE_MINUTES',
|
||||
'label': 'Order Expire (min)',
|
||||
'type': 'number',
|
||||
'default': '30',
|
||||
'description': 'USDT payment order expiration time in minutes'
|
||||
},
|
||||
{
|
||||
'key': 'BILLING_COST_AI_ANALYSIS',
|
||||
'label': 'AI Analysis Cost',
|
||||
'type': 'number',
|
||||
'default': '10',
|
||||
'description': 'Credits consumed per AI analysis request'
|
||||
'description': 'Credits per AI analysis request'
|
||||
},
|
||||
{
|
||||
'key': 'BILLING_COST_STRATEGY_RUN',
|
||||
'label': 'Strategy Run Cost',
|
||||
'type': 'number',
|
||||
'default': '5',
|
||||
'description': 'Credits consumed when starting a strategy'
|
||||
'description': 'Credits per strategy start'
|
||||
},
|
||||
{
|
||||
'key': 'BILLING_COST_BACKTEST',
|
||||
'label': 'Backtest Cost',
|
||||
'type': 'number',
|
||||
'default': '3',
|
||||
'description': 'Credits consumed per backtest run'
|
||||
'description': 'Credits per backtest run'
|
||||
},
|
||||
{
|
||||
'key': 'BILLING_COST_PORTFOLIO_MONITOR',
|
||||
'label': 'Portfolio Monitor Cost',
|
||||
'type': 'number',
|
||||
'default': '8',
|
||||
'description': 'Credits consumed per portfolio AI monitoring run'
|
||||
'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 customer service URL for recharge inquiries'
|
||||
'description': 'Telegram URL for recharge inquiries'
|
||||
},
|
||||
{
|
||||
'key': 'CREDITS_REGISTER_BONUS',
|
||||
@@ -965,44 +695,23 @@ CONFIG_SCHEMA = {
|
||||
'label': 'Referral Bonus',
|
||||
'type': 'number',
|
||||
'default': '50',
|
||||
'description': 'Credits awarded to referrer when someone signs up with their code'
|
||||
'description': 'Credits awarded to referrer for each signup'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
# ==================== 14. 应用配置 ====================
|
||||
# ==================== 12. 应用功能 ====================
|
||||
'app': {
|
||||
'title': 'Application',
|
||||
'icon': 'appstore',
|
||||
'order': 14,
|
||||
'order': 12,
|
||||
'items': [
|
||||
{
|
||||
'key': 'CORS_ORIGINS',
|
||||
'label': 'CORS Origins',
|
||||
'type': 'text',
|
||||
'default': '*',
|
||||
'description': 'Allowed CORS origins (* for all, or comma-separated list)'
|
||||
},
|
||||
{
|
||||
'key': 'RATE_LIMIT',
|
||||
'label': 'Rate Limit (req/min)',
|
||||
'type': 'number',
|
||||
'default': '100',
|
||||
'description': 'API rate limit per IP per minute'
|
||||
},
|
||||
{
|
||||
'key': 'ENABLE_CACHE',
|
||||
'label': 'Enable Cache',
|
||||
'type': 'boolean',
|
||||
'default': 'False',
|
||||
'description': 'Enable response caching for improved performance'
|
||||
},
|
||||
{
|
||||
'key': 'ENABLE_REQUEST_LOG',
|
||||
'label': 'Enable Request Log',
|
||||
'type': 'boolean',
|
||||
'default': 'True',
|
||||
'description': 'Log all API requests for debugging'
|
||||
'description': 'Allowed CORS origins (* for all, or comma-separated URLs)'
|
||||
},
|
||||
{
|
||||
'key': 'ENABLE_AI_ANALYSIS',
|
||||
|
||||
@@ -985,11 +985,61 @@ def get_system_strategies():
|
||||
'updated_at': updated_at
|
||||
})
|
||||
|
||||
# Compute summary stats
|
||||
all_running = [i for i in items if i['status'] == 'running']
|
||||
total_capital = sum(i['initial_capital'] for i in items)
|
||||
total_system_pnl = sum(i['total_pnl'] for i in items)
|
||||
total_running = len(all_running)
|
||||
# Compute summary stats from all matched strategies (not just current page items).
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
|
||||
# Aggregate strategy counts/capital by execution mode and running status.
|
||||
agg_sql = f"""
|
||||
SELECT
|
||||
COUNT(*) AS total_strategies,
|
||||
COALESCE(SUM(s.initial_capital), 0) AS total_capital,
|
||||
COALESCE(SUM(CASE WHEN s.status = 'running' THEN 1 ELSE 0 END), 0) AS running_strategies,
|
||||
COALESCE(SUM(CASE WHEN s.execution_mode = 'live' THEN 1 ELSE 0 END), 0) AS live_strategies,
|
||||
COALESCE(SUM(CASE WHEN s.execution_mode = 'signal' THEN 1 ELSE 0 END), 0) AS signal_strategies,
|
||||
COALESCE(SUM(CASE WHEN s.status = 'running' AND s.execution_mode = 'live' THEN 1 ELSE 0 END), 0) AS running_live_strategies,
|
||||
COALESCE(SUM(CASE WHEN s.status = 'running' AND s.execution_mode = 'signal' THEN 1 ELSE 0 END), 0) AS running_signal_strategies,
|
||||
COALESCE(SUM(CASE WHEN s.execution_mode = 'live' THEN s.initial_capital ELSE 0 END), 0) AS live_capital,
|
||||
COALESCE(SUM(CASE WHEN s.execution_mode = 'signal' THEN s.initial_capital ELSE 0 END), 0) AS signal_capital
|
||||
FROM qd_strategies_trading s
|
||||
LEFT JOIN qd_users u ON u.id = s.user_id
|
||||
{where_clause}
|
||||
"""
|
||||
cur.execute(agg_sql, tuple(params))
|
||||
agg_row = cur.fetchone() or {}
|
||||
|
||||
# Aggregate unrealized pnl from current positions.
|
||||
unreal_sql = f"""
|
||||
SELECT COALESCE(SUM(p.unrealized_pnl), 0) AS total_unrealized,
|
||||
COALESCE(SUM(CASE WHEN s.execution_mode = 'live' THEN p.unrealized_pnl ELSE 0 END), 0) AS live_unrealized,
|
||||
COALESCE(SUM(CASE WHEN s.execution_mode = 'signal' THEN p.unrealized_pnl ELSE 0 END), 0) AS signal_unrealized
|
||||
FROM qd_strategy_positions p
|
||||
JOIN qd_strategies_trading s ON s.id = p.strategy_id
|
||||
LEFT JOIN qd_users u ON u.id = s.user_id
|
||||
{where_clause}
|
||||
"""
|
||||
cur.execute(unreal_sql, tuple(params))
|
||||
unreal_row = cur.fetchone() or {}
|
||||
|
||||
# Aggregate realized pnl from trade history.
|
||||
realized_sql = f"""
|
||||
SELECT COALESCE(SUM(t.profit), 0) AS total_realized,
|
||||
COALESCE(SUM(CASE WHEN s.execution_mode = 'live' THEN t.profit ELSE 0 END), 0) AS live_realized,
|
||||
COALESCE(SUM(CASE WHEN s.execution_mode = 'signal' THEN t.profit ELSE 0 END), 0) AS signal_realized
|
||||
FROM qd_strategy_trades t
|
||||
JOIN qd_strategies_trading s ON s.id = t.strategy_id
|
||||
LEFT JOIN qd_users u ON u.id = s.user_id
|
||||
{where_clause}
|
||||
"""
|
||||
cur.execute(realized_sql, tuple(params))
|
||||
realized_row = cur.fetchone() or {}
|
||||
cur.close()
|
||||
|
||||
total_capital = float(agg_row.get('total_capital') or 0)
|
||||
total_running = int(agg_row.get('running_strategies') or 0)
|
||||
total_system_pnl = float(unreal_row.get('total_unrealized') or 0) + float(realized_row.get('total_realized') or 0)
|
||||
live_pnl = float(unreal_row.get('live_unrealized') or 0) + float(realized_row.get('live_realized') or 0)
|
||||
signal_pnl = float(unreal_row.get('signal_unrealized') or 0) + float(realized_row.get('signal_realized') or 0)
|
||||
|
||||
return jsonify({
|
||||
'code': 1,
|
||||
@@ -1000,11 +1050,19 @@ def get_system_strategies():
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'summary': {
|
||||
'total_strategies': total,
|
||||
'total_strategies': int(agg_row.get('total_strategies') or total),
|
||||
'running_strategies': total_running,
|
||||
'total_capital': round(total_capital, 2),
|
||||
'total_pnl': round(total_system_pnl, 4),
|
||||
'total_roi': round((total_system_pnl / total_capital * 100) if total_capital > 0 else 0, 2)
|
||||
'total_roi': round((total_system_pnl / total_capital * 100) if total_capital > 0 else 0, 2),
|
||||
'live_strategies': int(agg_row.get('live_strategies') or 0),
|
||||
'signal_strategies': int(agg_row.get('signal_strategies') or 0),
|
||||
'running_live_strategies': int(agg_row.get('running_live_strategies') or 0),
|
||||
'running_signal_strategies': int(agg_row.get('running_signal_strategies') or 0),
|
||||
'live_capital': round(float(agg_row.get('live_capital') or 0), 2),
|
||||
'signal_capital': round(float(agg_row.get('signal_capital') or 0), 2),
|
||||
'live_pnl': round(live_pnl, 4),
|
||||
'signal_pnl': round(signal_pnl, 4)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user