diff --git a/.gitignore b/.gitignore index 69d8c76..8e40828 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,7 @@ ipch/ # Visual Studio Trace Files *.e2e .tradingview_e2e/ +.worktrees/ # TFS 2012 Local Workspace $tf/ diff --git a/results/mlp_performance.md b/results/mlp_performance.md index 1a6608a..33ad9cf 100644 --- a/results/mlp_performance.md +++ b/results/mlp_performance.md @@ -1,6 +1,6 @@ # MLP Performance — COINBASE_BTCUSD -*Updated: 2026-06-25 23:29 UTC · IS: 2017-12-01 → 2026-02-28 · OOS: 2026-03-01+* +*Updated: 2026-08-21 01:51 UTC · IS: 2017-12-01 → 2026-02-28 · OOS: 2026-03-01+* | TF | IS PnL % | Max DD % | Win Rate | Thru 2026-06-25 | OOS PnL % | OOS Trades | |---|---:|---:|---:|---:|---:|---:| diff --git a/strategies/mlp_feature_groups.py b/strategies/mlp_feature_groups.py new file mode 100644 index 0000000..3d326ef --- /dev/null +++ b/strategies/mlp_feature_groups.py @@ -0,0 +1,206 @@ +"""Interpretable input routing for the grouped MLP experiment. + +Each first-layer unit receives one coherent family of features. Later layers +remain dense, so the model can learn interactions *between* those family-level +representations without mixing unrelated raw signals in the first transform. +""" +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +from typing import Sequence + +import numpy as np + + +@dataclass(frozen=True) +class FeatureFamily: + """A named set of raw features routed to one or more first-layer units.""" + + name: str + features: frozenset[str] + + +LOCAL_TECHNICAL = FeatureFamily("local_technical", frozenset({ + "stoch_norm", "macd_pred_norm", "osc_norm", "m3_momentum_norm", + "m2_tiny_norm", "stoch_div_norm", "vwap_div_norm", "stoch_peak_norm", + "m3_div_norm", "m2_nooff_norm", "m2_div_norm", "rsi_subtf_norm", + "bb_pct_b_norm", +})) +ON_CHAIN = FeatureFamily("on_chain", frozenset({ + "totalvol_norm", "newaddr_norm", "sendaddr_norm", "mvrv_zscore_value", + "mvrv_zscore_cont", "nupl_norm", "sopr_norm", "active1y_norm", "hr_norm", +})) +CANDLESTICKS = FeatureFamily("candlesticks", frozenset({ + "bearish_engulfing_score", "bullish_hammer_score", "bullish_engulfing_score", + "shooting_star_score", +})) +MACRO = FeatureFamily("macro", frozenset({ + "btc_spx_corr_30", "dxy_roc_norm", "vix_pctrank_inv", "btc_dom_roc_sign", + "us10y_roc_inv_sign", "spy_above_200ema", "gold_roc_pctrank", + "fed_net_liq_sign", "gc_position", "us2y_roc_inv_sign", "yield_curve_sign", +})) +RSID = FeatureFamily("rsid_structure", frozenset({ + "rsid_reg_bull_norm", "rsid_reg_bear_norm", "rsid_hid_bull_norm", + "rsid_hid_bear_norm", "rsid_rt_bull_norm", "rsid_rt_bear_norm", + "rsid_slow_bull_norm", "rsid_slow_bear_norm", "rsid_delayed_peak_norm", + "rsid_delayed_dip_norm", +})) +MARKET_FLOW = FeatureFamily("market_flow", frozenset({ + "oi_roc_norm", "usdt_d_norm", "basis_norm", "cvd_norm", "rvol_norm", +})) +CROSS_ASSET = FeatureFamily("cross_asset", frozenset({"btc_gold_norm"})) + +# 16 first-layer units: more capacity goes to broad, complementary families. +GROUPED_UNIT_ALLOCATION = ( + LOCAL_TECHNICAL, LOCAL_TECHNICAL, LOCAL_TECHNICAL, + ON_CHAIN, ON_CHAIN, ON_CHAIN, + MACRO, MACRO, MACRO, + CANDLESTICKS, + RSID, RSID, RSID, + MARKET_FLOW, MARKET_FLOW, + CROSS_ASSET, +) + +# Hybrid v1 reserves half of the 16 first-layer units for semantic specialists +# and leaves the other half dense, preserving direct cross-feature capacity. +HYBRID_GROUPED_SPECIALIST_ALLOCATION = ( + LOCAL_TECHNICAL, LOCAL_TECHNICAL, + ON_CHAIN, + MACRO, + CANDLESTICKS, + RSID, + MARKET_FLOW, + CROSS_ASSET, +) +HYBRID_GROUPED_DENSE_UNIT_COUNT = 8 +HYBRID_GROUPED_HIDDEN_SIZE = len(HYBRID_GROUPED_SPECIALIST_ALLOCATION) + HYBRID_GROUPED_DENSE_UNIT_COUNT +HYBRID_GROUPED_DENSE_ROW_INDICES = tuple( + range(len(HYBRID_GROUPED_SPECIALIST_ALLOCATION), HYBRID_GROUPED_HIDDEN_SIZE) +) + + +def grouped_first_layer_mask(feature_cols: Sequence[str], hidden_size: int) -> np.ndarray: + """Return the allowed ``(hidden_size, n_features)`` first-layer edges. + + The experiment deliberately targets the established 55→16→8→1 shape. + Failing on a different width prevents a silently arbitrary allocation. + """ + if hidden_size != len(GROUPED_UNIT_ALLOCATION): + raise ValueError( + f"Grouped input structure requires {len(GROUPED_UNIT_ALLOCATION)} first-layer units; " + f"received {hidden_size}." + ) + known = frozenset().union(*(family.features for family in GROUPED_UNIT_ALLOCATION)) + unknown = set(feature_cols) - known + if unknown: + raise ValueError(f"Grouped input structure has unknown features: {sorted(unknown)}") + mask = np.array( + [[feature in family.features for feature in feature_cols] + for family in GROUPED_UNIT_ALLOCATION], + dtype=bool, + ) + if not mask.any(axis=0).all(): + missing = [feature for feature, present in zip(feature_cols, mask.any(axis=0)) if not present] + raise ValueError(f"Grouped input structure left features unrouted: {missing}") + return mask + + +def grouped_structure_metadata(feature_cols: Sequence[str], hidden_size: int) -> dict[str, object]: + """Serializable provenance saved beside an experimental artifact.""" + grouped_first_layer_mask(feature_cols, hidden_size) + families = tuple(dict.fromkeys(GROUPED_UNIT_ALLOCATION)) + return { + "name": "grouped_first_layer_v1", + "unit_families": [family.name for family in GROUPED_UNIT_ALLOCATION], + "family_feature_counts": { + family.name: sum(feature in family.features for feature in feature_cols) + for family in families + }, + } + + +def hybrid_grouped_first_layer_mask(feature_cols: Sequence[str], hidden_size: int) -> np.ndarray: + """Return the v1 specialist-plus-dense first-layer connectivity mask. + + Rows 0--7 are hard-routed specialists, in the documented family order. + Rows 8--15 remain fully trainable across every input feature. Keeping the + architecture check strict makes the allocation auditable rather than an + accidental choice for another hidden width. + """ + if hidden_size != HYBRID_GROUPED_HIDDEN_SIZE: + raise ValueError( + f"Hybrid grouped input structure requires {HYBRID_GROUPED_HIDDEN_SIZE} " + f"first-layer units; received {hidden_size}." + ) + known = frozenset().union(*(family.features for family in HYBRID_GROUPED_SPECIALIST_ALLOCATION)) + unknown = set(feature_cols) - known + if unknown: + raise ValueError(f"Hybrid grouped input structure has unknown features: {sorted(unknown)}") + specialist_rows = np.array( + [[feature in family.features for feature in feature_cols] + for family in HYBRID_GROUPED_SPECIALIST_ALLOCATION], + dtype=bool, + ) + dense_rows = np.ones((len(HYBRID_GROUPED_DENSE_ROW_INDICES), len(feature_cols)), dtype=bool) + return np.vstack((specialist_rows, dense_rows)) + + +def hybrid_grouped_structure_metadata(feature_cols: Sequence[str], hidden_size: int) -> dict[str, object]: + """Return complete, reproducible provenance for the hybrid v1 mask.""" + mask = hybrid_grouped_first_layer_mask(feature_cols, hidden_size) + feature_text = "\x1f".join(feature_cols) + specialist_count = len(HYBRID_GROUPED_SPECIALIST_ALLOCATION) + return { + "name": "hybrid_grouped_first_layer_v1", + "version": 1, + "specialist_row_families": { + str(row): family.name + for row, family in enumerate(HYBRID_GROUPED_SPECIALIST_ALLOCATION) + }, + "dense_row_indices": list(HYBRID_GROUPED_DENSE_ROW_INDICES), + "feature_cols_sha256": hashlib.sha256(feature_text.encode()).hexdigest(), + "active_edges": int(mask.sum()), + "blocked_edges": int(mask.size - mask.sum()), + "row_active_edges": [int(count) for count in mask.sum(axis=1)], + "row_feature_indices": [np.flatnonzero(row).tolist() for row in mask], + "specialist_row_count": specialist_count, + "dense_row_count": len(HYBRID_GROUPED_DENSE_ROW_INDICES), + } + + +def random_sparse_first_layer_mask( + feature_cols: Sequence[str], hidden_size: int, mask_seed: int, +) -> np.ndarray: + """Return a seeded random control mask with the grouped row capacities. + + This deliberately samples features independently per hidden unit. It is a + capacity-matched control, not a second semantic feature routing scheme. + Calling the grouped builder first preserves its architecture and feature + validation while providing the exact per-row active-edge counts. + """ + grouped_mask = grouped_first_layer_mask(feature_cols, hidden_size) + rng = np.random.default_rng(mask_seed) + mask = np.zeros_like(grouped_mask, dtype=bool) + for row_index, active_edges in enumerate(grouped_mask.sum(axis=1)): + selected = rng.choice(len(feature_cols), size=int(active_edges), replace=False) + mask[row_index, selected] = True + return mask + + +def random_sparse_structure_metadata( + feature_cols: Sequence[str], hidden_size: int, mask_seed: int, +) -> dict[str, object]: + """Return complete, serializable provenance for a random sparse mask.""" + mask = random_sparse_first_layer_mask(feature_cols, hidden_size, mask_seed) + feature_text = "\x1f".join(feature_cols) + return { + "name": "random_sparse_first_layer_v1", + "mask_seed": mask_seed, + "generator": "numpy.default_rng(seed).choice(n_features, row_count, replace=False)", + "feature_cols_sha256": hashlib.sha256(feature_text.encode()).hexdigest(), + "active_edges": int(mask.sum()), + "blocked_edges": int(mask.size - mask.sum()), + "row_active_edges": [int(count) for count in mask.sum(axis=1)], + "row_feature_indices": [np.flatnonzero(row).tolist() for row in mask], + } diff --git a/strategies/params/mlp/mlp_weights_COINBASE_BTCUSD_6H_grouped_v1_seed9090.json b/strategies/params/mlp/mlp_weights_COINBASE_BTCUSD_6H_grouped_v1_seed9090.json new file mode 100644 index 0000000..d3ff873 --- /dev/null +++ b/strategies/params/mlp/mlp_weights_COINBASE_BTCUSD_6H_grouped_v1_seed9090.json @@ -0,0 +1 @@ +{"version": 1, "asset": "COINBASE_BTCUSD", "timeframe": "6H", "arch": [55, 16, 8, 1], "activation": "tanh_closed_form_clamp20", "feature_cols": ["stoch_norm", "macd_pred_norm", "osc_norm", "totalvol_norm", "m3_momentum_norm", "m2_tiny_norm", "newaddr_norm", "stoch_div_norm", "vwap_div_norm", "stoch_peak_norm", "sendaddr_norm", "m3_div_norm", "bearish_engulfing_score", "m2_nooff_norm", "m2_div_norm", "bullish_hammer_score", "bullish_engulfing_score", "shooting_star_score", "btc_spx_corr_30", "dxy_roc_norm", "vix_pctrank_inv", "btc_dom_roc_sign", "us10y_roc_inv_sign", "spy_above_200ema", "gold_roc_pctrank", "mvrv_zscore_value", "mvrv_zscore_cont", "nupl_norm", "fed_net_liq_sign", "gc_position", "us2y_roc_inv_sign", "yield_curve_sign", "sopr_norm", "rsid_reg_bull_norm", "rsid_reg_bear_norm", "rsid_hid_bull_norm", "rsid_hid_bear_norm", "rsid_rt_bull_norm", "rsid_rt_bear_norm", "rsid_slow_bull_norm", "rsid_slow_bear_norm", "rsid_delayed_peak_norm", "rsid_delayed_dip_norm", "oi_roc_norm", "usdt_d_norm", "basis_norm", "cvd_norm", "btc_gold_norm", "rsi_subtf_norm", "bb_pct_b_norm", "cvd_norm", "rvol_norm", "active1y_norm", "hr_norm", "sopr_norm"], "layers": [{"W": [[-0.41008136976768295, 0.24120393015473657, 0.014674539291339037, 0.0, 0.20874109806323335, -0.503020563047582, 0.0, 0.31837439349329655, -0.32995780645657813, -0.20247079122391137, 0.0, 0.2151830628162687, 0.0, 0.004439512934822261, -0.10015086233239584, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.02611619200027744, -0.14378586644243468, 0.0, 0.0, 0.0, 0.0, 0.0], [0.3358149323066063, -0.11424343161238247, 0.022822566510261662, 0.0, 0.29981711558187435, 0.07433399193202045, 0.0, 0.31722182833445944, -0.11383898331060209, 0.2162883391036723, 0.0, -0.27254151120679204, 0.0, 0.1725106964316952, -0.06783748102399471, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3416611768108252, 0.1695324688014158, 0.0, 0.0, 0.0, 0.0, 0.0], [-0.04051840876571839, 0.09249756803551805, 0.31180096824256875, 0.0, -0.39498490720367213, -0.3887054374354037, 0.0, 0.1394072915017805, -0.08061564508759028, -0.17383192695041383, 0.0, 0.388830846819876, 0.0, 0.23692978288250083, 0.034393579627670916, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.2503964575554732, -0.1686022699954186, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.042221932005217865, 0.0, 0.0, -0.03958760060316287, 0.0, 0.0, 0.0, 0.3963873373219431, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.07953368444424598, 0.006571004990620379, 0.07512851135896954, 0.0, 0.0, 0.0, 0.0, 0.15056440502026058, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.017697829931066455, 0.18175420150065086, -0.34444773022255804], [0.0, 0.0, 0.0, -0.07147495962072348, 0.0, 0.0, 0.05395964531968599, 0.0, 0.0, 0.0, -0.15233857270069417, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.2575218322339856, 0.07106340823585885, 0.14056958608700543, 0.0, 0.0, 0.0, 0.0, -0.24115330559381462, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.4251333384113327, -0.08903878108556879, 0.11489542829134422], [0.0, 0.0, 0.0, 0.053189046133066784, 0.0, 0.0, 0.10929273733199722, 0.0, 0.0, 0.0, -0.10463863689837849, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.034725169016101745, 0.10030309802231913, -0.42922402132610765, 0.0, 0.0, 0.0, 0.0, -0.03672463925250327, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.1361877945453587, 0.0933169861933686, 0.544430900184746], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.10932853030284988, 0.07533842347880952, 0.0143125138380266, 0.0917637645627976, -0.1437915290399244, -0.3596348102890376, 0.13832307306700475, 0.0, 0.0, 0.0, -0.03239587744993079, 0.18128140369850565, 0.49144423429853235, 0.04603629429571517, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.025816711468229392, -0.0023135517959855523, -0.10150465386768051, -0.037702348745961345, 0.12750755372135392, -0.024807230596612273, 0.18299704720004165, 0.0, 0.0, 0.0, 0.16437672430705097, -0.05744646916922584, -0.12037113550872702, -0.16770208316639762, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.014345675855157558, 0.3784182739126579, -0.16851145829094266, -0.09028625731729248, -0.005177494135115033, 0.03379033200291025, 0.003885735899219426, 0.0, 0.0, 0.0, -0.08704184709512408, 0.03966283151510022, 0.32715866507097296, -0.1621912421247323, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.30515161598476526, 0.0, 0.0, -0.07889624897645937, -0.07893741579560083, -0.04309906429831257, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.06664775937417207, -0.04430590703325411, -0.0024454924244149794, -0.02724353839302649, 0.4987887469921133, -0.07245247545285241, -0.3423903877036528, 0.1571323169873326, 0.2275684804430243, 0.08322273146900261, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3086779486217786, 0.11232791581129556, 0.23654005565613578, 0.2786870262102398, -0.35321129685920394, -0.13973956722919173, -0.0516559409378403, 0.09590603695055898, -0.010710970104945504, 0.14275530407864262, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.23311608891326063, -0.0040770103649861375, -0.7118022061745305, 0.2748402524376749, 0.17938341376502087, 0.18269699791409327, -0.015600546523389262, -0.04676027679400843, 0.02738951642113302, 0.3747095653283085, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.18826667795635596, -0.16336038606406303, 0.08738211431818488, -0.4005542646832332, 0.0, 0.0, 0.0, 0.18045167008158802, 0.25102346641250833, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.244734594438083, 0.019345567628664767, 0.17718545357354779, -0.16392297445811385, 0.0, 0.0, 0.0, 0.4461128165283955, -0.10492776177632845, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.07329976517332594, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], "b": [0.12741053800430416, -0.12973916240171096, 0.07988171234155532, -0.03135313450190845, -0.2971704279262123, -0.11764742235397821, -0.37754204882428755, 0.0120558243319285, -0.019579775888163297, 0.02264897701772285, 0.3015343720953952, 0.20435378649867486, -0.12617323507901204, -0.009864843652646421, -0.25457612630336324, -0.16115104898305943]}, {"W": [[-0.17916047612859842, -0.09852603927811993, 0.38180931324960793, -0.19751954397736463, 0.007809738503143787, 0.38823056434290854, -0.05978523796585472, 0.03392375794162229, -0.04480950272182971, -0.37365634357278915, 0.19226230437097527, 0.5618713839585742, 0.18212489412390942, 0.20913378115409553, 0.018277156407315386, 0.20249704058663365], [0.27327430513230117, 0.3652696767330947, -0.4396673065184616, -0.08846084539964263, -0.3275025285395878, 0.09532371978684781, -0.17402141722102346, -0.034371127097696935, -0.10411027000868615, 0.04504686868337761, -0.4565281941907469, 0.11856341733788679, -0.028805788085025597, -0.0498730324042054, 0.10452209912084942, -0.24224105359430298], [0.021092576327206434, 0.14352475831037625, 0.2512430194147529, -0.09469846574605245, -0.3809227341534125, 0.09249335152018393, -0.18979770111808664, 0.3991215850766625, 0.19058866213646394, 0.32707619821794987, 0.4120889916516711, -0.180219322494637, 0.372987132256862, -0.058949219416582134, -0.18357011344326563, -0.2127400210903267], [-0.375861752069034, -0.08192568370160812, 0.12131874699962983, -0.2229861440663157, -0.4021921072990281, -0.2569975783064283, 0.1216547851743327, -0.05131449496876288, 0.4465952096926998, -0.1360902272070054, -0.1449650943145706, 0.2663492533907641, 0.06700974286416364, 0.06795424640453737, -0.23434543870771185, 0.08802925923209548], [-0.3498510474956922, 0.03275183015302291, 0.19686258775673557, 0.17464092746006535, -0.09226252855305857, -0.05231637626467265, 0.22409131601667917, 0.33183224365151914, 0.3596330146369732, -0.03872286808071709, 0.48952006858845504, -0.40842434985144904, -0.30739886764353763, -0.44753950086531297, -0.14479793527723617, 0.4814372709593288], [-0.46104523151975974, 0.2721996003828956, 0.08680828797129288, 0.0770397831306083, -0.2634912210775598, -0.14122982231818287, 0.07157051148449135, 0.03096199884966508, -0.19929520550979954, -0.03269181895407455, -0.08387576324161146, -0.2550841932423257, 0.16182971295528067, 0.055455328898847966, -0.27836625837665935, 0.5925917622373976], [-0.07020288499499616, 0.03992580187170441, -0.13767886601606655, -0.20425218828609343, -0.4340469704320914, 0.7468650826610655, -0.006704092625078578, 0.2536098739710907, 0.08740536726416541, 0.16216021347698908, -0.4690762953531803, 0.07052141330511637, -0.2682625792055052, 0.0008791855849897001, -0.06080456114902174, 0.351885045515136], [0.19620197853535262, 0.08114321876058887, -0.3975835981703279, -0.1643776678563787, 0.14499544673084622, 0.25977460902887645, 0.40517691016888213, -0.15210137931467071, 0.03533221790025216, 0.24002903073312004, -0.5950812399791687, -0.7997854031402505, 0.4657069120727288, 0.4004410864907806, -0.080769739239997, -0.2525666125194656]], "b": [0.11524096753777457, -0.18138011188648748, -0.1248590955260559, -0.13221773341201476, 0.02325465612391236, -0.2831155734913464, -0.38658692777964043, -0.09421539737341415]}, {"W": [[-0.48883216086180703, 0.20510411060489164, 0.0010418210998776514, 0.06296327336606784, 0.44125595270350504, 0.2633349542909632, 0.4497052551018334, 0.39872215642992437]], "b": [0.029921155254434718]}], "training": {"phase1": {"device": "cpu", "arch": [55, 16, 8, 1], "epochs_run": 23, "best_val_mse": 0.2344736009836197, "train_bars": 11443, "val_bars": 591, "target_k": 10, "vol_window": 60, "target_scale": 2.0, "val_span": ["2025-10-01 00:00:00", "2026-02-28 00:00:00"]}, "data_file": "/Users/jameslopez/projects/TradingBot25/data/mlp/COINBASE_BTCUSD, 360.csv", "data_sha256": "4262012479e260b95fee1f738a614650e311fdb4b8b35a1adc95ead8ee7ac27b", "train_window": ["2015-01-08", "2026-02-28"], "seed": 9090, "regime_filter": "all", "input_structure": {"name": "grouped_first_layer_v1", "unit_families": ["local_technical", "local_technical", "local_technical", "on_chain", "on_chain", "on_chain", "macro", "macro", "macro", "candlesticks", "rsid_structure", "rsid_structure", "rsid_structure", "market_flow", "market_flow", "cross_asset"], "family_feature_counts": {"local_technical": 13, "on_chain": 10, "macro": 11, "candlesticks": 4, "rsid_structure": 10, "market_flow": 6, "cross_asset": 1}}, "created": "2026-08-20T23:59:29+00:00", "phase2": {"es_popsize": 32, "es_sigma": 0.05, "es_generations": 300, "l2_anchor": 0.01, "es_workers": 1, "fold_objective": "robust", "best_fitness": -0.43766636110486906, "fold_calmars": [0.43969448739415035, 0.09838826004281602, 1.5539997488579027, 2.7889862952999067, 0.8378024765041625, 0.0], "fold_mean_calmar": 0.9531452113498231, "fold_min_calmar": 0.0, "robust_diagnostics": {"valid_folds": 5, "drawdown_penalty": 0.0, "pnl_dd_penalty": 0.16666666666666666, "trade_penalty": 0.11111111111111112, "negative_fold_penalty": 0.16666666666666666, "fragility_penalty": 0.018, "fragile_0_05": 0, "fragile_1_0": 9, "trade_target": 15, "base_fold_score": 0.6672016479448761}}, "recommended_thresholds": {"i_long_entry_activation_threshold": -81.89604312230155, "i_long_exit_activation_threshold": 31.196653589448996, "i_long_exit_activation_confirmation_threshold": -32.083958601005826, "i_trailing_stop_threshold": 7.520590641572128, "i_use_long_exit_confirmation": 1.0, "i_use_long_entry_confirmation": false}}} \ No newline at end of file diff --git a/strategies/params/mlp/mlp_weights_COINBASE_BTCUSD_6H_grouped_v1_seed9191.json b/strategies/params/mlp/mlp_weights_COINBASE_BTCUSD_6H_grouped_v1_seed9191.json new file mode 100644 index 0000000..5f5303c --- /dev/null +++ b/strategies/params/mlp/mlp_weights_COINBASE_BTCUSD_6H_grouped_v1_seed9191.json @@ -0,0 +1 @@ +{"version": 1, "asset": "COINBASE_BTCUSD", "timeframe": "6H", "arch": [55, 16, 8, 1], "activation": "tanh_closed_form_clamp20", "feature_cols": ["stoch_norm", "macd_pred_norm", "osc_norm", "totalvol_norm", "m3_momentum_norm", "m2_tiny_norm", "newaddr_norm", "stoch_div_norm", "vwap_div_norm", "stoch_peak_norm", "sendaddr_norm", "m3_div_norm", "bearish_engulfing_score", "m2_nooff_norm", "m2_div_norm", "bullish_hammer_score", "bullish_engulfing_score", "shooting_star_score", "btc_spx_corr_30", "dxy_roc_norm", "vix_pctrank_inv", "btc_dom_roc_sign", "us10y_roc_inv_sign", "spy_above_200ema", "gold_roc_pctrank", "mvrv_zscore_value", "mvrv_zscore_cont", "nupl_norm", "fed_net_liq_sign", "gc_position", "us2y_roc_inv_sign", "yield_curve_sign", "sopr_norm", "rsid_reg_bull_norm", "rsid_reg_bear_norm", "rsid_hid_bull_norm", "rsid_hid_bear_norm", "rsid_rt_bull_norm", "rsid_rt_bear_norm", "rsid_slow_bull_norm", "rsid_slow_bear_norm", "rsid_delayed_peak_norm", "rsid_delayed_dip_norm", "oi_roc_norm", "usdt_d_norm", "basis_norm", "cvd_norm", "btc_gold_norm", "rsi_subtf_norm", "bb_pct_b_norm", "cvd_norm", "rvol_norm", "active1y_norm", "hr_norm", "sopr_norm"], "layers": [{"W": [[0.0468165774388492, -0.21296884053632242, -0.21937441794845, 0.0, -0.09438110804430146, -0.10119927813369375, 0.0, 0.24449469023394388, 0.13245775189133513, 0.24162698396302032, 0.0, 0.09494040766783247, 0.0, 0.09845698244345691, -0.17714921130115135, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.05235370198017244, -0.07177853815381888, 0.0, 0.0, 0.0, 0.0, 0.0], [0.1506911598614358, -0.008670564868837612, 0.058820884854975775, 0.0, -0.42738702828793756, -0.05506232323900702, 0.0, 0.06916504271056123, 0.0929848709860891, -0.09999607752989637, 0.0, 0.0709447745982021, 0.0, 0.2627304406906878, -0.009331039540961223, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.05654847336338403, -0.17040145242491456, 0.0, 0.0, 0.0, 0.0, 0.0], [-0.1202391628546658, 0.08522167232640143, 0.15972577878461083, 0.0, -0.09659226045856281, -0.3447224608780628, 0.0, 0.3154178994200004, 0.04769133691101988, -0.05962033150245198, 0.0, 0.3163459633523588, 0.0, 0.09610532075874209, 0.026089875930531622, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09115226725185786, 0.12285372207763726, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, -0.14411426688692133, 0.0, 0.0, 0.1999492459675934, 0.0, 0.0, 0.0, 0.2471579935260708, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.14014131592494714, -0.10349734814898337, 0.008667710089469347, 0.0, 0.0, 0.0, 0.0, 0.11635403181061278, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.12789540240492997, 0.07924677855014581, 0.05849091560153715], [0.0, 0.0, 0.0, 0.30556725823743475, 0.0, 0.0, -0.13564567023664162, 0.0, 0.0, 0.0, -0.4147622629418427, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.3050729761717584, 0.008226795895534064, -0.27017700186709814, 0.0, 0.0, 0.0, 0.0, 0.01097835670192626, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.19906485958641615, -0.004979895075764518, -0.13900471876063134], [0.0, 0.0, 0.0, 0.016906123119884216, 0.0, 0.0, 0.09769013901975693, 0.0, 0.0, 0.0, -0.07554518307119153, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.08251346541279295, -0.0638649424676097, -0.27188824854107707, 0.0, 0.0, 0.0, 0.0, -0.13503579929072582, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0037700165732338954, -0.07893677898517876, 0.16048050597753205], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0533467585505598, 0.12561770720848942, -0.3422419098021965, -0.16372281620818346, 0.01891287507746807, 0.18080686585012198, -0.040688865828400034, 0.0, 0.0, 0.0, -0.16377987030052743, -0.2902415698019249, 0.1272035833386798, -0.01980828974550081, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.17057815633761583, -0.08376605163586237, -0.06785896277780634, -0.05403961937190384, -0.35083529272163927, -0.041749075268316595, -0.15453339376720118, 0.0, 0.0, 0.0, -0.12049351039915708, 0.06467259077652436, 0.3176451872580849, 0.0033206500346986325, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.22960037195339372, 0.2076601553718613, -0.28037756524764545, -0.4327212586928157, -0.05909354735804676, 0.13625845067632947, -0.07842283030554817, 0.0, 0.0, 0.0, 0.16061884477888588, -0.18707709616798265, -0.21301628475297976, 0.12310399984792093, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.24478189873338302, 0.0, 0.0, -0.18005433409011629, -0.04621520033280005, 0.06626406588703501, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.05819186449680735, 0.013300437464309943, -0.037079469754641325, 0.1813154906839429, 0.08772812208694523, 0.001263847744077999, 0.06572458105015555, -0.08511713961486884, 0.3633916862826545, -0.03968560960426416, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.16651574438729733, -0.3379337348546071, 0.21291105986152858, -0.04225767314219683, -0.10536103873418307, 0.20162701227961172, 0.113795793613744, -0.20612520130820716, -0.01641463184367359, 0.11347000641505213, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.09936840178481288, 0.19834715621853966, 0.13638765827807345, -0.143169925399358, -0.043405745887077095, 0.02856238555135491, -0.058976722912328715, 0.1373308379505137, 0.3604872113539257, -0.04145485552414396, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.18398154254784016, 0.15035976290286102, 0.24260770309268762, -0.13393719686293312, 0.0, 0.0, 0.0, -0.20968643572072732, 0.2215540405784537, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.1758962115318111, 0.1712545254517, -0.16256494673454408, 0.1331243200066826, 0.0, 0.0, 0.0, -0.05094305866832074, -0.02666057810086017, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.0021430304104959434, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], "b": [-0.16656201614425178, -0.1608130087126468, 0.20027180283663945, -0.3036616182055222, 0.03840562280175574, -0.14523030662214512, 0.15519314299971543, 0.026100768907350035, -0.1979876933671213, 0.23228401766041043, -0.1958999141297779, -0.0651278809587639, -0.10633621071041123, -0.009549645625566037, -0.013154485437328242, 0.1400605474662721]}, {"W": [[-0.2589372551684251, 0.048473924402242484, 0.21846422895350934, 0.32456804587611665, 0.21796008176247758, 0.026643788062134237, -0.3107757386885827, 0.21276064844490425, 0.251312703513861, 0.03975643390195072, -0.08724534397366379, 0.014368815881679958, 0.10235198929903508, 0.029842476518752555, 0.090864331686632, 0.09165795358594464], [0.35939380855422054, -0.19360720683244348, -0.032821923339350356, -0.13692989837665312, 0.1086672702743208, 0.29382074325412905, 0.23385825912386657, -0.24992679711438134, 0.03690436693844239, -0.036796551994266735, -0.16452761781345904, -0.016352079738494084, 0.24421465054935743, -0.015951764533361598, -0.008189874727055662, -0.11866570049808312], [-0.08860482595557397, 0.18112591783244888, 0.3311133635204887, 0.02110632287591542, 0.22859205804957877, 0.24956873350087755, -0.288652837143895, 0.3222743611691659, 0.1457371520763158, -0.061145520645521714, -0.4752340213766586, 0.02993131551889438, -0.26444106832091296, 0.1718728957151177, -0.409386056954845, -0.006518848292935336], [-0.03633485336963628, 0.0810074922994748, -0.19229376599911205, 0.26007255619822994, -0.23785498258868054, -0.04646794225172221, -0.37688918469542987, -0.14570041934148917, -0.0656525314332485, -0.06205244101809776, -0.14422850786553637, -0.12391931224094592, 0.01969950980942972, -0.1981666199016046, 0.18364708063556145, 0.3012838200938763], [-0.01251985442034594, -0.2823244485303078, 0.16620752593362687, -0.00016821048354606453, 0.14730625514901471, -0.18683661083363898, -0.21715052138405375, 0.1127162131247838, -0.1172909179783839, -0.029383013138445525, -0.15323997048873472, 0.08235584359871478, 0.18041221858435086, 0.2557849950402145, 0.23581216023365148, -0.05605058512805994], [0.17263095764757155, 0.07120836400225741, 0.2593465728900535, 0.04892778525841398, 0.017486994654916717, -0.13715657779694554, 0.021439860002736778, 0.13874153386785415, -0.10780300731783239, -0.1231660766365051, -0.03561613730524434, -0.3187259154258283, 0.21155523189257996, 0.1992595434938335, 0.10154572218757751, 0.10878161608465423], [-0.14787754322865518, 0.30566570262690373, 0.3482996015321857, 0.3105024441526842, -0.017296123436754733, -0.3457767892754975, -0.3041453848491569, -0.10476505872927169, 0.16896292514635441, -0.09832034178803194, 0.01525757637888161, -0.17247169838231752, -0.3270553246146486, 0.034668064556161046, 0.1564813689957422, 0.05537046874867903], [0.10476244112331458, 0.12039155948142131, 0.17145371167138318, -0.0700430026620554, -0.23689883173645732, -0.00830710689354184, 0.34543606295581675, -0.016342550858160953, 0.31012805134229204, 0.1492392010377917, 0.3258219495104065, 0.08031768973221856, 0.15021633946633922, 0.1805569211941978, 0.08786457394805273, -0.050424797095465095]], "b": [-0.12804456681971182, -0.12238747582497939, -0.048821288080241616, 0.18785772318610977, -0.22699578391834227, 0.22047099838281614, 0.14525205500943905, 0.24727330087123808]}, {"W": [[-0.12978942667813562, -0.2434709290978225, -0.29412243076498573, 0.413154070941658, -0.10650300486170747, -0.052218895546484845, -0.025855891280827777, -0.21372110953593632]], "b": [-0.46667726966355033]}], "training": {"phase1": {"device": "cpu", "arch": [55, 16, 8, 1], "epochs_run": 27, "best_val_mse": 0.23833580315113068, "train_bars": 11443, "val_bars": 591, "target_k": 10, "vol_window": 60, "target_scale": 2.0, "val_span": ["2025-10-01 00:00:00", "2026-02-28 00:00:00"]}, "data_file": "/Users/jameslopez/projects/TradingBot25/data/mlp/COINBASE_BTCUSD, 360.csv", "data_sha256": "4262012479e260b95fee1f738a614650e311fdb4b8b35a1adc95ead8ee7ac27b", "train_window": ["2015-01-08", "2026-02-28"], "seed": 9191, "regime_filter": "all", "input_structure": {"name": "grouped_first_layer_v1", "unit_families": ["local_technical", "local_technical", "local_technical", "on_chain", "on_chain", "on_chain", "macro", "macro", "macro", "candlesticks", "rsid_structure", "rsid_structure", "rsid_structure", "market_flow", "market_flow", "cross_asset"], "family_feature_counts": {"local_technical": 13, "on_chain": 10, "macro": 11, "candlesticks": 4, "rsid_structure": 10, "market_flow": 6, "cross_asset": 1}}, "created": "2026-08-21T00:01:21+00:00", "phase2": {"es_popsize": 32, "es_sigma": 0.05, "es_generations": 300, "l2_anchor": 0.01, "es_workers": 1, "fold_objective": "robust", "best_fitness": -0.2900740316818856, "fold_calmars": [1.3068411311208663, 0.001926074350861533, 0.6389458764988504, 2.539551532642326, 0.34739233161816335, 0.0], "fold_mean_calmar": 0.805776157705178, "fold_min_calmar": 0.0, "robust_diagnostics": {"valid_folds": 5, "drawdown_penalty": 0.0, "pnl_dd_penalty": 0.16666666666666666, "trade_penalty": 0.17777777777777778, "negative_fold_penalty": 0.16666666666666666, "fragility_penalty": 0.046, "fragile_0_05": 0, "fragile_1_0": 23, "trade_target": 15, "base_fold_score": 0.5640433103936245}}, "recommended_thresholds": {"i_long_entry_activation_threshold": -15.179348038757484, "i_long_exit_activation_threshold": 32.83594955158125, "i_long_exit_activation_confirmation_threshold": -26.966460058834596, "i_trailing_stop_threshold": 7.081133947272206, "i_use_long_exit_confirmation": 1.0, "i_use_long_entry_confirmation": false}}} \ No newline at end of file diff --git a/strategies/params/mlp/mlp_weights_COINBASE_BTCUSD_6H_grouped_v1_seed9292.json b/strategies/params/mlp/mlp_weights_COINBASE_BTCUSD_6H_grouped_v1_seed9292.json new file mode 100644 index 0000000..5164277 --- /dev/null +++ b/strategies/params/mlp/mlp_weights_COINBASE_BTCUSD_6H_grouped_v1_seed9292.json @@ -0,0 +1 @@ +{"version": 1, "asset": "COINBASE_BTCUSD", "timeframe": "6H", "arch": [55, 16, 8, 1], "activation": "tanh_closed_form_clamp20", "feature_cols": ["stoch_norm", "macd_pred_norm", "osc_norm", "totalvol_norm", "m3_momentum_norm", "m2_tiny_norm", "newaddr_norm", "stoch_div_norm", "vwap_div_norm", "stoch_peak_norm", "sendaddr_norm", "m3_div_norm", "bearish_engulfing_score", "m2_nooff_norm", "m2_div_norm", "bullish_hammer_score", "bullish_engulfing_score", "shooting_star_score", "btc_spx_corr_30", "dxy_roc_norm", "vix_pctrank_inv", "btc_dom_roc_sign", "us10y_roc_inv_sign", "spy_above_200ema", "gold_roc_pctrank", "mvrv_zscore_value", "mvrv_zscore_cont", "nupl_norm", "fed_net_liq_sign", "gc_position", "us2y_roc_inv_sign", "yield_curve_sign", "sopr_norm", "rsid_reg_bull_norm", "rsid_reg_bear_norm", "rsid_hid_bull_norm", "rsid_hid_bear_norm", "rsid_rt_bull_norm", "rsid_rt_bear_norm", "rsid_slow_bull_norm", "rsid_slow_bear_norm", "rsid_delayed_peak_norm", "rsid_delayed_dip_norm", "oi_roc_norm", "usdt_d_norm", "basis_norm", "cvd_norm", "btc_gold_norm", "rsi_subtf_norm", "bb_pct_b_norm", "cvd_norm", "rvol_norm", "active1y_norm", "hr_norm", "sopr_norm"], "layers": [{"W": [[-0.22233154922004295, 0.2846570055236813, -0.3523678556100675, 0.0, -0.13187957161544228, -0.2553483153421745, 0.0, 0.037255228781896785, 0.3479639221565604, 0.10182032008354833, 0.0, 0.0035848538151141366, 0.0, 0.3008577710347536, 0.42443705671899784, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.3668293716284779, 0.008588249687997597, 0.0, 0.0, 0.0, 0.0, 0.0], [0.5035741551785029, 0.3984580143710039, 0.2417287184108769, 0.0, 0.30744387820983915, 0.17762004290622233, 0.0, -0.1523177825595025, -0.19291591218219434, 0.08054022412185587, 0.0, 0.1043736178154161, 0.0, 0.09418570900828618, 0.04495688760476793, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.009248176752504514, -0.011105139398225914, 0.0, 0.0, 0.0, 0.0, 0.0], [0.16394002308407968, 0.1452610053421865, -0.06964005234495696, 0.0, -0.23996364474679957, -0.40257662341241396, 0.0, 0.2739190838818984, 0.09295525196713929, -0.46498527597112416, 0.0, 0.1697487706210225, 0.0, 0.139768372066529, 0.10440786170390075, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.019107731713530363, -0.02054645135941685, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, -0.09063485537872541, 0.0, 0.0, -0.15722260877966712, 0.0, 0.0, 0.0, -0.10023137151924355, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.22186815004735425, 0.4684905050422736, -0.07576156654348684, 0.0, 0.0, 0.0, 0.0, -0.12363240937767955, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.29088591671734687, -0.11504497097076499, -0.19905168274330856], [0.0, 0.0, 0.0, 0.3725434882221645, 0.0, 0.0, 0.0060818458012821125, 0.0, 0.0, 0.0, 0.38804544348801234, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.12221057937540244, -0.23881013884416458, -0.3979001659547249, 0.0, 0.0, 0.0, 0.0, 0.3983092030408886, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.057125624812807065, 0.3548526710073812, 0.10770176524070729], [0.0, 0.0, 0.0, 0.22670412816312238, 0.0, 0.0, -0.025515289684006844, 0.0, 0.0, 0.0, -0.26691967531092725, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.42199199719670144, -0.20746290142115753, -0.051475947690423074, 0.0, 0.0, 0.0, 0.0, -0.19969998831019237, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25915903080273595, -0.1807456286950486, -0.3104158510153543], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.12429707992897503, 0.369664760540175, 0.37410375379958877, 0.0667692907457515, -0.3200740196160214, 0.13933673720874623, 0.430843202928091, 0.0, 0.0, 0.0, -0.4204276783324837, -0.18406446768866397, -0.011309040503871187, -0.4451552293587488, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.18142433324394808, -0.03203982679279979, 0.1870573733263851, -0.18222859438790528, -0.34246779297378266, 0.013974905215432144, -0.020335036074849787, 0.0, 0.0, 0.0, -0.05292769462208506, -0.022730572284516824, -0.01697837420852855, -0.04751545723920955, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.24172326495735166, 0.04144990614869583, -0.17881287084262446, 0.49854862783184206, 0.3339035360146971, -0.14358854005919292, 0.13758630162904437, 0.0, 0.0, 0.0, 0.10548193832595279, -0.1734326543323286, 0.3210670085185481, -0.21917121852845617, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.04676214884607777, 0.0, 0.0, -0.28111698791074424, -0.024973632473572475, 0.07700999591057062, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.17769513950774296, -0.15080799060631356, 0.06962563035324082, 0.22370392414441495, -0.03133257974229196, 0.2502044626372752, 0.1425968412407613, 0.03871026423949299, 0.22696981191670015, -0.22852969765078782, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.04498845570912753, -0.0750019801736272, -0.3031556608498135, -0.3564942072255943, 0.1393190255732869, 0.04575697485167465, 0.130956299595117, 0.04899369845555144, -0.056764050088836726, 0.17534545006110613, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.46662052323692205, 0.13095777054945645, 0.20997408765462255, -0.3638757445699631, 0.06986631883125449, 0.17606247586170104, 0.24931708283750942, 0.0541317314450343, -0.3099432467044308, 0.4747091373837512, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.36588031401444143, 0.049998617299238296, 0.18263471761949016, 0.0148494853388906, 0.0, 0.0, 0.0, -0.23185570540402053, 0.1426668727970106, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.367145574410853, -0.4076070047763074, 0.13084419593608002, -0.3528496709285713, 0.0, 0.0, 0.0, -0.4470434295661767, -0.051198250362697754, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1031181799057296, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], "b": [0.27041885996762377, -0.33955089165097446, 0.2766681229168429, -0.032611936070759295, -0.17453944510351582, 0.228666621304799, 0.20880165858400787, -0.08482640019285784, -0.18165388664906612, -0.0066188995226265, 0.0637058460794193, -0.06636407989532245, 0.31941540597336987, -0.06979079638243513, 0.23180037724404073, 0.011015054156013494]}, {"W": [[-0.20513305511214547, -0.04070796616751502, -0.3191866076822801, 0.35772633748919347, -0.060830748111779076, -0.35413819256794543, 0.22539001892063576, -0.10048097212028832, -0.11843482014454632, -0.03244170047081572, 0.19405575153055069, -0.13082046535724778, 0.19643682586740882, -0.16899845090760868, -0.4094117375274513, 0.17841734064458944], [-0.25660405417141274, -0.3307117531075191, 0.5176000607257166, 0.2720302783430539, 0.48522915749043066, -0.05702365627439331, -0.25536341780358174, -0.1532879376956754, 0.14136388125793714, -0.49428664326235183, -0.019715216851755136, -0.023936281253796526, 0.10747102675059238, 0.20529304416322966, 0.12433300910146666, -0.041355634949921664], [-0.0933553122632506, 0.047259641521353514, -0.2822418652923225, 0.01016616725580818, -0.361629665830683, -0.07202180556879383, -0.01253945605077043, 0.43366116187583387, -0.3749392289510453, -0.40752504802721295, -0.40477220395971986, -0.006550820760092703, -0.29263737829617187, -0.38679120482715035, 0.2790284725210953, -0.3341422013799951], [-0.3489421979584229, 0.07454889488042277, -0.31352907973419675, -0.03871807497627896, 0.009825745575769762, 0.057883135183024036, -0.3316760649395501, -0.1734041192906616, -0.20084751823499586, 0.05206442684056012, 0.05394771977533643, -0.3466566696317981, 0.3701578749941825, 0.4054017604779035, -0.014223703907255842, 0.11052242007376163], [0.1866776069630954, 0.014197702618628787, -0.16649281242769898, 0.37209556174701863, -0.26914556789734556, 0.27507326402539733, 0.3148206297498505, -0.0999351371292938, 0.2653141832845534, -0.16371978263232517, -0.04192568139513391, 0.2533266492314501, -0.13921975097186803, 0.1414722805953472, 0.32469663854063435, -0.6188685003222173], [0.3124437079350432, 0.009945452567432232, -0.1401751931439576, -0.29860279209847984, 0.22784336169031735, -0.027719678752308144, 0.03187633494053034, 0.0709059678640383, 0.43440367062414703, -0.011670844658872715, -0.2385106643423304, -0.40744649581697967, 0.2695567379507362, 0.11679300927836567, -0.44515882127120193, 0.1468573645933326], [-0.20887543836269534, -0.20737514934780216, 0.4180718205777097, -0.19428782684782314, 0.23994400330787954, -0.5638172397752733, -0.026080125167848175, 0.29205529769775324, -0.3292467572422643, 0.08212487334320467, -0.06136812916967144, 0.0405743742797782, -0.5407877481647152, -0.11137308451615413, -0.0368047542944279, -0.1394661165233885], [0.2790485575419618, 0.12628577195633942, 0.38268652299946226, 0.40832240458192237, -0.21377928491746792, 0.38033244496888907, -0.29419873594461843, 0.5978936731528494, -0.16178146176886782, -0.1182729290532849, 0.4596337093384341, 0.19065460470723675, 0.5225315649767992, 0.17507111017747948, 0.09669543889672615, 0.6402098230949042]], "b": [0.1547781578208645, -0.04907834764812345, 0.4060706882346292, 0.14091244800558325, 0.11127091936978932, -0.12414307540627774, -0.5484543488896456, 0.33620770150365986]}, {"W": [[0.32130443882535353, -0.031809463462632456, -0.04072769167347265, -0.09006470153827748, 0.01608182335441161, 0.3532584906364661, 0.10844980847501542, -0.9024423889337121]], "b": [-0.41734382298295714]}], "training": {"phase1": {"device": "cpu", "arch": [55, 16, 8, 1], "epochs_run": 36, "best_val_mse": 0.2350395768880844, "train_bars": 11443, "val_bars": 591, "target_k": 10, "vol_window": 60, "target_scale": 2.0, "val_span": ["2025-10-01 00:00:00", "2026-02-28 00:00:00"]}, "data_file": "/Users/jameslopez/projects/TradingBot25/data/mlp/COINBASE_BTCUSD, 360.csv", "data_sha256": "4262012479e260b95fee1f738a614650e311fdb4b8b35a1adc95ead8ee7ac27b", "train_window": ["2015-01-08", "2026-02-28"], "seed": 9292, "regime_filter": "all", "input_structure": {"name": "grouped_first_layer_v1", "unit_families": ["local_technical", "local_technical", "local_technical", "on_chain", "on_chain", "on_chain", "macro", "macro", "macro", "candlesticks", "rsid_structure", "rsid_structure", "rsid_structure", "market_flow", "market_flow", "cross_asset"], "family_feature_counts": {"local_technical": 13, "on_chain": 10, "macro": 11, "candlesticks": 4, "rsid_structure": 10, "market_flow": 6, "cross_asset": 1}}, "created": "2026-08-21T00:03:10+00:00", "phase2": {"es_popsize": 32, "es_sigma": 0.05, "es_generations": 300, "l2_anchor": 0.01, "es_workers": 1, "fold_objective": "robust", "best_fitness": -0.5730395087716855, "fold_calmars": [4.99501045270767, 0.08187592866693709, 0.6071044897624435, 0.9981826392782103, 0.32381339274825566, 0.0], "fold_mean_calmar": 1.167664483860586, "fold_min_calmar": 0.0, "robust_diagnostics": {"valid_folds": 5, "drawdown_penalty": 0.0, "pnl_dd_penalty": 0.16666666666666666, "trade_penalty": 0.12222222222222222, "negative_fold_penalty": 0.16666666666666666, "fragility_penalty": 0.03, "fragile_0_05": 0, "fragile_1_0": 15, "trade_target": 15, "base_fold_score": 0.8173651387024102}}, "recommended_thresholds": {"i_long_entry_activation_threshold": -64.30469381240171, "i_long_exit_activation_threshold": 44.79630996276374, "i_long_exit_activation_confirmation_threshold": -28.32016046575593, "i_trailing_stop_threshold": 28.905251419349987, "i_use_long_exit_confirmation": 1.0, "i_use_long_entry_confirmation": false}}} \ No newline at end of file diff --git a/strategies/strategy_mlp_scores.py b/strategies/strategy_mlp_scores.py index bbfdac9..d3830e8 100644 --- a/strategies/strategy_mlp_scores.py +++ b/strategies/strategy_mlp_scores.py @@ -40,6 +40,7 @@ from strategies.strategy_activation_scores import ( # noqa: E402 _prepare_features, calculate_metrics, # re-exported: validate_strategy / optimize_strategy look it up on this module ) +from strategies.mlp_temporal_features import inject_temporal_features, temporal_feature_cols # noqa: E402 from strategies.library_activation_scores import calculate_positions # noqa: E402 __all__ = [ @@ -333,7 +334,9 @@ def _calculate_positions_with_dz(entry_raw_arr, exit_raw_arr, exit_raw_fallback_ def _score_to_signals(df, params, stoch_peak_arr, close_arr, high_arr, - entry_gate=None, additional_exit=None): + entry_gate=None, additional_exit=None, + state_policy=None, policy_risk=None, + return_opportunities=False): """Threshold/regime/crossunder/position block — mirrors strategy_activation_scores.generate_signals lines 332-421 verbatim (that block is inline in the original and cannot be imported without modifying @@ -445,6 +448,25 @@ def _score_to_signals(df, params, stoch_peak_arr, close_arr, high_arr, fallback_arr = exit_raw_fallback.fillna(False).values.astype(bool) score_exit_arr = score_exit.values + if return_opportunities: + return { + 'entry': entry_arr, 'exit': exit_arr, 'fallback': fallback_arr, + 'score_exit': score_exit_arr, 'exit_threshold': exit_threshold, + 'time_ok': np.ones(len(df), dtype=bool) if time_ok_arr is None else time_ok_arr, + 'dead_zone': exit_window > 1, 'trail': trail_stop_pct, + } + + if state_policy is not None: + from strategies.mlp_state_policy import replay_state_policy + result = replay_state_policy( + entry_arr, exit_arr, fallback_arr, score_exit_arr, exit_threshold, + close_arr, high_arr, trail_stop_pct, policy=state_policy, + risk=policy_risk, time_ok=time_ok_arr, dead_zone=exit_window > 1, + ) + for column, values in result.items(): + df[column] = values + return df + if exit_window > 1: if trail_stop_pct > 0.0: in_pos, exec_entry, exec_exit = _apply_trailing_stop_with_dz( @@ -488,6 +510,10 @@ def generate_signals(df, **params): raise ValueError("strategy_mlp_scores requires the 'mlp_weights_file' param") art = load_mlp_artifact(weights_file) art_cols = art["feature_cols"] + df = df.copy() + df.columns = df.columns.str.lower().str.strip() + if any(column in art_cols for column in temporal_feature_cols()): + inject_temporal_features(df) if art_cols != FEATURE_COLS: # Allow artifacts trained on a strict subset of FEATURE_COLS (e.g. the old # 49-feature set during the transition to 50 features). Missing columns @@ -518,9 +544,6 @@ def generate_signals(df, **params): stacklevel=2, ) - df = df.copy() - df.columns = df.columns.str.lower().str.strip() - X = _prepare_features(df, art_cols) df['activation_score'] = mlp_forward(X, art["layers"]) diff --git a/tests/test_mlp_input_structure.py b/tests/test_mlp_input_structure.py new file mode 100644 index 0000000..1ae963f --- /dev/null +++ b/tests/test_mlp_input_structure.py @@ -0,0 +1,171 @@ +import hashlib +import sys + +import numpy as np +import pandas as pd +import pytest + +from strategies.mlp_feature_groups import ( + GROUPED_UNIT_ALLOCATION, + HYBRID_GROUPED_DENSE_ROW_INDICES, + HYBRID_GROUPED_SPECIALIST_ALLOCATION, + grouped_first_layer_mask, + hybrid_grouped_first_layer_mask, + hybrid_grouped_structure_metadata, + random_sparse_first_layer_mask, + random_sparse_structure_metadata, +) +from strategies.strategy_mlp_scores import FEATURE_COLS +from tools import run_mlp_train, train_mlp +from tools.train_mlp import flatten_layers, pretrain, unflatten_layers +from tools.run_mlp_train import effective_artifact_tag + + +def test_grouped_mask_routes_each_first_layer_unit_to_one_signal_family(): + mask = grouped_first_layer_mask(FEATURE_COLS, hidden_size=16) + + assert mask.shape == (16, len(FEATURE_COLS)) + assert tuple(mask.sum(axis=0) > 0) == (True,) * len(FEATURE_COLS) + for row, family in zip(mask, GROUPED_UNIT_ALLOCATION): + expected = {i for i, feature in enumerate(FEATURE_COLS) if feature in family.features} + assert set(np.flatnonzero(row)) == expected + + +def test_grouped_mask_rejects_unsupported_width_and_unknown_features(): + with pytest.raises(ValueError, match="16"): + grouped_first_layer_mask(FEATURE_COLS, hidden_size=8) + with pytest.raises(ValueError, match="unknown"): + grouped_first_layer_mask([*FEATURE_COLS, "future_feature"], hidden_size=16) + + +def test_random_sparse_mask_matches_grouped_row_sparsity_and_is_reproducible(): + grouped = grouped_first_layer_mask(FEATURE_COLS, hidden_size=16) + first = random_sparse_first_layer_mask(FEATURE_COLS, hidden_size=16, mask_seed=13001) + again = random_sparse_first_layer_mask(FEATURE_COLS, hidden_size=16, mask_seed=13001) + different = random_sparse_first_layer_mask(FEATURE_COLS, hidden_size=16, mask_seed=13102) + + assert first.shape == (16, len(FEATURE_COLS)) + assert first.sum() == grouped.sum() == 149 + np.testing.assert_array_equal(first.sum(axis=1), grouped.sum(axis=1)) + np.testing.assert_array_equal(first, again) + assert not np.array_equal(first, different) + + +def test_random_sparse_metadata_records_reproducible_mask_provenance(): + metadata = random_sparse_structure_metadata(FEATURE_COLS, hidden_size=16, mask_seed=13001) + + assert metadata["name"] == "random_sparse_first_layer_v1" + assert metadata["mask_seed"] == 13001 + assert metadata["active_edges"] == 149 + assert metadata["row_active_edges"] == [13, 13, 13, 10, 10, 10, 11, 11, 11, 4, 10, 10, 10, 6, 6, 1] + assert len(metadata["row_feature_indices"]) == 16 + + +def test_hybrid_grouped_mask_preserves_specialists_and_dense_cross_family_rows(): + mask = hybrid_grouped_first_layer_mask(FEATURE_COLS, hidden_size=16) + + assert mask.shape == (16, len(FEATURE_COLS)) + assert int(mask.sum()) == 508 + assert int(mask.size - mask.sum()) == 372 + for row, family in enumerate(HYBRID_GROUPED_SPECIALIST_ALLOCATION): + expected = {i for i, feature in enumerate(FEATURE_COLS) if feature in family.features} + assert set(np.flatnonzero(mask[row])) == expected + for row in HYBRID_GROUPED_DENSE_ROW_INDICES: + assert mask[row].all() + with pytest.raises(ValueError, match="16"): + hybrid_grouped_first_layer_mask(FEATURE_COLS, hidden_size=8) + + +def test_hybrid_grouped_metadata_audits_exact_v1_mask(): + metadata = hybrid_grouped_structure_metadata(FEATURE_COLS, hidden_size=16) + + assert metadata["name"] == "hybrid_grouped_first_layer_v1" + assert metadata["version"] == 1 + assert metadata["specialist_row_families"] == { + "0": "local_technical", "1": "local_technical", "2": "on_chain", + "3": "macro", "4": "candlesticks", "5": "rsid_structure", + "6": "market_flow", "7": "cross_asset", + } + assert metadata["dense_row_indices"] == list(range(8, 16)) + assert metadata["active_edges"] == 508 + assert metadata["blocked_edges"] == 372 + assert metadata["row_active_edges"] == [13, 13, 10, 11, 4, 10, 6, 1] + [55] * 8 + assert metadata["feature_cols_sha256"] == hashlib.sha256("\x1f".join(FEATURE_COLS).encode()).hexdigest() + mask = hybrid_grouped_first_layer_mask(FEATURE_COLS, hidden_size=16) + assert metadata["row_feature_indices"] == [np.flatnonzero(row).tolist() for row in mask] + + +def test_hybrid_phase1_and_cma_masking_preserve_specialist_blocks(): + rng = np.random.default_rng(11) + mask = hybrid_grouped_first_layer_mask(FEATURE_COLS, hidden_size=16) + X = rng.normal(size=(72, len(FEATURE_COLS))) + y = rng.normal(size=72) + valid = np.ones(72, dtype=bool) + times = pd.Series(pd.date_range("2020-01-01", periods=72, freq="h")) + + layers, _ = pretrain( + X, y, valid, times, [16, 8], epochs=2, seed=13, + val_start=times.iloc[60], val_end=times.iloc[-1], log=lambda _: None, + first_layer_mask=mask, + ) + assert np.count_nonzero(layers[0][0][~mask]) == 0 + assert mask[8:].all() + + masks = [mask, np.ones((8, 16), dtype=bool), np.ones((1, 8), dtype=bool)] + theta = flatten_layers(layers, masks) + restored = unflatten_layers(theta, [55, 16, 8, 1], masks) + assert len(theta) == 669 + assert len(flatten_layers(layers)) == 1041 + assert np.count_nonzero(restored[0][0][~mask]) == 0 + + +def test_masked_parameter_roundtrip_never_restores_blocked_connections(): + mask = grouped_first_layer_mask(FEATURE_COLS, hidden_size=16) + rng = np.random.default_rng(4) + layers = [ + (rng.normal(size=(16, len(FEATURE_COLS))), rng.normal(size=16)), + (rng.normal(size=(8, 16)), rng.normal(size=8)), + (rng.normal(size=(1, 8)), rng.normal(size=1)), + ] + masks = [mask, np.ones((8, 16), dtype=bool), np.ones((1, 8), dtype=bool)] + + theta = flatten_layers(layers, masks) + restored = unflatten_layers(theta, [55, 16, 8, 1], masks) + + assert np.all(restored[0][0][~mask] == 0.0) + np.testing.assert_array_equal(restored[0][0][mask], layers[0][0][mask]) + + +def test_grouped_training_uses_an_isolated_default_artifact_tag(): + assert effective_artifact_tag("bb50", "all", "grouped") == "bb50_grouped" + assert effective_artifact_tag("bb50", "bull", "grouped") == "bb50_grouped_bull" + assert effective_artifact_tag("grouped_v1", "all", "grouped") == "grouped_v1" + assert effective_artifact_tag("bb50", "all", "dense") == "bb50" + assert effective_artifact_tag("bb50", "all", "random_sparse") == "bb50_random_sparse" + assert effective_artifact_tag("bb50", "bull", "random_sparse") == "bb50_random_sparse_bull" + assert effective_artifact_tag("grouped_v1", "all", "random_sparse") == "grouped_v1" + assert effective_artifact_tag("bb50", "all", "hybrid_grouped") == "bb50_hybrid_grouped" + assert effective_artifact_tag("bb50", "bull", "hybrid_grouped") == "bb50_hybrid_grouped_bull" + assert effective_artifact_tag("hybrid_grouped_v1", "all", "hybrid_grouped") == "hybrid_grouped_v1" + with pytest.raises(ValueError, match="reserved"): + effective_artifact_tag("grouped_v1", "all", "hybrid_grouped") + with pytest.raises(ValueError, match="reserved"): + effective_artifact_tag("random_sparse_v1", "all", "hybrid_grouped") + with pytest.raises(ValueError, match="reserved"): + effective_artifact_tag("grouped_v1_bull", "all", "hybrid_grouped") + + +def test_hybrid_direct_trainer_requires_an_isolated_output(monkeypatch): + monkeypatch.setattr(sys, "argv", [ + "train_mlp.py", "--data", "unused.csv", "--input-structure", "hybrid_grouped", + ]) + with pytest.raises(SystemExit, match="2"): + train_mlp.main() + + +def test_hybrid_orchestrator_rejects_shared_sweep_and_preset_paths(monkeypatch): + monkeypatch.setattr(sys, "argv", [ + "run_mlp_train.py", "--input-structure", "hybrid_grouped", "--sweep", + ]) + with pytest.raises(SystemExit, match="2"): + run_mlp_train.main() diff --git a/tools/check_pine.py b/tools/check_pine.py index 098353b..da45ec4 100644 --- a/tools/check_pine.py +++ b/tools/check_pine.py @@ -70,7 +70,7 @@ def check_security_calls(lines): if "request.security(" in line and not line.lstrip().startswith("//")] direct = len(hits) estimated_total = direct + LIBRARY_SECURITY_CALLS - at_limit = estimated_total >= SECURITY_HARD_LIMIT + at_limit = estimated_total > SECURITY_HARD_LIMIT near_limit = estimated_total >= SECURITY_HARD_LIMIT * SECURITY_WARN_PCT return hits, direct, estimated_total, near_limit, at_limit @@ -88,7 +88,7 @@ def check_plot_calls(lines): direct = len(plot_hits) estimated_total = direct + LIBRARY_PLOT_CALLS near_limit = estimated_total >= PLOT_HARD_LIMIT * PLOT_WARN_PCT - at_limit = estimated_total >= PLOT_HARD_LIMIT + at_limit = estimated_total > PLOT_HARD_LIMIT return plot_hits, direct, estimated_total, near_limit, at_limit @@ -241,8 +241,10 @@ def main(): print(f" Library estimate: +{LIBRARY_SECURITY_CALLS} (from imported libs)") print(f" Estimated total: {estimated_total} / {SECURITY_HARD_LIMIT}") slots_remaining = SECURITY_HARD_LIMIT - estimated_total - if slots_remaining <= 0: - print(f" ❌ AT OR OVER LIMIT — cannot add more request.security() calls") + if slots_remaining < 0: + print(f" ❌ OVER LIMIT — reduce request.security() calls") + elif slots_remaining == 0: + print(f" ⚠️ AT LIMIT — cannot add more request.security() calls") else: print(f" Slots remaining: {slots_remaining}") print() @@ -255,8 +257,10 @@ def main(): print(f" Library estimate: +{LIBRARY_PLOT_CALLS} (from imported libs)") print(f" Estimated total: {estimated_plots} / {PLOT_HARD_LIMIT}") slots_remaining_plot = PLOT_HARD_LIMIT - estimated_plots - if slots_remaining_plot <= 0: - print(f" ❌ AT OR OVER LIMIT — cannot add more plot/plotchar calls") + if slots_remaining_plot < 0: + print(f" ❌ OVER LIMIT — reduce plot/plotchar calls") + elif slots_remaining_plot == 0: + print(f" ⚠️ AT LIMIT — cannot add more plot/plotchar calls") else: print(f" Slots remaining: {slots_remaining_plot}") print() diff --git a/tools/run_mlp_train.py b/tools/run_mlp_train.py index 5db7051..13488f0 100644 --- a/tools/run_mlp_train.py +++ b/tools/run_mlp_train.py @@ -75,10 +75,25 @@ def existing_seeds(asset: str, tf: str, tag: str = "bb50") -> list[int]: return sorted(seeds) +def effective_artifact_tag(tag: str, regime: str, input_structure: str) -> str: + """Keep non-dense experiments out of the production ``bb50`` namespace.""" + if input_structure == "hybrid_grouped" and tag.startswith(( + "grouped", "random_sparse", "bb50_grouped", "bb50_random_sparse", + )): + raise ValueError( + f"Artifact tag '{tag}' is reserved by an existing input-structure experiment; " + "use an isolated hybrid tag such as 'hybrid_grouped_v1'." + ) + if input_structure != "dense" and tag == "bb50": + tag = f"{tag}_{input_structure}" + return tag if regime == "all" else f"{tag}_{regime}" + + def train_one(asset: str, tf: str, seed: int, hidden: list[int], fold_objective: str, l2: float, es_workers: int, es_generations: int, tag: str, dry_run: bool, - regime: str = "all") -> tuple[str, bool, str]: + regime: str = "all", input_structure: str = "dense", + mask_seed: int | None = None, temporal_features: bool = False) -> tuple[str, bool, str]: out = artifact_path(asset, tf, seed, tag) data = data_path(asset, tf) label = f"{asset} {tf} seed{seed}" @@ -100,6 +115,14 @@ def train_one(asset: str, tf: str, seed: int, hidden: list[int], ] if regime != "all": cmd += ["--regime", regime] + if input_structure != "dense": + cmd += ["--input-structure", input_structure] + if input_structure == "random_sparse": + if mask_seed is None: + return label, False, "random_sparse requires a mask seed" + cmd += ["--mask-seed", str(mask_seed)] + if temporal_features: + cmd += ["--temporal-features"] if dry_run: print(f" [dry-run] {' '.join(cmd)}") return label, True, "dry-run" @@ -233,21 +256,43 @@ def main() -> None: parser.add_argument("--regime", choices=["bull", "bear", "sideways", "all"], default="all", help="Filter Phase-1 training to MVRV regime bars " "(requires mvrv_regime column). Auto-appends regime suffix to tag.") + parser.add_argument("--input-structure", choices=["dense", "grouped", "hybrid_grouped", "random_sparse"], default="dense", + help="Pass the first-layer connectivity experiment to each training run.") + parser.add_argument("--random-mask-seed-start", type=int, + help="Required with random_sparse; one explicit mask seed per training seed, " + "incremented by 101 in the same order as training seeds.") + parser.add_argument("--temporal-features", action="store_true", + help="Experimental causal EMA companions; cannot sweep, promote, or generate Pine presets.") args = parser.parse_args() - # Auto-suffix tag with regime so bull-trained artifacts don't overwrite bb50 files. - effective_tag = args.tag if args.regime == "all" else f"{args.tag}_{args.regime}" + if args.input_structure == "random_sparse" and args.random_mask_seed_start is None: + parser.error("--random-mask-seed-start is required with --input-structure random_sparse") + if args.input_structure == "hybrid_grouped" and (args.sweep or args.promote or args.presets): + parser.error("hybrid_grouped does not support sweep, promote, or presets; use canonical replay only") + if args.temporal_features and (args.sweep or args.promote or args.presets): + parser.error("temporal-features does not support sweep, promote, or presets; use canonical replay only") + if args.temporal_features and args.tag == "bb50": + args.tag = "temporal_ema_v1" + + # Keep experimental structures out of the production namespace, then suffix + # regime so bull-trained artifacts cannot overwrite all-regime candidates. + effective_tag = effective_artifact_tag(args.tag, args.regime, args.input_structure) if effective_tag != args.tag: log(f"regime={args.regime}: tag auto-suffixed '{args.tag}' → '{effective_tag}'") combos = [(a, tf) for a in args.assets for tf in args.tfs] seeds = seed_range(args.seed_start, args.seeds) - jobs = [(a, tf, s) for a, tf in combos for s in seeds] + mask_seeds = (seed_range(args.random_mask_seed_start, len(seeds)) + if args.input_structure == "random_sparse" else [None] * len(seeds)) + jobs = [ + (a, tf, seed, mask_seed) + for a, tf in combos for seed, mask_seed in zip(seeds, mask_seeds) + ] log(f"Plan: {len(args.assets)} assets × {len(args.tfs)} TFs × {len(seeds)} seeds " f"= {len(jobs)} training jobs") if args.skip_existing: - jobs = [(a, tf, s) for a, tf, s in jobs + jobs = [(a, tf, s, mask_seed) for a, tf, s, mask_seed in jobs if not artifact_path(a, tf, s, effective_tag).exists()] log(f" After skipping existing: {len(jobs)} jobs remaining") @@ -263,8 +308,8 @@ def main() -> None: futures = { pool.submit(train_one, a, tf, s, args.hidden, args.fold_objective, args.l2, args.es_workers, args.es_generations, effective_tag, - args.dry_run, args.regime): (a, tf, s) - for a, tf, s in jobs + args.dry_run, args.regime, args.input_structure, mask_seed, args.temporal_features): (a, tf, s) + for a, tf, s, mask_seed in jobs } for fut in concurrent.futures.as_completed(futures): label, ok, detail = fut.result() @@ -306,7 +351,7 @@ def main() -> None: log("Running regression table...") table_output = run_table(args.assets, args.dry_run) print(table_output) - elif not args.dry_run: + elif not args.dry_run and args.input_structure == "dense": # Always refresh the tracked artifact even without --table for asset in args.assets: subprocess.run( diff --git a/tools/train_mlp.py b/tools/train_mlp.py index 815324f..c024677 100644 --- a/tools/train_mlp.py +++ b/tools/train_mlp.py @@ -55,6 +55,15 @@ from strategies.strategy_mlp_scores import ( # noqa: E402 save_mlp_artifact, ) from strategies.strategy_activation_scores import _prepare_features # noqa: E402 +from strategies.mlp_feature_groups import ( # noqa: E402 + grouped_first_layer_mask, + grouped_structure_metadata, + hybrid_grouped_first_layer_mask, + hybrid_grouped_structure_metadata, + random_sparse_first_layer_mask, + random_sparse_structure_metadata, +) +from strategies.mlp_temporal_features import inject_temporal_features # noqa: E402 from tools.worker_utils import DEFAULT_WORKER_FRACTION, resolve_worker_count # noqa: E402 VOL_WINDOW = 60 # bars of past 1-bar log returns for the vol normaliser @@ -103,7 +112,8 @@ def build_target(df, k): # Phase 1 — supervised pretrain # --------------------------------------------------------------------------- -def pretrain(X, y, valid, times, hidden, epochs, seed, val_start, val_end, log): +def pretrain(X, y, valid, times, hidden, epochs, seed, val_start, val_end, log, + first_layer_mask=None): import torch device = ("cuda" if torch.cuda.is_available() @@ -120,6 +130,11 @@ def pretrain(X, y, valid, times, hidden, epochs, seed, val_start, val_end, log): modules += [lin, torch.nn.Tanh()] layers_t.append(lin) model = torch.nn.Sequential(*modules).to(device) + mask_t = None + if first_layer_mask is not None: + mask_t = torch.tensor(first_layer_mask, dtype=torch.bool, device=device) + with torch.no_grad(): + layers_t[0].weight.masked_fill_(~mask_t, 0.0) val_mask = valid & (times >= val_start).values & (times <= val_end).values train_mask = valid & (times < val_start).values @@ -143,6 +158,9 @@ def pretrain(X, y, valid, times, hidden, epochs, seed, val_start, val_end, log): loss = loss_fn(model(Xtr), ytr) loss.backward() opt.step() + if mask_t is not None: + with torch.no_grad(): + layers_t[0].weight.masked_fill_(~mask_t, 0.0) train_loss = loss.item() model.eval() with torch.no_grad(): @@ -170,17 +188,31 @@ def pretrain(X, y, valid, times, hidden, epochs, seed, val_start, val_end, log): # Phase 2 — CMA-ES on fold-mean Calmar # --------------------------------------------------------------------------- -def flatten_layers(layers): - return np.concatenate([np.concatenate([W.ravel(), b.ravel()]) for W, b in layers]) +def flatten_layers(layers, weight_masks=None): + """Flatten trainable weights, omitting structurally blocked connections.""" + if weight_masks is None: + weight_masks = [np.ones_like(W, dtype=bool) for W, _ in layers] + return np.concatenate([ + np.concatenate([np.asarray(W)[mask], np.asarray(b).ravel()]) + for (W, b), mask in zip(layers, weight_masks) + ]) -def unflatten_layers(theta, arch): +def unflatten_layers(theta, arch, weight_masks=None): layers, i = [], 0 for li in range(len(arch) - 1): n_out, n_in = arch[li + 1], arch[li] - W = theta[i:i + n_out * n_in].reshape(n_out, n_in); i += n_out * n_in + mask = (np.ones((n_out, n_in), dtype=bool) if weight_masks is None + else np.asarray(weight_masks[li], dtype=bool)) + if mask.shape != (n_out, n_in): + raise ValueError(f"Weight mask {li} has shape {mask.shape}; expected {(n_out, n_in)}") + W = np.zeros((n_out, n_in), dtype=np.float64) + n_active = int(mask.sum()) + W[mask] = theta[i:i + n_active]; i += n_active b = theta[i:i + n_out]; i += n_out layers.append((W, b)) + if i != len(theta): + raise ValueError("Parameter vector length does not match architecture and masks") return layers @@ -254,7 +286,7 @@ def robust_objective_score(calmars, fold_metrics, scores, thresholds, min_fold_t def make_fitness_context(df, X, arch, theta0_w, folds, min_fold_trades, min_valid_folds, l2, - fold_objective="mean"): + fold_objective="mean", weight_masks=None): return { "base_df": df[["time", "close"]].copy(), "times": df["time"].copy(), @@ -269,6 +301,7 @@ def make_fitness_context(df, X, arch, theta0_w, folds, min_fold_trades, min_vali "min_valid_folds": min_valid_folds, "l2": l2, "fold_objective": fold_objective, + "weight_masks": weight_masks, } @@ -276,7 +309,7 @@ def evaluate_theta(theta, context): theta = np.asarray(theta, dtype=np.float64) theta0_w = context["theta0_w"] n_w = len(theta0_w) - layers = unflatten_layers(theta[:n_w], context["arch"]) + layers = unflatten_layers(theta[:n_w], context["arch"], context["weight_masks"]) thresholds = np.clip( theta[n_w:] * THRESHOLD_SCALE, [-900.0, -900.0, -900.0, 0.0], @@ -352,10 +385,10 @@ def evaluate_theta_worker(theta): def make_fitness(df, X, arch, theta0_w, folds, min_fold_trades, min_valid_folds, l2, - fold_objective="mean"): + fold_objective="mean", weight_masks=None): context = make_fitness_context( df, X, arch, theta0_w, folds, min_fold_trades, min_valid_folds, l2, - fold_objective, + fold_objective, weight_masks, ) def fitness(theta): @@ -534,12 +567,28 @@ def main(): "from the current winner artifact's position state (one-cycle circularity " "per roadmap). Output artifact will have 52 features.") p.add_argument("--approach-b-params", - help="Winner CSV to derive position state from (default: auto-detect from asset/tf)") + help="Winner CSV to derive position state from (default: auto-detect from asset/tf)") + p.add_argument("--temporal-features", action="store_true", + help="Experiment: append causal 3- and 12-bar EMA companions for ten fast signals.") + p.add_argument("--input-structure", choices=["dense", "grouped", "hybrid_grouped", "random_sparse"], default="dense", + help="First-layer connectivity: dense is the production baseline; grouped " + "routes each of the 16 first-layer units to one logical signal family; " + "hybrid_grouped keeps eight family specialists plus eight dense rows; " + "random_sparse is the matched seeded control.") + p.add_argument("--mask-seed", type=int, + help="Required with random_sparse; seed for the auditable first-layer mask.") p.add_argument("--regime", choices=["bull", "bear", "sideways", "all"], default="all", help="Filter Phase-1 supervised training to MVRV regime bars only " "(requires mvrv_regime column). Phase-2 CMA-ES always uses all IS bars.") args = p.parse_args() + if args.input_structure == "random_sparse" and args.mask_seed is None: + p.error("--mask-seed is required with --input-structure random_sparse") + if args.input_structure == "hybrid_grouped" and args.out is None: + p.error("--out is required with --input-structure hybrid_grouped to protect live artifacts") + if args.temporal_features and args.out is None: + p.error("--temporal-features requires --out to protect live artifacts") + t0 = time.time() def log(msg): @@ -553,7 +602,8 @@ def main(): if args.smoke: df = df.tail(800).reset_index(drop=True) - args.hidden = [8, 4] + if args.input_structure == "dense": + args.hidden = [8, 4] args.phase1_epochs = min(args.phase1_epochs, 20) args.es_generations = min(args.es_generations, 5) args.es_popsize = min(args.es_popsize, 8) @@ -578,6 +628,24 @@ def main(): feature_cols = _inject_position_features(df, asset, tf, args, log) else: feature_cols = FEATURE_COLS + if args.temporal_features: + feature_cols = [*feature_cols, *inject_temporal_features(df)] + log(f"temporal-features: appended {len(feature_cols) - len(FEATURE_COLS)} causal EMA companions") + + first_layer_mask = None + if args.input_structure == "grouped": + first_layer_mask = grouped_first_layer_mask(feature_cols, args.hidden[0]) + log("input-structure=grouped: first-layer units are restricted to logical signal families") + elif args.input_structure == "hybrid_grouped": + first_layer_mask = hybrid_grouped_first_layer_mask(feature_cols, args.hidden[0]) + log("input-structure=hybrid_grouped: eight family specialists plus eight dense rows " + f"(active_edges={int(first_layer_mask.sum())})") + elif args.input_structure == "random_sparse": + first_layer_mask = random_sparse_first_layer_mask( + feature_cols, args.hidden[0], args.mask_seed, + ) + log(f"input-structure=random_sparse: seeded mask={args.mask_seed} " + f"active_edges={int(first_layer_mask.sum())}") X = _prepare_features(df, feature_cols) y, valid = build_target(df, args.target_k) @@ -597,7 +665,8 @@ def main(): # --- Phase 1 --- layers, p1_meta = pretrain(X, y, valid, df["time"], args.hidden, - args.phase1_epochs, args.seed, val_start, val_end, log) + args.phase1_epochs, args.seed, val_start, val_end, log, + first_layer_mask) arch = p1_meta["arch"] training_meta = { @@ -609,6 +678,15 @@ def main(): "train_window": [str(df["time"].iloc[0].date()), str(df["time"].iloc[-1].date())], "seed": args.seed, "regime_filter": args.regime, + "input_structure": ( + grouped_structure_metadata(feature_cols, args.hidden[0]) + if args.input_structure == "grouped" + else hybrid_grouped_structure_metadata(feature_cols, args.hidden[0]) + if args.input_structure == "hybrid_grouped" + else random_sparse_structure_metadata(feature_cols, args.hidden[0], args.mask_seed) + if args.input_structure == "random_sparse" + else {"name": "dense"} + ), "created": datetime.now(timezone.utc).isoformat(timespec="seconds"), } # Seed thresholds from the pretrained net's actual score distribution — @@ -627,18 +705,23 @@ def main(): # --- Phase 2 --- if not args.skip_phase2: - theta0_w = flatten_layers(layers) + weight_masks = None + if first_layer_mask is not None: + weight_masks = [first_layer_mask] + [ + np.ones((arch[i + 1], arch[i]), dtype=bool) for i in range(1, len(arch) - 1) + ] + theta0_w = flatten_layers(layers, weight_masks) theta0 = np.concatenate([theta0_w, thresholds / THRESHOLD_SCALE]) fitness = make_fitness(df, X, arch, theta0_w, folds, min_fold_trades, min_valid_folds, args.l2, - args.fold_objective) + args.fold_objective, weight_masks) f0, calmars0, _ = fitness(theta0) log(f"phase2: pretrain baseline fitness={f0:.4f} " f"fold_calmars={[round(c, 2) for c in calmars0]}") best = run_cma(fitness, theta0, args.es_sigma, args.es_popsize, args.es_generations, args.seed, log, args.es_workers) n_w = len(theta0_w) - layers = unflatten_layers(best["theta"][:n_w], arch) + layers = unflatten_layers(best["theta"][:n_w], arch, weight_masks) thresholds = best["thr"] training_meta["phase2"] = { "es_popsize": args.es_popsize, "es_sigma": args.es_sigma, diff --git a/tools/validate_chart_data.py b/tools/validate_chart_data.py index 7d1343e..096ce71 100644 --- a/tools/validate_chart_data.py +++ b/tools/validate_chart_data.py @@ -138,7 +138,8 @@ def validate_file(path: str) -> tuple[list[str], list[str], dict]: return ["empty file (0 rows)"], [], stats # --- Timestamps --- - t = pd.to_datetime(df["time"], errors="coerce", utc=True) + time_unit = "s" if pd.api.types.is_numeric_dtype(df["time"]) else None + t = pd.to_datetime(df["time"], unit=time_unit, errors="coerce", utc=True) if t.isna().any(): errors.append(f"{int(t.isna().sum())} unparseable timestamps") t = t.dt.tz_localize(None)