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:
TIANHE
2026-02-27 01:57:04 +08:00
parent ae82cc0d4e
commit ffdd2ffbae
71 changed files with 4067 additions and 6073 deletions
+3 -1
View File
@@ -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')
+105
View File
@@ -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),
+255 -112
View File
@@ -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
+2 -2
View File
@@ -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
}
+22 -6
View File
@@ -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()
+1 -1
View File
@@ -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)
+3 -14
View File
@@ -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
+173 -464
View File
@@ -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',
+65 -7
View File
@@ -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)
}
}
})