Skip to content

Commit e9e2f70

Browse files
v3.0.1
Signed-off-by: Dinger <quantdinger@gmail.com>
1 parent 94891c7 commit e9e2f70

33 files changed

Lines changed: 400 additions & 185 deletions

backend_api_python/app/routes/backtest.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44
from flask import Blueprint, request, jsonify, g
55
from datetime import datetime
6+
import calendar
67
import traceback
78
import json
89
import time
@@ -20,6 +21,27 @@
2021
backtest_service = BacktestService()
2122

2223

24+
def _add_months(dt: datetime, months: int) -> datetime:
25+
"""Add calendar months while keeping the day within the target month."""
26+
month_index = (dt.month - 1) + int(months or 0)
27+
year = dt.year + month_index // 12
28+
month = month_index % 12 + 1
29+
day = min(dt.day, calendar.monthrange(year, month)[1])
30+
return dt.replace(year=year, month=month, day=day)
31+
32+
33+
def _backtest_range_limit(timeframe: str, start_date: datetime) -> tuple[datetime, str]:
34+
"""Return the max allowed end date and human-readable label for a timeframe."""
35+
tf = str(timeframe or '').strip()
36+
if tf == '1m':
37+
return _add_months(start_date, 1), '1 month'
38+
if tf == '5m':
39+
return _add_months(start_date, 6), '6 months'
40+
if tf in ['15m', '30m']:
41+
return _add_months(start_date, 12), '1 year'
42+
return _add_months(start_date, 36), '3 years'
43+
44+
2345
def _openrouter_base_and_key() -> tuple[str, str]:
2446
from app.config import APIKeys
2547
# Use APIKeys to get the key (handles env var + config cache properly)
@@ -205,22 +227,11 @@ def run_backtest():
205227

206228
# 验证时间范围限制
207229
days_diff = (end_date - start_date).days
230+
max_end_date, max_range_text = _backtest_range_limit(timeframe, start_date)
231+
max_end_date = max_end_date.replace(hour=23, minute=59, second=59)
208232

