2025-04-20 17:52:49 +00:00

163 lines
5.4 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Below is a consolidated set of revision instructionsand the key code snippets youll needto 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!