哈哈
This commit is contained in:
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import heapq
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
@@ -25,7 +26,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
|
||||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||||
from pathlib import Path
|
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)
|
G_STEP_MIN = int(step_min)
|
||||||
|
|
||||||
|
|
||||||
def _eval_period_task(args: tuple[int, list[float]]) -> list[dict]:
|
def _eval_single_task(args: tuple[int, float]) -> dict:
|
||||||
period, std_list = args
|
"""单个 (period, std) 组合的回测任务"""
|
||||||
|
period, std = args
|
||||||
assert G_DF is not None
|
assert G_DF is not None
|
||||||
|
|
||||||
arr_touch_dir = None
|
arr_touch_dir = None
|
||||||
@@ -155,64 +156,96 @@ def _eval_period_task(args: tuple[int, list[float]]) -> list[dict]:
|
|||||||
bb_mid, _, _, _ = bollinger(close, period, 1.0)
|
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)
|
arr_touch_dir = get_1m_touch_direction(G_DF, G_DF_1M, bb_mid.values, kline_step_min=G_STEP_MIN)
|
||||||
|
|
||||||
rows: list[dict] = []
|
cfg = BBMidlineConfig(
|
||||||
for std in std_list:
|
bb_period=period,
|
||||||
cfg = BBMidlineConfig(
|
bb_std=float(std),
|
||||||
bb_period=period,
|
initial_capital=200.0,
|
||||||
bb_std=float(std),
|
margin_pct=0.01,
|
||||||
initial_capital=200.0,
|
leverage=100.0,
|
||||||
margin_pct=0.01,
|
cross_margin=True,
|
||||||
leverage=100.0,
|
fee_rate=0.0005,
|
||||||
cross_margin=True,
|
rebate_pct=0.90,
|
||||||
fee_rate=0.0005,
|
rebate_hour_utc=0,
|
||||||
rebate_pct=0.90,
|
fill_at_close=True,
|
||||||
rebate_hour_utc=0,
|
use_1m_touch_filter=G_USE_1M,
|
||||||
fill_at_close=True,
|
kline_step_min=G_STEP_MIN,
|
||||||
use_1m_touch_filter=G_USE_1M,
|
)
|
||||||
kline_step_min=G_STEP_MIN,
|
result = run_bb_midline_backtest(
|
||||||
)
|
G_DF,
|
||||||
result = run_bb_midline_backtest(
|
cfg,
|
||||||
G_DF,
|
df_1m=G_DF_1M if G_USE_1M else None,
|
||||||
cfg,
|
arr_touch_dir_override=arr_touch_dir,
|
||||||
df_1m=G_DF_1M if G_USE_1M else None,
|
)
|
||||||
arr_touch_dir_override=arr_touch_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
eq = result.equity_curve["equity"].dropna()
|
eq = result.equity_curve["equity"].dropna()
|
||||||
if len(eq) == 0:
|
if len(eq) == 0:
|
||||||
final_eq = 0.0
|
final_eq = 0.0
|
||||||
ret_pct = -100.0
|
ret_pct = -100.0
|
||||||
dd_u = -200.0
|
dd_u = -200.0
|
||||||
dd_pct = 100.0
|
dd_pct = 100.0
|
||||||
else:
|
else:
|
||||||
final_eq = float(eq.iloc[-1])
|
final_eq = float(eq.iloc[-1])
|
||||||
ret_pct = (final_eq - cfg.initial_capital) / cfg.initial_capital * 100.0
|
ret_pct = (final_eq - cfg.initial_capital) / cfg.initial_capital * 100.0
|
||||||
dd_u = float((eq.astype(float) - eq.astype(float).cummax()).min())
|
dd_u = float((eq.astype(float) - eq.astype(float).cummax()).min())
|
||||||
dd_pct = abs(dd_u) / cfg.initial_capital * 100.0
|
dd_pct = abs(dd_u) / cfg.initial_capital * 100.0
|
||||||
|
|
||||||
n_trades = len(result.trades)
|
n_trades = len(result.trades)
|
||||||
win_rate = (
|
win_rate = (
|
||||||
sum(1 for t in result.trades if t.net_pnl > 0) / n_trades * 100.0
|
sum(1 for t in result.trades if t.net_pnl > 0) / n_trades * 100.0
|
||||||
if n_trades > 0
|
if n_trades > 0
|
||||||
else 0.0
|
else 0.0
|
||||||
)
|
)
|
||||||
pnl = result.daily_stats["pnl"].astype(float)
|
pnl = result.daily_stats["pnl"].astype(float)
|
||||||
sharpe = float(pnl.mean() / pnl.std()) * math.sqrt(365.0) if pnl.std() > 0 else 0.0
|
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)
|
stable_score = score_stable(ret_pct, sharpe, dd_pct, n_trades)
|
||||||
|
|
||||||
rows.append({
|
return {
|
||||||
"period": period,
|
"period": period,
|
||||||
"std": round(float(std), 2),
|
"std": round(float(std), 2),
|
||||||
"final_eq": final_eq,
|
"final_eq": final_eq,
|
||||||
"ret_pct": ret_pct,
|
"ret_pct": ret_pct,
|
||||||
"n_trades": n_trades,
|
"n_trades": n_trades,
|
||||||
"win_rate": win_rate,
|
"win_rate": win_rate,
|
||||||
"sharpe": sharpe,
|
"sharpe": sharpe,
|
||||||
"max_dd_u": dd_u,
|
"max_dd_u": dd_u,
|
||||||
"max_dd_pct": dd_pct,
|
"max_dd_pct": dd_pct,
|
||||||
"stable_score": stable_score,
|
"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(
|
def run_grid_search(
|
||||||
@@ -229,100 +262,94 @@ def run_grid_search(
|
|||||||
meta: dict | None = None,
|
meta: dict | None = None,
|
||||||
checkpoint_interval: int = 5,
|
checkpoint_interval: int = 5,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
by_period: dict[int, set[float]] = defaultdict(set)
|
total_combos = len(params)
|
||||||
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)
|
|
||||||
rows: list[dict] = list(existing_rows) if existing_rows else []
|
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 ""
|
f", 断点续跑 (已有 {len(rows)} 条)" if rows else ""
|
||||||
))
|
))
|
||||||
start = time.time()
|
t_start = time.time()
|
||||||
last_save = 0
|
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):
|
def maybe_save():
|
||||||
nonlocal last_save
|
nonlocal last_save_count
|
||||||
if ckpt_path and meta_path and meta and n_done_periods > 0:
|
if ckpt_path and meta_path and meta and done_combos > 0:
|
||||||
if n_done_periods - last_save >= checkpoint_interval:
|
if done_combos - last_save_count >= ckpt_combo_interval:
|
||||||
save_checkpoint(ckpt_path, meta_path, meta, rows)
|
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:
|
if workers <= 1:
|
||||||
_init_worker(df_path, df_1m_path, use_1m, step_min)
|
_init_worker(df_path, df_1m_path, use_1m, step_min)
|
||||||
done_periods = 0
|
for p, s in params:
|
||||||
done_combos = 0
|
row = _eval_single_task((p, s))
|
||||||
for task in tasks:
|
on_result(row)
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
done_periods = 0
|
# 多进程:逐个 (period, std) 提交,实时返回
|
||||||
done_combos = 0
|
# 为避免提交 200 万个 future 占用过多内存,分批提交
|
||||||
|
batch_size = workers * 20 # 每批提交的任务数
|
||||||
try:
|
try:
|
||||||
with ProcessPoolExecutor(
|
with ProcessPoolExecutor(
|
||||||
max_workers=workers,
|
max_workers=workers,
|
||||||
initializer=_init_worker,
|
initializer=_init_worker,
|
||||||
initargs=(df_path, df_1m_path, use_1m, step_min),
|
initargs=(df_path, df_1m_path, use_1m, step_min),
|
||||||
) as ex:
|
) as ex:
|
||||||
future_map = {ex.submit(_eval_period_task, task): task for task in tasks}
|
idx = 0
|
||||||
for fut in as_completed(future_map):
|
while idx < total_combos:
|
||||||
period, stds = future_map[fut]
|
batch_end = min(idx + batch_size, total_combos)
|
||||||
res = fut.result()
|
batch = params[idx:batch_end]
|
||||||
# 打印该period的所有结果
|
future_map = {ex.submit(_eval_single_task, (p, s)): (p, s) for p, s in batch}
|
||||||
for row in res:
|
for fut in as_completed(future_map):
|
||||||
print(f"✓ period={int(row['period']):4d}, std={float(row['std']):7.2f} | "
|
row = fut.result()
|
||||||
f"收益: {row['ret_pct']:+7.2f}% | 回撤: {row['max_dd_pct']:6.2f}% | "
|
on_result(row)
|
||||||
f"夏普: {row['sharpe']:7.3f} | 交易: {int(row['n_trades']):6d} | "
|
idx = batch_end
|
||||||
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)
|
|
||||||
except (PermissionError, OSError) as e:
|
except (PermissionError, OSError) as e:
|
||||||
print(f"多进程不可用 ({e}),改用单进程...")
|
print(f"多进程不可用 ({e}),改用单进程...")
|
||||||
_init_worker(df_path, df_1m_path, use_1m, step_min)
|
_init_worker(df_path, df_1m_path, use_1m, step_min)
|
||||||
done_periods = 0
|
for p, s in params[done_combos:]:
|
||||||
done_combos = 0
|
row = _eval_single_task((p, s))
|
||||||
for task in tasks:
|
on_result(row)
|
||||||
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)
|
|
||||||
|
|
||||||
if ckpt_path and meta_path and meta and rows:
|
if ckpt_path and meta_path and meta and rows:
|
||||||
save_checkpoint(ckpt_path, meta_path, meta, rows)
|
save_checkpoint(ckpt_path, meta_path, meta, rows)
|
||||||
|
|
||||||
|
# 最终排行榜
|
||||||
|
_print_top_n(rows, n=20, label="最终 Top 20")
|
||||||
|
|
||||||
df = pd.DataFrame(rows)
|
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
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
39
test.py
39
test.py
@@ -1,5 +1,38 @@
|
|||||||
|
import subprocess
|
||||||
|
import time
|
||||||
from wxautox4 import WeChat
|
from wxautox4 import WeChat
|
||||||
|
|
||||||
wx = WeChat()
|
def make_wechat_call(contact_name, retry_times=3):
|
||||||
wx.ChatWith('Rainbow') # 切换到目标聊天
|
"""
|
||||||
# 再通过 UIAutomation 定位并点击语音/视频通话按钮
|
可靠的微信拨电话方法
|
||||||
|
|
||||||
|
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')
|
||||||
Reference in New Issue
Block a user