disaster recovery

This commit is contained in:
yasha
2025-04-20 17:52:49 +00:00
parent 75c3f82f2a
commit 40eb79e86c
53 changed files with 14974 additions and 240 deletions
+71
View File
@@ -0,0 +1,71 @@
### Streamlined Calibration for Baseline LR Gates
If youre mainly struggling with miscalibrated confidence on your edgefiltered checks, heres a **minimal** integration to fix it without heavy lifting.
---
#### 1. Add a toggle and holdout in `config.yaml`
```yaml
baseline:
calibration_enabled: true # turn on/off easily
calibration_method: "isotonic" # handles multiclass
calibration_holdout: 0.2 # 20% of your train split
random_state: 42
```
---
#### 2. Quick split for calibration
In your `run_baseline_checks` (before any CI gates):
```python
# original train/val split
X_main, X_val, y_main, y_val = train_test_split(
X_pruned, y_labels, test_size=0.2, random_state=seed
)
if self.config['baseline']['calibration_enabled']:
X_train, X_cal, y_train, y_cal = train_test_split(
X_main, y_main,
test_size=self.config['baseline']['calibration_holdout'],
random_state=self.config['baseline']['random_state']
)
else:
X_train, y_train = X_main, y_main
X_cal, y_cal = None, None
```
---
#### 3. Fit an isotonic calibrator only when needed
```python
# train raw LR
lr = LogisticRegression(...).fit(X_train, y_train)
if X_cal is not None:
from sklearn.calibration import CalibratedClassifierCV
calibrator = CalibratedClassifierCV(lr, method='isotonic', cv='prefit')
calibrator.fit(X_cal, y_cal)
else:
calibrator = lr
```
---
#### 4. Use calibrated probabilities in your gates
Replace all `lr.predict_proba(X)` calls with:
```python
probs = calibrator.predict_proba(X)
# binary: edge = |probs[:,1] - 0.5|
# ternary: edge = max(probs, axis=1) - 1/3
```
Then run your existing CI lowerbound checks as usual.
---
#### 5. (Optional) Skip persistence
For a quick fix you can skip saving/loading the calibrator—just build and use it in the same process.
---
With these five steps, youll correct your edgeconfidence estimates with minimal code and configuration. If your gates then pass, proceed to GRU training; if they still fail, the issue is likely weak features rather than calibration.
+163
View File
@@ -0,0 +1,163 @@
Below is a consolidated set of revision instructions — and the key code snippets youll need — to switch your GRU/SAC pipeline to supervise **logreturns** (so your “ret” and “gauss_params” heads are targeting logreturn), and to wire everything endtoend so you get a clean 55 %+ edge before SAC.
---
## 🛠 Revision Playbook
1. **Compute forward logreturns in your data pipeline**
In `TradingPipeline.define_labels_and_align` (or wherever you set up `df`):
```python
# Replace any raw-return calculation with logreturn
N = config['gru']['prediction_horizon']
df['fwd_log_ret'] = np.log(df['close'].shift(-N) / df['close'])
df['direction_label'] = (df['fwd_log_ret'] > 0).astype(int)
# If you do ternary:
flat_thr = config['gru']['flat_sigma_multiplier'] * df['fwd_log_ret'].rolling(…)
df['dir3_label'] = pd.cut(df['fwd_log_ret'],
bins=[-np.inf, -flat_thr, flat_thr, np.inf],
labels=[0,1,2]).astype(int)
```
2. **Align your targets**
Drop the last N rows so `fwd_log_ret` has no NaNs:
```python
df = df.iloc[:-N]
```
3. **Pass logreturn into both heads**
When you build your sequences and target dicts:
```python
y_ret_seq = ... # shape (n_seq, 1) from fwd_log_ret
y_dir3_seq = ... # onehot from dir3_label
y_train = {'mu': y_ret_seq, # Huber head
'gauss_params': y_ret_seq, # NLL head uses same target
'dir3': y_dir3_seq} # classification head
```
4. **Update your GRU builder to match**
Make sure your v3 model has exactly three outputs:
```python
model = Model(inputs, outputs=[mu_output, gauss_params_output, dir3_output])
model.compile(
optimizer=Adam(lr),
loss={
'mu': Huber(delta),
'gauss_params': gaussian_nll,
'dir3': categorical_focal_loss
},
loss_weights={'mu':1.0, 'gauss_params':0.2, 'dir3':0.4},
metrics={'dir3':'accuracy'}
)
```
5. **Train with the new targets dict**
In `GRUModelHandler.train(...)`, replace your fit call with:
```python
history = model.fit(
X_train_seq,
y_train_dict,
validation_data=(X_val_seq, y_val_dict),
…callbacks…
)
```
6. **Calibrate on the “dir3” softmax outputs**
Your calibrator (Temp/Vector) must now consume the 3class logits or probabilities:
```python
raw_logits = handler.predict_logits(X_val_seq)
calibrator.fit(raw_logits, y_val_dir3)
```
7. **Feed SAC the logreturn μ and σ**
In your `TradingEnv`, when you construct the state:
```python
mu, log_sigma, probs = gru_handler.predict(X_step)
sigma = np.exp(log_sigma)
edge = 2 * calibrated_p_up - 1 # if binary
z_score = np.abs(mu) / sigma
state = [mu, sigma, edge, z_score, prev_position]
```
8. **Rerun baseline check on logreturns**
Your logistic baseline in `run_baseline_checks` should now be trained on `X_train_pruned` vs `y_dir3_label` (or binary), ensuring the CI ≥ 0.52 before you even build your GRU.
9. **Validate endtoend edge**
After these changes, you should see:
- Baseline logistic CI LB ≥ 0.52
- GRU “edge” hitrate ≥ 0.55 on validation
- SAC backtest hitting meaningful Sharpe/Winrate gates
---
## 🔧 Example Code Snippets
### 1) Gaussian NLL stays the same:
```python
@saving.register_keras_serializable(package='GRU')
def gaussian_nll(y_true, y_pred):
mu, log_sigma = tf.split(y_pred, 2, axis=-1)
y_true = tf.reshape(y_true, tf.shape(mu))
inv_var = tf.exp(-2*log_sigma)
return tf.reduce_mean(0.5 * inv_var * tf.square(y_true-mu) + log_sigma)
```
### 2) Build & compile v3 model:
```python
def build_gru_model_v3(...):
inp = layers.Input((lookback, n_features))
x = layers.GRU(gru_units, return_sequences=True)(inp)
x = layers.LayerNormalization()(x)
if attention_units>0:
x = layers.MultiHeadAttention(...)(x,x)
x = layers.GlobalAveragePooling1D()(x)
gauss = layers.Dense(2, name='gauss_params')(x)
mu = layers.Lambda(lambda z: z[:,0:1], name='mu')(gauss)
dir3_logits = layers.Dense(3, name='dir3_logits')(x)
dir3 = layers.Activation('softmax', name='dir3')(dir3_logits)
model = Model(inp, [mu, gauss, dir3])
model.compile(
optimizer=Adam(lr),
loss={'mu':Huber(delta),
'gauss_params':gaussian_nll,
'dir3':categorical_focal_loss},
loss_weights={'mu':1.0,'gauss_params':0.2,'dir3':0.4},
metrics={'dir3':'accuracy'}
)
return model
```
### 3) Fitting in your handler:
```python
history = self.model.fit(
X_train_seq, y_train_dict,
validation_data=(X_val_seq, y_val_dict),
epochs=max_epochs,
batch_size=batch_size,
callbacks=[early_stop, csv_logger, TqdmCallback()]
)
```
---
### Why these changes boost edge?
- **Logreturns** stabilize variance & symmetrize up/down moves.
- **NLL + Huber on logreturn** gives the model both distributional uncertainty (σ) and a robust error measure.
- **Proper softmax head** on three classes (up/flat/down) cleans up classification.
- **Calibration + optimized edge threshold** ensures your SAC agent only sees highconfidence signals (edge≥thr).
Together, this gets your baseline GRU above 55 % “edge” on validation, so the SAC agent can then learn a meaningful sizing policy rather than fight noise.
Let me know if you need any further refinements!
+187
View File
@@ -0,0 +1,187 @@
## Revision Instructions for AI DevAgent
Implement endtoend missingbar handling in GRU and SAC. Apply the steps below in sequence, with small PRs and CI green at each stage.
---
### 1 | Config updates
**File:** `config.yaml`
Add under `data` and create new sections for `gru` and `sac`:
```yaml
data:
bar_frequency: "1T"
missing:
strategy: "neutral" # drop | neutral | ffill | interpolate
max_gap: 5 # max consecutive missing bars allowed
interpolate:
method: "linear"
limit: 10
gru:
drop_imputed_sequences: true # drop any sequence containing imputed bars
sac:
imputed_handling: "hold" # hold | skip | penalty
action_penalty: 0.05 # used if imputed_handling=penalty
```
---
### 2 | Detect & fill missing bars
**File:** `src/data_loader.py`
1. **Import** at top:
```python
import pandas as pd
from .io_manager import IOManager
```
2. **Implement** `find_missing_bars(df, freq)` and `_consecutive_gaps` helpers.
3. **Implement** `report_missing(missing, cfg, io, logger)` as described.
4. **Implement** `fill_missing_bars(df, cfg, io, logger)`:
- Detect missing timestamps.
- Call `report_missing`.
- Reindex to full date_range.
- Apply `strategy`:
- `drop`: return original df.
- `neutral`: ffill close, set open=high=low=close, volume=0.
- `ffill`: `df_full.ffill().bfill()`.
- `interpolate`: use `df_full.interpolate(...)`.
- **After filling**, add column:
```python
df['bar_imputed'] = df.index.isin(missing)
```
- **Error** if longest gap > `cfg.data.missing.max_gap`.
5. **Integrate** in `TradingPipeline.load_and_preprocess_data` **before** feature engineering:
```python
df = fill_missing_bars(df, self.cfg, io, logger)
```
---
### 3 | Sequence creation respects imputed bars
**File:** `src/trading_pipeline.py`
1. In `create_sequences`, after building `X_seq` and `y_seq`, **build** `mask_seq` of shape `(n, lookback)` from `df['bar_imputed']`.
2. **Conditionally drop** sequences:
```python
if self.cfg.gru.drop_imputed_sequences:
valid = ~mask_seq.any(axis=1)
X_seq = X_seq[valid]; y_seq = y_seq[valid]
```
3. **Log**:
```python
logger.info(f"Generated {orig_n} sequences, dropped {orig_n - X_seq.shape[0]} with imputed bars")
```
4. **Include** `bar_imputed` as a feature column in `minimal_whitelist`.
---
### 4 | GRU model input channel
**File:** `src/model_gru_v3.py` (or `model_gru.py` if v3)
1. **Update input shape**: increase `n_features` by 1 to include `bar_imputed`.
2. **No further architectural change**; the model now sees the imputedflag channel.
---
### 5 | SAC environment handles imputed bars
**File:** `src/trading_env.py`
1. **Read** `bar_imputed` into `self.bar_imputed` aligned with your sequences.
2. **In `step(action)`**, at the top:
```python
imputed = self.bar_imputed[self.current_step]
if imputed:
mode = self.cfg.sac.imputed_handling
if mode == "skip":
self.current_step += 1
return next_state, 0.0, False, {}
if mode == "hold":
action = self.position
if mode == "penalty":
reward = - self.cfg.sac.action_penalty * (action - self.position)**2
self._update_position(action)
self.current_step += 1
return next_state, reward, False, {}
# existing normal step follows
```
3. **Ensure** imputed transitions are added to replay buffer only when `mode` ≠ `skip`.
4. **Log**:
```python
logger.debug(f"SAC step {self.current_step} on imputed bar: handling={mode}")
```
---
### 6 | Logging & artefacts
1. **Data load** warning:
```
WARNING Detected {total} missing bars, longest gap {longest}; applied strategy={strategy}
```
2. **Sequence creation** info:
```
INFO Generated {orig_n} sequences, dropped {dropped} with imputed bars
```
3. **SAC training** debug:
```
DEBUG SAC on imputed bar at step {step}: handling={mode}
```
4. **Report** saved under `results/<run_id>/`:
- `missing_bars_summary.json`
- `imputed_sequence_summary.json` with counts.
- `sac_imputed_transitions.csv` (optional detailed log).
---
### 7 | Unit tests
**Files:** `tests/test_data_loader.py`, `tests/test_sequence_creation.py`, `tests/test_trading_env.py`
1. **`test_data_loader.py`**:
- Synthetic gappy DataFrame → assert `bar_imputed` flags and each strategys output.
2. **`test_sequence_creation.py`**:
- Build toy DataFrame with `bar_imputed`; assert sequences dropped when `drop_imputed_sequences=True`.
3. **`test_trading_env.py`**:
- Create `TradingEnv` with known imputed steps; for each `imputed_handling` mode assert `step()` behavior:
- `skip` moves without adding to buffer;
- `hold` returns same position;
- `penalty` returns negative reward equal to penalty formula.
---
### 8 | Documentation
1. **README.md** → add **Data Quality** section describing missingbar handling, config keys, and recommended defaults.
2. **docs/v3_changelog.md** → note new missingbar feature and cfg flags.
---
**Rollout Plan:**
- **PR 1:** Config + data_loader missingbar detection & fill + tests.
- **PR 2:** Sequence creation & GRU channel update + tests.
- **PR 3:** SAC env updates + tests.
- **PR 4:** Logging/artefacts + docs.
Merge each after CI passes.
+140
View File
@@ -0,0 +1,140 @@
## **Revision Document v3 Output Contract & Figure Specifications**
This single guide merges **I/O plumbing**, **logging**, **CI hooks**, **artefact paths**, and **figure design** into one actionable playbook.
Apply the steps **in order**, submitting small PRs so CI remains green throughout.
---
### 0 ▪ Foundations
| Step | File(s) | Action |
|------|---------|--------|
| 0.1 | **`config.yaml`** | Add: ```yaml base_dirs: {results: results, models: models, logs: logs} output: {figure_dpi: 150, figure_size: [16, 9], log_level: INFO}``` |
| 0.2 | `src/utils/run_id.py` | `make_run_id()` → `"20250418_152310_ab12cd"` (timestamp + short githash). |
| 0.3 | `src/__init__.py` | Expose `__version__`, `GIT_SHA`, `BUILD_DATE`. |
---
### 1 ▪ Core I/O & Logging
| File | Content |
|------|---------|
| **`src/io_manager.py`** | `IOManager(cfg, run_id)` <br>• `path(section, name)`: returns full path under `results|models|logs|figures`.<br>• `save_json`, `save_df` (CSV ≤ 100 MB else Parquet), `save_figure` (uses cfg dpi/size). |
| **`src/logger_setup.py`** | `setup_logger(cfg, run_id, io)` with colourised console (INFO) + rotating file handler (DEBUG) in `logs/<run_id>/`. |
**`run.py` entry banner**
```python
run_id = make_run_id()
cfg = load_config(args.config)
io = IOManager(cfg, run_id)
logger = setup_logger(cfg, run_id, io)
logger.info(f"GRUSAC v{__version__} | commit {GIT_SHA} | run {run_id}")
logger.info(f"Loaded config file: {args.config}")
```
---
### 2 ▪ Stage Outputs
| Stage | Implementation notes | Artefacts |
|-------|---------------------|-----------|
| **Data load & preprocess** | After sampling/NaN purge save: <br>`io.save_json(summary, "preprocess_summary")`<br>`io.save_df(df.head(20), "head_preprocessed")` | `results/<run_id>/preprocess_summary.txt`<br>`head_preprocessed.csv` |
| **Feature engineering** | Generate correlation heatmap (see figure table) → `io.save_figure(...,"feature_corr_heatmap")` | 〃 |
| **Label generation** | Log distribution; produce histogram figure. | `label_histogram.png` |
| **Baseline 1 & 2** | Consolidate in `baseline_checker.py`; each returns dict with accuracy, CI etc. <br>`io.save_json(report,"baseline1_report")` (and 2). | `baseline1_report.txt / baseline2_report.txt` |
| **Feature whitelist** | Save JSON to `models/<run_id>/final_whitelist_<run_id>.json`. | — |
| **GRU training** | Use Keras CSVLogger to `logs/<run_id>/gru_history.csv`; after training plot learning curve. | `gru_learning_curve.png` + `.keras` model |
| **Calibration (Vector)** | Save `calibrator_vec_<run_id>.npy`; plot reliability curve. | `reliability_curve_val_<run_id>.png` |
| **SAC training** | Write `episode_rewards.csv`, plot reward curve, save final agent under `models/sac_train_<run_id>/`. | `sac_reward_plot.png` |
| **Backtest** | Save steplevel CSV, metrics JSON, summary figure. | `backtest_results_<run_id>.csv`<br>`performance_metrics_<run_id>.txt`<br>`backtest_summary_<run_id>.png` |
---
### 3 ▪ Figure Specifications
| File | Visualises | Layout / Details |
|------|-------------|------------------|
| **feature_corr_heatmap.png** | Pearson correlation of engineered features (preprune). | Square heatmap, features sorted by |ρ| vs target; diverging palette centred at 0; annotate |ρ| > 0.5; colourbar. |
| **label_histogram.png** | Directionlabel class mix (train split). | Bar chart: Down / Flat / Up (binary shows two). Percentages on bar tops; title shows ε value. |
| **gru_learning_curve.png** | GRU training progress. | 3 stacked panes: total loss (logy), val dir3 accuracy, vertical dashed “earlystop”; share epochaxis. |
| **reliability_curve_val_*.png** | Calibration quality postVector scaling. | Left 70 %: reliability diagram (10 equalfreq bins). Right 30 %: histogram of predicted p_up. Title shows ECE & Brier. |
| **sac_reward_plot.png** | Offline SAC learning curve. | Smoothed episode reward (EMA 0.2) vs steps; actionvariance on twin yaxis; checkpoint ticks. |
| **backtest_summary_*.png** | Live backtest overview. | 3 stacked plots:<br>1) Price line + blue/red background for edge ≥ 0.1.<br>2) Position size stepgraph.<br>3) Equity curve with shaded drawdowns; textbox shows Sharpe & Max DD. |
_All figs_: 16 × 9 in, 150 DPI, `plt.tight_layout()`, footer `"© GRUSAC v3"` rightbottom.
---
### 4 ▪ Unit Tests
* `tests/test_output_contract.py`
* Run minipipeline (`tests/smoke.yaml`), assert each required file exists > 2 KB.
* Validate JSON keys (`accuracy`, `ci_lower` etc.).
* `assert_any_close(softmax(logits), probs)` for logits view.
---
### 5 ▪ CI Workflow (`.github/workflows/pipeline.yml`)
```yaml
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with: {python-version: "3.10"}
- run: pip install -r requirements.txt
- run: black --check .
- run: ruff .
- run: pytest -q
- name: Smoke e2e
run: python run.py --config tests/smoke.yaml
- name: Upload artefacts
uses: actions/upload-artifact@v4
with:
name: run-${{ github.sha }}
path: |
results/*/*
logs/*/*
```
---
### 6 ▪ Documentation Updates
* **`README.md`** → new *Outputs* section reproducing the artefact table.
* **`docs/v3_changelog.md`** → onepager summarising v3 versus v2 differences (labels, calibration, outputs).
---
### 7 ▪ Rollout Plan (5PR cadence)
1. **PR #1**  runid, IOManager, logger, CI log upload.
2. **PR #2**  data & feature stage outputs + tests.
3. **PR #3**  GRU training outputs + calibration figure.
4. **PR #4**  SAC & backtest outputs, reward & summary figs.
5. **PR #5**  docs & README refresh.
Tag `v3.0.0` after PR #5 passes.
---
### 8 ▪ Success Criteria for CI
Fail the pipeline when **any** occurs:
* `baseline1_report.txt` CILB < 0.52
* `edge_filtered_accuracy` (val) < 0.60
* Backtest Sharpe < 1.2 or Max DD > 15 %
---
Implementing this **single integrated revision** provides:
* **Deterministic artefact paths** for every run.
* **Rich, shareable figures** for quick diagnostics.
* **Auditready logs/reports** for research traceability.
Merge each step once CI is green; youll have a reproducible, fully instrumented pipeline ready for iterative accuracy pushes toward the 65 % target.
+55
View File
@@ -0,0 +1,55 @@
Refactoring Plan for trading_pipeline.py
=======================================
Goal: Break down the large TradingPipeline class into smaller, dedicated modules for better maintainability and readability, while minimizing disruption to the existing E2E system.
Strategy:
1. **Keep `TradingPipeline` as the Orchestrator:**
* The main `TradingPipeline` class in `trading_pipeline.py` remains.
* Responsibilities:
* Load configuration (`__init__`).
* Initialize core components (`DataLoader`, `FeatureEngineer`, etc.).
* Manage overall state (instance variables like `df_raw`, `gru_model`, etc.).
* Run the main execution flow (`execute`), including the walk-forward loop.
* Call functions from new stage-specific modules.
2. **Create Stage-Specific Modules:**
* Create a new sub-directory: `src/pipeline_stages`.
* Move stage-specific logic from `TradingPipeline` methods into functions within these modules.
* Proposed Modules:
* `src/pipeline_stages/data_processing.py`: Loading, feature engineering, label generation, alignment, splitting.
* `src/pipeline_stages/feature_processing.py`: Scaling, feature selection, pruning.
* `src/pipeline_stages/sequence_creation.py`: GRU sequence creation.
* `src/pipeline_stages/modelling.py`: GRU/SAC training/loading, calibration, SAC aggregation.
* `src/pipeline_stages/evaluation.py`: Baseline checks, backtesting, results saving, metric aggregation, final decision.
3. **Refactor `TradingPipeline` Methods:**
* Simplify existing methods in `TradingPipeline` (e.g., `load_and_preprocess_data`, `engineer_features`).
* These methods will now primarily:
* Import the corresponding function from the `pipeline_stages` module.
* Call the imported function, passing necessary data and components (`config`, `data_loader`, `io`, state variables).
* Receive results and update the `TradingPipeline` instance's state.
4. **Data Flow:**
* Emphasize explicit data passing via function arguments and return values between stages.
* The `TradingPipeline.execute` method orchestrates this flow.
* State required across multiple stages/folds remains as `TradingPipeline` instance attributes.
5. **Dependencies:**
* Pass `config`, `io`, and component instances (`DataLoader`, `FeatureEngineer`, etc.) as arguments to the stage functions that need them.
Benefits:
* **Readability:** `trading_pipeline.py` becomes a clearer orchestrator.
* **Maintainability:** Easier to isolate and modify specific stages.
* **Testability:** Stage functions are potentially easier to unit test.
* **Reduced Risk:** Focuses on moving logic, minimizing E2E breakage compared to a full rewrite.
Implementation Steps:
1. Create the `src/pipeline_stages` directory and module files.
2. Incrementally move logic for each stage into the corresponding module's functions.
3. Update `TradingPipeline` methods to import and call these new functions.
4. Adjust imports and function signatures as needed.
5. Proceed stage by stage, verifying structure and data flow.
+55
View File
@@ -0,0 +1,55 @@
Below is a consolidated list of every “validation gate” we enforce in the pipeline—split by the **GRU** (prediction) stage and the **SAC** (positionsizing) stage. Each check either **aborts** the run (hardfail) or **warns** you that a noncritical gate didnt clear.
---
## GRUstage Validation Gates
| Gate # | Check | Data used | Threshold | Action on Fail |
|--------|----------------------------------------------------------------|-----------------------|------------------|--------------------|
| **G1** | **Raw binary LR** (Internal split) | train 80/20 split | CI LB ≥ `binary_ci_lb` (0.52) | **Abort** |
| **G2** | **Raw binary RF** (Internal split, optional) | train 80/20 split | CI LB ≥ `binary_rf_ci_lb` (0.54) | **Abort** (if enabled) |
| **G3** | **Raw ternary LR** (Internal split, if `use_ternary`) | train 80/20 split | CI LB ≥ `ternary_ci_lb` (0.40) | **Warn** |
| **G4** | **Raw ternary RF** (Internal split, optional) | train 80/20 split | CI LB ≥ `ternary_rf_ci_lb` (0.42) | **Warn** |
| **G5** | **Forwardfold binary LR** (True OOS, test fold t+1) | folds test set | CI LB ≥ `forward_ci_lb` (0.52) | **Abort** |
| **G6** | **Featureselection rebaseline** (postprune binary LR) | pruned train 80/20 | CI LB ≥ `binary_ci_lb` (0.52) | **Abort** |
| **G7** | **Calibration check** (edgefiltered p_cal on val split) | val split | CI LB ≥ `calibration_ci_lb` (0.55) | **Abort** |
> **Notes:**
> • G1 catches “no predictive signal” cheaply.
> • G5 ensures it actually *generalises* forward.
> • G7 makes sure your calibrated probabilities have real edge before SAC ever sees them.
---
## SACstage Validation Gates
| Gate # | Check | Data used | Threshold | Action on Fail |
|---------|-------------------------------------------------------------|-----------------------|---------------------|----------------------|
| **G8** | **Edgefiltered binary LR** | val split probabilities | CI LB ≥ `edge_binary_ci_lb` (0.60) | **Abort** |
| **G9** | **Edgefiltered binary RF** | val split probabilities | CI LB ≥ `edge_binary_rf_ci_lb` (0.62) | **Abort** |
| **G10** | **Edgefiltered ternary LR** (if `use_ternary`) | val split p_cat[:,2]p_cat[:,0] | CI LB ≥ `edge_ternary_ci_lb` (0.57) | **Warn** |
| **G11** | **Edgefiltered ternary RF** (if `use_ternary`) | val split p_cat[:,2]p_cat[:,0] | CI LB ≥ `edge_ternary_rf_ci_lb` (0.58) | **Warn** |
| **G12** | **Backtest performance** (Sharpe / Max DD on test fold) | aggregated test folds | Sharpe ≥ `backtest.sharpe_lb` (1.2)<br>Max DD ≤ `backtest.max_dd_ub` (15 %) | **Abort** if violated|
> **Notes:**
> • G8G9 gate the **highconfidence edge** you feed into SAC. If they fail, SAC will only ever go allin/flat, so we abort.
> • G10G11 warn you if your flat/nomove sizing is shaky—SAC can still run, but youll get a console warning suggesting you tweak your flat thresholds or features.
> • G12 validates final **livelike** performance; if you cant hit the Sharpe/Max DD targets on the unseen test folds, the entire run is considered a nogo.
---
### What to do on each pattern
1. **Any GRUabort gate (G1, G2, G5, G6, G7) fails** →
**Stop** before training. Improve features, horizon, calibration settings, or prune strategy.
2. **GRU passes but SACbinary edge gates (G8/G9) fail** →
**Stop** before SAC training. Means your probabilities have no reliable highconfidence edge—tweak calibration threshold or retrain GRU.
3. **GRU & SACbinary gates pass, but SACternary edge gates (G10/G11) warn** →
**Proceed** with a warning: consider adding flatspecific features or raising the `edge_threshold`.
4. **All gates pass** →
Full pipeline runs to completion: GRU training, SAC training, backtest, resulting in models, logs, and performance reports.
By strictly enforcing these gates, you ensure every GRU and SAC model you train has demonstrable, forwardtested edge—maximizing your chances of hitting that 65 % directional target in live trading.
+124
View File
@@ -0,0 +1,124 @@
1. Nested Cross-Validation (for GRU Hyperparameter Tuning)
Goal: To tune GRU hyperparameters (like gru_units, learning_rate, etc.) robustly for each main walk-forward fold, using only the training data allocated to that fold. This prevents hyperparameters from being influenced by data that will later appear in the fold's validation or test set.
Current Implementation: The hyperparameter_tuning.gru.sweep_enabled flag exists, but the tuning logic isn't currently nested within the fold processing loop in train_or_load_gru_fold.
Implementation Strategy:
Modify train_or_load_gru_fold (in gru_sac_predictor/src/pipeline_stages/modelling.py): This is the function responsible for training or loading the GRU for a specific outer walk-forward fold.
Check sweep_enabled: Inside this function, right before the actual GRU training would normally occur (i.e., if config['gru']['train_gru'] is true and a model isn't being loaded), check if config['hyperparameter_tuning']['gru']['sweep_enabled'] is also true.
Inner CV Loop: If sweep is enabled:
Data: Use the X_train_seq and y_train_seq_dict passed into this function (these represent the training data for the current outer fold).
Inner Splits: Use a time-series-appropriate splitter (like sklearn.model_selection.TimeSeriesSplit) on the sequence indices (train_indices_new if returned, otherwise derive from X_train_seq) to create, say, 3 or 5 inner train/validation splits within the outer fold's training data.
Optuna Study: Create a new Optuna study (or similar hyperparameter optimization framework) specific to this outer fold.
Objective Function: Define an Optuna objective function that takes a trial object:
It suggests hyperparameters based on config['hyperparameter_tuning']['gru']['search_space'].
It iterates through the inner CV splits. For each inner split:
Instantiate a temporary GRUModelHandler (or just the model) with the trial's hyperparameters.
Train the model on the inner training data slice.
Evaluate it on the inner validation data slice (e.g., calculate val_loss).
Return the average performance (e.g., average val_loss) across the inner splits.
Run Study: Execute study.optimize with the objective function and n_trials from the config.
Best Parameters: Retrieve the study.best_params after optimization.
Final Fold Training: Instantiate the GRUModelHandler (gru_handler passed into the function) or build the GRU model using these best_params. Train this single, final model for the outer fold on the entire X_train_seq and y_train_seq_dict.
Return: Return this optimally tuned GRU model and handler for the outer fold to proceed.
Configuration:
The existing hyperparameter_tuning.gru section is mostly sufficient.
You might add a key like inner_cv_splits: 3 to control the inner loop.
Considerations: This significantly increases computation time, as n_trials * inner_cv_splits models are trained per outer fold.
2. Gap and Regime-Aware Folds
Heres a minimal “wrapper” you can drop around your existing `_generate_walk_forward_folds` to get both gapaware **and** regimeaware filtering, without rewriting your core logic:
```python
def generate_filtered_folds(df, config):
# 1) Tag regimes once, right after loading & featureengineering the full dataset
if config['walk_forward']['regime']['enabled']:
df = add_regime_tags(
df,
indicator=config['walk_forward']['regime']['indicator'],
window=config['walk_forward']['regime']['indicator_params']['window'],
quantiles=config['walk_forward']['regime']['quantiles']
)
min_pct = config['walk_forward']['regime']['min_regime_representation_pct']
# 2) Split into contiguous chunks on data gaps
chunks = split_into_contiguous_chunks(
df,
config['walk_forward']['gap_threshold_minutes']
)
# 3) For each chunk, run your normal foldgenerator, then filter by regime
for chunk_start, chunk_end in chunks:
df_chunk = df.loc[chunk_start:chunk_end]
# skip tiny chunks
if (chunk_end - chunk_start).days < config['walk_forward'].get('min_chunk_days', 1):
continue
# your existing generator (rolling or block)—
# it yields tuples of (train_start, train_end, val_start, val_end, test_start, test_end)
for (t0, t1, v0, v1, e0, e1) in self._original_fold_generator(df_chunk, config):
# if regime gating is off, just yield
if not config['walk_forward']['regime']['enabled']:
yield (t0, t1, v0, v1, e0, e1)
continue
# 4) Check regime balance in each period
periods = {
'train': df_chunk.loc[t0:t1],
'val': df_chunk.loc[v0:v1],
'test': df_chunk.loc[e0:e1],
}
bad = False
for name, subdf in periods.items():
counts = subdf['regime_tag'].value_counts(normalize=True) * 100
# ensure every regime appears ≥ min_pct
for regime in sorted(df['regime_tag'].unique()):
pct = counts.get(regime, 0.0)
if pct < min_pct:
bad = True
break
if bad:
break
if bad:
# you can log which period/regime failed here
continue
# otherwise its a valid fold
yield (t0, t1, v0, v1, e0, e1)
```
### Explanation of the steps
1. **Regime Tagging**
- Run once, upfront: compute your volatility or trend indicator over the full series, cut it into quantile bins, and assign each row a `regime_tag` of 0/1/2.
2. **Gap Partitioning**
- Split the DataFrame into contiguous “chunks” wherever index gaps exceed your `gap_threshold_minutes`.
- This avoids forcing folds that straddle a hole in the data.
3. **Fold Generation (Unchanged)**
- Call your existing `_generate_walk_forward_folds` (rolling or block) on each contiguous chunk.
4. **RegimeBalance Filter**
- For each candidate fold, slice out the train/val/test segments, compute the fraction of each regime tag, and **skip** any fold where any regime appears below your `min_regime_representation_pct`.
---
#### Configuration sketch
```yaml
walk_forward:
# existing fields…
gap_threshold_minutes: 5
regime:
enabled: true
indicator: volatility
indicator_params:
window: 20
quantiles: [0.33, 0.66]
min_regime_representation_pct: 10
```
With this wrapper, you get:
- **Automatic split** at data outages > 5 min
- **Dynamic skip** of any timeslice folds that would be blind to a market regime (e.g. all highvol or all lowvol)
- **No changes** to your core split logic—just filter its outputs.