Signed-off-by: Dinger <quantdinger@gmail.com>
This commit is contained in:
Dinger
2026-03-25 18:08:45 +08:00
parent 05117f3f5b
commit 15c901364b
4 changed files with 199 additions and 12 deletions
+150
View File
@@ -1026,4 +1026,154 @@ def clear_notifications():
return jsonify({'code': 1, 'msg': 'success'})
except Exception as e:
logger.error(f"clear_notifications failed: {str(e)}")
return jsonify({'code': 0, 'msg': str(e)}), 500
# ===== Script Strategy Endpoints =====
@strategy_bp.route('/strategies/verify-code', methods=['POST'])
@login_required
def verify_strategy_code():
"""Verify script strategy code syntax and safety."""
try:
payload = request.get_json() or {}
code = payload.get('code', '')
if not code.strip():
return jsonify({'success': False, 'message': 'Code is empty'})
required_funcs = ['on_bar', 'on_init']
found = [f for f in required_funcs if f'def {f}' in code]
missing = [f for f in required_funcs if f not in found]
if missing:
return jsonify({
'success': False,
'message': f'Missing required functions: {", ".join(missing)}'
})
try:
compile(code, '<strategy>', 'exec')
except SyntaxError as se:
return jsonify({
'success': False,
'message': f'Syntax error at line {se.lineno}: {se.msg}'
})
return jsonify({'success': True, 'message': 'Code verification passed'})
except Exception as e:
logger.error(f"verify_strategy_code failed: {str(e)}")
return jsonify({'success': False, 'message': str(e)})
@strategy_bp.route('/strategies/ai-generate', methods=['POST'])
@login_required
def ai_generate_strategy():
"""Generate strategy code using AI."""
try:
payload = request.get_json() or {}
prompt = payload.get('prompt', '')
if not prompt.strip():
return jsonify({'code': '', 'msg': 'Prompt is empty'})
system_prompt = """You are a quantitative trading strategy code generator.
Generate Python strategy code that follows this framework:
- def on_init(ctx): Initialize strategy parameters using ctx.param(name, default)
- def on_bar(ctx, bar): Core logic called on each K-line bar
- bar has: open, high, low, close, volume, timestamp
- ctx.buy(price, amount), ctx.sell(price, amount), ctx.close_position()
- ctx.position (current position), ctx.balance, ctx.equity
- ctx.bars(n) to get last N bars, ctx.log(message) to log
- def on_order_filled(ctx, order): Optional callback when order fills
- def on_stop(ctx): Optional cleanup when strategy stops
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'})
content = llm.call_llm_api(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
],
model=llm.get_code_generation_model(),
temperature=0.7,
use_json_mode=False
)
content = content.strip()
if content.startswith("```python"):
content = content[9:]
elif content.startswith("```"):
content = content[3:]
if content.endswith("```"):
content = content[:-3]
content = content.strip()
if content:
return jsonify({'code': content, 'msg': 'success'})
else:
return jsonify({'code': '', 'msg': 'AI generation returned empty result'})
except Exception as e:
logger.error(f"ai_generate_strategy failed: {str(e)}")
return jsonify({'code': '', 'msg': str(e)})
@strategy_bp.route('/strategies/performance', methods=['GET'])
@login_required
def get_strategy_performance():
"""Get strategy performance metrics (aggregated from equity curve and trades)."""
try:
strategy_id = request.args.get('id')
if not strategy_id:
return jsonify({'code': 0, 'msg': 'Strategy ID required'})
svc = get_strategy_service()
equity_data = svc.get_equity_curve(int(strategy_id))
return jsonify({
'code': 1,
'msg': 'success',
'data': {
'equity_curve': equity_data
}
})
except Exception as e:
logger.error(f"get_strategy_performance failed: {str(e)}")
return jsonify({'code': 0, 'msg': str(e)}), 500
@strategy_bp.route('/strategies/logs', methods=['GET'])
@login_required
def get_strategy_logs():
"""Get strategy running logs."""
try:
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'})
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
SELECT id, strategy_id, level, message, timestamp
FROM qd_strategy_logs
WHERE strategy_id = ?
ORDER BY id DESC
LIMIT ?
""",
(int(strategy_id), limit)
)
rows = cur.fetchall() or []
cur.close()
logs = list(reversed(rows))
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)):
return jsonify({'code': 1, 'msg': 'success', 'data': []})
logger.error(f"get_strategy_logs failed: {str(e)}")
return jsonify({'code': 0, 'msg': str(e)}), 500
+20 -9
View File
@@ -1274,15 +1274,21 @@ class BacktestService:
signals = pd.Series(0, index=df.index)
try:
# Prepare execution environment
# Reset DatetimeIndex to integer so user code can use df.at[0, ...] or df.iloc[0, ...]
df_for_exec = df.copy()
if isinstance(df_for_exec.index, pd.DatetimeIndex):
df_for_exec = df_for_exec.reset_index(drop=False)
if 'time' not in df_for_exec.columns:
df_for_exec.rename(columns={df_for_exec.columns[0]: 'time'}, inplace=True)
local_vars = {
'df': df.copy(),
'open': df['open'],
'high': df['high'],
'low': df['low'],
'close': df['close'],
'volume': df['volume'],
'signals': signals,
'df': df_for_exec,
'open': df_for_exec['open'],
'high': df_for_exec['high'],
'low': df_for_exec['low'],
'close': df_for_exec['close'],
'volume': df_for_exec['volume'],
'signals': pd.Series(0, index=df_for_exec.index),
'np': np,
'pd': pd,
}
@@ -1366,8 +1372,13 @@ import pandas as pd
if not exec_result['success']:
raise RuntimeError(f"Code execution failed: {exec_result['error']}")
# Get the executed df
# Get the executed df, restore DatetimeIndex for signal alignment
executed_df = exec_env.get('df', df)
if isinstance(df.index, pd.DatetimeIndex) and not isinstance(executed_df.index, pd.DatetimeIndex):
if 'time' in executed_df.columns:
executed_df = executed_df.set_index('time')
elif len(executed_df) == len(df):
executed_df.index = df.index
# Validation: if chart signals are provided, df['buy']/df['sell'] must exist for backtest normalization.
# This keeps indicator scripts simple and consistent (chart=buy/sell, execution=normalized in backend).
+8 -3
View File
@@ -670,6 +670,9 @@ class StrategyService:
trading_config['long_ratio'] = long_ratio
trading_config['rebalance_frequency'] = rebalance_frequency
strategy_mode = payload.get('strategy_mode') or 'signal'
strategy_code = payload.get('strategy_code') or ''
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
@@ -678,9 +681,9 @@ class StrategyService:
(user_id, strategy_name, strategy_type, market_category, execution_mode, notification_config,
status, symbol, timeframe, initial_capital, leverage, market_type,
exchange_config, indicator_config, trading_config, ai_model_config, decide_interval,
strategy_group_id, group_base_name,
strategy_group_id, group_base_name, strategy_mode, strategy_code,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
""",
(
user_id,
@@ -701,7 +704,9 @@ class StrategyService:
self._dump_json_or_encrypt(payload.get('ai_model_config') or {}, encrypt=False),
int(payload.get('decide_interval') or 300),
strategy_group_id,
group_base_name
group_base_name,
strategy_mode,
strategy_code
)
)
new_id = cur.lastrowid
+21
View File
@@ -199,6 +199,8 @@ CREATE TABLE IF NOT EXISTS qd_strategies_trading (
decide_interval INTEGER DEFAULT 300,
strategy_group_id VARCHAR(100) DEFAULT '',
group_base_name VARCHAR(255) DEFAULT '',
strategy_mode VARCHAR(20) DEFAULT 'signal',
strategy_code TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
@@ -207,6 +209,25 @@ CREATE INDEX IF NOT EXISTS idx_strategies_user_id ON qd_strategies_trading(user_
CREATE INDEX IF NOT EXISTS idx_strategies_status ON qd_strategies_trading(status);
CREATE INDEX IF NOT EXISTS idx_strategies_group_id ON qd_strategies_trading(strategy_group_id);
-- Add strategy_mode and strategy_code columns (script strategy support)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_strategies_trading' AND column_name = 'strategy_mode'
) THEN
ALTER TABLE qd_strategies_trading ADD COLUMN strategy_mode VARCHAR(20) DEFAULT 'signal';
RAISE NOTICE 'Added strategy_mode column to qd_strategies_trading';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_strategies_trading' AND column_name = 'strategy_code'
) THEN
ALTER TABLE qd_strategies_trading ADD COLUMN strategy_code TEXT DEFAULT '';
RAISE NOTICE 'Added strategy_code column to qd_strategies_trading';
END IF;
END$$;
-- Add last_rebalance_at column for cross-sectional strategies (if not exists)
DO $$
BEGIN