209-
# 根据周期设置不同的时间限制
210-
if timeframe == '1m':
211-
max_days = 30 # 1分钟K线最多1个月
212-
max_range_text = '1 month'
213-
elif timeframe == '5m':
214-
max_days = 180 # 5分钟K线最多6个月
215-
max_range_text = '6 months'
216-
elif timeframe in ['15m', '30m']:
217-
max_days = 365 # 15分钟和30分钟K线最多1年
218-
max_range_text = '1 year'
219-
else: # 1H, 4H, 1D, 1W
220-
max_days = 1095 # 1小时及以上最多3年
221-
max_range_text = '3 years'
222-
223-
if days_diff > max_days:
233+
if end_date > max_end_date:
234+
max_days = (max_end_date - start_date).days
224235
return jsonify({
225236
'code': 0,
226237
'msg': f'Backtest range exceeds limit: timeframe {timeframe} supports up to {max_range_text} ({max_days} days), but you selected {days_diff} days',

backend_api_python/app/routes/indicator.py

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -291,35 +291,74 @@ def _indicator_debug_summary(validation: Dict[str, Any] | None = None) -> Dict[s
291291
}
292292

293293

294-
def _indicator_hint_to_text(hint_code: str, params: Dict[str, Any] | None = None) -> str:
294+
def _request_lang(default: str = "zh-CN") -> str:
295+
raw = (
296+
request.headers.get("X-App-Lang")
297+
or request.headers.get("Accept-Language")
298+
or default
299+
)
300+
lang = str(raw or default).split(",", 1)[0].strip()
301+
return lang or default
302+
303+
304+
def _is_zh_lang(lang: str | None) -> bool:
305+
return str(lang or "zh-CN").strip().lower().startswith("zh")
306+
307+
308+
def _indicator_ai_text(key: str, lang: str = "zh-CN") -> str:
309+
is_zh = _is_zh_lang(lang)
310+
texts = {
311+
"prompt_required": "提示词不能为空" if is_zh else "Prompt cannot be empty",
312+
"insufficient_credits": "积分不足,请充值后重试" if is_zh else "Insufficient credits. Please top up and try again.",
313+
}
314+
return texts.get(key, key)
315+
316+
317+
def _indicator_hint_to_text(hint_code: str, params: Dict[str, Any] | None = None, lang: str = "zh-CN") -> str:
295318
params = params or {}
319+
is_zh = _is_zh_lang(lang)
296320
if hint_code == "DECLARED_PARAMS_NOT_READ_VIA_PARAMS_GET":
297321
names = params.get("names") or []
298-
joined = "、".join(names) if names else "参数"
299-
return f"已检测到声明的参数未通过 params.get(...) 读取:{joined}。"
322+
joined = "、".join(names) if (names and is_zh) else ", ".join(names)
323+
if not joined:
324+
joined = "参数" if is_zh else "parameters"
325+
return (
326+
f"已检测到声明的参数未通过 params.get(...) 读取:{joined}。"
327+
if is_zh else
328+
f"Declared parameters are not being read via params.get(...): {joined}."
329+
)
300330
if hint_code == "SIGNAL_MARKERS_USE_WHERE_NONE":
301-
return "已检测到信号标记使用 where(..., None).tolist(),建议改为显式 None 列表以避免 NaN 渲染问题。"
331+
return (
332+
"已检测到信号标记使用 where(..., None).tolist(),建议改为显式 None 列表以避免 NaN 渲染问题。"
333+
if is_zh else
334+
"Signal markers use where(..., None).tolist(); prefer an explicit None list to avoid NaN rendering issues."
335+
)
302336
if hint_code == "MISSING_OUTPUT":
303-
return "缺少 output 字典。"
337+
return "缺少 output 字典。" if is_zh else "Missing output dictionary."
304338
if hint_code == "MISSING_BUY_SELL_COLUMNS":
305-
return "缺少 df['buy'] 或 df['sell'] 信号列。"
339+
return "缺少 df['buy'] 或 df['sell'] 信号列。" if is_zh else "Missing df['buy'] or df['sell'] signal columns."
306340
if hint_code == "MISSING_DF_COPY":
307-
return "缺少 df = df.copy()。"
341+
return "缺少 df = df.copy()。" if is_zh else "Missing df = df.copy()."
308342
if hint_code == "MISSING_INDICATOR_NAME":
309-
return "缺少 my_indicator_name。"
343+
return "缺少 my_indicator_name。" if is_zh else "Missing my_indicator_name."
310344
if hint_code == "MISSING_INDICATOR_DESCRIPTION":
311-
return "缺少 my_indicator_description。"
345+
return "缺少 my_indicator_description。" if is_zh else "Missing my_indicator_description."
312346
if hint_code == "UNKNOWN_STRATEGY_KEY":
313-
return f"存在未知的 @strategy 键:{params.get('key') or 'unknown'}。"
347+
key = params.get('key') or 'unknown'
348+
return (
349+
f"存在未知的 @strategy 键:{key}。"
350+
if is_zh else
351+
f"Unknown @strategy key detected: {key}."
352+
)
314353
if hint_code == "NO_STRATEGY_ANNOTATIONS":
315-
return "没有声明任何 @strategy 默认配置。"
354+
return "没有声明任何 @strategy 默认配置。" if is_zh else "No @strategy default configuration was declared."
316355
if hint_code == "NO_STOP_AND_TAKE_PROFIT":
317-
return "未声明止损和止盈默认配置。"
356+
return "未声明止损和止盈默认配置。" if is_zh else "Stop-loss and take-profit defaults are not declared."
318357
if hint_code == "NO_STOP_LOSS":
319-
return "未声明止损默认配置。"
358+
return "未声明止损默认配置。" if is_zh else "Stop-loss default is not declared."
320359
if hint_code == "NO_TAKE_PROFIT":
321-
return "未声明止盈默认配置。"
322-
return f"检测到代码提示:{hint_code}"
360+
return "未声明止盈默认配置。" if is_zh else "Take-profit default is not declared."
361+
return f"检测到代码提示:{hint_code}" if is_zh else f"Code hint detected: {hint_code}"
323362

324363

325364
def _indicator_human_summary(
@@ -328,7 +367,9 @@ def _indicator_human_summary(
328367
auto_fix_applied: bool,
329368
auto_fix_succeeded: bool,
330369
returned_candidate: str,
370+
lang: str = "zh-CN",
331371
) -> Dict[str, Any]:
372+
is_zh = _is_zh_lang(lang)
332373
initial_hints = initial_validation.get("hints") or []
333374
final_hints = final_validation.get("hints") or []
334375
initial_codes = {h.get("code") for h in initial_hints if h.get("code")}
@@ -337,27 +378,27 @@ def _indicator_human_summary(
337378
remaining_codes = sorted(final_codes)
338379

339380
fixed_messages = [
340-
_indicator_hint_to_text(h.get("code"), h.get("params"))
381+
_indicator_hint_to_text(h.get("code"), h.get("params"), lang=lang)
341382
for h in initial_hints
342383
if h.get("code") in fixed_codes
343384
]
344385
remaining_messages = [
345-
_indicator_hint_to_text(h.get("code"), h.get("params"))
386+
_indicator_hint_to_text(h.get("code"), h.get("params"), lang=lang)
346387
for h in final_hints
347388
if h.get("code") in remaining_codes
348389
]
349390

350391
if auto_fix_applied and auto_fix_succeeded:
351-
title = "AI 已自动修复并返回更稳定的指标代码"
392+
title = "AI 已自动修复并返回更稳定的指标代码" if is_zh else "AI auto-fixed the indicator code and returned a more stable version"
352393
elif auto_fix_applied:
353-
title = "AI 尝试自动修复,但仍保留部分问题"
394+
title = "AI 尝试自动修复,但仍保留部分问题" if is_zh else "AI attempted to auto-fix the code, but some issues still remain"
354395
else:
355-
title = "AI 已生成指标代码,并通过当前质检流程"
396+
title = "AI 已生成指标代码,并通过当前质检流程" if is_zh else "AI generated indicator code and it passed the current QA flow"
356397

357398
if returned_candidate == "repaired":
358-
returned_text = "当前返回的是自动修复后的代码。"
399+
returned_text = "当前返回的是自动修复后的代码。" if is_zh else "The returned code is the auto-fixed version."
359400
else:
360-
returned_text = "当前返回的是首次生成的代码。"
401+
returned_text = "当前返回的是首次生成的代码。" if is_zh else "The returned code is the initially generated version."
361402

362403
return {
363404
"title": title,
@@ -687,13 +728,14 @@ def ai_generate():
687728
Local-first: if OpenRouter key is not configured, we return a reasonable template.
688729
"""
689730
data = request.get_json() or {}
731+
lang = _request_lang()
690732
prompt = (data.get("prompt") or "").strip()
691733
existing = (data.get("existingCode") or "").strip()
692734

693735
if not prompt:
694736
# Keep SSE contract (match PHP behavior) so frontend doesn't look "stuck".
695737
def _err_stream():
696-
yield "data: " + json.dumps({"error": "提示词不能为空"}, ensure_ascii=False) + "\n\n"
738+
yield "data: " + json.dumps({"error": _indicator_ai_text("prompt_required", lang)}, ensure_ascii=False) + "\n\n"
697739
yield "data: [DONE]\n\n"
698740

699741
return Response(
@@ -1019,7 +1061,7 @@ def _generate_final_code() -> tuple[str, Dict[str, Any]]:
10191061
"final_validation": _indicator_debug_summary(validation),
10201062
}
10211063
debug["human_summary"] = _indicator_human_summary(
1022-
validation, validation, False, False, "initial"
1064+
validation, validation, False, False, "initial", lang=lang
10231065
)
10241066
logger.info("ai_generate debug=%s", json.dumps(debug, ensure_ascii=False))
10251067
return code_text, debug
@@ -1038,7 +1080,7 @@ def _generate_final_code() -> tuple[str, Dict[str, Any]]:
10381080
"auto_fix_error": str(e),
10391081
}
10401082
debug["human_summary"] = _indicator_human_summary(
1041-
validation, validation, True, False, "initial"
1083+
validation, validation, True, False, "initial", lang=lang
10421084
)
10431085
logger.info("ai_generate debug=%s", json.dumps(debug, ensure_ascii=False))
10441086
return code_text, debug
@@ -1054,7 +1096,7 @@ def _generate_final_code() -> tuple[str, Dict[str, Any]]:
10541096
"final_validation": _indicator_debug_summary(repaired_validation),
10551097
}
10561098
debug["human_summary"] = _indicator_human_summary(
1057-
validation, repaired_validation, True, True, "repaired"
1099+
validation, repaired_validation, True, True, "repaired", lang=lang
10581100
)
10591101
logger.info("ai_generate debug=%s", json.dumps(debug, ensure_ascii=False))
10601102
return repaired, debug
@@ -1070,7 +1112,7 @@ def _generate_final_code() -> tuple[str, Dict[str, Any]]:
10701112
"final_validation": _indicator_debug_summary(repaired_validation),
10711113
}
10721114
debug["human_summary"] = _indicator_human_summary(
1073-
validation, repaired_validation, True, True, "repaired"
1115+
validation, repaired_validation, True, True, "repaired", lang=lang
10741116
)
10751117
logger.info("ai_generate debug=%s", json.dumps(debug, ensure_ascii=False))
10761118
return repaired, debug
@@ -1085,7 +1127,7 @@ def _generate_final_code() -> tuple[str, Dict[str, Any]]:
10851127
"final_validation": _indicator_debug_summary(repaired_validation),
10861128
}
10871129
debug["human_summary"] = _indicator_human_summary(
1088-
validation, repaired_validation, True, False, "initial"
1130+
validation, repaired_validation, True, False, "initial", lang=lang
10891131
)
10901132
logger.info("ai_generate debug=%s", json.dumps(debug, ensure_ascii=False))
10911133
return code_text, debug
@@ -1098,7 +1140,7 @@ def _generate_final_code() -> tuple[str, Dict[str, Any]]:
10981140
"final_validation": _indicator_debug_summary(repaired_validation),
10991141
}
11001142
debug["human_summary"] = _indicator_human_summary(
1101-
validation, repaired_validation, True, False, "repaired"
1143+
validation, repaired_validation, True, False, "repaired", lang=lang
11021144
)
11031145
logger.info("ai_generate debug=%s", json.dumps(debug, ensure_ascii=False))
11041146
return repaired, debug
@@ -1114,7 +1156,8 @@ def stream():
11141156
reference_id=f"ai_code_gen_{user_id}_{int(time.time())}"
11151157
)
11161158
if not ok:
1117-
yield "data: " + json.dumps({"error": f"积分不足: {msg}"}, ensure_ascii=False) + "\n\n"
1159+
error_msg = f"积分不足: {msg}" if _is_zh_lang(lang) and msg else _indicator_ai_text("insufficient_credits", lang)
1160+
yield "data: " + json.dumps({"error": error_msg}, ensure_ascii=False) + "\n\n"
11181161
yield "data: [DONE]\n\n"
11191162
return
11201163

0 commit comments

Comments
 (0)