Skip to content

Commit de3fd09

Browse files
v3.0.1
Signed-off-by: Dinger <quantdinger@gmail.com>
1 parent 3ca291a commit de3fd09

91 files changed

Lines changed: 1504 additions & 405 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎backend_api_python/app/routes/quick_trade.py‎

Lines changed: 255 additions & 63 deletions
Large diffs are not rendered by default.

‎backend_api_python/app/routes/strategy.py‎

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from flask import Blueprint, request, jsonify, g
55
from datetime import datetime
66
import json
7+
import re
78
import traceback
89
import time
910

@@ -770,15 +771,16 @@ def start_strategy():
770771

771772
# Get strategy type
772773
strategy_type = get_strategy_service().get_strategy_type(strategy_id)
773-
774-
# Update strategy status
775-
get_strategy_service().update_strategy_status(strategy_id, 'running', user_id=user_id)
776-
777-
# Local backend: AI strategy executor was removed. Only indicator strategies are supported.
774+
775+
# IndicatorStrategy and ScriptStrategy are executed by TradingExecutor.
778776
if strategy_type == 'PromptBasedStrategy':
779-
return jsonify({'code': 0, 'msg': 'AI strategy has been removed; local edition does not support starting AI strategies', 'data': None}), 400
777+
return jsonify({
778+
'code': 0,
779+
'msg': 'AI strategy has been removed; local edition does not support starting AI strategies',
780+
'data': None
781+
}), 400
782+
get_strategy_service().update_strategy_status(strategy_id, 'running', user_id=user_id)
780783

781-
# Indicator strategy
782784
success = get_trading_executor().start_strategy(strategy_id)
783785

784786
if not success:
@@ -1241,12 +1243,65 @@ def verify_strategy_code():
12411243
@strategy_bp.route('/strategies/ai-generate', methods=['POST'])
12421244
@login_required
12431245
def ai_generate_strategy():
1244-
"""Generate strategy code using AI."""
1246+
"""Generate strategy code or suggest template parameter updates using AI."""
12451247
try:
12461248
payload = request.get_json() or {}
12471249
prompt = payload.get('prompt', '')
12481250
if not prompt.strip():
1249-
return jsonify({'code': '', 'msg': 'Prompt is empty'})
1251+
return jsonify({'code': '', 'msg': 'Prompt is empty', 'params': None})
1252+
1253+
intent = (payload.get('intent') or 'generate_code').strip()
1254+
from app.services.llm import LLMService
1255+
llm = LLMService()
1256+
api_key = llm.get_api_key()
1257+
if not api_key:
1258+
return jsonify({'code': '', 'msg': 'No LLM API key configured', 'params': None})
1259+
1260+
if intent == 'adjust_params':
1261+
template_key = payload.get('template_key') or ''
1262+
current_params = payload.get('params') or {}
1263+
code_snapshot = (payload.get('code') or '')[:8000]
1264+
system_prompt = """You tune quantitative strategy template parameters from the user's request.
1265+
Return ONLY a single JSON object: keys are parameter names (strings), values are JSON numbers or booleans.
1266+
You may return a partial object (only keys that should change) or a full object.
1267+
Do not use markdown fences, do not add explanations before or after the JSON."""
1268+
1269+
user_content = (
1270+
f"Template key: {template_key}\n"
1271+
f"Current parameters (JSON):\n{json.dumps(current_params, ensure_ascii=False)}\n\n"
1272+
f"Strategy code excerpt (context):\n{code_snapshot}\n\n"
1273+
f"User request:\n{prompt.strip()}\n\n"
1274+
"Respond with JSON only."
1275+
)
1276+
1277+
content = llm.call_llm_api(
1278+
messages=[
1279+
{"role": "system", "content": system_prompt},
1280+
{"role": "user", "content": user_content},
1281+
],
1282+
model=llm.get_code_generation_model(),
1283+
temperature=0.3,
1284+
use_json_mode=False
1285+
)
1286+
1287+
raw = (content or '').strip()
1288+
if raw.startswith('```'):
1289+
raw = re.sub(r'^```[a-zA-Z]*', '', raw).strip()
1290+
if raw.endswith('```'):
1291+
raw = raw[:-3].strip()
1292+
updates = None
1293+
try:
1294+
updates = json.loads(raw)
1295+
except json.JSONDecodeError:
1296+
m = re.search(r'\{[\s\S]*\}', raw)
1297+
if m:
1298+
try:
1299+
updates = json.loads(m.group(0))
1300+
except json.JSONDecodeError:
1301+
updates = None
1302+
if not isinstance(updates, dict):
1303+
return jsonify({'code': '', 'params': None, 'msg': 'AI did not return valid JSON parameters'})
1304+
return jsonify({'code': '', 'params': updates, 'msg': 'success'})
12501305

