This commit is contained in:
ddrwode
2026-02-28 16:43:38 +08:00
parent 088f94c5c4
commit 0ec23b28eb
2 changed files with 191 additions and 131 deletions

View File

@@ -18,6 +18,7 @@ from __future__ import annotations
import argparse
import hashlib
import heapq
import io
import json
import math
@@ -25,7 +26,6 @@ import os
import sys
import tempfile
import time
from collections import defaultdict
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
@@ -145,8 +145,9 @@ def _init_worker(df_path: str, df_1m_path: str | None, use_1m: bool, step_min: i
G_STEP_MIN = int(step_min)
def _eval_period_task(args: tuple[int, list[float]]) -> list[dict]:
period, std_list = args
def _eval_single_task(args: tuple[int, float]) -> dict:
"""单个 (period, std) 组合的回测任务"""
period, std = args
assert G_DF is not None
arr_touch_dir = None
@@ -155,8 +156,6 @@ def _eval_period_task(args: tuple[int, list[float]]) -> list[dict]:
bb_mid, _, _, _ = bollinger(close, period, 1.0)
arr_touch_dir = get_1m_touch_direction(G_DF, G_DF_1M, bb_mid.values, kline_step_min=G_STEP_MIN)
rows: list[dict] = []
for std in std_list:
cfg = BBMidlineConfig(
bb_period=period,
bb_std=float(std),
@@ -200,7 +199,7 @@ def _eval_period_task(args: tuple[int, list[float]]) -> list[dict]:
sharpe = float(pnl.mean() / pnl.std()) * math.sqrt(365.0) if pnl.std() > 0 else 0.0
stable_score = score_stable(ret_pct, sharpe, dd_pct, n_trades)
rows.append({
return {
"period": period,
"std": round(float(std), 2),
"final_eq": final_eq,
@@ -211,8 +210,42 @@ def _eval_period_task(args: tuple[int, list[float]]) -> list[dict]:
"max_dd_u": dd_u,
"max_dd_pct": dd_pct,
"stable_score": stable_score,
})
return rows
}
def _eval_period_task(args: tuple[int, list[float]]) -> list[dict]:
"""兼容旧接口:一个 period 组的批量回测"""
period, std_list = args
return [_eval_single_task((period, s)) for s in std_list]
def _format_eta(seconds: float) -> str:
"""格式化剩余时间"""
if seconds < 60:
return f"{seconds:.0f}s"
elif seconds < 3600:
return f"{seconds / 60:.1f}min"
else:
h = int(seconds // 3600)
m = int((seconds % 3600) // 60)
return f"{h}h{m:02d}m"
def _print_top_n(rows: list[dict], n: int = 10, label: str = "当前 Top 10") -> None:
"""打印当前 Top N 排行榜"""
if not rows:
return
sorted_rows = sorted(rows, key=lambda r: r["stable_score"], reverse=True)[:n]
print(f"\n{'' * 90}", flush=True)
print(f" 📊 {label} (按稳定性评分排序)", flush=True)
print(f" {'排名':>4s} {'period':>6s} {'std':>7s} {'收益%':>9s} {'回撤%':>7s} {'夏普':>7s} {'交易数':>6s} {'评分':>8s}", flush=True)
print(f" {'' * 82}", flush=True)
for i, r in enumerate(sorted_rows, 1):
print(f" {i:4d} {int(r['period']):6d} {r['std']:7.2f} {r['ret_pct']:+9.2f} "
f"{r['max_dd_pct']:7.2f} {r['sharpe']:7.3f} {int(r['n_trades']):6d} "
f"{r['stable_score']:8.1f}", flush=True)
print(f"{'' * 90}\n", flush=True)
def run_grid_search(
@@ -229,100 +262,94 @@ def run_grid_search(
meta: dict | None = None,
checkpoint_interval: int = 5,
) -> pd.DataFrame:
by_period: dict[int, set[float]] = defaultdict(set)
for p, s in params:
by_period[int(p)].add(round(float(s), 2))
tasks = [(p, sorted(stds)) for p, stds in sorted(by_period.items())]
total_periods = len(tasks)
total_combos = sum(len(stds) for _, stds in tasks)
total_combos = len(params)
rows: list[dict] = list(existing_rows) if existing_rows else []
print(f"待运行: {total_combos} 组合 ({total_periods} period组), workers={workers}" + (
print(f"待运行: {total_combos} 组合, workers={workers}" + (
f", 断点续跑 (已有 {len(rows)} 条)" if rows else ""
))
start = time.time()
last_save = 0
t_start = time.time()
done_combos = 0
last_save_count = 0
last_top_time = t_start
# checkpoint 按组合数保存,默认每 500 个组合保存一次
ckpt_combo_interval = max(100, checkpoint_interval * 50)
# Top N 排行榜刷新间隔(秒)
top_n_interval = 30.0
def maybe_save(n_done_periods: int):
nonlocal last_save
if ckpt_path and meta_path and meta and n_done_periods > 0:
if n_done_periods - last_save >= checkpoint_interval:
def maybe_save():
nonlocal last_save_count
if ckpt_path and meta_path and meta and done_combos > 0:
if done_combos - last_save_count >= ckpt_combo_interval:
save_checkpoint(ckpt_path, meta_path, meta, rows)
last_save = n_done_periods
last_save_count = done_combos
def maybe_print_top():
nonlocal last_top_time
now = time.time()
if now - last_top_time >= top_n_interval:
_print_top_n(rows)
last_top_time = now
def on_result(row: dict):
nonlocal done_combos
rows.append(row)
done_combos += 1
elapsed = time.time() - t_start
speed = done_combos / elapsed if elapsed > 0 else 0
remaining = total_combos - done_combos
eta = remaining / speed if speed > 0 else 0
pct = done_combos / total_combos * 100
# 每个结果都实时打印
print(f"✓ [{done_combos:>7d}/{total_combos} {pct:5.1f}% ETA {_format_eta(eta)}] "
f"p={int(row['period']):4d} s={row['std']:7.2f} | "
f"收益:{row['ret_pct']:+8.2f}% 回撤:{row['max_dd_pct']:6.2f}% "
f"夏普:{row['sharpe']:7.3f} 交易:{int(row['n_trades']):5d} "
f"评分:{row['stable_score']:8.1f}", flush=True)
maybe_save()
maybe_print_top()
if workers <= 1:
_init_worker(df_path, df_1m_path, use_1m, step_min)
done_periods = 0
done_combos = 0
for task in tasks:
res = _eval_period_task(task)
period = task[0]
# 打印该period的所有结果
for row in res:
print(f"✓ period={int(row['period']):4d}, std={float(row['std']):7.2f} | "
f"收益: {row['ret_pct']:+7.2f}% | 回撤: {row['max_dd_pct']:6.2f}% | "
f"夏普: {row['sharpe']:7.3f} | 交易: {int(row['n_trades']):6d} | "
f"评分: {row['stable_score']:7.1f}", flush=True)
rows.extend(res)
done_periods += 1
done_combos += len(task[1])
maybe_save(done_periods)
if done_periods % max(1, total_periods // 20) == 0 or done_periods == total_periods:
elapsed = time.time() - start
print(f"进度 {done_combos}/{total_combos} ({done_periods}/{total_periods} periods), 用时 {elapsed:.0f}s", flush=True)
for p, s in params:
row = _eval_single_task((p, s))
on_result(row)
else:
done_periods = 0
done_combos = 0
# 多进程:逐个 (period, std) 提交,实时返回
# 为避免提交 200 万个 future 占用过多内存,分批提交
batch_size = workers * 20 # 每批提交的任务数
try:
with ProcessPoolExecutor(
max_workers=workers,
initializer=_init_worker,
initargs=(df_path, df_1m_path, use_1m, step_min),
) as ex:
future_map = {ex.submit(_eval_period_task, task): task for task in tasks}
idx = 0
while idx < total_combos:
batch_end = min(idx + batch_size, total_combos)
batch = params[idx:batch_end]
future_map = {ex.submit(_eval_single_task, (p, s)): (p, s) for p, s in batch}
for fut in as_completed(future_map):
period, stds = future_map[fut]
res = fut.result()
# 打印该period的所有结果
for row in res:
print(f"✓ period={int(row['period']):4d}, std={float(row['std']):7.2f} | "
f"收益: {row['ret_pct']:+7.2f}% | 回撤: {row['max_dd_pct']:6.2f}% | "
f"夏普: {row['sharpe']:7.3f} | 交易: {int(row['n_trades']):6d} | "
f"评分: {row['stable_score']:7.1f}", flush=True)
rows.extend(res)
done_periods += 1
done_combos += len(stds)
maybe_save(done_periods)
if done_periods % max(1, total_periods // 20) == 0 or done_periods == total_periods:
elapsed = time.time() - start
print(f"进度 {done_combos}/{total_combos} ({done_periods}/{total_periods} periods), 用时 {elapsed:.0f}s", flush=True)
row = fut.result()
on_result(row)
idx = batch_end
except (PermissionError, OSError) as e:
print(f"多进程不可用 ({e}),改用单进程...")
_init_worker(df_path, df_1m_path, use_1m, step_min)
done_periods = 0
done_combos = 0
for task in tasks:
res = _eval_period_task(task)
# 打印该period的所有结果
for row in res:
print(f"✓ period={int(row['period']):4d}, std={float(row['std']):7.2f} | "
f"收益: {row['ret_pct']:+7.2f}% | 回撤: {row['max_dd_pct']:6.2f}% | "
f"夏普: {row['sharpe']:7.3f} | 交易: {int(row['n_trades']):6d} | "
f"评分: {row['stable_score']:7.1f}", flush=True)
rows.extend(res)
done_periods += 1
done_combos += len(task[1])
maybe_save(done_periods)
if done_periods % max(1, total_periods // 20) == 0 or done_periods == total_periods:
elapsed = time.time() - start
print(f"进度 {done_combos}/{total_combos} ({done_periods}/{total_periods} periods), 用时 {elapsed:.0f}s", flush=True)
for p, s in params[done_combos:]:
row = _eval_single_task((p, s))
on_result(row)
if ckpt_path and meta_path and meta and rows:
save_checkpoint(ckpt_path, meta_path, meta, rows)
# 最终排行榜
_print_top_n(rows, n=20, label="最终 Top 20")
df = pd.DataFrame(rows)
print(f"完成, 总用时 {time.time() - start:.1f}s")
print(f"完成, 总用时 {time.time() - t_start:.1f}s, 平均 {total_combos / (time.time() - t_start):.1f} 组合/秒")
return df

39
test.py
View File

@@ -1,5 +1,38 @@
import subprocess
import time
from wxautox4 import WeChat
wx = WeChat()
wx.ChatWith('Rainbow') # 切换到目标聊天
# 再通过 UIAutomation 定位并点击语音/视频通话按钮
def make_wechat_call(contact_name, retry_times=3):
"""
可靠的微信拨电话方法
Args:
contact_name: 联系人名称
retry_times: 重试次数
"""
for attempt in range(retry_times):
try:
# 打开微信应用
subprocess.Popen(['/Applications/WeChat.app/Contents/MacOS/WeChat'])
time.sleep(2) # 等待微信启动
# 初始化
wx = WeChat()
time.sleep(1)
# 切换聊天
wx.ChatWith(contact_name)
time.sleep(1)
print(f"✓ 已切换到 {contact_name} 聊天")
return True
except Exception as e:
print(f"⚠ 第 {attempt+1} 次尝试失败: {e}")
if attempt < retry_times - 1:
time.sleep(1)
return False
# 使用
make_wechat_call('Rainbow')