2025-12-29 03:06:49 +08:00
"""
2026-01-14 05:29:55 +08:00
Trading Strategy API Routes
2025-12-29 03:06:49 +08:00
"""
2026-04-09 14:30:51 +07:00
2026-04-06 01:39:25 +08:00
import json
2026-04-06 23:19:41 +08:00
import re
2025-12-29 03:06:49 +08:00
import time
2026-04-09 14:30:51 +07:00
import traceback
from datetime import datetime
from flask import Blueprint , g , jsonify , request
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
from app import get_trading_executor
from app.services.backtest import BacktestService
2025-12-29 03:06:49 +08:00
from app.services.strategy import StrategyService
from app.services.strategy_compiler import StrategyCompiler
2026-04-06 01:39:25 +08:00
from app.services.strategy_snapshot import StrategySnapshotResolver
2025-12-29 03:06:49 +08:00
from app.utils.db import get_db_connection
2026-04-09 14:30:51 +07:00
from app.utils.logger import get_logger
2026-04-07 22:47:07 +08:00
try :
from psycopg2.errors import UndefinedTable as PgUndefinedTable
except Exception : # pragma: no cover
PgUndefinedTable = None # type: ignore
2025-12-29 03:06:49 +08:00
from app.data_sources import DataSourceFactory
2026-04-09 14:30:51 +07:00
from app.utils.auth import login_required
2025-12-29 03:06:49 +08:00
logger = get_logger ( __name__ )
2026-04-09 14:30:51 +07:00
strategy_bp = Blueprint ( "strategy" , __name__ )
2025-12-29 03:06:49 +08:00
# Local mode: avoid heavy initialization during module import.
# Instantiate services lazily on first use to keep startup clean.
_strategy_service = None
2026-04-06 01:39:25 +08:00
_backtest_service = None
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
def get_strategy_service () -> StrategyService :
global _strategy_service
if _strategy_service is None :
_strategy_service = StrategyService ()
return _strategy_service
2026-04-06 01:39:25 +08:00
def get_backtest_service () -> BacktestService :
global _backtest_service
if _backtest_service is None :
_backtest_service = BacktestService ()
return _backtest_service
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies" , methods = [ "GET" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def list_strategies ():
"""
2026-01-14 05:29:55 +08:00
List strategies for the current user.
2025-12-29 03:06:49 +08:00
"""
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2025-12-29 03:06:49 +08:00
items = get_strategy_service () . list_strategies ( user_id = user_id )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : { "strategies" : items }})
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "list_strategies failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : { "strategies" : []}}), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/detail" , methods = [ "GET" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def get_strategy_detail ():
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" , type = int )
2025-12-29 03:06:49 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Missing strategy id parameter" , "data" : None }), 400
2026-01-14 05:29:55 +08:00
st = get_strategy_service () . get_strategy ( strategy_id , user_id = user_id )
2025-12-29 03:06:49 +08:00
if not st :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy not found" , "data" : None }), 404
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : st })
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "get_strategy_detail failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/backtest" , methods = [ "POST" ])
2026-04-06 01:39:25 +08:00
@login_required
def run_strategy_backtest ():
try :
payload = request . get_json () or {}
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = int ( payload . get ( "strategyId" ) or 0 )
2026-04-06 01:39:25 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "strategyId is required" , "data" : None }), 400
2026-04-06 01:39:25 +08:00
2026-04-09 14:30:51 +07:00
start_date_str = str ( payload . get ( "startDate" ) or "" ) . strip ()
end_date_str = str ( payload . get ( "endDate" ) or "" ) . strip ()
2026-04-06 01:39:25 +08:00
if not start_date_str or not end_date_str :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "startDate and endDate are required" , "data" : None }), 400
2026-04-06 01:39:25 +08:00
strategy = get_strategy_service () . get_strategy ( strategy_id , user_id = user_id )
if not strategy :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy not found" , "data" : None }), 404
2026-04-06 01:39:25 +08:00
resolver = StrategySnapshotResolver ( user_id = user_id )
2026-04-09 14:30:51 +07:00
snapshot = resolver . resolve ( strategy , payload . get ( "overrideConfig" ) or {})
snapshot [ "user_id" ] = user_id
2026-04-06 01:39:25 +08:00
2026-04-09 14:30:51 +07:00
start_date = datetime . strptime ( start_date_str , "%Y-%m- %d " )
end_date = datetime . strptime ( end_date_str , "%Y-%m- %d " ) . replace ( hour = 23 , minute = 59 , second = 59 )
2026-04-06 01:39:25 +08:00
days_diff = ( end_date - start_date ) . days
2026-04-09 14:30:51 +07:00
timeframe = snapshot . get ( "timeframe" ) or "1D"
if timeframe == "1m" :
2026-04-06 01:39:25 +08:00
max_days = 30
2026-04-09 14:30:51 +07:00
max_range_text = "1 month"
elif timeframe == "5m" :
2026-04-06 01:39:25 +08:00
max_days = 180
2026-04-09 14:30:51 +07:00
max_range_text = "6 months"
elif timeframe in [ "15m" , "30m" ]:
2026-04-06 01:39:25 +08:00
max_days = 365
2026-04-09 14:30:51 +07:00
max_range_text = "1 year"
2026-04-06 01:39:25 +08:00
else :
max_days = 1095
2026-04-09 14:30:51 +07:00
max_range_text = "3 years"
2026-04-06 01:39:25 +08:00
if days_diff > max_days :
2026-04-09 14:30:51 +07:00
return jsonify (
{
"code" : 0 ,
"msg" : f "Backtest range exceeds limit: timeframe { timeframe } supports up to { max_range_text } ( { max_days } days), but you selected { days_diff } days" ,
"data" : None ,
}
), 400
2026-04-06 01:39:25 +08:00
svc = get_backtest_service ()
result = svc . run_strategy_snapshot ( snapshot , start_date = start_date , end_date = end_date )
run_id = svc . persist_run (
user_id = user_id ,
2026-04-09 14:30:51 +07:00
indicator_id = snapshot . get ( "indicator_id" ),
strategy_id = snapshot . get ( "strategy_id" ),
strategy_name = snapshot . get ( "strategy_name" ) or "" ,
run_type = snapshot . get ( "run_type" ) or "strategy_indicator" ,
market = snapshot . get ( "market" ) or "" ,
symbol = snapshot . get ( "symbol" ) or "" ,
timeframe = snapshot . get ( "timeframe" ) or "" ,
2026-04-06 01:39:25 +08:00
start_date_str = start_date_str ,
end_date_str = end_date_str ,
2026-04-09 14:30:51 +07:00
initial_capital = float ( snapshot . get ( "initial_capital" ) or 0 ),
commission = float ( snapshot . get ( "commission" ) or 0 ),
slippage = float ( snapshot . get ( "slippage" ) or 0 ),
leverage = int ( snapshot . get ( "leverage" ) or 1 ),
trade_direction = str ( snapshot . get ( "trade_direction" ) or "long" ),
strategy_config = snapshot . get ( "strategy_config" ) or {},
config_snapshot = snapshot . get ( "config_snapshot" ) or {},
status = "success" ,
error_message = "" ,
2026-04-06 01:39:25 +08:00
result = result ,
2026-04-09 14:30:51 +07:00
code = snapshot . get ( "code" ) or "" ,
2026-04-06 01:39:25 +08:00
)
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : { "runId" : run_id , "result" : result }})
2026-04-06 01:39:25 +08:00
except ValueError as e :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 400
2026-04-06 01:39:25 +08:00
except Exception as e :
logger . error ( f "run_strategy_backtest failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
try :
payload = payload if isinstance ( payload , dict ) else {}
2026-04-09 14:30:51 +07:00
strategy_id = int ( payload . get ( "strategyId" ) or 0 )
2026-04-06 01:39:25 +08:00
strategy = get_strategy_service () . get_strategy ( strategy_id , user_id = g . user_id ) if strategy_id else None
if strategy :
resolver = StrategySnapshotResolver ( user_id = g . user_id )
2026-04-09 14:30:51 +07:00
snapshot = resolver . resolve ( strategy , payload . get ( "overrideConfig" ) or {})
snapshot [ "user_id" ] = g . user_id
2026-04-06 01:39:25 +08:00
get_backtest_service () . persist_run (
user_id = g . user_id ,
2026-04-09 14:30:51 +07:00
indicator_id = snapshot . get ( "indicator_id" ),
strategy_id = snapshot . get ( "strategy_id" ),
strategy_name = snapshot . get ( "strategy_name" ) or "" ,
run_type = snapshot . get ( "run_type" ) or "strategy_indicator" ,
market = snapshot . get ( "market" ) or "" ,
symbol = snapshot . get ( "symbol" ) or "" ,
timeframe = snapshot . get ( "timeframe" ) or "" ,
start_date_str = str ( payload . get ( "startDate" ) or "" ),
end_date_str = str ( payload . get ( "endDate" ) or "" ),
initial_capital = float ( snapshot . get ( "initial_capital" ) or 0 ),
commission = float ( snapshot . get ( "commission" ) or 0 ),
slippage = float ( snapshot . get ( "slippage" ) or 0 ),
leverage = int ( snapshot . get ( "leverage" ) or 1 ),
trade_direction = str ( snapshot . get ( "trade_direction" ) or "long" ),
strategy_config = snapshot . get ( "strategy_config" ) or {},
config_snapshot = snapshot . get ( "config_snapshot" ) or {},
status = "failed" ,
2026-04-06 01:39:25 +08:00
error_message = str ( e ),
result = None ,
2026-04-09 14:30:51 +07:00
code = snapshot . get ( "code" ) or "" ,
2026-04-06 01:39:25 +08:00
)
except Exception :
pass
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2026-04-06 01:39:25 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/backtest/history" , methods = [ "GET" ])
2026-04-06 01:39:25 +08:00
@login_required
def get_strategy_backtest_history ():
try :
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = int ( request . args . get ( "strategyId" ) or request . args . get ( "id" ) or 0 )
2026-04-06 01:39:25 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "strategyId is required" , "data" : None }), 400
limit = max ( 1 , min ( int ( request . args . get ( "limit" ) or 50 ), 200 ))
offset = max ( 0 , int ( request . args . get ( "offset" ) or 0 ))
symbol = ( request . args . get ( "symbol" ) or "" ) . strip ()
market = ( request . args . get ( "market" ) or "" ) . strip ()
timeframe = ( request . args . get ( "timeframe" ) or "" ) . strip ()
2026-04-06 01:39:25 +08:00
rows = get_backtest_service () . list_runs (
user_id = user_id ,
strategy_id = strategy_id ,
limit = limit ,
offset = offset ,
symbol = symbol ,
market = market ,
timeframe = timeframe ,
)
2026-04-09 14:30:51 +07:00
rows = [ r for r in rows if str ( r . get ( "run_type" ) or "" ) . startswith ( "strategy_" )]
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : rows })
2026-04-06 01:39:25 +08:00
except Exception as e :
logger . error ( f "get_strategy_backtest_history failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2026-04-06 01:39:25 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/backtest/get" , methods = [ "GET" ])
2026-04-06 01:39:25 +08:00
@login_required
def get_strategy_backtest_run ():
try :
user_id = g . user_id
2026-04-09 14:30:51 +07:00
run_id = int ( request . args . get ( "runId" ) or 0 )
2026-04-06 01:39:25 +08:00
if not run_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "runId is required" , "data" : None }), 400
2026-04-06 01:39:25 +08:00
row = get_backtest_service () . get_run ( user_id = user_id , run_id = run_id )
2026-04-09 14:30:51 +07:00
if not row or not str ( row . get ( "run_type" ) or "" ) . startswith ( "strategy_" ):
return jsonify ({ "code" : 0 , "msg" : "run not found" , "data" : None }), 404
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : row })
2026-04-06 01:39:25 +08:00
except Exception as e :
logger . error ( f "get_strategy_backtest_run failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2026-04-06 01:39:25 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/create" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def create_strategy ():
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2025-12-29 03:06:49 +08:00
payload = request . get_json () or {}
2026-01-14 05:29:55 +08:00
# Use current user's ID
2026-04-09 14:30:51 +07:00
payload [ "user_id" ] = user_id
payload [ "strategy_type" ] = payload . get ( "strategy_type" ) or "IndicatorStrategy"
2025-12-29 03:06:49 +08:00
new_id = get_strategy_service () . create_strategy ( payload )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : { "id" : new_id }})
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "create_strategy failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/batch-create" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2026-01-11 22:15:54 +08:00
def batch_create_strategies ():
"""
2026-01-14 05:29:55 +08:00
Batch create strategies (multiple symbols)
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
Request body:
strategy_name: Base strategy name
symbols: Array of symbols, e.g. ["Crypto:BTC/USDT", "Crypto:ETH/USDT"]
... other strategy config
2026-01-11 22:15:54 +08:00
"""
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-01-11 22:15:54 +08:00
payload = request . get_json () or {}
2026-04-09 14:30:51 +07:00
payload [ "user_id" ] = user_id
payload [ "strategy_type" ] = payload . get ( "strategy_type" ) or "IndicatorStrategy"
2026-01-11 22:15:54 +08:00
result = get_strategy_service () . batch_create_strategies ( payload )
2026-04-09 14:30:51 +07:00
if result [ "success" ]:
return jsonify (
{ "code" : 1 , "msg" : f "Successfully created { result [ 'total_created' ] } strategies" , "data" : result }
)
2026-01-11 22:15:54 +08:00
else :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Batch creation failed" , "data" : result })
2026-01-11 22:15:54 +08:00
except Exception as e :
logger . error ( f "batch_create_strategies failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2026-01-11 22:15:54 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/batch-start" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2026-01-11 22:15:54 +08:00
def batch_start_strategies ():
"""
2026-01-14 05:29:55 +08:00
Batch start strategies
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
Request body:
strategy_ids: Array of strategy IDs
or
strategy_group_id: Strategy group ID
2026-01-11 22:15:54 +08:00
"""
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-01-11 22:15:54 +08:00
payload = request . get_json () or {}
2026-04-09 14:30:51 +07:00
strategy_ids = payload . get ( "strategy_ids" ) or []
strategy_group_id = payload . get ( "strategy_group_id" )
2026-01-14 05:29:55 +08:00
# If strategy_group_id provided, get all strategies in the group
2026-01-11 22:15:54 +08:00
if strategy_group_id and not strategy_ids :
2026-01-14 05:29:55 +08:00
strategy_ids = get_strategy_service () . get_strategies_by_group ( strategy_group_id , user_id = user_id )
2026-04-09 14:30:51 +07:00
2026-01-11 22:15:54 +08:00
if not strategy_ids :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Please provide strategy IDs" , "data" : None }), 400
2026-01-14 05:29:55 +08:00
# Update database status first
result = get_strategy_service () . batch_start_strategies ( strategy_ids , user_id = user_id )
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
# Then start executor
2026-01-11 22:15:54 +08:00
executor = get_trading_executor ()
2026-04-09 14:30:51 +07:00
for sid in result . get ( "success_ids" , []):
2026-01-11 22:15:54 +08:00
try :
executor . start_strategy ( sid )
except Exception as e :
logger . error ( f "Failed to start executor for strategy { sid } : { e } " )
2026-04-09 14:30:51 +07:00
return jsonify (
{
"code" : 1 if result [ "success" ] else 0 ,
"msg" : f "Successfully started { len ( result . get ( 'success_ids' , [])) } strategies" ,
"data" : result ,
}
)
2026-01-11 22:15:54 +08:00
except Exception as e :
logger . error ( f "batch_start_strategies failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2026-01-11 22:15:54 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/batch-stop" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2026-01-11 22:15:54 +08:00
def batch_stop_strategies ():
"""
2026-01-14 05:29:55 +08:00
Batch stop strategies
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
Request body:
strategy_ids: Array of strategy IDs
or
strategy_group_id: Strategy group ID
2026-01-11 22:15:54 +08:00
"""
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-01-11 22:15:54 +08:00
payload = request . get_json () or {}
2026-04-09 14:30:51 +07:00
strategy_ids = payload . get ( "strategy_ids" ) or []
strategy_group_id = payload . get ( "strategy_group_id" )
2026-01-11 22:15:54 +08:00
if strategy_group_id and not strategy_ids :
2026-01-14 05:29:55 +08:00
strategy_ids = get_strategy_service () . get_strategies_by_group ( strategy_group_id , user_id = user_id )
2026-04-09 14:30:51 +07:00
2026-01-11 22:15:54 +08:00
if not strategy_ids :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Please provide strategy IDs" , "data" : None }), 400
2026-01-14 05:29:55 +08:00
# Stop executor first
2026-01-11 22:15:54 +08:00
executor = get_trading_executor ()
for sid in strategy_ids :
try :
executor . stop_strategy ( sid )
except Exception as e :
logger . error ( f "Failed to stop executor for strategy { sid } : { e } " )
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
# Then update database status
result = get_strategy_service () . batch_stop_strategies ( strategy_ids , user_id = user_id )
2026-04-09 14:30:51 +07:00
return jsonify (
{
"code" : 1 if result [ "success" ] else 0 ,
"msg" : f "Successfully stopped { len ( result . get ( 'success_ids' , [])) } strategies" ,
"data" : result ,
}
)
2026-01-11 22:15:54 +08:00
except Exception as e :
logger . error ( f "batch_stop_strategies failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2026-01-11 22:15:54 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/batch-delete" , methods = [ "DELETE" ])
2026-01-14 05:29:55 +08:00
@login_required
2026-01-11 22:15:54 +08:00
def batch_delete_strategies ():
"""
2026-01-14 05:29:55 +08:00
Batch delete strategies
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
Request body:
strategy_ids: Array of strategy IDs
or
strategy_group_id: Strategy group ID
2026-01-11 22:15:54 +08:00
"""
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-01-11 22:15:54 +08:00
payload = request . get_json () or {}
2026-04-09 14:30:51 +07:00
strategy_ids = payload . get ( "strategy_ids" ) or []
strategy_group_id = payload . get ( "strategy_group_id" )
2026-01-11 22:15:54 +08:00
if strategy_group_id and not strategy_ids :
2026-01-14 05:29:55 +08:00
strategy_ids = get_strategy_service () . get_strategies_by_group ( strategy_group_id , user_id = user_id )
2026-04-09 14:30:51 +07:00
2026-01-11 22:15:54 +08:00
if not strategy_ids :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Please provide strategy IDs" , "data" : None }), 400
2026-01-14 05:29:55 +08:00
# Stop executor first
2026-01-11 22:15:54 +08:00
executor = get_trading_executor ()
for sid in strategy_ids :
try :
executor . stop_strategy ( sid )
2026-04-09 14:30:51 +07:00
except Exception :
2026-01-14 05:29:55 +08:00
pass # Ignore stop errors
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
# Then delete
result = get_strategy_service () . batch_delete_strategies ( strategy_ids , user_id = user_id )
2026-04-09 14:30:51 +07:00
return jsonify (
{
"code" : 1 if result [ "success" ] else 0 ,
"msg" : f "Successfully deleted { len ( result . get ( 'success_ids' , [])) } strategies" ,
"data" : result ,
}
)
2026-01-11 22:15:54 +08:00
except Exception as e :
logger . error ( f "batch_delete_strategies failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2026-01-11 22:15:54 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/update" , methods = [ "PUT" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def update_strategy ():
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" , type = int )
2025-12-29 03:06:49 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Missing strategy id parameter" , "data" : None }), 400
2025-12-29 03:06:49 +08:00
payload = request . get_json () or {}
2026-01-14 05:29:55 +08:00
ok = get_strategy_service () . update_strategy ( strategy_id , payload , user_id = user_id )
2025-12-29 03:06:49 +08:00
if not ok :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy not found" , "data" : None }), 404
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : None })
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "update_strategy failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/delete" , methods = [ "DELETE" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def delete_strategy ():
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" , type = int )
2025-12-29 03:06:49 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Missing strategy id parameter" , "data" : None }), 400
2026-01-14 05:29:55 +08:00
ok = get_strategy_service () . delete_strategy ( strategy_id , user_id = user_id )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 if ok else 0 , "msg" : "success" if ok else "failed" , "data" : None })
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "delete_strategy failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/trades" , methods = [ "GET" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def get_trades ():
2026-01-14 05:29:55 +08:00
"""Get trade records for the current user's strategy."""
2025-12-29 03:06:49 +08:00
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" , type = int )
2025-12-29 03:06:49 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify (
{ "code" : 0 , "msg" : "Missing strategy id parameter" , "data" : { "trades" : [], "items" : []}}
), 400
2026-01-14 05:29:55 +08:00
# Verify strategy belongs to user
st = get_strategy_service () . get_strategy ( strategy_id , user_id = user_id )
if not st :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy not found" , "data" : { "trades" : [], "items" : []}}), 404
2025-12-29 03:06:49 +08:00
with get_db_connection () as db :
cur = db . cursor ()
cur . execute (
"""
2025-12-29 04:45:59 +08:00
SELECT id, strategy_id, symbol, type, price, amount, value, commission, commission_ccy, profit, created_at
2025-12-29 03:06:49 +08:00
FROM qd_strategy_trades
WHERE strategy_id = ?
ORDER BY id DESC
""" ,
2026-04-09 14:30:51 +07:00
( strategy_id ,),
2025-12-29 03:06:49 +08:00
)
rows = cur . fetchall () or []
cur . close ()
2026-04-09 14:30:51 +07:00
2026-01-24 03:22:14 +08:00
# Convert created_at to UTC timestamp (seconds) for frontend
# This ensures consistent timezone handling
processed_rows = []
for row in rows :
trade = dict ( row )
2026-04-09 14:30:51 +07:00
created_at = trade . get ( "created_at" )
2026-01-24 03:22:14 +08:00
if created_at :
2026-04-09 14:30:51 +07:00
if hasattr ( created_at , "timestamp" ):
2026-01-24 03:22:14 +08:00
# datetime object - convert to UTC timestamp
2026-04-09 14:30:51 +07:00
trade [ "created_at" ] = int ( created_at . timestamp ())
2026-01-24 03:22:14 +08:00
elif isinstance ( created_at , str ):
# ISO string - parse and convert
try :
from datetime import datetime
2026-04-09 14:30:51 +07:00
dt = datetime . fromisoformat ( created_at . replace ( "Z" , "+00:00" ))
trade [ "created_at" ] = int ( dt . timestamp ())
2026-01-24 03:22:14 +08:00
except Exception :
pass
processed_rows . append ( trade )
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
# Frontend expects data.trades; keep data.items for compatibility with list-style components.
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : { "trades" : processed_rows , "items" : processed_rows }})
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "get_trades failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : { "trades" : [], "items" : []}}), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/positions" , methods = [ "GET" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def get_positions ():
2026-01-14 05:29:55 +08:00
"""Get position records for the current user's strategy."""
2025-12-29 03:06:49 +08:00
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" , type = int )
2025-12-29 03:06:49 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify (
{ "code" : 0 , "msg" : "Missing strategy id parameter" , "data" : { "positions" : [], "items" : []}}
), 400
2026-01-14 05:29:55 +08:00
# Verify strategy belongs to user
st = get_strategy_service () . get_strategy ( strategy_id , user_id = user_id )
if not st :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy not found" , "data" : { "positions" : [], "items" : []}}), 404
2025-12-29 03:06:49 +08:00
with get_db_connection () as db :
cur = db . cursor ()
cur . execute (
"""
SELECT id, strategy_id, symbol, side, size, entry_price, current_price, highest_price,
unrealized_pnl, pnl_percent, equity, updated_at
FROM qd_strategy_positions
WHERE strategy_id = ?
ORDER BY id DESC
""" ,
2026-04-09 14:30:51 +07:00
( strategy_id ,),
2025-12-29 03:06:49 +08:00
)
rows = cur . fetchall () or []
cur . close ()
# Sync current price and PnL on read (frontend polls every few seconds).
def _calc_unrealized_pnl ( side : str , entry_price : float , current_price : float , size : float ) -> float :
ep = float ( entry_price or 0.0 )
cp = float ( current_price or 0.0 )
sz = float ( size or 0.0 )
if ep <= 0 or cp <= 0 or sz <= 0 :
return 0.0
s = ( side or "" ) . strip () . lower ()
if s == "short" :
return ( ep - cp ) * sz
return ( cp - ep ) * sz
def _calc_pnl_percent ( entry_price : float , size : float , pnl : float ) -> float :
ep = float ( entry_price or 0.0 )
sz = float ( size or 0.0 )
denom = ep * sz
if denom <= 0 :
return 0.0
return float ( pnl ) / denom * 100.0
now = int ( time . time ())
# Fetch prices once per symbol to reduce API calls.
2026-01-12 23:10:47 +08:00
sym_to_price : dict [ str , float ] = {}
2025-12-29 03:06:49 +08:00
ds = DataSourceFactory . get_source ( "Crypto" )
for r in rows :
sym = ( r . get ( "symbol" ) or "" ) . strip ()
if not sym :
continue
if sym in sym_to_price :
continue
try :
t = ds . get_ticker ( sym ) or {}
px = float ( t . get ( "last" ) or t . get ( "close" ) or 0.0 )
if px > 0 :
sym_to_price [ sym ] = px
except Exception :
continue
# Apply to rows and persist best-effort
out = []
with get_db_connection () as db :
cur = db . cursor ()
for r in rows :
sym = ( r . get ( "symbol" ) or "" ) . strip ()
side = ( r . get ( "side" ) or "" ) . strip () . lower ()
entry = float ( r . get ( "entry_price" ) or 0.0 )
size = float ( r . get ( "size" ) or 0.0 )
cp = float ( sym_to_price . get ( sym ) or r . get ( "current_price" ) or 0.0 )
pnl = _calc_unrealized_pnl ( side , entry , cp , size )
pct = _calc_pnl_percent ( entry , size , pnl )
rr = dict ( r )
2026-04-06 16:47:36 +07:00
# Make sure entry_price has a value (if it is NULL in the database, use the calculated entry value)
2026-02-10 15:18:45 +08:00
if not rr . get ( "entry_price" ) or float ( rr . get ( "entry_price" ) or 0.0 ) <= 0 :
rr [ "entry_price" ] = float ( entry or 0.0 )
else :
rr [ "entry_price" ] = float ( rr . get ( "entry_price" ) or 0.0 )
2025-12-29 03:06:49 +08:00
rr [ "current_price" ] = float ( cp or 0.0 )
rr [ "unrealized_pnl" ] = float ( pnl )
rr [ "pnl_percent" ] = float ( pct )
rr [ "updated_at" ] = now
out . append ( rr )
try :
cur . execute (
"""
UPDATE qd_strategy_positions
2026-01-14 05:29:55 +08:00
SET current_price = ?, unrealized_pnl = ?, pnl_percent = ?, updated_at = NOW()
2025-12-29 03:06:49 +08:00
WHERE id = ?
""" ,
2026-01-14 05:29:55 +08:00
( float ( cp or 0.0 ), float ( pnl ), float ( pct ), int ( rr . get ( "id" ))),
2025-12-29 03:06:49 +08:00
)
except Exception :
pass
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : { "positions" : out , "items" : out }})
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "get_positions failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : { "positions" : [], "items" : []}}), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/equityCurve" , methods = [ "GET" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def get_equity_curve ():
2026-01-14 05:29:55 +08:00
"""Get equity curve for the current user's strategy."""
2025-12-29 03:06:49 +08:00
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" , type = int )
2025-12-29 03:06:49 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Missing strategy id parameter" , "data" : []}), 400
2025-12-29 03:06:49 +08:00
2026-01-14 05:29:55 +08:00
st = get_strategy_service () . get_strategy ( strategy_id , user_id = user_id ) or {}
if not st :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy not found" , "data" : []}), 404
initial = float ( st . get ( "initial_capital" ) or ( st . get ( "trading_config" ) or {}) . get ( "initial_capital" ) or 0 )
2025-12-29 03:06:49 +08:00
if initial <= 0 :
initial = 1000.0
with get_db_connection () as db :
cur = db . cursor ()
cur . execute (
"""
SELECT created_at, profit
FROM qd_strategy_trades
WHERE strategy_id = ?
ORDER BY created_at ASC
""" ,
2026-04-09 14:30:51 +07:00
( strategy_id ,),
2025-12-29 03:06:49 +08:00
)
rows = cur . fetchall () or []
2026-04-07 22:47:07 +08:00
cur . execute (
"""
SELECT COALESCE(SUM(unrealized_pnl), 0) AS u
FROM qd_strategy_positions
WHERE strategy_id = ?
""" ,
( strategy_id ,),
)
prow = cur . fetchone () or {}
2025-12-29 03:06:49 +08:00
cur . close ()
equity = initial
curve = []
for r in rows :
try :
2026-04-09 14:30:51 +07:00
equity += float ( r . get ( "profit" ) or 0 )
2025-12-29 03:06:49 +08:00
except Exception :
pass
2026-04-09 14:30:51 +07:00
created_at = r . get ( "created_at" )
if created_at and hasattr ( created_at , "timestamp" ):
2026-01-17 01:37:32 +07:00
ts = int ( created_at . timestamp ())
elif created_at :
ts = int ( created_at )
else :
ts = int ( time . time ())
2026-04-09 14:30:51 +07:00
curve . append ({ "time" : ts , "equity" : round ( equity , 2 )})
2026-04-07 22:47:07 +08:00
# 将未实现盈亏并入曲线末端,便于「持仓中」也能在绩效里看到浮动权益
try :
2026-04-09 14:30:51 +07:00
unreal = float ( prow . get ( "u" ) or prow . get ( "U" ) or 0 )
2026-04-07 22:47:07 +08:00
except Exception :
unreal = 0.0
live_equity = float ( equity ) + unreal
now_ts = int ( time . time ())
if abs ( unreal ) > 1e-12 or not curve :
2026-04-09 14:30:51 +07:00
curve . append ({ "time" : now_ts , "equity" : round ( live_equity , 2 )})
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : curve })
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "get_equity_curve failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : []}), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/stop" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def stop_strategy ():
"""
2026-01-14 05:29:55 +08:00
Stop a strategy for the current user.
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
Params:
id: Strategy ID
2025-12-29 03:06:49 +08:00
"""
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" , type = int )
2025-12-29 03:06:49 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Missing strategy id parameter" , "data" : None }), 400
2026-01-14 05:29:55 +08:00
# Verify strategy belongs to user
st = get_strategy_service () . get_strategy ( strategy_id , user_id = user_id )
if not st :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy not found" , "data" : None }), 404
2026-01-14 05:29:55 +08:00
# Get strategy type
2025-12-29 03:06:49 +08:00
strategy_type = get_strategy_service () . get_strategy_type ( strategy_id )
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
# Local backend: AI strategy executor was removed. Only indicator strategies are supported.
2026-04-09 14:30:51 +07:00
if strategy_type == "PromptBasedStrategy" :
return jsonify (
{
"code" : 0 ,
"msg" : "AI strategy has been removed; local edition does not support starting/stopping AI strategies" ,
"data" : None ,
}
), 400
2025-12-29 03:06:49 +08:00
2026-01-14 05:29:55 +08:00
# Indicator strategy
2025-12-29 03:06:49 +08:00
get_trading_executor () . stop_strategy ( strategy_id )
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
# Update strategy status
2026-04-09 14:30:51 +07:00
get_strategy_service () . update_strategy_status ( strategy_id , "stopped" , user_id = user_id )
return jsonify ({ "code" : 1 , "msg" : "Stopped successfully" , "data" : None })
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "Failed to stop strategy: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : f "Failed to stop strategy: { str ( e ) } " , "data" : None }), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/start" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def start_strategy ():
"""
2026-01-14 05:29:55 +08:00
Start a strategy for the current user.
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
Params:
id: Strategy ID
2025-12-29 03:06:49 +08:00
"""
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" , type = int )
2025-12-29 03:06:49 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Missing strategy id parameter" , "data" : None }), 400
2026-01-14 05:29:55 +08:00
# Verify strategy belongs to user
st = get_strategy_service () . get_strategy ( strategy_id , user_id = user_id )
if not st :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy not found" , "data" : None }), 404
2026-01-14 05:29:55 +08:00
# Get strategy type
2025-12-29 03:06:49 +08:00
strategy_type = get_strategy_service () . get_strategy_type ( strategy_id )
2026-04-06 23:19:41 +08:00
# IndicatorStrategy and ScriptStrategy are executed by TradingExecutor.
2026-04-09 14:30:51 +07:00
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 )
2025-12-29 03:06:49 +08:00
success = get_trading_executor () . start_strategy ( strategy_id )
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
if not success :
2026-01-14 05:29:55 +08:00
# If start failed, restore status
2026-04-09 14:30:51 +07:00
get_strategy_service () . update_strategy_status ( strategy_id , "stopped" , user_id = user_id )
return jsonify ({ "code" : 0 , "msg" : "Failed to start strategy executor" , "data" : None }), 500
return jsonify ({ "code" : 1 , "msg" : "Started successfully" , "data" : None })
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "Failed to start strategy: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : f "Failed to start strategy: { str ( e ) } " , "data" : None }), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/test-connection" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def test_connection ():
"""
2026-01-14 05:29:55 +08:00
Test exchange connection.
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
Request body:
2026-02-27 19:57:23 +08:00
exchange_config: Exchange configuration (may contain credential_id or inline keys)
2025-12-29 03:06:49 +08:00
"""
try :
data = request . get_json () or {}
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Log request data (for debugging, but do not log sensitive information)
2025-12-29 03:06:49 +08:00
logger . debug ( f "Connection test request keys: { list ( data . keys ()) } " )
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Get exchange configuration
2026-04-09 14:30:51 +07:00
exchange_config = data . get ( "exchange_config" , data )
2025-12-29 03:06:49 +08:00
# Local deployment: no encryption/decryption; accept dict or JSON string.
if isinstance ( exchange_config , str ):
try :
import json
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
exchange_config = json . loads ( exchange_config )
except Exception :
pass
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Verify exchange_config is a dictionary
2025-12-29 03:06:49 +08:00
if not isinstance ( exchange_config , dict ):
logger . error ( f "Invalid exchange_config type: { type ( exchange_config ) } , data: { str ( exchange_config )[: 200 ] } " )
# Frontend expects HTTP 200 with {code:0} for business failures.
2026-04-09 14:30:51 +07:00
return jsonify (
{ "code" : 0 , "msg" : "Invalid exchange config format; please check your payload" , "data" : None }
)
2026-02-27 19:57:23 +08:00
# Resolve credential_id → full config (merges credential keys with any overrides).
# This allows the frontend to send just {credential_id: 5} without raw api_key/secret_key.
from app.services.exchange_execution import resolve_exchange_config
2026-04-09 14:30:51 +07:00
user_id = g . user_id if hasattr ( g , "user_id" ) else 1
2026-02-27 19:57:23 +08:00
resolved = resolve_exchange_config ( exchange_config , user_id = user_id )
2026-04-06 16:47:36 +07:00
# Verify required fields (check resolved config after credential merge)
2026-04-09 14:30:51 +07:00
if not resolved . get ( "exchange_id" ):
return jsonify ({ "code" : 0 , "msg" : "Please select an exchange" , "data" : None })
api_key = resolved . get ( "api_key" , "" )
secret_key = resolved . get ( "secret_key" , "" )
2026-04-06 16:47:36 +07:00
# Detailed log troubleshooting
2026-02-27 19:57:23 +08:00
logger . info ( f "Testing connection: exchange_id= { resolved . get ( 'exchange_id' ) } " )
if api_key :
logger . info ( f "API Key: { api_key [: 5 ] } ... (len= { len ( api_key ) } )" )
if secret_key :
logger . info ( f "Secret Key: { secret_key [: 5 ] } ... (len= { len ( secret_key ) } )" )
2026-04-09 14:30:51 +07:00
2026-04-06 16:47:36 +07:00
# Check if there are special characters
2026-02-27 19:57:23 +08:00
if api_key and api_key . strip () != api_key :
2025-12-29 03:06:49 +08:00
logger . warning ( "API key contains leading/trailing whitespace" )
2026-02-27 19:57:23 +08:00
if secret_key and secret_key . strip () != secret_key :
2025-12-29 03:06:49 +08:00
logger . warning ( "Secret key contains leading/trailing whitespace" )
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
if not api_key or not secret_key :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Please provide API key and secret key" , "data" : None })
2026-02-27 19:57:23 +08:00
# Pass the resolved config (with actual keys) to the service
2026-04-06 01:39:25 +08:00
result = get_strategy_service () . test_exchange_connection ( resolved , user_id = user_id )
2026-04-09 14:30:51 +07:00
if result [ "success" ]:
return jsonify (
{ "code" : 1 , "msg" : result . get ( "message" ) or "Connection successful" , "data" : result . get ( "data" )}
)
2025-12-29 03:06:49 +08:00
# Always return HTTP 200 for business-level failures.
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : result . get ( "message" ) or "Connection failed" , "data" : result . get ( "data" )})
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "Connection test failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : f "Connection test failed: { str ( e ) } " , "data" : None }), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/get-symbols" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def get_symbols ():
"""
2026-01-14 05:29:55 +08:00
Get exchange trading pairs list.
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
Request body:
exchange_config: Exchange configuration
2025-12-29 03:06:49 +08:00
"""
try :
data = request . get_json () or {}
2026-04-09 14:30:51 +07:00
exchange_config = data . get ( "exchange_config" , data )
2025-12-29 03:06:49 +08:00
result = get_strategy_service () . get_exchange_symbols ( exchange_config )
2026-04-09 14:30:51 +07:00
if result [ "success" ]:
return jsonify ({ "code" : 1 , "msg" : result [ "message" ], "data" : { "symbols" : result [ "symbols" ]}})
2025-12-29 03:06:49 +08:00
else :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : result [ "message" ], "data" : { "symbols" : []}})
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "Failed to fetch symbols: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : f "Failed to fetch symbols: { str ( e ) } " , "data" : { "symbols" : []}}), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/preview-compile" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def preview_compile ():
"""
2026-01-14 05:29:55 +08:00
Preview compiled strategy result.
2025-12-29 03:06:49 +08:00
"""
try :
data = request . get_json () or {}
# strategy_config is passed as 'config'
2026-04-09 14:30:51 +07:00
config = data . get ( "config" )
2025-12-29 03:06:49 +08:00
if not config :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Missing config" }), 400
2025-12-29 03:06:49 +08:00
# Compile
compiler = StrategyCompiler ()
try :
code = compiler . compile ( config )
except Exception as e :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : f "Compilation failed: { str ( e ) } " }), 400
2025-12-29 03:06:49 +08:00
# Execute
2026-04-09 14:30:51 +07:00
symbol = config . get ( "symbol" , "BTC/USDT" )
timeframe = config . get ( "timeframe" , "4h" )
2025-12-29 03:06:49 +08:00
backtest_service = BacktestService ()
2026-04-09 14:30:51 +07:00
result = backtest_service . run_code_strategy ( code = code , symbol = symbol , timeframe = timeframe , limit = 500 )
if result . get ( "error" ):
return jsonify ({ "code" : 0 , "msg" : f "Execution failed: { result [ 'error' ] } " }), 400
return jsonify ({ "code" : 1 , "msg" : "Success" , "data" : result })
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "Preview failed: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e )}), 500
2025-12-29 03:06:49 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/notifications" , methods = [ "GET" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def get_strategy_notifications ():
"""
2026-01-14 05:29:55 +08:00
Strategy signal notifications for the current user.
2025-12-29 03:06:49 +08:00
Query:
- id: strategy id (optional)
- limit: default 50, max 200
- since_id: return rows with id > since_id (optional)
"""
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" , type = int )
limit = request . args . get ( "limit" , type = int ) or 50
2025-12-29 03:06:49 +08:00
limit = max ( 1 , min ( 200 , int ( limit )))
2026-04-09 14:30:51 +07:00
since_id = request . args . get ( "since_id" , type = int ) or 0
2025-12-29 03:06:49 +08:00
2026-01-14 05:29:55 +08:00
# Get user's strategy IDs for filtering notifications
user_strategy_ids = []
with get_db_connection () as db :
cur = db . cursor ()
cur . execute ( "SELECT id FROM qd_strategies_trading WHERE user_id = ?" , ( user_id ,))
rows = cur . fetchall () or []
2026-04-09 14:30:51 +07:00
user_strategy_ids = [ r . get ( "id" ) for r in rows if r . get ( "id" )]
2026-01-14 05:29:55 +08:00
cur . close ()
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
where = []
args = []
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
# Filter by user's strategies
2025-12-29 03:06:49 +08:00
if strategy_id :
2026-01-14 05:29:55 +08:00
if strategy_id in user_strategy_ids :
where . append ( "strategy_id = ?" )
args . append ( int ( strategy_id ))
else :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : { "items" : []}})
2026-01-14 05:29:55 +08:00
else :
2026-01-25 01:03:46 +08:00
if user_strategy_ids :
placeholders = "," . join ([ "?" ] * len ( user_strategy_ids ))
where . append ( f "(strategy_id IN ( { placeholders } ) OR (strategy_id IS NULL AND user_id = ?))" )
args . extend ( user_strategy_ids )
args . append ( user_id )
else :
# Only portfolio monitor notifications (strategy_id is NULL)
where . append ( "strategy_id IS NULL AND user_id = ?" )
args . append ( user_id )
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
if since_id :
where . append ( "id > ?" )
args . append ( int ( since_id ))
where_sql = ( "WHERE " + " AND " . join ( where )) if where else ""
with get_db_connection () as db :
cur = db . cursor ()
cur . execute (
f """
SELECT *
FROM qd_strategy_notifications
{ where_sql }
ORDER BY id DESC
LIMIT ?
""" ,
tuple ( args + [ int ( limit )]),
)
rows = cur . fetchall () or []
cur . close ()
2026-01-24 03:22:14 +08:00
# Convert created_at to UTC timestamp (seconds) for frontend
2026-03-25 13:27:48 +08:00
from datetime import timezone as _dt_tz
2026-04-09 14:30:51 +07:00
2026-01-24 03:22:14 +08:00
processed_rows = []
for row in rows :
item = dict ( row )
2026-04-09 14:30:51 +07:00
created_at = item . get ( "created_at" )
2026-01-24 03:22:14 +08:00
if created_at :
2026-04-09 14:30:51 +07:00
if hasattr ( created_at , "timestamp" ):
2026-04-06 16:47:36 +07:00
# No time zone datetime: The connection has SET TIME ZONE UTC, interpret it according to UTC and then transfer to Unix to avoid misjudgment of the local TZ on the server side.
2026-04-09 14:30:51 +07:00
if getattr ( created_at , "tzinfo" , None ) is None :
2026-03-25 13:27:48 +08:00
created_at = created_at . replace ( tzinfo = _dt_tz . utc )
2026-04-09 14:30:51 +07:00
item [ "created_at" ] = int ( created_at . timestamp ())
2026-01-24 03:22:14 +08:00
elif isinstance ( created_at , str ):
try :
from datetime import datetime
2026-04-09 14:30:51 +07:00
dt = datetime . fromisoformat ( created_at . replace ( "Z" , "+00:00" ))
item [ "created_at" ] = int ( dt . timestamp ())
2026-01-24 03:22:14 +08:00
except Exception :
pass
processed_rows . append ( item )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : { "items" : processed_rows }})
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "get_strategy_notifications failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : { "items" : []}}), 500
2026-01-12 22:36:10 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/notifications/unread-count" , methods = [ "GET" ])
2026-03-17 20:30:04 +08:00
@login_required
def get_unread_notification_count ():
"""
Get unread notification count for the current user.
Used by frontend header badge (cap at 99+ on UI).
"""
try :
user_id = g . user_id
with get_db_connection () as db :
cur = db . cursor ()
cur . execute ( "SELECT id FROM qd_strategies_trading WHERE user_id = ?" , ( user_id ,))
rows = cur . fetchall () or []
2026-04-09 14:30:51 +07:00
user_strategy_ids = [ r . get ( "id" ) for r in rows if r . get ( "id" )]
2026-03-17 20:30:04 +08:00
cur . close ()
where = [ "is_read = 0" ]
args = []
if user_strategy_ids :
placeholders = "," . join ([ "?" ] * len ( user_strategy_ids ))
where . append ( f "(strategy_id IN ( { placeholders } ) OR (strategy_id IS NULL AND user_id = ?))" )
args . extend ( user_strategy_ids )
args . append ( user_id )
else :
where . append ( "strategy_id IS NULL AND user_id = ?" )
args . append ( user_id )
where_sql = "WHERE " + " AND " . join ( where )
with get_db_connection () as db :
cur = db . cursor ()
cur . execute (
f "SELECT COUNT(1) AS cnt FROM qd_strategy_notifications { where_sql } " ,
tuple ( args ),
)
cnt = int (( cur . fetchone () or {}) . get ( "cnt" ) or 0 )
cur . close ()
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : { "unread" : cnt }})
2026-03-17 20:30:04 +08:00
except Exception as e :
logger . error ( f "get_unread_notification_count failed: { str ( e ) } " )
logger . error ( traceback . format_exc ())
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : { "unread" : 0 }}), 500
2026-03-17 20:30:04 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/notifications/read" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2026-01-12 22:36:10 +08:00
def mark_notification_read ():
2026-01-14 05:29:55 +08:00
"""Mark a single notification as read for the current user."""
2026-01-12 22:36:10 +08:00
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-01-12 22:36:10 +08:00
data = request . get_json ( force = True , silent = True ) or {}
2026-04-09 14:30:51 +07:00
notification_id = data . get ( "id" )
2026-01-12 22:36:10 +08:00
if not notification_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Missing id" }), 400
2026-01-12 22:36:10 +08:00
2026-01-25 01:03:46 +08:00
# Update notifications for user's strategies OR portfolio monitor notifications
2026-01-12 22:36:10 +08:00
with get_db_connection () as db :
cur = db . cursor ()
cur . execute (
2026-01-14 05:29:55 +08:00
"""
2026-04-09 14:30:51 +07:00
UPDATE qd_strategy_notifications SET is_read = 1
2026-01-25 01:03:46 +08:00
WHERE id = ? AND (
strategy_id IN (SELECT id FROM qd_strategies_trading WHERE user_id = ?)
OR (strategy_id IS NULL AND user_id = ?)
)
2026-01-14 05:29:55 +08:00
""" ,
2026-04-09 14:30:51 +07:00
( int ( notification_id ), user_id , user_id ),
2026-01-12 22:36:10 +08:00
)
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" })
2026-01-12 22:36:10 +08:00
except Exception as e :
logger . error ( f "mark_notification_read failed: { str ( e ) } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e )}), 500
2026-01-12 22:36:10 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/notifications/read-all" , methods = [ "POST" ])
2026-01-14 05:29:55 +08:00
@login_required
2026-01-12 22:36:10 +08:00
def mark_all_notifications_read ():
2026-01-14 05:29:55 +08:00
"""Mark all notifications as read for the current user."""
2026-01-12 22:36:10 +08:00
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-01-12 22:36:10 +08:00
with get_db_connection () as db :
cur = db . cursor ()
2026-01-14 05:29:55 +08:00
cur . execute (
"""
2026-04-09 14:30:51 +07:00
UPDATE qd_strategy_notifications SET is_read = 1
2026-01-14 05:29:55 +08:00
WHERE strategy_id IN (SELECT id FROM qd_strategies_trading WHERE user_id = ?)
2026-01-25 01:03:46 +08:00
OR (strategy_id IS NULL AND user_id = ?)
2026-01-14 05:29:55 +08:00
""" ,
2026-04-09 14:30:51 +07:00
( user_id , user_id ),
2026-01-14 05:29:55 +08:00
)
2026-01-12 22:36:10 +08:00
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" })
2026-01-12 22:36:10 +08:00
except Exception as e :
logger . error ( f "mark_all_notifications_read failed: { str ( e ) } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e )}), 500
2026-01-12 22:36:10 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/notifications/clear" , methods = [ "DELETE" ])
2026-01-14 05:29:55 +08:00
@login_required
2026-01-12 22:36:10 +08:00
def clear_notifications ():
2026-01-14 05:29:55 +08:00
"""Clear all notifications for the current user."""
2026-01-12 22:36:10 +08:00
try :
2026-01-14 05:29:55 +08:00
user_id = g . user_id
2026-01-12 22:36:10 +08:00
with get_db_connection () as db :
cur = db . cursor ()
2026-01-14 05:29:55 +08:00
cur . execute (
"""
2026-04-09 14:30:51 +07:00
DELETE FROM qd_strategy_notifications
2026-01-14 05:29:55 +08:00
WHERE strategy_id IN (SELECT id FROM qd_strategies_trading WHERE user_id = ?)
2026-01-25 01:03:46 +08:00
OR (strategy_id IS NULL AND user_id = ?)
2026-01-14 05:29:55 +08:00
""" ,
2026-04-09 14:30:51 +07:00
( user_id , user_id ),
2026-01-14 05:29:55 +08:00
)
2026-01-12 22:36:10 +08:00
db . commit ()
cur . close ()
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" })
2026-01-12 22:36:10 +08:00
except Exception as e :
logger . error ( f "clear_notifications failed: { str ( e ) } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e )}), 500
2026-03-25 18:08:45 +08:00
# ===== Script Strategy Endpoints =====
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/verify-code" , methods = [ "POST" ])
2026-03-25 18:08:45 +08:00
@login_required
def verify_strategy_code ():
"""Verify script strategy code syntax and safety."""
try :
payload = request . get_json () or {}
2026-04-09 14:30:51 +07:00
code = payload . get ( "code" , "" )
2026-03-25 18:08:45 +08:00
if not code . strip ():
2026-04-09 14:30:51 +07:00
return jsonify ({ "success" : False , "message" : "Code is empty" })
2026-03-25 18:08:45 +08:00
2026-04-09 14:30:51 +07:00
required_funcs = [ "on_bar" , "on_init" ]
found = [ f for f in required_funcs if f "def { f } " in code ]
2026-03-25 18:08:45 +08:00
missing = [ f for f in required_funcs if f not in found ]
if missing :
2026-04-09 14:30:51 +07:00
return jsonify ({ "success" : False , "message" : f "Missing required functions: { ', ' . join ( missing ) } " })
2026-03-25 18:08:45 +08:00
try :
2026-04-09 14:30:51 +07:00
compile ( code , "<strategy>" , "exec" )
2026-03-25 18:08:45 +08:00
except SyntaxError as se :
2026-04-09 14:30:51 +07:00
return jsonify ({ "success" : False , "message" : f "Syntax error at line { se . lineno } : { se . msg } " })
2026-03-25 18:08:45 +08:00
2026-04-09 14:30:51 +07:00
return jsonify ({ "success" : True , "message" : "Code verification passed" })
2026-03-25 18:08:45 +08:00
except Exception as e :
logger . error ( f "verify_strategy_code failed: { str ( e ) } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "success" : False , "message" : str ( e )})
2026-03-25 18:08:45 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/ai-generate" , methods = [ "POST" ])
2026-03-25 18:08:45 +08:00
@login_required
def ai_generate_strategy ():
2026-04-06 23:19:41 +08:00
"""Generate strategy code or suggest template parameter updates using AI."""
2026-03-25 18:08:45 +08:00
try :
payload = request . get_json () or {}
2026-04-09 14:30:51 +07:00
prompt = payload . get ( "prompt" , "" )
2026-03-25 18:08:45 +08:00
if not prompt . strip ():
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : "" , "msg" : "Prompt is empty" , "params" : None })
2026-04-06 23:19:41 +08:00
2026-04-09 14:30:51 +07:00
intent = ( payload . get ( "intent" ) or "generate_code" ) . strip ()
2026-04-06 23:19:41 +08:00
from app.services.llm import LLMService
2026-04-09 14:30:51 +07:00
2026-04-06 23:19:41 +08:00
llm = LLMService ()
api_key = llm . get_api_key ()
if not api_key :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : "" , "msg" : "No LLM API key configured" , "params" : None })
2026-04-06 23:19:41 +08:00
2026-04-09 14:30:51 +07:00
if intent == "adjust_params" :
template_key = payload . get ( "template_key" ) or ""
current_params = payload . get ( "params" ) or {}
code_snapshot = ( payload . get ( "code" ) or "" )[: 8000 ]
2026-04-06 23:19:41 +08:00
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 ,
2026-04-09 14:30:51 +07:00
use_json_mode = False ,
2026-04-06 23:19:41 +08:00
)
2026-04-09 14:30:51 +07:00
raw = ( content or "" ) . strip ()
if raw . startswith ( "```" ):
raw = re . sub ( r "^```[a-zA-Z]*" , "" , raw ) . strip ()
if raw . endswith ( "```" ):
2026-04-06 23:19:41 +08:00
raw = raw [: - 3 ] . strip ()
updates = None
try :
updates = json . loads ( raw )
except json . JSONDecodeError :
2026-04-09 14:30:51 +07:00
m = re . search ( r "\{[\s\S]*\}" , raw )
2026-04-06 23:19:41 +08:00
if m :
try :
updates = json . loads ( m . group ( 0 ))
except json . JSONDecodeError :
updates = None
if not isinstance ( updates , dict ):
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : "" , "params" : None , "msg" : "AI did not return valid JSON parameters" })
return jsonify ({ "code" : "" , "params" : updates , "msg" : "success" })
2026-03-25 18:08:45 +08:00
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
2026-04-06 01:39:25 +08:00
- bar supports both bar.close and bar['close'] access, and has: open, high, low, close, volume, timestamp
2026-03-25 18:08:45 +08:00
- ctx.buy(price, amount), ctx.sell(price, amount), ctx.close_position()
2026-04-06 01:39:25 +08:00
- ctx.position supports both numeric checks and dict-style fields:
- if not ctx.position / if ctx.position > 0 / if ctx.position < 0
- ctx.position['side'], ctx.position['size'], ctx.position['entry_price']
- ctx.balance, ctx.equity
2026-03-25 18:08:45 +08:00
- 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."""
2026-04-09 14:30:51 +07:00
extra = ""
template_key = payload . get ( "template_key" )
params = payload . get ( "params" )
code_ctx = ( payload . get ( "code" ) or "" ) . strip ()
2026-04-06 23:19:41 +08:00
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 :
2026-04-09 14:30:51 +07:00
extra_parts . append ( "Current template parameters (JSON): \n " + json . dumps ( params , ensure_ascii = False ))
2026-04-06 23:19:41 +08:00
if code_ctx :
2026-04-09 14:30:51 +07:00
extra_parts . append ( "Current code (may be long): \n " + code_ctx [: 12000 ])
extra = " \n\n " + " \n\n " . join ( extra_parts )
2026-04-06 23:19:41 +08:00
user_prompt = prompt . strip () + extra
2026-03-25 18:08:45 +08:00
content = llm . call_llm_api (
messages = [
{ "role" : "system" , "content" : system_prompt },
2026-04-06 23:19:41 +08:00
{ "role" : "user" , "content" : user_prompt },
2026-03-25 18:08:45 +08:00
],
model = llm . get_code_generation_model (),
temperature = 0.7 ,
2026-04-09 14:30:51 +07:00
use_json_mode = False ,
2026-03-25 18:08:45 +08:00
)
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 :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : content , "msg" : "success" , "params" : None })
2026-03-25 18:08:45 +08:00
else :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : "" , "msg" : "AI generation returned empty result" , "params" : None })
2026-03-25 18:08:45 +08:00
except Exception as e :
logger . error ( f "ai_generate_strategy failed: { str ( e ) } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : "" , "msg" : str ( e ), "params" : None })
2026-03-25 18:08:45 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/performance" , methods = [ "GET" ])
2026-03-25 18:08:45 +08:00
@login_required
def get_strategy_performance ():
"""Get strategy performance metrics (aggregated from equity curve and trades)."""
try :
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" )
2026-03-25 18:08:45 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy ID required" })
2026-03-25 18:08:45 +08:00
svc = get_strategy_service ()
equity_data = svc . get_equity_curve ( int ( strategy_id ))
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : { "equity_curve" : equity_data }})
2026-03-25 18:08:45 +08:00
except Exception as e :
logger . error ( f "get_strategy_performance failed: { str ( e ) } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e )}), 500
2026-03-25 18:08:45 +08:00
2026-04-09 14:30:51 +07:00
@strategy_bp.route ( "/strategies/logs" , methods = [ "GET" ])
2026-03-25 18:08:45 +08:00
@login_required
def get_strategy_logs ():
"""Get strategy running logs."""
try :
2026-04-07 22:47:07 +08:00
user_id = g . user_id
2026-04-09 14:30:51 +07:00
strategy_id = request . args . get ( "id" )
limit = int ( request . args . get ( "limit" , 200 ))
2026-03-25 18:08:45 +08:00
if not strategy_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy ID required" })
2026-03-25 18:08:45 +08:00
2026-04-07 22:47:07 +08:00
st = get_strategy_service () . get_strategy ( int ( strategy_id ), user_id = user_id )
if not st :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Strategy not found" }), 404
2026-04-07 22:47:07 +08:00
2026-03-25 18:08:45 +08:00
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 ?
""" ,
2026-04-09 14:30:51 +07:00
( int ( strategy_id ), limit ),
2026-03-25 18:08:45 +08:00
)
rows = cur . fetchall () or []
cur . close ()
2026-04-07 22:47:07 +08:00
out = []
for r in rows or []:
if not isinstance ( r , dict ):
continue
rr = dict ( r )
2026-04-09 14:30:51 +07:00
ts = rr . get ( "timestamp" )
if ts is not None and hasattr ( ts , "isoformat" ):
rr [ "timestamp" ] = ts . isoformat ()
2026-04-07 22:47:07 +08:00
out . append ( rr )
logs = list ( reversed ( out ))
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : logs })
2026-03-25 18:08:45 +08:00
except Exception as e :
2026-04-07 22:47:07 +08:00
if PgUndefinedTable is not None and isinstance ( e , PgUndefinedTable ):
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : []})
2026-04-07 22:47:07 +08:00
el = str ( e ) . lower ()
2026-04-09 14:30:51 +07:00
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" : []})
2026-03-25 18:08:45 +08:00
logger . error ( f "get_strategy_logs failed: { str ( e ) } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e )}), 500