12511306
system_prompt = """You are a quantitative trading strategy code generator.
12521307
Generate Python strategy code that follows this framework:
@@ -1264,16 +1319,26 @@ def ai_generate_strategy():
12641319
12651320
Return ONLY the Python code, no explanations."""
12661321

1267-
from app.services.llm import LLMService
1268-
llm = LLMService()
1269-
api_key = llm.get_api_key()
1270-
if not api_key:
1271-
return jsonify({'code': '', 'msg': 'No LLM API key configured'})
1322+
extra = ''
1323+
template_key = payload.get('template_key')
1324+
params = payload.get('params')
1325+
code_ctx = (payload.get('code') or '').strip()
1326+
if template_key or params is not None or code_ctx:
1327+
extra_parts = []
1328+
if template_key:
1329+
extra_parts.append(f"Current template key: {template_key}")
1330+
if isinstance(params, dict) and params:
1331+
extra_parts.append('Current template parameters (JSON):\n' + json.dumps(params, ensure_ascii=False))
1332+
if code_ctx:
1333+
extra_parts.append('Current code (may be long):\n' + code_ctx[:12000])
1334+
extra = '\n\n' + '\n\n'.join(extra_parts)
1335+
1336+
user_prompt = prompt.strip() + extra
12721337

12731338
content = llm.call_llm_api(
12741339
messages=[
12751340
{"role": "system", "content": system_prompt},
1276-
{"role": "user", "content": prompt},
1341+
{"role": "user", "content": user_prompt},
12771342
],
12781343
model=llm.get_code_generation_model(),
12791344
temperature=0.7,
@@ -1290,12 +1355,12 @@ def ai_generate_strategy():
12901355
content = content.strip()
12911356

12921357
if content:
1293-
return jsonify({'code': content, 'msg': 'success'})
1358+
return jsonify({'code': content, 'msg': 'success', 'params': None})
12941359
else:
1295-
return jsonify({'code': '', 'msg': 'AI generation returned empty result'})
1360+
return jsonify({'code': '', 'msg': 'AI generation returned empty result', 'params': None})
12961361
except Exception as e:
12971362
logger.error(f"ai_generate_strategy failed: {str(e)}")
1298-
return jsonify({'code': '', 'msg': str(e)})
1363+
return jsonify({'code': '', 'msg': str(e), 'params': None})
12991364

13001365

13011366
@strategy_bp.route('/strategies/performance', methods=['GET'])

‎backend_api_python/app/services/backtest.py‎

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,12 @@ def run_multi_timeframe(
504504
result['precision_info']['message'] = 'Using standard backtest because scale rules are not fully supported in MTF mode'
505505
elif fallback_reason == 'signal_timing_not_supported_in_mtf':
506506
result['precision_info']['message'] = 'Using standard backtest because this execution timing is not fully supported in MTF mode'
507+
ea = result.get('executionAssumptions') or {}
508+
ea['mtfRequested'] = bool(enable_mtf)
509+
ea['mtfActive'] = False
510+
if fallback_reason:
511+
ea['mtfFallbackReason'] = fallback_reason
512+
result['executionAssumptions'] = ea
507513
return result
508514

509515
logger.info(f"Multi-timeframe backtest: strategy_tf={timeframe}, exec_tf={exec_tf}, range={start_date} ~ {end_date}")
@@ -554,6 +560,11 @@ def run_multi_timeframe(
554560
'reason': 'data_unavailable',
555561
'message': f'Cannot fetch {exec_tf} data, using standard backtest'
556562
}
563+
ea = result.get('executionAssumptions') or {}
564+
ea['mtfRequested'] = bool(enable_mtf)
565+
ea['mtfActive'] = False
566+
ea['mtfFallbackReason'] = 'data_unavailable'
567+
result['executionAssumptions'] = ea
557568
return result
558569

559570
logger.info(f"Data fetched: signal_candles={len(df_signal)}, exec_candles={len(df_exec)}")
@@ -598,6 +609,14 @@ def run_multi_timeframe(
598609
result['execution_timeframe'] = exec_tf
599610
result['signal_candles'] = len(df_signal)
600611
result['execution_candles'] = len(df_exec)
612+
result['executionAssumptions'] = self._execution_assumptions(
613+
strategy_config,
614+
simulation_mode='mtf',
615+
signal_timeframe=timeframe,
616+
execution_timeframe=exec_tf,
617+
mtf_requested=True,
618+
mtf_active=True,
619+
)
601620
logger.info("Backtest result formatted successfully")
602621
except Exception as e:
603622
logger.error(f"Failed to format result: {str(e)}")
@@ -1487,6 +1506,11 @@ def _run_script_strategy(
14871506
'precision': 'standard',
14881507
'message': 'Using standard strategy script backtest'
14891508
}
1509+
result['executionAssumptions'] = self._execution_assumptions(
1510+
strategy_config,
1511+
simulation_mode='standard',
1512+
signal_timeframe=timeframe,
1513+
)
14901514
return result
14911515

14921516
def run_code_strategy(
@@ -1607,7 +1631,13 @@ def run(
16071631
metrics = self._calculate_metrics(equity_curve, trades, initial_capital, timeframe, start_date, end_date, total_commission)
16081632

16091633
# 5. Format result
1610-
return self._format_result(metrics, equity_curve, trades)
1634+
result = self._format_result(metrics, equity_curve, trades)
1635+
result['executionAssumptions'] = self._execution_assumptions(
1636+
strategy_config,
1637+
simulation_mode='standard',
1638+
signal_timeframe=timeframe,
1639+
)
1640+
return result
16111641

16121642
def _fetch_kline_data(
16131643
self,
@@ -4740,6 +4770,46 @@ def _calculate_sharpe(self, values: List[float], timeframe: str = '1D', risk_fre
47404770
logger.warning(f"Sharpe ratio calculation failed: {e}")
47414771
return 0
47424772

4773+
def _execution_assumptions(
4774+
self,
4775+
strategy_config: Optional[Dict[str, Any]],
4776+
*,
4777+
simulation_mode: str,
4778+
signal_timeframe: Optional[str] = None,
4779+
execution_timeframe: Optional[str] = None,
4780+
mtf_requested: bool = False,
4781+
mtf_active: bool = False,
4782+
mtf_fallback_reason: Optional[str] = None,
4783+
) -> Dict[str, Any]:
4784+
"""
4785+
Human-facing metadata so the UI can explain how trades were timed vs chart markers.
4786+
Keys use camelCase for JSON consumers (frontend).
4787+
"""
4788+
cfg = strategy_config or {}
4789+
raw = str((cfg.get('execution') or {}).get('signalTiming') or 'next_bar_open').strip().lower()
4790+
is_next_open = raw in ('next_bar_open', 'next_open', 'nextopen', 'next')
4791+
if raw in ('bar_close', 'close', 'same_bar_close', 'current_bar_close'):
4792+
timing_key = 'same_bar_close'
4793+
elif is_next_open:
4794+
timing_key = 'next_bar_open'
4795+
else:
4796+
timing_key = raw
4797+
default_fill = 'open' if is_next_open else 'close'
4798+
payload: Dict[str, Any] = {
4799+
'signalTiming': timing_key,
4800+
'signalTimingRaw': raw,
4801+
'defaultFillPrice': default_fill,
4802+
'simulationMode': simulation_mode,
4803+
'strategyTimeframe': signal_timeframe,
4804+
'executionTimeframe': execution_timeframe,
4805+
'engineVersion': self.ENGINE_VERSION,
4806+
'mtfRequested': bool(mtf_requested),
4807+
'mtfActive': bool(mtf_active),
4808+
}
4809+
if mtf_fallback_reason:
4810+
payload['mtfFallbackReason'] = mtf_fallback_reason
4811+
return payload
4812+
47434813
def _format_result(
47444814
self,
47454815
metrics: Dict,

‎backend_api_python/app/services/live_trading/binance.py‎

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ def __init__(self, *, api_key: str, secret_key: str, base_url: str = None, enabl
4040
self._dual_side_cache: Optional[Tuple[float, bool]] = None
4141
self._dual_side_cache_ttl_sec = 60.0
4242

43+
# serverTime - local_ms; avoids Binance -1021 when the host clock is ahead of Binance.
44+
self._time_offset_ms: int = 0
45+
self._time_sync_monotonic: float = 0.0
46+
4347
@staticmethod
4448
def _to_dec(x: Any) -> Decimal:
4549
try:
@@ -163,6 +167,26 @@ def _sign(self, query_string: str) -> str:
163167
def _signed_headers(self) -> Dict[str, str]:
164168
return {"X-MBX-APIKEY": self.api_key}
165169

170+
def _ensure_server_time(self, *, force: bool = False) -> None:
171+
"""
172+
Align signed request timestamps with Binance server time (GET /fapi/v1/time).
173+
"""
174+
now_m = time.monotonic()
175+
if not force and (now_m - float(self._time_sync_monotonic or 0.0)) < 300.0:
176+
return
177+
try:
178+
code, data, _ = self._request("GET", "/fapi/v1/time")
179+
if code != 200 or not isinstance(data, dict):
180+
return
181+
server_ms = int(data.get("serverTime") or 0)
182+
if server_ms <= 0:
183+
return
184+
local_ms = int(time.time() * 1000)
185+
self._time_offset_ms = server_ms - local_ms
186+
self._time_sync_monotonic = now_m
187+
except Exception:
188+
pass
189+
166190
def _format_client_order_id(self, client_order_id: Optional[str]) -> str:
167191
raw = str(client_order_id or "").strip()
168192
broker_id = str(self.broker_id or "").strip()
@@ -179,17 +203,34 @@ def _format_client_order_id(self, client_order_id: Optional[str]) -> str:
179203
return f"{prefix}{raw[:suffix_budget]}"
180204

181205
def _signed_request(self, method: str, path: str, *, params: Dict[str, Any]) -> Dict[str, Any]:
182-
p = dict(params or {})
183-
# Use server-accepted timestamp in ms.
184-
p["timestamp"] = int(time.time() * 1000)
185-
qs = urlencode(p, doseq=True)
186-
p["signature"] = self._sign(qs)
187-
code, data, text = self._request(method, path, params=p, headers=self._signed_headers())
188-
if code >= 400:
189-
raise LiveTradingError(f"Binance HTTP {code}: {text[:500]}")
190-
if isinstance(data, dict) and data.get("code") and int(data.get("code")) < 0:
191-
raise LiveTradingError(f"Binance error: {data}")
192-
return data if isinstance(data, dict) else {"raw": data}
206+
self._ensure_server_time()
207+
last_err: Optional[LiveTradingError] = None
208+
for attempt in range(2):
209+
p = dict(params or {})
210+
p["timestamp"] = int(time.time() * 1000) + int(self._time_offset_ms)
211+
if "recvWindow" not in p:
212+
p["recvWindow"] = 10000
213+
qs = urlencode(p, doseq=True)
214+
p["signature"] = self._sign(qs)
215+
code, data, text = self._request(method, path, params=p, headers=self._signed_headers())
216+
if code >= 400:
217+
err = LiveTradingError(f"Binance HTTP {code}: {text[:500]}")
218+
if attempt == 0 and ("-1021" in text or "1021" in text):
219+
self._ensure_server_time(force=True)
220+
last_err = err
221+
continue
222+
raise err
223+
if isinstance(data, dict) and data.get("code") and int(data.get("code")) < 0:
224+
err = LiveTradingError(f"Binance error: {data}")
225+
if attempt == 0 and int(data.get("code") or 0) == -1021:
226+
self._ensure_server_time(force=True)
227+
last_err = err
228+
continue
229+
raise err
230+
return data if isinstance(data, dict) else {"raw": data}
231+
if last_err:
232+
raise last_err
233+
raise LiveTradingError("Binance signed request failed")
193234

194235
def _public_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
195236
code, data, text = self._request(method, path, params=params, headers=None, json_body=None, data=None)
@@ -870,12 +911,41 @@ def cancel_order(self, *, symbol: str, order_id: str = "", client_order_id: str
870911
raise LiveTradingError("Binance cancel_order requires order_id or client_order_id")
871912
return self._signed_request("DELETE", "/fapi/v1/order", params=params)
872913

873-
def get_positions(self) -> Any:
914+
def set_margin_type(self, *, symbol: str, margin_mode: str) -> Dict[str, Any]:
874915
"""
875-
Return all futures positions (position risk endpoint).
916+
Set symbol margin mode on USDT-M futures.
917+
918+
Endpoint: POST /fapi/v1/marginType
919+
margin_mode: cross | crossed | isolated
920+
"""
921+
sym = to_binance_futures_symbol(symbol)
922+
m = (margin_mode or "").strip().lower()
923+
if m in ("cross", "crossed"):
924+
mt = "CROSSED"
925+
elif m in ("isolated", "iso"):
926+
mt = "ISOLATED"
927+
else:
928+
raise LiveTradingError(f"Invalid margin_mode for Binance: {margin_mode}")
929+
return self._signed_request("POST", "/fapi/v1/marginType", params={"symbol": sym, "marginType": mt})
930+
931+
def get_positions(self, *, symbol: str = "") -> Any:
932+
"""
933+
Futures positions (position risk). Optional ``symbol`` filters to one contract.
876934
877935
Endpoint: GET /fapi/v2/positionRisk
878936
"""
879-
return self._signed_request("GET", "/fapi/v2/positionRisk", params={})
937+
raw = self._signed_request("GET", "/fapi/v2/positionRisk", params={})
938+
rows: list
939+
if isinstance(raw, list):
940+
rows = raw
941+
elif isinstance(raw, dict) and isinstance(raw.get("raw"), list):
942+
rows = raw["raw"]
943+
else:
944+
rows = []
945+
want = (symbol or "").strip()
946+
if not want:
947+
return rows
948+
sym = to_binance_futures_symbol(want)
949+
return [p for p in rows if isinstance(p, dict) and str(p.get("symbol") or "") == sym]
880950

881951

0 commit comments

Comments
 (0)