Files
2026-07-06 05:06:15 +08:00

433 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import re
import argparse
import unicodedata
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from pathlib import Path
def sanitize_path_name(name: str) -> str:
name = unicodedata.normalize('NFKD', name)
name = name.encode('ascii', 'ignore').decode('ascii')
name = re.sub(r'[^\w\-_.]', '_', name)
name = re.sub(r'_{2,}', '_', name)
return name.strip('_')
@dataclass
class SetParameter:
name: str
param_type: str
default: float
min_value: float
max_value: float
step: float
optimize: bool
@dataclass
class SetFileResult:
file_path: str
ea_name: str
parameters: Dict[str, SetParameter] = field(default_factory=dict)
optimize_count: int = 0
fixed_count: int = 0
MT5_NATIVE_PATTERN = re.compile(r'^(\w+)\s+<([^>]+)>\s+<([^>]+)>')
OUR_FORMAT_PATTERN = re.compile(r'^(\w+)=([^|]+)\|\|([^|]+)\|\|([^|]+)\|\|([^|]+)\|\|([YN])')
def detect_encoding(file_path: str) -> str:
with open(file_path, 'rb') as f:
raw = f.read(4)
if raw[:2] == b'\xff\xfe':
return 'utf-16-le'
elif raw[:2] == b'\xfe\xff':
return 'utf-16-be'
elif raw[:3] == b'\xef\xbb\xbf':
return 'utf-8-sig'
return 'utf-8'
def detect_set_format(line: str) -> str:
if '=' in line and '||' in line:
return 'our_format'
elif '<' in line and '>' in line:
return 'mt5_native'
elif line.strip().startswith(';'):
return 'comment'
elif line.strip() == '':
return 'empty'
return 'unknown'
def parse_set_line(line: str) -> Optional[SetParameter]:
line = line.strip()
if not line or line.startswith(';'):
return None
fmt = detect_set_format(line)
if fmt == 'our_format':
m = OUR_FORMAT_PATTERN.match(line)
if m:
name, cur, start, step, stop, opt = m.groups()
cur_lower = cur.lower()
if cur_lower in ('true', 'false'):
is_true = cur_lower == 'true'
return SetParameter(
name=name,
param_type='bool',
default=1.0 if is_true else 0.0,
min_value=0.0,
max_value=1.0,
step=1.0,
optimize=(opt.upper() == 'Y')
)
return SetParameter(
name=name,
param_type='double',
default=float(cur),
min_value=float(start),
max_value=float(stop),
step=float(step),
optimize=(opt.upper() == 'Y')
)
elif fmt == 'mt5_native':
m = MT5_NATIVE_PATTERN.match(line)
if m:
name, val1, val2 = m.groups()
val1l = val1.lower().strip()
val2l = val2.lower().strip()
if val1l in ('true', 'false') or val2l in ('true', 'false'):
is_true = val1l == 'true' or val2l == 'true'
return SetParameter(
name=name,
param_type='bool',
default=1.0 if is_true else 0.0,
min_value=0.0,
max_value=1.0,
step=1.0,
optimize=False
)
try:
v1 = float(val1)
v2 = float(val2)
return SetParameter(
name=name,
param_type='double',
default=v1,
min_value=min(v1, v2),
max_value=max(v1, v2),
step=abs(v2 - v1) if v1 != v2 else 0,
optimize=False
)
except ValueError:
pass
return None
def parse_set_file(file_path: str) -> SetFileResult:
result = SetFileResult(file_path=file_path, ea_name='')
path = Path(file_path)
result.ea_name = path.stem.replace('_optimization', '').replace('_params', '')
enc = detect_encoding(file_path)
with open(file_path, 'r', encoding=enc) as f:
content = f.read()
for line in content.split('\n'):
param = parse_set_line(line)
if param:
result.parameters[param.name] = param
result.optimize_count = sum(1 for p in result.parameters.values() if p.optimize)
result.fixed_count = sum(1 for p in result.parameters.values() if not p.optimize)
return result
def parse_set_content(content: str) -> SetFileResult:
result = SetFileResult(file_path='memory', ea_name='')
for line in content.split('\n'):
param = parse_set_line(line)
if param:
result.parameters[param.name] = param
result.optimize_count = sum(1 for p in result.parameters.values() if p.optimize)
result.fixed_count = sum(1 for p in result.parameters.values() if not p.optimize)
return result
def scan_folder(folder_path: str) -> List[SetFileResult]:
results = []
folder = Path(folder_path)
if folder.is_file():
return [parse_set_file(str(folder))]
for set_file in folder.rglob('*.set'):
try:
result = parse_set_file(str(set_file))
results.append(result)
except Exception as e:
print(f'Warning: Failed to parse {set_file}: {e}')
return sorted(results, key=lambda x: x.ea_name)
def batch_scan(file_paths: List[str]) -> List[SetFileResult]:
results = []
for path in file_paths:
if os.path.exists(path):
try:
result = parse_set_file(path)
results.append(result)
except Exception as e:
print(f'Warning: Failed to parse {path}: {e}')
return results
def generate_prompt(results: List[SetFileResult], output_dir: str = '.') -> List[str]:
output_files = []
for result in results:
lines = []
lines.append('=' * 80)
lines.append(f'EA 参数优化分析请求 - {result.ea_name}')
lines.append('=' * 80)
lines.append('')
lines.append('## 基本信息')
lines.append(f'- EA名称: {result.ea_name}')
lines.append(f'- SET文件: {result.file_path}')
lines.append(f'- 参数总数: {len(result.parameters)} 个')
lines.append('')
lines.append('## 参数详情表')
lines.append('')
lines.append('| 参数名 | 类型 | 当前值 | 最小值 | 最大值 | 步进 | 建议范围 |')
lines.append('|--------|------|--------|--------|--------|------|----------|')
for name, param in sorted(result.parameters.items()):
suggest = f'{param.min_value}~{param.max_value}'
if param.param_type == 'int':
suggest = f'{int(param.min_value)}~{int(param.max_value)} (步进:{int(param.step)})'
elif param.param_type == 'double':
suggest = f'{param.min_value}~{param.max_value} (步进:{param.step})'
lines.append(f'| {name} | {param.param_type} | {param.default} | {param.min_value} | {param.max_value} | {param.step} | {suggest} |')
lines.append('')
lines.append('## 参数分析任务')
lines.append('')
lines.append('请分析以上所有参数,根据策略逻辑和交易逻辑判断:')
lines.append('')
lines.append('1. **哪些参数应该参与优化(optimize: true**')
lines.append(' - 通常是影响策略核心逻辑、盈亏比、风险的关键参数')
lines.append(' - 例如:手数、止损止盈倍数、加仓间隔、风控阈值等')
lines.append('')
lines.append('2. **哪些参数应该保持固定(optimize: false**')
lines.append(' - 通常是风控类、开关类、显示类参数')
lines.append(' - 例如:开关标识、魔数、面板位置等')
lines.append('')
lines.append('3. **推荐优化范围调整**')
lines.append(' - 检查当前范围是否合理')
lines.append(' - 给出你认为更合适的 min/max/step')
lines.append('')
lines.append('4. **重要参数优先级**')
lines.append(' - 哪些2-3个参数对策略影响最大,需要重点优化')
lines.append('')
lines.append('## 输出要求')
lines.append('')
lines.append('请输出符合以下格式的完整JSON配置文件:')
lines.append('')
lines.append('```json')
lines.append('{')
lines.append(f' "ea_name": "{result.ea_name}",')
safe_ea_name = sanitize_path_name(result.ea_name)
lines.append(f' "ea_path": "MY-EA\\\\{safe_ea_name}.ex5", // {result.ea_name}')
lines.append(' "description": "Edited via GUI",')
lines.append(' "search_strategy": "auto",')
lines.append(' "parameters": {')
param_list = list(result.parameters.items())
for i, (name, param) in enumerate(param_list):
comma = ',' if i < len(param_list) - 1 else ''
lines.append(f' "{name}": {{')
lines.append(f' "type": "{param.param_type}",')
lines.append(f' "default": {param.default},')
lines.append(f' "min": {param.min_value},')
lines.append(f' "max": {param.max_value},')
lines.append(f' "step": {param.step},')
lines.append(f' "optimize": true // 请根据分析填写 true 或 false')
lines.append(f' }}{comma}')
lines.append(' },')
lines.append(' "test_config": {')
lines.append(' "symbol": "XAUUSD", // 请填写交易品种')
lines.append(' "period": "M1", // 请填写周期: M1/M5/M15/H1/H4/D1')
lines.append(' "from_date": "2026.01.01", // 请填写开始日期')
lines.append(' "to_date": "2026.01.04", // 请填写结束日期')
lines.append(' "model": 0, // 建模方式: 0=Every Tick, 1=1分钟OHLC, 2=仅开盘价, 3=数学计算, 4=真实Tick')
lines.append(' "execution_delay": 0, // 执行延迟: 0=无延迟, -1=随机延迟, 正数=固定延迟毫秒')
lines.append(' "optimization_mode": 2, // 优化模式: 0=禁用, 1=慢速完整算法, 2=快速遗传算法, 3=MarketWatch所有符号')
lines.append(' "optimization_criterion": 1, // 优化指标: 0=余额最大, 1=盈利因子最大, 2=期望收益, 3=回撤最小, 4=恢复因子, 5=夏普比率, 6=自定义, 7=复合指标')
lines.append(' "deposit": 10000, // 初始保证金')
lines.append(' "leverage": "1:100", // 杠杆')
lines.append(' "timeout_min": 60 // 等待优化结果的超时(分钟)')
lines.append(' },')
lines.append(' "walk_forward": {')
lines.append(' "enabled": true, // 是否启用前向测试')
lines.append(' "forward_mode": 2, // 前向模式: 0=禁用, 1=OOS 50%, 2=OOS 33%, 3=OOS 25%, 4=自定义日期')
lines.append(' "forward_date": "" // 仅当 forward_mode=4 时必填, 格式 YYYY.MM.DD')
lines.append(' }')
lines.append('}')
lines.append('```')
lines.append('')
lines.append('注意:')
lines.append(f'- EA原始名称: {result.ea_name}')
lines.append(f'- ea_path 使用ASCII安全名称: {safe_ea_name}')
lines.append('- 实际EA文件应放在 MY-EA 目录下,文件名需与 ea_path 中的名称一致')
lines.append('- 每个参数的 "optimize" 字段需要你根据策略分析来填写')
lines.append('- optimize: true = 参与优化, optimize: false = 保持固定')
lines.append('- test_config 和 walk_forward 部分请根据回测需求填写或调整')
lines.append('- ea_path 路径使用双反斜杠 `\\\\`')
lines.append('')
lines.append('=' * 80)
output_path = os.path.join(output_dir, f'{result.ea_name}_optimization_prompt.txt')
with open(output_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
output_files.append(output_path)
print(f'Generated: {output_path}')
return output_files
def interactive_scan():
print('请选择扫描模式:')
print('1. 扫描单个 SET 文件')
print('2. 扫描文件夹下所有 SET 文件')
print('3. 批量扫描多个 SET 文件')
print('4. 从剪贴板读取 SET 内容')
print('0. 退出')
print('')
choice = input('请输入选项 (0-4): ').strip()
if choice == '1':
path = input('请输入 SET 文件路径: ').strip()
if os.path.exists(path):
results = [parse_set_file(path)]
output_dir = os.path.dirname(path) or '.'
generate_prompt(results, output_dir)
else:
print('文件不存在')
elif choice == '2':
path = input('请输入文件夹路径: ').strip()
if os.path.exists(path):
results = scan_folder(path)
generate_prompt(results, path)
else:
print('文件夹不存在')
elif choice == '3':
print('请输入 SET 文件路径(每行一个,输入空行结束):')
paths = []
while True:
line = input().strip()
if not line:
break
if os.path.exists(line):
paths.append(line)
else:
print(f'文件不存在: {line}')
if paths:
results = batch_scan(paths)
generate_prompt(results, os.path.dirname(paths[0]) if len(paths) == 1 else '.')
elif choice == '4':
print('请粘贴 SET 文件内容(输入空行结束):')
lines = []
while True:
line = input()
if not line.strip():
break
lines.append(line)
if lines:
content = '\n'.join(lines)
result = parse_set_content(content)
result.ea_name = input('请输入 EA 名称: ').strip() or 'EA'
generate_prompt([result], '.')
def main():
parser = argparse.ArgumentParser(
description='SET 文件扫描工具 - 生成 AI 优化提示词',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
python scan_set_prompt.py --file C:\\path\\to\\EA.set
python scan_set_prompt.py --folder C:\\path\\to\\set_files
python scan_set_prompt.py --batch a.set b.set c.set
python scan_set_prompt.py --interactive
'''
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--file', '-f', metavar='PATH', help='扫描单个 SET 文件')
group.add_argument('--folder', '-d', metavar='PATH', help='扫描文件夹下所有 SET 文件')
group.add_argument('--batch', '-b', nargs='+', metavar='PATH', help='批量扫描多个 SET 文件')
group.add_argument('--interactive', '-i', action='store_true', help='交互式扫描')
parser.add_argument('--output', '-o', metavar='DIR', default='.', help='输出目录 (默认当前目录)')
args = parser.parse_args()
if args.interactive:
interactive_scan()
return
results = []
if args.file:
if not os.path.exists(args.file):
print(f'Error: 文件不存在 {args.file}')
return
results = [parse_set_file(args.file)]
output_dir = os.path.dirname(args.file) or args.output
elif args.folder:
if not os.path.exists(args.folder):
print(f'Error: 文件夹不存在 {args.folder}')
return
results = scan_folder(args.folder)
output_dir = args.folder
elif args.batch:
existing = [p for p in args.batch if os.path.exists(p)]
if not existing:
print('Error: 所有文件都不存在')
return
results = batch_scan(existing)
output_dir = args.output
if not results:
print('Warning: 没有找到任何 SET 文件')
return
output_files = generate_prompt(results, output_dir)
print(f'\n完成!共生成 {len(output_files)} 个提示词文件')
if __name__ == '__main__':
main()