@@ -1026,4 +1026,154 @@ def clear_notifications():
|
|||||||
return jsonify({'code': 1, 'msg': 'success'})
|
return jsonify({'code': 1, 'msg': 'success'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"clear_notifications failed: {str(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
|
return jsonify({'code': 0, 'msg': str(e)}), 500
|
||||||
@@ -1274,15 +1274,21 @@ class BacktestService:
|
|||||||
signals = pd.Series(0, index=df.index)
|
signals = pd.Series(0, index=df.index)
|
||||||
|
|
||||||
try:
|
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 = {
|
local_vars = {
|
||||||
'df': df.copy(),
|
'df': df_for_exec,
|
||||||
'open': df['open'],
|
'open': df_for_exec['open'],
|
||||||
'high': df['high'],
|
'high': df_for_exec['high'],
|
||||||
'low': df['low'],
|
'low': df_for_exec['low'],
|
||||||
'close': df['close'],
|
'close': df_for_exec['close'],
|
||||||
'volume': df['volume'],
|
'volume': df_for_exec['volume'],
|
||||||
'signals': signals,
|
'signals': pd.Series(0, index=df_for_exec.index),
|
||||||
'np': np,
|
'np': np,
|
||||||
'pd': pd,
|
'pd': pd,
|
||||||
}
|
}
|
||||||
@@ -1366,8 +1372,13 @@ import pandas as pd
|
|||||||
if not exec_result['success']:
|
if not exec_result['success']:
|
||||||
raise RuntimeError(f"Code execution failed: {exec_result['error']}")
|
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)
|
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.
|
# 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).
|
# 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['long_ratio'] = long_ratio
|
||||||
trading_config['rebalance_frequency'] = rebalance_frequency
|
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:
|
with get_db_connection() as db:
|
||||||
cur = db.cursor()
|
cur = db.cursor()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -678,9 +681,9 @@ class StrategyService:
|
|||||||
(user_id, strategy_name, strategy_type, market_category, execution_mode, notification_config,
|
(user_id, strategy_name, strategy_type, market_category, execution_mode, notification_config,
|
||||||
status, symbol, timeframe, initial_capital, leverage, market_type,
|
status, symbol, timeframe, initial_capital, leverage, market_type,
|
||||||
exchange_config, indicator_config, trading_config, ai_model_config, decide_interval,
|
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)
|
created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
user_id,
|
user_id,
|
||||||
@@ -701,7 +704,9 @@ class StrategyService:
|
|||||||
self._dump_json_or_encrypt(payload.get('ai_model_config') or {}, encrypt=False),
|
self._dump_json_or_encrypt(payload.get('ai_model_config') or {}, encrypt=False),
|
||||||
int(payload.get('decide_interval') or 300),
|
int(payload.get('decide_interval') or 300),
|
||||||
strategy_group_id,
|
strategy_group_id,
|
||||||
group_base_name
|
group_base_name,
|
||||||
|
strategy_mode,
|
||||||
|
strategy_code
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
new_id = cur.lastrowid
|
new_id = cur.lastrowid
|
||||||
|
|||||||
@@ -199,6 +199,8 @@ CREATE TABLE IF NOT EXISTS qd_strategies_trading (
|
|||||||
decide_interval INTEGER DEFAULT 300,
|
decide_interval INTEGER DEFAULT 300,
|
||||||
strategy_group_id VARCHAR(100) DEFAULT '',
|
strategy_group_id VARCHAR(100) DEFAULT '',
|
||||||
group_base_name VARCHAR(255) DEFAULT '',
|
group_base_name VARCHAR(255) DEFAULT '',
|
||||||
|
strategy_mode VARCHAR(20) DEFAULT 'signal',
|
||||||
|
strategy_code TEXT DEFAULT '',
|
||||||
created_at TIMESTAMP DEFAULT NOW(),
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
updated_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_status ON qd_strategies_trading(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_strategies_group_id ON qd_strategies_trading(strategy_group_id);
|
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)
|
-- Add last_rebalance_at column for cross-sectional strategies (if not exists)
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
|
|||||||
Reference in New Issue
Block a user