Files
zhutoutoutousan 605faf5310 Prepare source-only public release for develop.
Add cluster audit pipeline, united EA updates, brochure generators, and publication hygiene (gitignore, MT5 path desensitization, pre-upload scan). Remove tracked reports, models, and binary artifacts from the repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 15:03:43 +02:00

318 lines
9.8 KiB
Python

#!/usr/bin/env python3
"""
MT5 lot-size sweep per production sub-strategy, then combined portfolio.
Constraints:
- ORCH_ReferenceBalance = 3000 (deposit also 3000)
- Stock lots capped at 15 shares
Usage:
python -m cluster_audit.run_lot_optimize
python -m cluster_audit.run_lot_optimize --only RS_NVDA
python -m cluster_audit.run_lot_optimize --apply
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from cluster_audit.united_mt5_manifest import (
ALL_ENABLE_KEYS,
HIGH_MARGIN_STOCK_ENABLES,
LOT_CLASS_BY_ID,
LOT_GRIDS,
PRODUCTION_IDS,
UNITED_MT5_STRATEGIES,
)
from cluster_audit.united_mt5_runner import (
BASE_SET,
CLUSTER,
DEPOSIT,
FROM_DATE,
TO_DATE,
deploy_united,
mt5_context,
patch_set,
run_backtest,
)
OUT = Path(__file__).resolve().parent / "reports" / "lot_optimize"
STOCK_LOT_MAX = 15.0
REF_BALANCE = 3000.0
MIN_TRADES = 20
def g(m: dict, k: str) -> float:
v = m.get(k)
return float(v) if v is not None else 0.0
def spec_map() -> dict[str, dict]:
return {s["id"]: s for s in UNITED_MT5_STRATEGIES}
def lot_class(sid: str) -> str:
return LOT_CLASS_BY_ID.get(sid, "forex")
def lot_grid(sid: str) -> list[float]:
cls = lot_class(sid)
grid = list(LOT_GRIDS.get(cls, LOT_GRIDS["forex"]))
if cls == "stock":
grid = [x for x in grid if x <= STOCK_LOT_MAX]
return grid
def score(m: dict) -> float:
if not m.get("ready"):
return -1e12
trades = int(m.get("total_trades") or 0)
if trades < MIN_TRADES:
return -1e11 + trades
net = g(m, "net_profit")
pf = g(m, "profit_factor")
sharpe = g(m, "sharpe")
if net <= 0 or pf < 1.0:
return -1e10 + net
return sharpe * 2000.0 + net / 500.0 + pf * 50.0
def common_patches() -> dict[str, float | bool]:
o: dict[str, float | bool] = {
"ORCH_ReferenceBalance": REF_BALANCE,
"ORCH_ScaleLotsByBalance": True,
"GAP_Enable": False,
"OPT_GuardOptimizationMode": True,
}
for key in HIGH_MARGIN_STOCK_ENABLES:
o[key] = False
return o
def production_enables() -> dict[str, bool]:
prod = set(PRODUCTION_IDS)
o: dict[str, bool] = {}
for s in UNITED_MT5_STRATEGIES:
o[s["enable"]] = s["id"] in prod
return o
def solo_overrides(spec: dict) -> dict[str, bool]:
o: dict[str, bool] = {k: False for k in ALL_ENABLE_KEYS}
o[spec["enable"]] = True
return o
def apply_lots_to_set_text(text: str, lots: dict[str, float]) -> str:
lines_out: list[str] = []
for line in text.splitlines():
if "=" not in line or line.strip().startswith(";"):
lines_out.append(line)
continue
key = line.split("=", 1)[0].strip()
if key in lots:
val = lots[key]
sval = str(int(val)) if val == int(val) else str(val)
if "||" in line:
parts = line.split("||")
parts[0] = f"{key}={sval}"
lines_out.append("||".join(parts))
else:
lines_out.append(f"{key}={sval}")
else:
lines_out.append(line)
for key, val in lots.items():
if not any(l.startswith(f"{key}=") for l in lines_out):
sval = str(int(val)) if val == int(val) else str(val)
lines_out.append(f"{key}={sval}||{sval}||0||{sval}||N")
return "\n".join(lines_out) + "\n"
def apply_lots_to_mq5(text: str, lots: dict[str, float]) -> str:
for key, val in lots.items():
sval = str(int(val)) if val == int(val) else str(val)
text, n = re.subn(
rf"(input double {re.escape(key)} = )[0-9.]+;",
rf"\g<1>{sval};",
text,
count=1,
)
if n == 0:
print(f" warn: {key} not found in main.mq5", flush=True)
text, n = re.subn(
r"(input double ORCH_ReferenceBalance = )[0-9.]+;",
rf"\g<1>{REF_BALANCE};",
text,
count=1,
)
return text
def optimize_one(ctx: dict, spec: dict, sm: dict[str, dict]) -> dict:
sid = spec["id"]
lot_key = spec["lot"]
grid = lot_grid(sid)
print(f"\n[{sid}] grid={grid} class={lot_class(sid)}", flush=True)
best_lot = grid[0]
best_m: dict = {"ready": False}
best_sc = -1e12
trials: list[dict] = []
for lot in grid:
ov: dict = {**common_patches(), **solo_overrides(spec), lot_key: lot}
body = patch_set(BASE_SET, ov)
m = run_backtest(
ctx["data"], ctx["mt5_path"], ctx["login"], ctx["server"],
body, f"lot_{sid}_{lot}.set", f"lot_{sid}_{lot}",
test_symbol=spec.get("test_symbol"),
)
sc = score(m)
trials.append({"lot": lot, "score": sc, "metrics": m})
print(
f" lot={lot:6} sc={sc:10.1f} PF={m.get('profit_factor')} "
f"net={m.get('net_profit')} sharpe={m.get('sharpe')} trades={m.get('total_trades')}",
flush=True,
)
if sc > best_sc:
best_sc = sc
best_lot = lot
best_m = m
return {
"id": sid,
"lot_key": lot_key,
"lot_class": lot_class(sid),
"best_lot": best_lot,
"best_score": best_sc,
"best_metrics": best_m,
"trials": trials,
}
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--from", dest="from_date", default=FROM_DATE)
p.add_argument("--to", dest="to_date", default=TO_DATE)
p.add_argument("--only", action="append", default=[], help="Strategy id(s) to optimize")
p.add_argument("--apply", action="store_true", help="Write best lots to main.mq5 and 123.set")
args = p.parse_args()
import cluster_audit.united_mt5_runner as runner
runner.FROM_DATE = args.from_date.replace("-", ".")
runner.TO_DATE = args.to_date.replace("-", ".")
runner.DEPOSIT = int(REF_BALANCE)
OUT.mkdir(parents=True, exist_ok=True)
ctx = mt5_context()
deploy_united(ctx["data"], ctx["mt5_path"])
sm = spec_map()
ids = args.only if args.only else list(PRODUCTION_IDS)
print(
f"Lot optimize deposit={DEPOSIT} ref={REF_BALANCE} "
f"{runner.FROM_DATE}->{runner.TO_DATE} n={len(ids)}",
flush=True,
)
results: list[dict] = []
best_lots: dict[str, float] = {}
for sid in ids:
if sid not in sm:
print(f"skip unknown id {sid}", flush=True)
continue
r = optimize_one(ctx, sm[sid], sm)
results.append(r)
best_lots[r["lot_key"]] = r["best_lot"]
# Baseline combined (123.set lots + production enables)
base_ov: dict = {**common_patches(), **production_enables()}
baseline = run_backtest(
ctx["data"], ctx["mt5_path"], ctx["login"], ctx["server"],
patch_set(BASE_SET, base_ov),
"lot_combined_baseline.set", "lot_combined_baseline",
)
print(
f"\nCOMBINED baseline PF={baseline.get('profit_factor')} net={baseline.get('net_profit')} "
f"sharpe={baseline.get('sharpe')} trades={baseline.get('total_trades')}",
flush=True,
)
opt_ov: dict = {**common_patches(), **production_enables(), **best_lots}
optimized = run_backtest(
ctx["data"], ctx["mt5_path"], ctx["login"], ctx["server"],
patch_set(BASE_SET, opt_ov),
"lot_combined_optimized.set", "lot_combined_optimized",
)
print(
f"COMBINED optimized PF={optimized.get('profit_factor')} net={optimized.get('net_profit')} "
f"sharpe={optimized.get('sharpe')} trades={optimized.get('total_trades')} "
f"dNet={g(optimized, 'net_profit') - g(baseline, 'net_profit'):.0f} "
f"dSharpe={g(optimized, 'sharpe') - g(baseline, 'sharpe'):.3f}",
flush=True,
)
summary = {
"timestamp": datetime.now().isoformat(timespec="seconds"),
"period": {"from": runner.FROM_DATE, "to": runner.TO_DATE},
"deposit": DEPOSIT,
"reference_balance": REF_BALANCE,
"stock_lot_max": STOCK_LOT_MAX,
"solo": results,
"best_lots": best_lots,
"combined_baseline": baseline,
"combined_optimized": optimized,
"combined_delta": {
"net_profit": g(optimized, "net_profit") - g(baseline, "net_profit"),
"sharpe": g(optimized, "sharpe") - g(baseline, "sharpe"),
"pf": g(optimized, "profit_factor") - g(baseline, "profit_factor"),
},
}
out_path = OUT / "lot_optimize_summary.json"
out_path.write_text(json.dumps(summary, indent=2), encoding="utf-8")
print(f"\nSaved {out_path}", flush=True)
print("\nBest lots:", flush=True)
for sid in ids:
if sid not in sm:
continue
row = next(x for x in results if x["id"] == sid)
m = row["best_metrics"]
print(
f" {sid:12} {row['lot_key']}={row['best_lot']} "
f"PF={m.get('profit_factor')} sharpe={m.get('sharpe')} net={m.get('net_profit')}",
flush=True,
)
if args.apply:
mq5_path = CLUSTER / "main.mq5"
set_path = CLUSTER / "123.set"
mq5_path.write_text(apply_lots_to_mq5(mq5_path.read_text(encoding="utf-8"), best_lots), encoding="utf-8")
set_path.write_text(
apply_lots_to_set_text(set_path.read_text(encoding="utf-8"), best_lots),
encoding="utf-8",
)
# Ensure reference balance in set
set_text = set_path.read_text(encoding="utf-8")
set_text = re.sub(
r"ORCH_ReferenceBalance=[^\n]+",
f"ORCH_ReferenceBalance={int(REF_BALANCE)}||{int(REF_BALANCE)}.0||100.000000||10000.000000||N",
set_text,
count=1,
)
set_path.write_text(set_text, encoding="utf-8")
print(f"Applied lots to {mq5_path} and {set_path}", flush=True)
if __name__ == "__main__":
main()