@@ -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
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user