473 lines
17 KiB
Python
473 lines
17 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import random
|
||
|
|
import sqlite3
|
||
|
|
from dataclasses import asdict, fields, replace
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import pandas as pd
|
||
|
|
|
||
|
|
from qfr.strategy.etf_trend import Constraints, TrendParams, UniverseAsset, run_backtest
|
||
|
|
|
||
|
|
|
||
|
|
def load_universe(config_path: Path) -> tuple[list[UniverseAsset], Constraints, str, str]:
|
||
|
|
conf = json.loads(config_path.read_text(encoding="utf-8"))
|
||
|
|
universe = [UniverseAsset(**a) for a in conf["assets"]]
|
||
|
|
|
||
|
|
cons = conf.get("constraints", {})
|
||
|
|
constraints = Constraints(
|
||
|
|
max_positions=int(cons.get("max_positions", 3)),
|
||
|
|
must_commodity=int(cons.get("must_include", {}).get("commodity", 0)),
|
||
|
|
must_rates=int(cons.get("must_include", {}).get("rates", 0)),
|
||
|
|
must_equity=int(cons.get("must_include", {}).get("equity", 0)),
|
||
|
|
)
|
||
|
|
|
||
|
|
risk_proxy = cons.get("risk_proxy") or (universe[0].ts_code if universe else "510300.SH")
|
||
|
|
rates_fallback = cons.get("rates_fallback", "511010.SH")
|
||
|
|
return universe, constraints, str(risk_proxy), str(rates_fallback)
|
||
|
|
|
||
|
|
|
||
|
|
def load_prices(raw_dir: Path, universe: list[UniverseAsset], start: str, end: str) -> dict[str, pd.DataFrame]:
|
||
|
|
out: dict[str, pd.DataFrame] = {}
|
||
|
|
for a in universe:
|
||
|
|
fn = raw_dir / (a.ts_code.replace(".", "") + ".parquet")
|
||
|
|
df = pd.read_parquet(fn)
|
||
|
|
df = df.copy()
|
||
|
|
df["trade_date"] = df["trade_date"].astype(str)
|
||
|
|
df = df[(df["trade_date"] >= start) & (df["trade_date"] <= end)]
|
||
|
|
out[a.ts_code] = df
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def perf_stats(equity: pd.Series) -> dict[str, float]:
|
||
|
|
r = equity.pct_change().dropna()
|
||
|
|
if r.empty:
|
||
|
|
return {}
|
||
|
|
ann_ret = float((equity.iloc[-1] / equity.iloc[0]) ** (252 / len(r)) - 1)
|
||
|
|
ann_vol = float(r.std(ddof=1) * (252**0.5))
|
||
|
|
dd = float((equity / equity.cummax() - 1.0).min())
|
||
|
|
sharpe = float(ann_ret / ann_vol) if ann_vol > 0 else float("nan")
|
||
|
|
return {"ann_return": ann_ret, "ann_vol": ann_vol, "max_drawdown": dd, "sharpe": sharpe}
|
||
|
|
|
||
|
|
|
||
|
|
def trades_per_year(trades: pd.DataFrame | None, start: str, end: str) -> float:
|
||
|
|
if trades is None or getattr(trades, "empty", True):
|
||
|
|
return 0.0
|
||
|
|
years = max(1, (int(end[:4]) - int(start[:4]) + 1))
|
||
|
|
return float(len(trades) / years)
|
||
|
|
|
||
|
|
|
||
|
|
def ensure_db(db_path: Path, param_cols: list[str]) -> None:
|
||
|
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
with sqlite3.connect(str(db_path)) as con:
|
||
|
|
con.execute("PRAGMA journal_mode=WAL")
|
||
|
|
con.execute("PRAGMA synchronous=NORMAL")
|
||
|
|
con.execute(
|
||
|
|
"""
|
||
|
|
CREATE TABLE IF NOT EXISTS trials (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
run_id TEXT NOT NULL,
|
||
|
|
ts_utc TEXT NOT NULL,
|
||
|
|
code_version TEXT,
|
||
|
|
config_path TEXT,
|
||
|
|
start TEXT,
|
||
|
|
end TEXT,
|
||
|
|
seed INTEGER,
|
||
|
|
trial INTEGER,
|
||
|
|
jobs INTEGER,
|
||
|
|
ann_return REAL,
|
||
|
|
ann_vol REAL,
|
||
|
|
max_drawdown REAL,
|
||
|
|
sharpe REAL,
|
||
|
|
trades_per_year REAL
|
||
|
|
)
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
for c in param_cols:
|
||
|
|
try:
|
||
|
|
con.execute(f"ALTER TABLE trials ADD COLUMN {c} REAL")
|
||
|
|
except sqlite3.OperationalError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
def insert_rows(db_path: Path, param_cols: list[str], rows: list[dict[str, Any]]) -> None:
|
||
|
|
if not rows:
|
||
|
|
return
|
||
|
|
cols = [
|
||
|
|
"run_id",
|
||
|
|
"ts_utc",
|
||
|
|
"code_version",
|
||
|
|
"config_path",
|
||
|
|
"start",
|
||
|
|
"end",
|
||
|
|
"seed",
|
||
|
|
"trial",
|
||
|
|
"jobs",
|
||
|
|
"ann_return",
|
||
|
|
"ann_vol",
|
||
|
|
"max_drawdown",
|
||
|
|
"sharpe",
|
||
|
|
"trades_per_year",
|
||
|
|
*param_cols,
|
||
|
|
]
|
||
|
|
q = ",".join(["?"] * len(cols))
|
||
|
|
join_cols = ",".join(cols)
|
||
|
|
sql = f"INSERT INTO trials ({join_cols}) VALUES ({q})"
|
||
|
|
vals = []
|
||
|
|
for r in rows:
|
||
|
|
vals.append([r.get(c) for c in cols])
|
||
|
|
with sqlite3.connect(str(db_path)) as con:
|
||
|
|
con.executemany(sql, vals)
|
||
|
|
con.commit()
|
||
|
|
|
||
|
|
|
||
|
|
def load_state(path: Path) -> dict:
|
||
|
|
if path.exists():
|
||
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
||
|
|
return {"best": None, "last_reported_ann_return": None, "history": []}
|
||
|
|
|
||
|
|
|
||
|
|
def save_state(path: Path, state: dict) -> None:
|
||
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
|
path.write_text(json.dumps(state, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
|
||
|
|
|
||
|
|
|
||
|
|
def infer_code_version(repo_dir: Path) -> str:
|
||
|
|
head = repo_dir / ".git" / "HEAD"
|
||
|
|
if head.exists():
|
||
|
|
try:
|
||
|
|
txt = head.read_text(encoding="utf-8").strip()
|
||
|
|
if txt.startswith("ref:"):
|
||
|
|
ref = txt.split(" ", 1)[1]
|
||
|
|
ref_path = repo_dir / ".git" / ref
|
||
|
|
if ref_path.exists():
|
||
|
|
return ref_path.read_text(encoding="utf-8").strip()
|
||
|
|
return txt
|
||
|
|
except Exception:
|
||
|
|
return "unknown"
|
||
|
|
return "nogit"
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument("--config", default="configs/etf_universe_industry_profiled.json")
|
||
|
|
ap.add_argument("--rawdir", default="data/raw")
|
||
|
|
ap.add_argument("--start", default="20200101")
|
||
|
|
ap.add_argument("--end", default="20251231")
|
||
|
|
ap.add_argument("--trials", type=int, default=20)
|
||
|
|
ap.add_argument("--seed", type=int, default=1)
|
||
|
|
ap.add_argument("--jobs", type=int, default=1)
|
||
|
|
ap.add_argument("--state", default="data/opt_state.json")
|
||
|
|
ap.add_argument("--db", default="data/experiments.sqlite")
|
||
|
|
ap.add_argument("--max_trades_per_year", type=float, default=80.0)
|
||
|
|
ap.add_argument("--progress_every", type=int, default=5)
|
||
|
|
ap.add_argument(
|
||
|
|
"--tweak",
|
||
|
|
action="append",
|
||
|
|
default=[],
|
||
|
|
help=(
|
||
|
|
"Enable a tweak group. Repeatable. Options: macro, churn, stops, score, switches, switches2, signal1, orth_ma, orth_weights, orth_mech, asym_fast, positions, exits. "
|
||
|
|
"(Each group adjusts <=4 params around current best.)"
|
||
|
|
),
|
||
|
|
)
|
||
|
|
args = ap.parse_args()
|
||
|
|
|
||
|
|
rng = random.Random(int(args.seed))
|
||
|
|
np.random.seed(int(args.seed))
|
||
|
|
|
||
|
|
config_path = Path(args.config)
|
||
|
|
universe, constraints, risk_proxy, rates_fallback = load_universe(config_path)
|
||
|
|
prices = load_prices(Path(args.rawdir), universe, str(args.start), str(args.end))
|
||
|
|
|
||
|
|
state_path = Path(args.state)
|
||
|
|
state = load_state(state_path)
|
||
|
|
best_row = state.get("best")
|
||
|
|
if not best_row:
|
||
|
|
raise SystemExit("opt_state.json missing best")
|
||
|
|
|
||
|
|
tp_fields = {f.name for f in fields(TrendParams)}
|
||
|
|
|
||
|
|
defaults = TrendParams(max_positions=constraints.max_positions)
|
||
|
|
best_params = {k: best_row[k] for k in best_row.keys() if k in tp_fields}
|
||
|
|
|
||
|
|
typed: dict[str, Any] = {}
|
||
|
|
for k, v in best_params.items():
|
||
|
|
t = type(getattr(defaults, k))
|
||
|
|
if t is int:
|
||
|
|
typed[k] = int(v)
|
||
|
|
elif t is float:
|
||
|
|
typed[k] = float(v)
|
||
|
|
else:
|
||
|
|
typed[k] = v
|
||
|
|
|
||
|
|
base = replace(defaults, **typed)
|
||
|
|
|
||
|
|
tweaks = set(args.tweak or [])
|
||
|
|
|
||
|
|
def sample_params() -> TrendParams:
|
||
|
|
p = base
|
||
|
|
|
||
|
|
if "macro" in tweaks:
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
macro_min_breadth=float(rng.choice([0.10, 0.12, 0.15, 0.18, 0.20])),
|
||
|
|
macro_down_frac=float(rng.choice([0.75, 0.78, 0.80, 0.82, 0.85])),
|
||
|
|
)
|
||
|
|
|
||
|
|
if "churn" in tweaks:
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
lazy_days=int(rng.choice([6, 8, 10])),
|
||
|
|
min_hold_days=int(rng.choice([2, 3, 4, 5])),
|
||
|
|
replace_score_gap=float(rng.choice([0.5, 0.8, 1.2, 1.6])),
|
||
|
|
)
|
||
|
|
|
||
|
|
if "switches" in tweaks:
|
||
|
|
# switch/constraint knobs (exactly 4 factors)
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
desired_positions_min=int(rng.choice([1, 2, 3])),
|
||
|
|
replace_score_gap=float(rng.choice([0.0, 0.3, 0.5, 0.8, 1.2])),
|
||
|
|
lazy_days=int(rng.choice([4, 6, 8, 10, 12])),
|
||
|
|
min_hold_days=int(rng.choice([1, 2, 3, 4, 5])),
|
||
|
|
)
|
||
|
|
|
||
|
|
if "switches2" in tweaks:
|
||
|
|
# route D churn control without forcing higher min holdings (desired_positions_min fixed)
|
||
|
|
# exactly 4 factors: replace_score_gap, lazy_days, min_hold_days, cooldown_days
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
desired_positions_min=int(1),
|
||
|
|
replace_score_gap=float(rng.choice([0.5, 0.8, 1.0, 1.2, 1.6])),
|
||
|
|
lazy_days=int(rng.choice([8, 10, 12, 14, 16])),
|
||
|
|
min_hold_days=int(rng.choice([3, 5, 7, 10])),
|
||
|
|
cooldown_days=int(rng.choice([0, 2, 4, 6, 8, 10])),
|
||
|
|
)
|
||
|
|
|
||
|
|
if "signal1" in tweaks:
|
||
|
|
# route D: improve signal quality (exactly 4 factors)
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
min_score=float(rng.choice([0.0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30])),
|
||
|
|
trend_strength_weight=float(rng.choice([0.0, 0.2, 0.4, 0.6, 0.8, 1.0])),
|
||
|
|
score_vol_denom_floor=float(rng.choice([0.01, 0.02, 0.03, 0.04, 0.05])),
|
||
|
|
macro_min_breadth=float(rng.choice([0.10, 0.15, 0.20, 0.25, 0.30])),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
if "orth_ma" in tweaks:
|
||
|
|
# route R: orthogonal to score/stops/exits; explore timing knobs (exactly 4 factors)
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
sma_fast=int(rng.choice([3, 5, 7, 9, 12])),
|
||
|
|
sma_slow=int(rng.choice([20, 30, 40, 60, 90])),
|
||
|
|
rebalance_every=int(rng.choice([1, 2, 3, 5])),
|
||
|
|
max_replaces_per_day=int(rng.choice([0, 1, 2])),
|
||
|
|
)
|
||
|
|
if p.sma_fast >= p.sma_slow:
|
||
|
|
p = replace(p, sma_fast=max(3, int(p.sma_slow // 6)))
|
||
|
|
|
||
|
|
|
||
|
|
if "orth_weights" in tweaks:
|
||
|
|
# route R: orthogonal portfolio weight shape (exactly 4 factors)
|
||
|
|
max_positions = int(rng.choice([2, 3, 4, 5]))
|
||
|
|
desired_min = int(rng.choice([1, 2, 3]))
|
||
|
|
desired_max = int(rng.choice([2, 3, 4, 5]))
|
||
|
|
desired_min = min(desired_min, desired_max)
|
||
|
|
desired_max = min(desired_max, max_positions)
|
||
|
|
desired_min = min(desired_min, desired_max)
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
max_positions=max_positions,
|
||
|
|
desired_positions_min=desired_min,
|
||
|
|
desired_positions_max=desired_max,
|
||
|
|
max_weight_per_asset=float(rng.choice([0.35, 0.45, 0.60, 0.75, 0.90, 1.00])),
|
||
|
|
)
|
||
|
|
# concentration_power exists in TrendParams; adjust it separately (still counts as one factor)
|
||
|
|
p = replace(p, concentration_power=float(rng.choice([1.2, 1.6, 2.0, 2.2, 2.6, 3.0])))
|
||
|
|
|
||
|
|
|
||
|
|
if "orth_mech" in tweaks:
|
||
|
|
# route R: mechanism/turnover knobs (exactly 4 factors)
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
rebalance_every=int(rng.choice([1, 2, 3, 5])),
|
||
|
|
replace_score_gap=float(rng.choice([0.0, 0.3, 0.5, 0.8, 1.2])),
|
||
|
|
max_replaces_per_day=int(rng.choice([0, 1, 2, 3])),
|
||
|
|
cooldown_days=int(rng.choice([0, 2, 4, 6, 8, 10])),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
if "asym_fast" in tweaks:
|
||
|
|
# asymmetric bull/bear risk controls (fast-run) (exactly 4 factors)
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
regime_confirm_days=int(rng.choice([2, 3, 4, 5])),
|
||
|
|
bull_atr_mult=float(rng.choice([3.0, 3.2, 3.4, 3.6])),
|
||
|
|
bear_atr_mult=float(rng.choice([2.0, 2.2, 2.4, 2.6, 2.8])),
|
||
|
|
bear_stop_loss_atr=float(rng.choice([2.0, 2.2, 2.4, 2.6, 2.8])),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
if "positions" in tweaks:
|
||
|
|
# concentration/positioning knobs (exactly 4 factors)
|
||
|
|
max_positions = int(rng.choice([2, 3, 4]))
|
||
|
|
desired_min = int(rng.choice([1, 2, 3]))
|
||
|
|
desired_max = int(rng.choice([2, 3, 4]))
|
||
|
|
# keep consistent
|
||
|
|
desired_min = min(desired_min, desired_max)
|
||
|
|
desired_max = min(desired_max, max_positions)
|
||
|
|
desired_min = min(desired_min, desired_max)
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
max_positions=max_positions,
|
||
|
|
desired_positions_min=desired_min,
|
||
|
|
desired_positions_max=desired_max,
|
||
|
|
max_weight_per_asset=float(rng.choice([0.45, 0.60, 0.75, 0.90, 1.00])),
|
||
|
|
)
|
||
|
|
|
||
|
|
if "stops" in tweaks:
|
||
|
|
# risk-control fine search (route D: prefer higher sharpe / lower drawdown)
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
atr_mult=float(rng.choice([3.0, 3.2, 3.4, 3.6])),
|
||
|
|
stop_loss_atr=float(rng.choice([2.4, 2.6, 2.8, 3.0, 3.2])),
|
||
|
|
profit_tighten_atr=float(rng.choice([4.0, 6.0, 8.0])),
|
||
|
|
atr_mult_profit=float(rng.choice([1.3, 1.5, 1.8, 2.0])),
|
||
|
|
)
|
||
|
|
|
||
|
|
if "exits" in tweaks:
|
||
|
|
# anomaly exits fine search (route D) - exactly 4 factors
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
bias_window=int(rng.choice([10, 15, 20, 30])),
|
||
|
|
bias_exit=float(rng.choice([0.12, 0.16, 0.20, 0.25, 0.30])),
|
||
|
|
vol_short=int(rng.choice([3, 5, 8, 10])),
|
||
|
|
vol_ratio_exit=float(rng.choice([2.0, 2.5, 3.0, 3.5, 4.0])),
|
||
|
|
)
|
||
|
|
|
||
|
|
if "score" in tweaks:
|
||
|
|
# aggressive weight search for higher ann_return
|
||
|
|
p = replace(
|
||
|
|
p,
|
||
|
|
min_score=float(rng.choice([-0.10, 0.00, 0.05, 0.10, 0.20, 0.30, 0.40])),
|
||
|
|
trend_strength_weight=float(rng.choice([0.00, 0.20, 0.40, 0.60, 0.80, 1.00])),
|
||
|
|
w_r20=float(rng.choice([0.20, 0.35, 0.50, 0.65, 0.80])),
|
||
|
|
w_r60=float(rng.choice([0.00, 0.10, 0.20, 0.35, 0.50])),
|
||
|
|
)
|
||
|
|
remain = 1.0 - (p.w_r20 + p.w_r60)
|
||
|
|
w_r5 = float(max(0.0, min(0.6, remain * 0.6)))
|
||
|
|
w_r120 = float(max(0.0, remain - w_r5))
|
||
|
|
p = replace(p, w_r5=w_r5, w_r120=w_r120)
|
||
|
|
|
||
|
|
return p
|
||
|
|
|
||
|
|
param_cols = sorted(asdict(base).keys())
|
||
|
|
db_path = Path(args.db)
|
||
|
|
ensure_db(db_path, param_cols=param_cols)
|
||
|
|
|
||
|
|
run_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + f"_bestlocal_seed{int(args.seed)}" + ("_" + "-".join(sorted(tweaks)) if tweaks else "")
|
||
|
|
code_version = infer_code_version(Path("."))
|
||
|
|
|
||
|
|
best_ann = float(best_row.get("ann_return") or float("-inf"))
|
||
|
|
|
||
|
|
rows_for_db: list[dict[str, Any]] = []
|
||
|
|
valid = 0
|
||
|
|
for t in range(int(args.trials)):
|
||
|
|
p = sample_params()
|
||
|
|
|
||
|
|
equity, _w, tr = run_backtest(
|
||
|
|
prices,
|
||
|
|
universe,
|
||
|
|
constraints,
|
||
|
|
p,
|
||
|
|
rates_fallback=rates_fallback,
|
||
|
|
risk_proxy=risk_proxy,
|
||
|
|
)
|
||
|
|
st = perf_stats(equity["equity"])
|
||
|
|
if not st:
|
||
|
|
continue
|
||
|
|
|
||
|
|
tpy = trades_per_year(tr, str(args.start), str(args.end))
|
||
|
|
if tpy > float(args.max_trades_per_year):
|
||
|
|
continue
|
||
|
|
|
||
|
|
valid += 1
|
||
|
|
row = {**st, "trades_per_year": float(tpy), **asdict(p)}
|
||
|
|
row["trial"] = int(t)
|
||
|
|
row["seed"] = int(args.seed)
|
||
|
|
|
||
|
|
if float(row["ann_return"]) > best_ann:
|
||
|
|
best_ann = float(row["ann_return"])
|
||
|
|
state["best"] = row
|
||
|
|
save_state(state_path, state)
|
||
|
|
|
||
|
|
db_row = {
|
||
|
|
"run_id": run_id,
|
||
|
|
"ts_utc": datetime.now(timezone.utc).isoformat(),
|
||
|
|
"code_version": code_version,
|
||
|
|
"config_path": str(config_path),
|
||
|
|
"start": str(args.start),
|
||
|
|
"end": str(args.end),
|
||
|
|
"seed": int(args.seed),
|
||
|
|
"trial": int(t),
|
||
|
|
"jobs": int(args.jobs),
|
||
|
|
"ann_return": float(row["ann_return"]),
|
||
|
|
"ann_vol": float(row["ann_vol"]),
|
||
|
|
"max_drawdown": float(row["max_drawdown"]),
|
||
|
|
"sharpe": float(row["sharpe"]),
|
||
|
|
"trades_per_year": float(row["trades_per_year"]),
|
||
|
|
}
|
||
|
|
for c in param_cols:
|
||
|
|
db_row[c] = row.get(c)
|
||
|
|
rows_for_db.append(db_row)
|
||
|
|
|
||
|
|
if int(args.progress_every) > 0 and valid % int(args.progress_every) == 0:
|
||
|
|
print(f"progress valid={valid} best_ann={best_ann:.4f}", flush=True)
|
||
|
|
|
||
|
|
if rows_for_db:
|
||
|
|
insert_rows(db_path, param_cols=param_cols, rows=rows_for_db)
|
||
|
|
|
||
|
|
state.setdefault("history", []).append(
|
||
|
|
{
|
||
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||
|
|
"run_id": run_id,
|
||
|
|
"code_version": code_version,
|
||
|
|
"config": str(args.config),
|
||
|
|
"start": str(args.start),
|
||
|
|
"end": str(args.end),
|
||
|
|
"trials": int(args.trials),
|
||
|
|
"jobs": int(args.jobs),
|
||
|
|
"best_ann_return": float(best_ann) if np.isfinite(best_ann) else None,
|
||
|
|
"db": str(args.db),
|
||
|
|
"base_from": "opt_state.best",
|
||
|
|
"tweaks": sorted(tweaks),
|
||
|
|
}
|
||
|
|
)
|
||
|
|
save_state(state_path, state)
|
||
|
|
|
||
|
|
df = pd.DataFrame(rows_for_db).sort_values(["ann_return"], ascending=False)
|
||
|
|
view_cols = [
|
||
|
|
"ann_return",
|
||
|
|
"ann_vol",
|
||
|
|
"max_drawdown",
|
||
|
|
"sharpe",
|
||
|
|
"trades_per_year",
|
||
|
|
"atr_mult",
|
||
|
|
"stop_loss_atr",
|
||
|
|
"profit_tighten_atr",
|
||
|
|
"atr_mult_profit",
|
||
|
|
]
|
||
|
|
view_cols = [c for c in view_cols if c in df.columns]
|
||
|
|
print("run_id", run_id)
|
||
|
|
print(df[view_cols].head(8).to_string(index=False))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|