This commit is contained in:
dienakdz
2026-04-08 09:12:41 +07:00
147 changed files with 4587 additions and 639 deletions
+128 -22
View File
@@ -4,6 +4,7 @@ Trading Strategy API Routes
from flask import Blueprint, request, jsonify, g
from datetime import datetime
import json
import re
import traceback
import time
@@ -14,6 +15,11 @@ from app.services.strategy_snapshot import StrategySnapshotResolver
from app import get_trading_executor
from app.utils.logger import get_logger
from app.utils.db import get_db_connection
try:
from psycopg2.errors import UndefinedTable as PgUndefinedTable
except Exception: # pragma: no cover
PgUndefinedTable = None # type: ignore
from app.utils.auth import login_required
from app.data_sources import DataSourceFactory
@@ -661,6 +667,15 @@ def get_equity_curve():
(strategy_id,)
)
rows = cur.fetchall() or []
cur.execute(
"""
SELECT COALESCE(SUM(unrealized_pnl), 0) AS u
FROM qd_strategy_positions
WHERE strategy_id = ?
""",
(strategy_id,),
)
prow = cur.fetchone() or {}
cur.close()
equity = initial
@@ -677,7 +692,17 @@ def get_equity_curve():
ts = int(created_at)
else:
ts = int(time.time())
curve.append({'time': ts, 'equity': equity})
curve.append({'time': ts, 'equity': round(equity, 2)})
# 将未实现盈亏并入曲线末端,便于「持仓中」也能在绩效里看到浮动权益
try:
unreal = float(prow.get('u') or prow.get('U') or 0)
except Exception:
unreal = 0.0
live_equity = float(equity) + unreal
now_ts = int(time.time())
if abs(unreal) > 1e-12 or not curve:
curve.append({'time': now_ts, 'equity': round(live_equity, 2)})
return jsonify({'code': 1, 'msg': 'success', 'data': curve})
except Exception as e:
@@ -770,15 +795,16 @@ def start_strategy():
# Get strategy type
strategy_type = get_strategy_service().get_strategy_type(strategy_id)
# Update strategy status
get_strategy_service().update_strategy_status(strategy_id, 'running', user_id=user_id)
# Local backend: AI strategy executor was removed. Only indicator strategies are supported.
if strategy_type == 'PromptBasedStrategy':
return jsonify({'code': 0, 'msg': 'AI strategy has been removed; local edition does not support starting AI strategies', 'data': None}), 400
# Indicator strategy
# IndicatorStrategy and ScriptStrategy are executed by TradingExecutor.
if strategy_type == 'PromptBasedStrategy':
return jsonify({
'code': 0,
'msg': 'AI strategy has been removed; local edition does not support starting AI strategies',
'data': None
}), 400
get_strategy_service().update_strategy_status(strategy_id, 'running', user_id=user_id)
success = get_trading_executor().start_strategy(strategy_id)
if not success:
@@ -1241,12 +1267,65 @@ def verify_strategy_code():
@strategy_bp.route('/strategies/ai-generate', methods=['POST'])
@login_required
def ai_generate_strategy():
"""Generate strategy code using AI."""
"""Generate strategy code or suggest template parameter updates using AI."""
try:
payload = request.get_json() or {}
prompt = payload.get('prompt', '')
if not prompt.strip():
return jsonify({'code': '', 'msg': 'Prompt is empty'})
return jsonify({'code': '', 'msg': 'Prompt is empty', 'params': None})
intent = (payload.get('intent') or 'generate_code').strip()
from app.services.llm import LLMService
llm = LLMService()
api_key = llm.get_api_key()
if not api_key:
return jsonify({'code': '', 'msg': 'No LLM API key configured', 'params': None})
if intent == 'adjust_params':
template_key = payload.get('template_key') or ''
current_params = payload.get('params') or {}
code_snapshot = (payload.get('code') or '')[:8000]
system_prompt = """You tune quantitative strategy template parameters from the user's request.
Return ONLY a single JSON object: keys are parameter names (strings), values are JSON numbers or booleans.
You may return a partial object (only keys that should change) or a full object.
Do not use markdown fences, do not add explanations before or after the JSON."""
user_content = (
f"Template key: {template_key}\n"
f"Current parameters (JSON):\n{json.dumps(current_params, ensure_ascii=False)}\n\n"
f"Strategy code excerpt (context):\n{code_snapshot}\n\n"
f"User request:\n{prompt.strip()}\n\n"
"Respond with JSON only."
)
content = llm.call_llm_api(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
],
model=llm.get_code_generation_model(),
temperature=0.3,
use_json_mode=False
)
raw = (content or '').strip()
if raw.startswith('```'):
raw = re.sub(r'^```[a-zA-Z]*', '', raw).strip()
if raw.endswith('```'):
raw = raw[:-3].strip()
updates = None
try:
updates = json.loads(raw)
except json.JSONDecodeError:
m = re.search(r'\{[\s\S]*\}', raw)
if m:
try:
updates = json.loads(m.group(0))
except json.JSONDecodeError:
updates = None
if not isinstance(updates, dict):
return jsonify({'code': '', 'params': None, 'msg': 'AI did not return valid JSON parameters'})
return jsonify({'code': '', 'params': updates, 'msg': 'success'})
system_prompt = """You are a quantitative trading strategy code generator.
Generate Python strategy code that follows this framework:
@@ -1264,16 +1343,26 @@ Generate Python strategy code that follows this framework:
Return ONLY the Python code, no explanations."""
from app.services.llm import LLMService
llm = LLMService()
api_key = llm.get_api_key()
if not api_key:
return jsonify({'code': '', 'msg': 'No LLM API key configured'})
extra = ''
template_key = payload.get('template_key')
params = payload.get('params')
code_ctx = (payload.get('code') or '').strip()
if template_key or params is not None or code_ctx:
extra_parts = []
if template_key:
extra_parts.append(f"Current template key: {template_key}")
if isinstance(params, dict) and params:
extra_parts.append('Current template parameters (JSON):\n' + json.dumps(params, ensure_ascii=False))
if code_ctx:
extra_parts.append('Current code (may be long):\n' + code_ctx[:12000])
extra = '\n\n' + '\n\n'.join(extra_parts)
user_prompt = prompt.strip() + extra
content = llm.call_llm_api(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
{"role": "user", "content": user_prompt},
],
model=llm.get_code_generation_model(),
temperature=0.7,
@@ -1290,12 +1379,12 @@ Return ONLY the Python code, no explanations."""
content = content.strip()
if content:
return jsonify({'code': content, 'msg': 'success'})
return jsonify({'code': content, 'msg': 'success', 'params': None})
else:
return jsonify({'code': '', 'msg': 'AI generation returned empty result'})
return jsonify({'code': '', 'msg': 'AI generation returned empty result', 'params': None})
except Exception as e:
logger.error(f"ai_generate_strategy failed: {str(e)}")
return jsonify({'code': '', 'msg': str(e)})
return jsonify({'code': '', 'msg': str(e), 'params': None})
@strategy_bp.route('/strategies/performance', methods=['GET'])
@@ -1326,11 +1415,16 @@ def get_strategy_performance():
def get_strategy_logs():
"""Get strategy running logs."""
try:
user_id = g.user_id
strategy_id = request.args.get('id')
limit = int(request.args.get('limit', 200))
if not strategy_id:
return jsonify({'code': 0, 'msg': 'Strategy ID required'})
st = get_strategy_service().get_strategy(int(strategy_id), user_id=user_id)
if not st:
return jsonify({'code': 0, 'msg': 'Strategy not found'}), 404
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -1346,10 +1440,22 @@ def get_strategy_logs():
rows = cur.fetchall() or []
cur.close()
logs = list(reversed(rows))
out = []
for r in rows or []:
if not isinstance(r, dict):
continue
rr = dict(r)
ts = rr.get('timestamp')
if ts is not None and hasattr(ts, 'isoformat'):
rr['timestamp'] = ts.isoformat()
out.append(rr)
logs = list(reversed(out))
return jsonify({'code': 1, 'msg': 'success', 'data': logs})
except Exception as e:
if 'qd_strategy_logs' in str(e) and ('does not exist' in str(e) or 'no such table' in str(e)):
if PgUndefinedTable is not None and isinstance(e, PgUndefinedTable):
return jsonify({'code': 1, 'msg': 'success', 'data': []})
el = str(e).lower()
if 'qd_strategy_logs' in el and ('does not exist' in el or 'no such table' in el):
return jsonify({'code': 1, 'msg': 'success', 'data': []})
logger.error(f"get_strategy_logs failed: {str(e)}")
return jsonify({'code': 0, 'msg': str(e)}), 500