From 09e3c962d0b5bdffeecb2f1157b69f56138aea6a Mon Sep 17 00:00:00 2001 From: yasha Date: Fri, 18 Apr 2025 17:34:41 +0000 Subject: [PATCH] Configure Git to only track essential project files --- gru_sac_predictor/.gitignore | 68 ++++-- .../__pycache__/__init__.cpython-310.pyc | Bin 160 -> 0 bytes .../__pycache__/main.cpython-310.pyc | Bin 11716 -> 0 bytes .../__pycache__/run.cpython-310.pyc | Bin 1252 -> 0 bytes gru_sac_predictor/docs/v3_changelog.md | 1 - .../src/__pycache__/__init__.cpython-310.pyc | Bin 164 -> 0 bytes .../__pycache__/backtester.cpython-310.pyc | Bin 13596 -> 0 bytes .../__pycache__/calibrator.cpython-310.pyc | Bin 6911 -> 0 bytes .../__pycache__/data_loader.cpython-310.pyc | Bin 13456 -> 0 bytes .../feature_engineer.cpython-310.pyc | Bin 12181 -> 0 bytes .../src/__pycache__/features.cpython-310.pyc | Bin 2912 -> 0 bytes .../gru_model_handler.cpython-310.pyc | Bin 7620 -> 0 bytes .../src/__pycache__/model_gru.cpython-310.pyc | Bin 2652 -> 0 bytes .../src/__pycache__/sac_agent.cpython-310.pyc | Bin 16827 -> 0 bytes .../__pycache__/sac_trainer.cpython-310.pyc | Bin 15525 -> 0 bytes .../__pycache__/trading_env.cpython-310.pyc | Bin 3778 -> 0 bytes gru_sac_predictor/src/utils/run_id.py | 60 ------ gru_sac_predictor/src/utils/running_stats.py | 144 ------------- gru_sac_predictor/tests/test_calibration.py | 183 ---------------- .../tests/test_feature_engineer.py | 125 ----------- .../tests/test_feature_pruning.py | 87 -------- gru_sac_predictor/tests/test_integration.py | 117 ---------- gru_sac_predictor/tests/test_labels.py | 201 ------------------ gru_sac_predictor/tests/test_leakage.py | 133 ------------ gru_sac_predictor/tests/test_metrics.py | 136 ------------ gru_sac_predictor/tests/test_model_shapes.py | 139 ------------ gru_sac_predictor/tests/test_sac_agent.py | 110 ---------- gru_sac_predictor/tests/test_sac_sanity.py | 121 ----------- gru_sac_predictor/tests/test_time_encoding.py | 94 -------- 29 files changed, 47 insertions(+), 1672 deletions(-) delete mode 100644 gru_sac_predictor/__pycache__/__init__.cpython-310.pyc delete mode 100644 gru_sac_predictor/__pycache__/main.cpython-310.pyc delete mode 100644 gru_sac_predictor/__pycache__/run.cpython-310.pyc delete mode 100644 gru_sac_predictor/docs/v3_changelog.md delete mode 100644 gru_sac_predictor/src/__pycache__/__init__.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/__pycache__/backtester.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/__pycache__/calibrator.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/__pycache__/data_loader.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/__pycache__/feature_engineer.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/__pycache__/features.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/__pycache__/gru_model_handler.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/__pycache__/model_gru.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/__pycache__/sac_agent.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/__pycache__/sac_trainer.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/__pycache__/trading_env.cpython-310.pyc delete mode 100644 gru_sac_predictor/src/utils/run_id.py delete mode 100644 gru_sac_predictor/src/utils/running_stats.py delete mode 100644 gru_sac_predictor/tests/test_calibration.py delete mode 100644 gru_sac_predictor/tests/test_feature_engineer.py delete mode 100644 gru_sac_predictor/tests/test_feature_pruning.py delete mode 100644 gru_sac_predictor/tests/test_integration.py delete mode 100644 gru_sac_predictor/tests/test_labels.py delete mode 100644 gru_sac_predictor/tests/test_leakage.py delete mode 100644 gru_sac_predictor/tests/test_metrics.py delete mode 100644 gru_sac_predictor/tests/test_model_shapes.py delete mode 100644 gru_sac_predictor/tests/test_sac_agent.py delete mode 100644 gru_sac_predictor/tests/test_sac_sanity.py delete mode 100644 gru_sac_predictor/tests/test_time_encoding.py diff --git a/gru_sac_predictor/.gitignore b/gru_sac_predictor/.gitignore index d15ca8a7..6fc1dff5 100644 --- a/gru_sac_predictor/.gitignore +++ b/gru_sac_predictor/.gitignore @@ -1,26 +1,52 @@ - # Python cache - __pycache__/ - *.py[cod] - *$py.class +# Ignore everything by default +* - # Virtual environment - .venv/ - venv/ - ENV/ +# Un-ignore specific files to track - # Data / Models / Results (if large or generated) - data/ - models/ - results/ - logs/ +# Scripts +!scripts/aggregate_metrics.py +!scripts/run_validation.sh - # IDE / Editor specific - .vscode/ - .idea/ - *.swp +# Package initialization +!__init__.py +!src/__init__.py - # OS specific - .DS_Store - Thumbs.db +# Core source files +!src/backtester.py +!src/calibrator_vector.py +!src/baseline_checker.py +!src/calibrator.py +!src/calibrate.py +!src/data_loader.py +!src/gru_hyper_tuner.py +!src/feature_engineer.py +!src/features.py +!src/gru_model_handler.py +!src/io_manager.py +!src/logger_setup.py +!src/metrics.py +!src/sac_agent.py +!src/sac_trainer.py +!src/trading_env.py +!src/trading_pipeline.py - cuda* \ No newline at end of file +# Configuration files +!config.yaml +!config_baseline.yaml + +# Documentation and logs +!README.md +!requirements.txt +!revisions.txt +!main_v7.log + +# Entry points +!run.py +!train_sac_runner.py + +# Git configuration +!.gitignore + +# Make sure parent directories are un-ignored for nesting to work +!src/ +!scripts/ \ No newline at end of file diff --git a/gru_sac_predictor/__pycache__/__init__.cpython-310.pyc b/gru_sac_predictor/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 7a6cb7279d282006f6af81fd4fe6f76276609f73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160 zcmd1j<>g`k0u5t^3`QXR7{oyaAVCKpE@lA|DGb33nv8xc8Hzx{2;!Hsenx(7s(xi+ zaYmwkN@`hZPJV%YdQoY7abj|OK~ZW-W^ze>5f+j7_{_Y_lK6PNg34PQHo5sJr8%i~ MAj6B9fCLKz075J!>Hq)$ diff --git a/gru_sac_predictor/__pycache__/main.cpython-310.pyc b/gru_sac_predictor/__pycache__/main.cpython-310.pyc deleted file mode 100644 index d57dfbae25cc0f7945d471a3bedd2750c89084a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11716 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 NHR-j~B8OZ3F-S diff --git a/gru_sac_predictor/__pycache__/run.cpython-310.pyc b/gru_sac_predictor/__pycache__/run.cpython-310.pyc deleted file mode 100644 index d25f30fc0c5844011f68ec6bf7842cb08eb276be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1252 zcmZ`%Piq@T6rWkGtc|=@>^OGGZPH$>gQQ+tN~kGmNeU*|1k){a8Ec-Tk@wFqGqR#R zltRz`1fgIb^OfeBQ=p%qA&@tt#5GO2%g*fk_j~X6-mAB^x&)uMPwDJsn~>LDxczwR z_xKq?2@yolf@JHPW;DoA76F~NuHtaEVdI)!)2ss>AoJD*)errBl|bj3Qqm+C6BT#L zZq}vblvH2F1U6ScQ<9}<-G?pFg7hW%EBX_?QHsM&u6m;F<4?&iSVNZnnpW-gy`)P5 zcg4neq<@EKzAZW;xun?+?9A_pJFqLL=!#8|UbYd>>$ZZfC+>dKUERMX*&gQF4!OGX zeb^H_m+SoZyiU-Q@b!1!_}(1#sY~^u%9T`5$O@W=kF~BflclP)mDQAev@kQ)QrT}= zbN;w83k}T9;DeG$!w`i{bX^9u;uxvgl z*9B6UGR(~jd8WD750UAnfHet}o87_SUk4aqd3xpOQS;mJO&C&yQUL;Zy1|1TF=Ig) zCKlS~IK!eX7|RZ71ke&QuyI{YpS}2 z=?;g!PszBDIoDIuJh*A`x^ZkGD`{ppnEn^8Wmd*TwQ&jh;=~QOSaCZ;YEoCcM2`H- z1Pu;wA`8fK*IfhU=;AiGQcwxE_sLQWjUMaBkACw>*u=1vj)H3|?7MX_ypY!Kh0lYq zy+b`+l(=)xd>sfK>d`LcLU<~{=2||q8%%+pi# Attention -> LayerNorm` structure.\n* **New Output Heads (Task 3.2):**\n * `dir3`: Dense(3, softmax) for ternary classification.\n * `mu`: Dense(1, linear) for return prediction.\n* **New Loss Configuration (Task 3.3):**\n * Uses `CategoricalFocalCrossentropy` for `dir3` and `Huber` for `mu`.\n * Loss weights configurable.\n* **Configurable Hyperparameters (Task 3.4):**\n * New `gru_v3` section in `config.yaml` exposes `gru_units`, `attention_units`, `learning_rate`, loss parameters (`focal_gamma`, `focal_label_smoothing`, `huber_delta`), and loss weights (`loss_weight_mu`, `loss_weight_dir3`).\n* **Model Selection (Task 3.5):**\n * Added `control.use_v3` (bool) flag to switch between GRU v2 and v3 logic within `GRUModelHandler`.\n\n### 4. Vector Scaling Calibration (`config.calibration`)\n\n* **New Calibrator (Task 4.1):**\n * Added `calibrator_vector.py` with `VectorCalibrator` class implementing vector scaling (optimizes diagonal matrix `W` and bias `b`).\n* **Method Selection (Task 4.2):**\n * Added `calibration.method` config option (`temperature` or `vector`). `TradingPipeline` routes to the appropriate calibrator.\n* **Parameter Handling (Task 4.3):**\n * `VectorCalibrator` saves/loads its parameters (`[W_diag, b]`) to `.npy` files.\n* **Logits Requirement:**\n * Vector scaling requires pre-softmax logits. Added `GRUModelHandler.predict_logits` method using an inference-only model view to retrieve these without altering the main model structure.\n\n### 5. SAC Stabilisation (`config.sac`, `config.environment`)\n\n* **Reward Scaling (Task 5.1):**\n * Environment reward is multiplied by a scaling factor.\n * Config: `environment.reward_scale` (float).\n* **State Normalization (Task 5.2):**\n * Added `utils.running_stats.MeanStdFilter`.\n * `SACTrainer` optionally normalizes environment states using this filter.\n * Config: `sac.use_state_filter` (bool).\n * Filter state is saved/loaded with agent checkpoints.\n* **Target Entropy Calculation (Task 5.3):**\n * `SACTradingAgent` automatically calculates target entropy as `-0.5 * log(4)` if `alpha_auto_tune` is true and the default `target_entropy` (`-action_dim`) is used.\n * Config: `sac.target_entropy` (float or null).\n* **Action Penalty (Task 5.4):**\n * Added quadratic penalty to the environment reward based on action magnitude.\n * Config: `environment.action_penalty_lambda` (float).\n* **Oracle Buffer Seeding (Task 5.5):**\n * `SACTrainer` can pre-populate a percentage of the replay buffer using a heuristic policy based on GRU predictions.\n * Config: `sac.oracle_seeding_pct` (float).\n* **Metadata Update (Task 5.6):**\n * `reward_scale` and `lambda` (action penalty) are now saved in `agent_metadata.json`.\n\n### 6. Metrics & Validation (`config.calibration`, `src/metrics.py`)\n\n* **Edge-Filtered Accuracy (Task 6.1):**\n * Added `metrics.edge_filtered_accuracy` function.\n* **Validation Check (Task 6.2):**\n * Added a check in `TradingPipeline` after calibration. Calculates edge-filtered accuracy on the validation set and computes the 95% CI lower bound.\n * Pipeline fails if CI lower bound < 0.60.\n* **Re-centred Sharpe Ratio (Task 6.3):**\n * Added `metrics.calculate_sharpe_ratio` function allowing custom benchmark return (defaults to 0).\n* **Backtester Reporting (Task 6.4):**\n * `Backtester` now calculates and saves edge-filtered accuracy and re-centred Sharpe ratio to the metrics file.\n\n## Configuration Summary\n\nSee the updated `config.yaml` for details on the following new/modified sections and parameters:\n\n* `data`: `vol_sampling`, `vol_window`, `vol_quantile`, `label_smoothing`\n* `gru`: `use_ternary`, `flat_sigma_multiplier`\n* `gru_v3`: (New section with architecture, training, and compilation parameters)\n* `calibration`: `method`\n* `sac`: `use_state_filter`, `target_entropy` (updated behaviour), `oracle_seeding_pct`\n* `environment`: `reward_scale`, `action_penalty_lambda`\n* `control`: `use_v3`\n\n*(Note: Some parameters under `gru` like epochs/batch_size/patience primarily apply when `control.use_v3` is false)*.\n \ No newline at end of file diff --git a/gru_sac_predictor/src/__pycache__/__init__.cpython-310.pyc b/gru_sac_predictor/src/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index c0103c9d8b510acff2bf436a97fc44b680ffd449..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 164 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 diff --git a/gru_sac_predictor/src/__pycache__/calibrator.cpython-310.pyc b/gru_sac_predictor/src/__pycache__/calibrator.cpython-310.pyc deleted file mode 100644 index e847f726440efdf3ec6361a18262a427d7b69238..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6911 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 diff --git a/gru_sac_predictor/src/__pycache__/data_loader.cpython-310.pyc b/gru_sac_predictor/src/__pycache__/data_loader.cpython-310.pyc deleted file mode 100644 index 47aec995c74cc42fca75b369ea194b59930f6c2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13456 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 X4X4HuD`081MWfySqYcuWQ8E4xl<;8w diff --git a/gru_sac_predictor/src/__pycache__/feature_engineer.cpython-310.pyc b/gru_sac_predictor/src/__pycache__/feature_engineer.cpython-310.pyc deleted file mode 100644 index 7123a32c05bd9f91b1be2123ae17dc32af3f25ff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12181 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 X4acugo7n)gieZ*(M(s}JX>0L+{*3C* diff --git a/gru_sac_predictor/src/__pycache__/gru_model_handler.cpython-310.pyc b/gru_sac_predictor/src/__pycache__/gru_model_handler.cpython-310.pyc deleted file mode 100644 index 0d7894cfda6954fefa7bd6a63f6a374d7b963435..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7620 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 diff --git a/gru_sac_predictor/src/__pycache__/model_gru.cpython-310.pyc b/gru_sac_predictor/src/__pycache__/model_gru.cpython-310.pyc deleted file mode 100644 index 20dbc1a580be2d73bdfa8cb2dc210025eb9f8f04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2652 zcmZuz&5s<#74Pb=`PkX@vL0-MiBxQe;$F-wSSW`L{vbw}D8`Dwk)mNqyQZslrq}&3 zR8=n?yGJtEaLOD=PKk2Bx$!?B#5EF1q})=cXmdb&ORk70d9P-6ZIEj;)vsQ?`gPTN z@As<9<>j^m?Ss$BXusn)|H8qe&4f7z~E>94>u$d2W!{Z-Fg}N5`Cd}AGNM(XIOD;0gN?4W+SaMUD zP_ezV7)GuMu2yDkb< zN>q)d;3kwpSF%W4+{>ah;2FaIGP>#4AZTZo)Pxi6a&P8syHj`SY3Eb7hM7;j+I>JC zxKkfwpZm4fbsmsuz=JyI;(Sp1wR;M5U8nX=I@1tzVI4jo;DWIFEpePHtWqjv1ueu7 zys#(s?|zb%Lz<;GMV5}rlGB}1(hUWUU~+?MCWk_+Ub{`9T^|XWqoZi5C{wD+1*f{C zcsy_pP0Au+S`_f-^qssqO94Ap=`d&SE+DSJJBveDx|K6M+PX2hL0Q2MVK6p_GusHV z4Jg|d<;GdN4-NaccJ(UkV!p3lk6NIu`4G3djS4u#ovYLM4%klVdD@XIx%1NW&bcYw z7PrP{UO4v=O5Y5;!Lknl|w$@@s(zUw%*|!e1N_zz)LcP ztx%X8q*O&}1NFr`2qw>8K)*Y zF4ATOlmQLUhx+>dW-FJA~YN2jS18cX0b%d|Xyc#V- z9n@}()Cz|lKgL&Q-n5~^-NtTnx5b~}YcrzTi}4aa4prz$eq!eFlYjE2O@4|$#hME5`b-BDui-~BUv4T_UEbM7oHST4@5 z+c3prCRwi5tu=k;ub+SQp`|eW+r6(oeD^WSUpib4r43dcwf(!lr?{G`EKQIHvdrnD zuXsEbOQ0m#;(UG``Cg`c{qPMumAEee9?NpDAQH%cPi3lvvXCZ7i*cnRSDu8-Fg_mc z;1Y@+BxFS7k3p8JsF-Kw;>Zbv;HaUDY4AA{yK3woiYq3u*kURLXR)kKY&RaHJI!sH%tsT#_=IM9b0xR1^Fh!zGc^Ca@`K!mHK~ zmiQ9AZ-SdbGPscuO~76KMl}#}UN}zvEG?LvypBG+EOEZjvV^+RNXmSiW+H0K9|DH_ z5dwuaIg}wI)0$@^c+7-(72~rBPKM%E4oqM}BVWaZKgEt7bvClcz86FcmU<4lwbmM0 z^G>*3(sfrnm#mTSzn#zzTr&TK-}<5a1sF!*yg)A_Fji@4B`lCuf_b}b1hspYRTT02%rPL<=oWOEFB97BaTDv4R_pW$A4;$b=1yJ>(;9qX@oW1vt0L a#03H>%dU>obWgdfqzS(cX@?!Ma{B*$4D;*& diff --git a/gru_sac_predictor/src/__pycache__/sac_agent.cpython-310.pyc b/gru_sac_predictor/src/__pycache__/sac_agent.cpython-310.pyc deleted file mode 100644 index 9a0098060831019671f43b6fcbbc5c9dab1b116e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16827 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 XRy#v6H#QEP&HwZXO+~NF)!zAksiiI- diff --git a/gru_sac_predictor/src/__pycache__/sac_trainer.cpython-310.pyc b/gru_sac_predictor/src/__pycache__/sac_trainer.cpython-310.pyc deleted file mode 100644 index c7adc6b864f23208962061b6b3ab3d041fc07b31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15525 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 g84q2_z?kW zy2tU3W)bpQhzmzfA=0jI{trkTIBaFK}p6YCI(Km2C`JuV<^(zT!u-tQmQa5&5&QK2xutX3f>?1M@yBzVa?0b6`F)O=DUbn`0v|#|)N@Em&)8!`fp9))~97F1%qp zN}iy84MF{yx`!{Q_ky6_t#a{jNnpgT_mLsIkF6uTM>G(~=J2nkVfcJ<{it4WJd(WJ zkzAf24?vrCx~2NX#&L~W=cB#Be^NsaKuog!6kfdj>F+*y)alNR{L;S64t|gU&2bW! zd%XRpgHImuA8`3FZ<9}|c24>F(uQvE+hG!KE1eR#%C^Jp+FRB`cEpk7aT+NZ7LvE? zteBtNJRCOotvX%i*0e`xIW>_}u-V|7WvEI@~JFYk5}SC0SZ6Pg|x**eptHXdvQ291pyIf@eP>nDl;1|m}$RoE!WgP=9)fk z{X^R&HadF};_lQ#<5{RJk%!H(c?9Ya_C@1WW8Z-sN z_l546>z$T5OO2~A6-)EV8A+8D>H^ihUXq37x2~yW>as%S47FR~c2PMR;0hXTbUgJ6 zwcB8)vhr%CL7}FJvT~*9OFU_gf+ETk6&*co4T#!YAqRfYmjq9s?%SvrUxR6}Rn}rH z6F;_U+7>hGpKY?wZP$Hnd!zHS+j;C^ZX;<^>FA(0@^ushak-(noVJQM-74)vM`*?# z8;9rl^-G%qt9=xy1LQ++(p`K%V;dbE1ygw4S&7~VyQbet(%b#9)g z(j^#dj@%yh)f&oAO&!V8adEVh_t2rBly-sUhUw!Tt;{T4cdFbvQzP2z=|!_5DC^b# z3k3orZi4YyfY1^Ep^275`v@@+R*d7orcvSuH8zDUoFi+Wkv{HvfGvT33TG(au7`fj zv3+C;_mXjrKiMPE`W^oeNXEvQo=TNTTn>nqCxH7GZ<7;w;ICz4%KF6m;V%A zTEe?;?&FO#yZt7$&|5|0w`|`ZH`EX-SQON5Ky!UPV&NnEW6TI^YL2~eW9-v%7W)`8`1ofRF3R3Bt|wctV)F=Uuei4?~_sCxU2w+JL} zL0697dAC0P%}aVY%oDl6+o^mw!T#DrlGeDu?AD+Hy$mcIm7QZxqpO>kE>tc_@aC{r zLX2kN*#j*WJ5qEtBJcHUB>dy|)4~l+0~)g(R(Vs#Z%o%PdTkcNQ*S&hb(r|*OqahM zLqVbY`oOlqM^^V*ud{SQCpEY|Xe;H1hPV1E=26*E66cyYv_e!Cjv9)%TkDn5iE$a# z&O)6GZ_=P;2s@?rbb^&nQXm70b+t-+7hr#I#-5xht5%*SL2shj)gnpuNflgAnqS!z z;L1M?kZhd8BJlw>&A z;8ehL*z09!m`=LgnZrQLm8`A~v-SQ#l&zmA##131SaKtLyae1{pdW%oq{`MI_e3U9@2u z26G|o&%yjJY{vcrE%>)>voDy#K4-4~e5Ji|kvCe`^r((sc|jnu2orVE;UM3uXXxhm z91Dwz<)ZTF5Cd#hO(5$%eV|b{@uE6}S}b2OsjBh)It15AR0^7&Z8V$)Q8KjZzv+_e AJ^%m! diff --git a/gru_sac_predictor/src/utils/run_id.py b/gru_sac_predictor/src/utils/run_id.py deleted file mode 100644 index 7210abb2..00000000 --- a/gru_sac_predictor/src/utils/run_id.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Utility function for generating unique run IDs. - -Ref: revisions.txt Task 0.2 -""" - -import datetime -import subprocess -import logging -import os - -logger = logging.getLogger(__name__) - -def get_git_sha(short: bool = True) -> str | None: - """Gets the current Git commit SHA (short or long).""" - try: - # Determine project root (assuming this file is in src/utils/) - script_dir = os.path.dirname(os.path.abspath(__file__)) - project_root = os.path.dirname(os.path.dirname(script_dir)) - - command = ['git', 'rev-parse'] - if short: - command.append('--short') - command.append('HEAD') - - result = subprocess.run(command, - capture_output=True, text=True, check=False, # Allow failure - cwd=project_root) - if result.returncode == 0: - return result.stdout.strip() - else: - logger.warning(f"Could not get Git SHA: {result.stderr.strip()}") - return None - except FileNotFoundError: - logger.warning("Git command not found. Cannot get Git SHA.") - return None - except Exception as e: - logger.warning(f"Error getting Git SHA: {e}") - return None - -def make_run_id() -> str: - """ - Generates a run ID string in the format: YYYYMMDD_HHMMSS_shortgit. - Falls back to just timestamp if Git SHA cannot be retrieved. - """ - timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - short_sha = get_git_sha(short=True) - - if short_sha: - run_id = f"{timestamp}_{short_sha}" - else: - logger.warning("Could not retrieve Git SHA, using timestamp only for run ID.") - run_id = timestamp - - logger.debug(f"Generated run ID: {run_id}") - return run_id - -# Example usage: -if __name__ == '__main__': - print(f"Example Run ID: {make_run_id()}") \ No newline at end of file diff --git a/gru_sac_predictor/src/utils/running_stats.py b/gru_sac_predictor/src/utils/running_stats.py deleted file mode 100644 index 224dc183..00000000 --- a/gru_sac_predictor/src/utils/running_stats.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Utility for calculating running mean and standard deviation. - -Used for observation normalization in RL environments. -Ref: revisions.txt Task 5.2 -Based on Welford's online algorithm. -""" - -import numpy as np - -class MeanStdFilter: - """ - Computes the mean and standard deviation of observations online. - Uses Welford's algorithm for numerical stability. - https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm - """ - def __init__(self, shape, epsilon=1e-4, clip=10.0): - """ - Initialize the filter. - - Args: - shape: Shape of the observations. - epsilon: Small value to avoid division by zero. - clip: Value to clip normalized observations to [-clip, clip]. - """ - self.mean = np.zeros(shape, dtype=np.float64) - self.var = np.ones(shape, dtype=np.float64) - self.count = epsilon # Initialize count slightly > 0 to avoid division by zero initially - self.epsilon = epsilon - self.clip = clip - - def __call__(self, x: np.ndarray, update: bool = True) -> np.ndarray: - """ - Update the running stats and return the normalized observation. - - Args: - x: Input observation (or batch of observations). - update: Whether to update the running mean/std statistics. - - Returns: - Normalized observation(s). - """ - x = np.asarray(x, dtype=np.float64) - original_shape = x.shape - - # Handle batch input (flatten batch dim, keep feature dim) - if len(original_shape) > len(self.mean.shape): - batch_size = original_shape[0] - x_flat = x.reshape(batch_size, -1) - else: - batch_size = 1 - x_flat = x.reshape(1, -1) - - if update: - # Welford's algorithm update steps - for i in range(batch_size): - self.count += 1 - delta = x_flat[i] - self.mean - self.mean += delta / self.count - delta2 = x_flat[i] - self.mean # New delta using updated mean - # M2 is the sum of squares of differences from the *current* mean - # M2 = self.var * (self.count -1) # Previous M2 approx - M2 = self.var * (self.count - 1) if self.count > 1 else np.zeros_like(self.var) - M2 += delta * delta2 - self.var = M2 / self.count if self.count > 0 else np.ones_like(self.var) - # Ensure variance is non-negative - self.var = np.maximum(self.var, 0.0) - - # Normalize the observation(s) - std_dev = np.sqrt(self.var + self.epsilon) - normalized_x_flat = (x_flat - self.mean) / std_dev - - # Clip the normalized observations - normalized_x_flat = np.clip(normalized_x_flat, -self.clip, self.clip) - - # Reshape back to original input shape (potentially excluding batch dim if single input) - if len(original_shape) > len(self.mean.shape): - normalized_x = normalized_x_flat.reshape(original_shape) - else: - normalized_x = normalized_x_flat.reshape(self.mean.shape) # Reshape to feature shape - - return normalized_x.astype(np.float32) # Return as float32 for TF/PyTorch - - @property - def std(self) -> np.ndarray: - """Returns the current standard deviation.""" - return np.sqrt(self.var + self.epsilon) - - def get_state(self) -> dict: - """Returns the internal state for saving.""" - return { - 'mean': self.mean, - 'var': self.var, - 'count': self.count - } - - def set_state(self, state: dict) -> None: - """Loads the internal state from a dictionary.""" - self.mean = state.get('mean', self.mean) - self.var = state.get('var', self.var) - self.count = state.get('count', self.count) - -# Example usage: -if __name__ == '__main__': - obs_shape = (5,) - running_filter = MeanStdFilter(shape=obs_shape) - - print("Initial Mean:", running_filter.mean) - print("Initial Var:", running_filter.var) - print("Initial Count:", running_filter.count) - - # Simulate some observations - observations = [] - for _ in range(100): - obs = np.random.randn(*obs_shape) * np.array([1, 2, 0.5, 10, 0.1]) + np.array([0, -1, 0.5, 5, 1]) - observations.append(obs) - norm_obs = running_filter(obs, update=True) - # print(f"Raw: {obs.round(2)}, Norm: {norm_obs.round(2)}") - - print("\nAfter 100 updates:") - print("Final Mean:", running_filter.mean.round(3)) - print("Final Var:", running_filter.var.round(3)) - print("Final Std:", running_filter.std.round(3)) - print("Final Count:", running_filter.count) - - # Test normalization without update - test_obs = np.array([0.5, -0.5, 0.6, 6.0, 0.9]) - norm_test_obs = running_filter(test_obs, update=False) - print("\nTest Obs Raw:", test_obs) - print("Test Obs Norm:", norm_test_obs.round(3)) - - # Test batch normalization - batch_obs = np.array(observations[-5:]) # Last 5 observations - norm_batch = running_filter(batch_obs, update=False) - print("\nBatch Obs Raw Shape:", batch_obs.shape) - print("Batch Obs Norm Shape:", norm_batch.shape) - print("Last Norm Batch Obs:", norm_batch[-1].round(3)) - - # Test state saving/loading - state = running_filter.get_state() - new_filter = MeanStdFilter(shape=obs_shape) - new_filter.set_state(state) - print("\nLoaded Filter Mean:", new_filter.mean.round(3)) - assert np.allclose(running_filter.mean, new_filter.mean) \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_calibration.py b/gru_sac_predictor/tests/test_calibration.py deleted file mode 100644 index 2634b72f..00000000 --- a/gru_sac_predictor/tests/test_calibration.py +++ /dev/null @@ -1,183 +0,0 @@ -""" -Tests for probability calibration (Sec 6 of revisions.txt). -""" -import pytest -import numpy as np -from scipy.stats import binomtest -from scipy.special import logit, expit -import os - -# Try to import the modules; skip tests if not found (e.g., path issues) -try: - from gru_sac_predictor.src import calibrate -except ImportError: - calibrate = None - -# --- Import VectorCalibrator (Task 4) --- # -try: - from gru_sac_predictor.src.calibrator_vector import VectorCalibrator -except ImportError: - VectorCalibrator = None -# --- End Import --- # - -# --- Helper Function for ECE --- # -def _calculate_ece(probs: np.ndarray, y_true: np.ndarray, n_bins: int = 10) -> float: - """ - Calculates the Expected Calibration Error (ECE). - - Args: - probs (np.ndarray): Predicted probabilities for the positive class (N,) or all classes (N, K). - y_true (np.ndarray): True labels (0 or 1 for binary, or class index for multi-class). - n_bins (int): Number of bins to divide probabilities into. - - Returns: - float: The calculated ECE score. - """ - if len(probs.shape) == 1: # Binary case - p_max = probs - y_pred_class = (probs > 0.5).astype(int) - y_true_class = y_true - elif len(probs.shape) == 2: # Multi-class case - p_max = np.max(probs, axis=1) - y_pred_class = np.argmax(probs, axis=1) - # If y_true is one-hot, convert to class index - if len(y_true.shape) == 2 and y_true.shape[1] > 1: - y_true_class = np.argmax(y_true, axis=1) - else: - y_true_class = y_true # Assume already class index - else: - raise ValueError("probs array must be 1D or 2D") - - ece = 0.0 - bin_boundaries = np.linspace(0, 1, n_bins + 1) - - for i in range(n_bins): - in_bin = (p_max > bin_boundaries[i]) & (p_max <= bin_boundaries[i+1]) - prop_in_bin = np.mean(in_bin) - - if prop_in_bin > 0: - accuracy_in_bin = np.mean(y_pred_class[in_bin] == y_true_class[in_bin]) - avg_confidence_in_bin = np.mean(p_max[in_bin]) - ece += np.abs(accuracy_in_bin - avg_confidence_in_bin) * prop_in_bin - - return ece -# --- End ECE Helper --- # - -# --- Fixtures --- -@pytest.fixture(scope="module") -def calibration_data(): - """ - Generate sample raw probabilities and true outcomes. - Simulates an overconfident model (T_implied < 1) where true probability drifts. - """ - np.random.seed(42) - n_samples = 2500 - # Simulate drifting true probability centered around 0.5 - drift = 0.05 * np.sin(np.linspace(0, 3 * np.pi, n_samples)) - true_prob = np.clip(0.5 + drift + np.random.randn(n_samples) * 0.05, 0.05, 0.95) - # Simulate overconfidence (implied T ~ 0.7) - raw_logits = logit(true_prob) / 0.7 - p_raw = expit(raw_logits) - # Generate true outcomes - y_true = (np.random.rand(n_samples) < true_prob).astype(int) - return p_raw, y_true - -# --- Tests --- -@pytest.mark.skipif(calibrate is None, reason="Module gru_sac_predictor.src.calibrate not found") -def test_optimise_temperature(calibration_data): - """Check if optimise_temperature runs and returns a plausible value.""" - p_raw, y_true = calibration_data - optimal_T = calibrate.optimise_temperature(p_raw, y_true) - print(f"\nOptimised T: {optimal_T:.4f}") - # Expect T > 0. A T near 0.7 would undo the simulated effect. - assert optimal_T > 0.1 and optimal_T < 5.0, "Optimised temperature seems out of expected range." - -@pytest.mark.skipif(calibrate is None, reason="Module gru_sac_predictor.src.calibrate not found") -def test_calibration_hit_rate_threshold(calibration_data): - """ - Verify that the lower 95% CI of the hit-rate for non-zero calibrated - signals is >= 0.55 (using the module's EDGE_THR). - """ - p_raw, y_true = calibration_data - optimal_T = calibrate.optimise_temperature(p_raw, y_true) - p_cal = calibrate.calibrate(p_raw, optimal_T) - action_signals = calibrate.action_signal(p_cal) - - # Filter for non-zero signals - non_zero_idx = action_signals != 0 - if not np.any(non_zero_idx): - pytest.fail("No non-zero action signals generated for hit-rate test.") - - signals_taken = action_signals[non_zero_idx] - actual_direction = y_true[non_zero_idx] - - # Hit: signal matches actual direction (1 vs 1, -1 vs 0) - hits = np.sum((signals_taken == 1) & (actual_direction == 1)) + \ - np.sum((signals_taken == -1) & (actual_direction == 0)) - total_trades = len(signals_taken) - - if total_trades < 30: - pytest.skip(f"Insufficient non-zero signals ({total_trades}) for reliable CI.") - - # Calculate 95% lower CI using binomial test - try: - # Ensure hits is integer - hits = int(hits) - result = binomtest(hits, total_trades, p=0.5, alternative='greater') - lower_ci = result.proportion_ci(confidence_level=0.95).low - except Exception as e: - pytest.fail(f"Binomial test failed: {e}") - - hit_rate = hits / total_trades - required_threshold = calibrate.EDGE_THR # Use threshold from module - - print(f"\nCalibration Test: EDGE_THR={required_threshold:.3f}") - print(f" Trades={total_trades}, Hits={hits}, Hit Rate={hit_rate:.4f}") - print(f" 95% Lower CI: {lower_ci:.4f}") - - assert lower_ci >= required_threshold, \ - f"Hit rate lower CI ({lower_ci:.4f}) is below module threshold ({required_threshold:.3f})" - -# --- Vector Scaling Test (Task 4.4) --- # -@pytest.mark.skipif(VectorCalibrator is None, reason="VectorCalibrator not found") -def test_vector_scaling_calibration(): - """Check if Vector Scaling reduces ECE on sample multi-class data.""" - np.random.seed(123) - n_samples = 5000 - num_classes = 3 - - # Simulate slightly miscalibrated logits (e.g., too peaky or too flat) - # True distribution is uniform-ish - true_labels = np.random.randint(0, num_classes, n_samples) - y_onehot = tf.keras.utils.to_categorical(true_labels, num_classes=num_classes) - - # Generate logits - make class 1 slightly more likely, and make logits "peaky" - logits_raw = np.random.randn(n_samples, num_classes) * 0.5 # Base noise - logits_raw[:, 1] += 0.5 # Bias towards class 1 - # Add systematic miscalibration (e.g., scale up logits -> overconfidence) - logits_miscalibrated = logits_raw * 1.8 - - # Instantiate calibrator - vector_cal = VectorCalibrator() - - # Calculate ECE before calibration - probs_uncal = vector_cal._softmax(logits_miscalibrated) - ece_before = _calculate_ece(probs_uncal, true_labels) - - # Fit vector scaling - vector_cal.fit(logits_miscalibrated, y_onehot) - assert vector_cal.W is not None and vector_cal.b is not None, "Vector scaling fit failed" - - # Calibrate probabilities - probs_cal = vector_cal.calibrate(logits_miscalibrated) - - # Calculate ECE after calibration - ece_after = _calculate_ece(probs_cal, true_labels) - - print(f"\nVector Scaling Test: ECE Before = {ece_before:.4f}, ECE After = {ece_after:.4f}") - - # Assert that ECE improved (decreased) - # Allow for slight numerical noise, but expect significant improvement - assert ece_after < ece_before * 0.7, f"ECE did not improve significantly after Vector Scaling (Before: {ece_before:.4f}, After: {ece_after:.4f})" - # Assert ECE is reasonably low after calibration - assert ece_after < 0.05, f"ECE after Vector Scaling ({ece_after:.4f}) is higher than expected (< 0.05)" \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_feature_engineer.py b/gru_sac_predictor/tests/test_feature_engineer.py deleted file mode 100644 index cc6ccf3b..00000000 --- a/gru_sac_predictor/tests/test_feature_engineer.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -Tests for the FeatureEngineer class and its methods. - -Ref: revisions.txt Task 2.5 -""" - -import pytest -import pandas as pd -import numpy as np -import sys, os -from unittest.mock import patch, MagicMock - -# --- Add path for src imports --- # -script_dir = os.path.dirname(os.path.abspath(__file__)) -project_root = os.path.dirname(script_dir) -src_path = os.path.join(project_root, 'src') -if src_path not in sys.path: - sys.path.insert(0, src_path) -# --- End Add path --- # - -from feature_engineer import FeatureEngineer -# Import minimal_whitelist from features to pass to constructor -from features import minimal_whitelist as base_minimal_whitelist - -# --- Fixtures --- # - -@pytest.fixture -def sample_engineer() -> FeatureEngineer: - """Provides a FeatureEngineer instance with a basic whitelist.""" - # Use a copy to avoid modifying the original during tests - test_whitelist = base_minimal_whitelist.copy() - return FeatureEngineer(minimal_whitelist=test_whitelist) - -@pytest.fixture -def sample_feature_data() -> pd.DataFrame: - """Creates sample features for testing selection.""" - np.random.seed(42) - data = { - 'return_1m': np.random.randn(100) * 0.01, - 'EMA_50': 100 + np.random.randn(100).cumsum() * 0.1, - 'ATR_14': np.random.rand(100) * 0.5, - 'hour_sin': np.sin(np.linspace(0, 2 * np.pi, 100)), - 'highly_correlated_1': 100 + np.random.randn(100).cumsum() * 0.1, # Copy EMA_50 roughly - 'highly_correlated_2': 101 + np.random.randn(100).cumsum() * 0.1, # Copy EMA_50 roughly - 'constant_feat': np.ones(100), - 'nan_feat': np.full(100, np.nan), - 'inf_feat': np.full(100, np.inf) - } - index = pd.date_range(start='2023-01-01', periods=100, freq='min', tz='UTC') - df = pd.DataFrame(data, index=index) - # Add the correlation - df['highly_correlated_1'] = df['EMA_50'] * (1 + np.random.randn(100) * 0.01) - df['highly_correlated_2'] = df['highly_correlated_1'] * (1 + np.random.randn(100) * 0.01) - return df - -@pytest.fixture -def sample_target_data() -> pd.Series: - """Creates sample binary target variable.""" - np.random.seed(123) - # Create somewhat predictable target based on EMA_50 trend - ema = 100 + np.random.randn(100).cumsum() * 0.1 - target = (np.diff(ema, prepend=0) > 0).astype(int) - index = pd.date_range(start='2023-01-01', periods=100, freq='min', tz='UTC') - return pd.Series(target, index=index) - -# --- Tests --- # - -def test_select_features_vif_skip(sample_engineer, sample_feature_data, sample_target_data): - """ - Test 2.5: Assert VIF calculation is skipped if skip_vif=True in config. - We need to mock the config access within select_features. - """ - engineer = sample_engineer - X_train = sample_feature_data - y_train = sample_target_data - - # Mock the config dictionary that would be passed or accessed - # For now, assume select_features might take an optional config or we patch where it reads it. - # Since it doesn't currently take config, we have to modify the method or mock dependencies. - # Let's *assume* for this test that select_features *will be* modified to check a config. - # We will patch the VIF function itself and assert it's not called. - - # Add a feature that would definitely be removed by VIF to ensure the check matters - X_train['perfectly_correlated'] = X_train['EMA_50'] * 2 - - with patch('feature_engineer.variance_inflation_factor') as mock_vif: - # We also need to mock the SelectFromModel part to return *some* features initially - with patch('feature_engineer.SelectFromModel') as mock_select_from_model: - # Configure the mock selector to return a subset of features including correlated ones - mock_instance = MagicMock() - initial_selection = [True] * 5 + [False] * 4 + [True] # Select first 5 + perfectly_correlated - mock_instance.get_support.return_value = np.array(initial_selection) - mock_select_from_model.return_value = mock_instance - - # Call select_features - **modify it conceptually to accept skip_vif** - # Since we can't modify the source directly here, we test by asserting VIF wasn't called. - # This implicitly tests the skip logic. - - # Simulate the call as if skip_vif=True was passed/checked internally - # Patch the VIF calculation call site directly - with patch('feature_engineer.sm.add_constant') as mock_add_constant: # VIF loop uses this - # Call the function normally - the patch on VIF itself is the key - selected_features = engineer.select_features(X_train, y_train) - - # Assert that variance_inflation_factor was NOT called - mock_vif.assert_not_called() - # Assert that add_constant (used within VIF loop) was also NOT called - mock_add_constant.assert_not_called() - - # Assert that the features returned are those from the mocked L1 selection - # (potentially plus minimal whitelist, depending on implementation) - # The exact output depends on how L1 + whitelist are combined *before* VIF step - # Let's just assert the correlated feature IS included, as VIF didn't remove it - assert 'perfectly_correlated' in selected_features - - # We should also check that the log message indicating VIF skip was printed - # (This requires capturing logs, omitted here for brevity) - -# TODO: Add more tests for FeatureEngineer -# - Test feature calculation methods (_add_cyclical_features, _add_imbalance_features, _add_ta_features) -# - Test add_base_features orchestration -# - Test select_features VIF logic *when enabled* (e.g., check correlated feature is removed) -# - Test select_features LogReg L1 logic (e.g., check constant feature is removed) -# - Test handling of NaNs/Infs in select_features -# - Test prune_features (although covered in test_feature_pruning.py) \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_feature_pruning.py b/gru_sac_predictor/tests/test_feature_pruning.py deleted file mode 100644 index 89c6141a..00000000 --- a/gru_sac_predictor/tests/test_feature_pruning.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Tests for feature pruning logic. - -Ref: revisions.txt Step 1-D -""" -import pytest -import pandas as pd - -# TODO: Import prune_features function and minimal_whitelist from src.features -# from gru_sac_predictor.src.features import prune_features, minimal_whitelist - -# Mock minimal_whitelist for testing if import fails -minimal_whitelist = ['feat_a', 'feat_b', 'feat_c', 'hour_sin'] - -# Mock prune_features if import fails -def prune_features(df: pd.DataFrame, whitelist: list[str] | None = None) -> pd.DataFrame: - if whitelist is None: - whitelist = minimal_whitelist - cols_to_keep = [c for c in whitelist if c in df.columns] - df_pruned = df[cols_to_keep].copy() - assert set(df_pruned.columns) == set(cols_to_keep), \ - f"Pruning failed: Output columns {set(df_pruned.columns)} != Expected intersection {set(cols_to_keep)}" - return df_pruned - - -@pytest.fixture -def sample_dataframe() -> pd.DataFrame: - """Create a sample DataFrame for testing.""" - data = { - 'feat_a': [1, 2, 3], - 'feat_b': [4, 5, 6], - 'feat_extra': [7, 8, 9], - 'hour_sin': [0.1, 0.2, 0.3] - } - return pd.DataFrame(data) - - -def test_prune_to_minimal_whitelist(sample_dataframe): - """Test pruning to the default minimal whitelist.""" - df_pruned = prune_features(sample_dataframe, whitelist=minimal_whitelist) - - expected_cols = {'feat_a', 'feat_b', 'hour_sin'} - assert set(df_pruned.columns) == expected_cols - assert 'feat_extra' not in df_pruned.columns - -def test_prune_with_custom_whitelist(sample_dataframe): - """Test pruning with a custom whitelist.""" - custom_whitelist = ['feat_a', 'feat_extra'] - df_pruned = prune_features(sample_dataframe, whitelist=custom_whitelist) - - expected_cols = {'feat_a', 'feat_extra'} - assert set(df_pruned.columns) == expected_cols - assert 'feat_b' not in df_pruned.columns - assert 'hour_sin' not in df_pruned.columns - -def test_prune_missing_whitelist_cols(sample_dataframe): - """Test when whitelist contains columns not in the dataframe.""" - custom_whitelist = ['feat_a', 'feat_c', 'hour_sin'] # feat_c is not in sample_dataframe - df_pruned = prune_features(sample_dataframe, whitelist=custom_whitelist) - - expected_cols = {'feat_a', 'hour_sin'} # Only existing columns are kept - assert set(df_pruned.columns) == expected_cols - assert 'feat_c' not in df_pruned.columns - -def test_prune_empty_whitelist(): - """Test pruning with an empty whitelist.""" - df = pd.DataFrame({'a': [1], 'b': [2]}) - df_pruned = prune_features(df, whitelist=[]) - assert df_pruned.empty - assert df_pruned.columns.empty - -def test_prune_empty_dataframe(): - """Test pruning an empty dataframe.""" - df = pd.DataFrame() - df_pruned = prune_features(df, whitelist=minimal_whitelist) - assert df_pruned.empty - assert df_pruned.columns.empty - -def test_prune_assertion(sample_dataframe): - """Verify the assertion within prune_features catches mismatches (requires mocking or specific setup).""" - # This test might be tricky without modifying the function or using complex mocks. - # The assertion `assert set(df_pruned.columns) == set(cols_to_keep)` should generally hold - # if the logic `df_pruned = df[cols_to_keep].copy()` is correct. - # We rely on the other tests implicitly covering this assertion. - pytest.skip("Assertion test might require specific mocking setup.") - -# Add tests for edge cases like DataFrames with duplicate column names if relevant. \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_integration.py b/gru_sac_predictor/tests/test_integration.py deleted file mode 100644 index 3f95759f..00000000 --- a/gru_sac_predictor/tests/test_integration.py +++ /dev/null @@ -1,117 +0,0 @@ -""" -Integration tests for cross-module interactions. -""" -import pytest -import os -import numpy as np -import tempfile -import json - -# Try to import the module; skip tests if not found -try: - from gru_sac_predictor.src import sac_agent - import tensorflow as tf # Needed for agent init/load -except ImportError: - sac_agent = None - tf = None - -@pytest.fixture -def sac_agent_for_integration(): - """Provides a basic SAC agent instance.""" - if sac_agent is None or tf is None: - pytest.skip("SAC Agent module or TF not found.") - # Use minimal params for saving/loading tests - agent = sac_agent.SACTradingAgent( - state_dim=5, action_dim=1, - buffer_capacity=100, min_buffer_size=10 - ) - # Build models - try: - agent.actor(tf.zeros((1, 5))) - agent.critic1([tf.zeros((1, 5)), tf.zeros((1, 1))]) - agent.critic2([tf.zeros((1, 5)), tf.zeros((1, 1))]) - agent.update_target_networks(tau=1.0) - except Exception as e: - pytest.fail(f"Failed to build agent models: {e}") - return agent - -@pytest.mark.skipif(sac_agent is None or tf is None, reason="SAC Agent module or TF not found") -def test_save_load_metadata(sac_agent_for_integration): - """Test if metadata is saved and loaded correctly.""" - agent = sac_agent_for_integration - with tempfile.TemporaryDirectory() as tmpdir: - save_path = os.path.join(tmpdir, "sac_test_save") - agent.save(save_path) - - # Check if metadata file exists - meta_path = os.path.join(save_path, 'agent_metadata.json') - assert os.path.exists(meta_path), "Metadata file was not saved." - - # Create a new agent and load - new_agent = sac_agent.SACTradingAgent(state_dim=5, action_dim=1) - loaded_meta = new_agent.load(save_path) - - assert isinstance(loaded_meta, dict), "Load method did not return a dict." - assert loaded_meta.get('state_dim') == 5, "Loaded state_dim incorrect." - assert loaded_meta.get('action_dim') == 1, "Loaded action_dim incorrect." - # Check alpha status (default is auto_tune=True) - assert loaded_meta.get('log_alpha_saved') == True, "log_alpha status incorrect." - -@pytest.mark.skipif(sac_agent is None or tf is None, reason="SAC Agent module or TF not found") -def test_replay_buffer_purge_on_change(sac_agent_for_integration): - """ - Simulate loading an agent where the edge_threshold has changed - and verify the buffer is cleared. - """ - agent_to_save = sac_agent_for_integration - original_edge_thr = 0.55 - agent_to_save.edge_threshold_config = original_edge_thr # Manually set for saving - - with tempfile.TemporaryDirectory() as tmpdir: - save_path = os.path.join(tmpdir, "sac_purge_test") - - # 1. Save agent with original threshold in metadata - agent_to_save.save(save_path) - meta_path = os.path.join(save_path, 'agent_metadata.json') - assert os.path.exists(meta_path) - with open(meta_path, 'r') as f: - saved_meta = json.load(f) - assert saved_meta.get('edge_threshold_config') == original_edge_thr - - # 2. Create a new agent instance to load into - new_agent = sac_agent.SACTradingAgent( - state_dim=5, action_dim=1, - buffer_capacity=100, min_buffer_size=10 - ) - # Build models for the new agent - try: - new_agent.actor(tf.zeros((1, 5))) - new_agent.critic1([tf.zeros((1, 5)), tf.zeros((1, 1))]) - new_agent.critic2([tf.zeros((1, 5)), tf.zeros((1, 1))]) - new_agent.update_target_networks(tau=1.0) - except Exception as e: - pytest.fail(f"Failed to build new agent models: {e}") - - # Add dummy data to the *new* agent's buffer *before* loading - for _ in range(20): - dummy_state = np.random.rand(5).astype(np.float32) - dummy_action = np.random.rand(1).astype(np.float32) - new_agent.buffer.add(dummy_state, dummy_action, 0.0, dummy_state, 0.0) - assert len(new_agent.buffer) == 20, "Buffer should have data before load." - - # 3. Simulate loading with a *different* current edge threshold config - current_config_edge_thr = 0.60 - assert abs(current_config_edge_thr - original_edge_thr) > 1e-6 - - loaded_meta = new_agent.load(save_path) - saved_edge_thr = loaded_meta.get('edge_threshold_config') - - # 4. Perform the check and clear if needed (simulating pipeline logic) - if saved_edge_thr is not None and abs(saved_edge_thr - current_config_edge_thr) > 1e-6: - print(f"\nEdge threshold mismatch detected (Saved={saved_edge_thr}, Current={current_config_edge_thr}). Clearing buffer.") - new_agent.clear_buffer() - else: - print(f"\nEdge threshold match or not saved. Buffer not cleared.") - - # 5. Assert buffer is now empty - assert len(new_agent.buffer) == 0, "Buffer was not cleared after edge threshold mismatch." \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_labels.py b/gru_sac_predictor/tests/test_labels.py deleted file mode 100644 index 48456d9a..00000000 --- a/gru_sac_predictor/tests/test_labels.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -Tests for label generation and potential leakage. - -Ref: revisions.txt Step 1-A, 1.4 -""" -import pytest -import pandas as pd -import numpy as np -import sys, os - -# --- Add path for src imports --- # -# Assuming tests is one level down from the package root -script_dir = os.path.dirname(os.path.abspath(__file__)) -project_root = os.path.dirname(script_dir) # Go up one level -src_path = os.path.join(project_root, 'src') -if src_path not in sys.path: - sys.path.insert(0, src_path) -# --- End Add path --- # - -# Import the function to test -from trading_pipeline import _generate_direction_labels - -# --- Fixtures --- # -@pytest.fixture -def sample_close_data() -> pd.DataFrame: - """Creates a sample DataFrame with close prices and DatetimeIndex.""" - # Generate data with some variation - np.random.seed(42) - prices = 100 + np.cumsum(np.random.randn(200) * 0.5) - data = {'close': prices} - index = pd.date_range(start='2023-01-01', periods=len(data['close']), freq='min', tz='UTC') - df = pd.DataFrame(data, index=index) - return df - -@pytest.fixture -def sample_config() -> dict: - """Provides a basic config dictionary.""" - return { - 'gru': { - 'prediction_horizon': 5, - 'use_ternary': False, - 'flat_sigma_multiplier': 0.25 - }, - 'data': { - 'label_smoothing': 0.0 - } - } - -# --- Tests --- # - -def test_lookahead_bias(sample_close_data, sample_config): - """ - Test 1.4.a: Verify labels don't depend on information *beyond* the prediction horizon. - Strategy: Modify future close prices (beyond horizon) and check if labels change. - """ - df = sample_close_data - config = sample_config - horizon = config['gru']['prediction_horizon'] - - # Generate baseline labels (binary) - df_labeled_base, label_col_base = _generate_direction_labels(df.copy(), config) - - # Modify close prices far into the future (beyond the horizon needed for any label) - df_modified = df.copy() - future_index = len(df) - 1 # Index of the last point - modify_point = future_index - horizon - 5 # Index well beyond the last needed future price - if modify_point > 0: - df_modified.iloc[modify_point:, df_modified.columns.get_loc('close')] *= 1.5 # Modify future prices - - # Generate labels with modified future data - df_labeled_mod, label_col_mod = _generate_direction_labels(df_modified.copy(), config) - - # Align based on index (label function drops NaNs at the end) - common_index = df_labeled_base.index.intersection(df_labeled_mod.index) - labels_base_aligned = df_labeled_base.loc[common_index, label_col_base] - labels_mod_aligned = df_labeled_mod.loc[common_index, label_col_mod] - - # Assert: Labels should be identical, as modification was beyond the horizon - pd.testing.assert_series_equal(labels_base_aligned, labels_mod_aligned, check_names=False) - - # --- Repeat for Ternary --- # - config['gru']['use_ternary'] = True - df_labeled_base_t, label_col_base_t = _generate_direction_labels(df.copy(), config) - df_labeled_mod_t, label_col_mod_t = _generate_direction_labels(df_modified.copy(), config) - - common_index_t = df_labeled_base_t.index.intersection(df_labeled_mod_t.index) - labels_base_aligned_t = df_labeled_base_t.loc[common_index_t, label_col_base_t] - labels_mod_aligned_t = df_labeled_mod_t.loc[common_index_t, label_col_mod_t] - - # Assert: Ternary labels should also be identical - # Need careful comparison for list/array column - assert labels_base_aligned_t.equals(labels_mod_aligned_t) - -def test_binary_label_distribution(sample_close_data, sample_config): - """ - Test 1.4.b: Check binary label distribution has >= 5% in each class. - """ - df = sample_close_data - config = sample_config - config['gru']['use_ternary'] = False - config['data']['label_smoothing'] = 0.0 # Ensure hard binary for this test - - df_labeled, label_col = _generate_direction_labels(df.copy(), config) - - assert not df_labeled.empty, "Label generation resulted in empty DataFrame" - assert label_col in df_labeled.columns, f"Label column '{label_col}' not found" - - labels = df_labeled[label_col] - counts = labels.value_counts(normalize=True) - - assert len(counts) == 2, f"Expected 2 binary classes, found {len(counts)}" - assert counts.min() >= 0.05, f"Minimum binary class proportion ({counts.min():.2%}) is less than 5%" - print(f"\nBinary Dist: {counts.to_dict()}") # Print for info - -def test_soft_binary_label_distribution(sample_close_data, sample_config): - """ - Test 1.4.b: Check soft binary label distribution has >= 5% in each effective class. - """ - df = sample_close_data - config = sample_config - config['gru']['use_ternary'] = False - config['data']['label_smoothing'] = 0.2 # Example smoothing - smoothing = config['data']['label_smoothing'] - low_label = smoothing / 2.0 - high_label = 1.0 - smoothing / 2.0 - - df_labeled, label_col = _generate_direction_labels(df.copy(), config) - - assert not df_labeled.empty, "Label generation resulted in empty DataFrame" - assert label_col in df_labeled.columns, f"Label column '{label_col}' not found" - - labels = df_labeled[label_col] - counts = labels.value_counts(normalize=True) - - assert len(counts) == 2, f"Expected 2 soft binary classes, found {len(counts)}" - assert counts.min() >= 0.05, f"Minimum soft binary class proportion ({counts.min():.2%}) is less than 5%" - assert low_label in counts.index, f"Low label {low_label} not found in counts" - assert high_label in counts.index, f"High label {high_label} not found in counts" - print(f"\nSoft Binary Dist: {counts.to_dict()}") - -def test_ternary_label_distribution(sample_close_data, sample_config): - """ - Test 1.4.b: Check ternary label distribution (flat=[0.15, 0.45], others >= 0.10). - Uses default k=0.25. - """ - df = sample_close_data - config = sample_config - config['gru']['use_ternary'] = True - k = config['gru']['flat_sigma_multiplier'] # Should be 0.25 from fixture - - df_labeled, label_col = _generate_direction_labels(df.copy(), config) - - assert not df_labeled.empty, "Label generation resulted in empty DataFrame" - assert label_col in df_labeled.columns, f"Label column '{label_col}' not found" - - # Decode one-hot labels back to ordinal for distribution check - labels_one_hot = np.stack(df_labeled[label_col].values) - assert labels_one_hot.shape[1] == 3, "Ternary labels should have 3 columns" - ordinal_labels = np.argmax(labels_one_hot, axis=1) - - counts = np.bincount(ordinal_labels, minlength=3) - total = len(ordinal_labels) - dist_pct = counts / total * 100 - - print(f"\nTernary Dist (k={k}): Down={dist_pct[0]:.1f}%, Flat={dist_pct[1]:.1f}%, Up={dist_pct[2]:.1f}%") - - # Check constraints based on design doc / implementation - assert 15.0 <= dist_pct[1] <= 45.0, f"Flat class ({dist_pct[1]:.1f}%) out of expected range [15%, 45%] for k={k}" - assert dist_pct[0] >= 10.0, f"Down class ({dist_pct[0]:.1f}%) is less than 10% (check impl threshold)" - assert dist_pct[2] >= 10.0, f"Up class ({dist_pct[2]:.1f}%) is less than 10% (check impl threshold)" - -# --- Old Tests (Keep or Remove?) --- -# The original tests checked 'future_close', which is related but not the final label. -# We can keep test_future_close_shift as it verifies the shift logic used internally. -# The NaN test is less relevant now as the main function handles NaN dropping. - -def test_future_close_shift(sample_close_data): - """Verify that 'future_close' is correctly shifted and has NaNs at the end.""" - df = sample_close_data - horizon = 5 # Example horizon - - # Apply the logic directly for testing the shift itself - df['future_close'] = df['close'].shift(-horizon) - df['fwd_log_ret'] = np.log(df['future_close'] / df['close']) - - # Assertions - # 1. Check for correct shift in fwd_log_ret - # The first valid fwd_log_ret depends on close[0] and close[horizon] - assert pd.notna(df['fwd_log_ret'].iloc[0]) - # The last valid fwd_log_ret depends on close[end-horizon-1] and close[end-1] - assert pd.notna(df['fwd_log_ret'].iloc[len(df) - horizon - 1]) - - # 2. Check for NaNs at the end due to shift - assert pd.isna(df['fwd_log_ret'].iloc[-horizon:]).all() - assert pd.notna(df['fwd_log_ret'].iloc[:-horizon]).all() - -# def test_no_nan_in_future_close_output(): -# """Unit test to ensure no unexpected NaNs in the output of label creation (specific to the function).""" -# # Setup similar to above, potentially call the actual DataLoader/label function -# # Assert pd.notna(output_df['future_close'][:-horizon]).all() -# pytest.skip("Test covered by NaN dropping in _generate_direction_labels and its tests.") \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_leakage.py b/gru_sac_predictor/tests/test_leakage.py deleted file mode 100644 index f96d3860..00000000 --- a/gru_sac_predictor/tests/test_leakage.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -Tests for data leakage (Sec 6 of revisions.txt). -""" -import pytest -import pandas as pd -import numpy as np - -# Assume test data is loaded via fixtures later -@pytest.fixture(scope="module") -def sample_data_for_leakage(): - """ - Provides sample features and target for leakage tests. - Includes correctly shifted features, a feature with direct leakage, - and a rolling feature calculated correctly vs incorrectly. - """ - np.random.seed(43) - dates = pd.date_range(start='2023-01-01', periods=500, freq='T') - n = len(dates) - df = pd.DataFrame(index=dates) - df['noise'] = np.random.randn(n) - df['close'] = 100 + np.cumsum(df['noise'] * 0.1) - df['y_ret'] = np.log(df['close'].shift(-1) / df['close']) - - # --- Features --- - # OK: Based on past noise - df['feature_ok_past_noise'] = df['noise'].shift(1) - # OK: Rolling mean on correctly shifted past data - df['feature_ok_rolling_shifted'] = df['noise'].shift(1).rolling(10).mean() - # LEAKY: Uses future return directly - df['feature_leaky_direct'] = df['y_ret'] - # LEAKY: Rolling mean calculated *before* shifting target relationship - df['feature_leaky_rolling_unaligned'] = df['close'].rolling(5).mean() - - # Drop rows with NaNs from shifts/rolls AND the last row where y_ret is NaN - df.dropna(inplace=True) - - # Define features and target for the test - y_target = df['y_ret'] - features_df = df.drop(columns=['close', 'y_ret', 'noise']) # Exclude raw data used for generation - - return features_df, y_target - -@pytest.mark.parametrize("leakage_threshold", [0.02]) -def test_feature_leakage_correlation(sample_data_for_leakage, leakage_threshold): - """ - Verify that no feature has correlation > threshold with the correctly shifted target. - """ - features_df, y_target = sample_data_for_leakage - - max_abs_corr = 0.0 - leaky_col = "None" - all_corrs = {} - - print(f"\nTesting {features_df.shape[1]} features for leakage (threshold={leakage_threshold})...") - for col in features_df.columns: - if pd.api.types.is_numeric_dtype(features_df[col]): - # Handle potential NaNs introduced by feature engineering (though fixture avoids it) - temp_df = pd.concat([features_df[col], y_target], axis=1).dropna() - if len(temp_df) < 0.5 * len(features_df): - print(f" Skipping {col} due to excessive NaNs after merging with target.") - continue - - correlation = temp_df[col].corr(temp_df['y_ret']) - all_corrs[col] = correlation - # print(f" Corr({col}, y_ret): {correlation:.4f}") - if abs(correlation) > max_abs_corr: - max_abs_corr = abs(correlation) - leaky_col = col - else: - print(f" Skipping non-numeric column: {col}") - - print(f"Correlations found: { {k: round(v, 4) for k, v in all_corrs.items()} }") - print(f"Maximum absolute correlation found: {max_abs_corr:.4f} (feature: {leaky_col})") - - assert max_abs_corr < leakage_threshold, \ - f"Feature '{leaky_col}' has correlation {max_abs_corr:.4f} > threshold {leakage_threshold}, suggesting leakage." - -@pytest.mark.skipif(features is None, reason="Module gru_sac_predictor.src.features not found") -def test_ta_feature_leakage(sample_data_for_leakage, leakage_threshold=0.02): - """ - Specifically test TA features (EMA, MACD etc.) for leakage. - Ensures they were calculated on shifted data. - """ - features_df, y_target = sample_data_for_leakage - # Add TA features using the helper (simulating pipeline) - # We need OHLC in the input df for add_ta_features - # Recreate a df with shifted OHLC + other features for TA calc - np.random.seed(43) # Ensure consistent data with primary fixture - dates = pd.date_range(start='2023-01-01', periods=500, freq='T') - n = len(dates) - df_ohlc = pd.DataFrame(index=dates) - df_ohlc['close'] = 100 + np.cumsum(np.random.randn(n) * 0.1) - df_ohlc['open'] = df_ohlc['close'].shift(1) * (1 + np.random.randn(n) * 0.001) - df_ohlc['high'] = df_ohlc[['open','close']].max(axis=1) * (1 + np.random.rand(n) * 0.001) - df_ohlc['low'] = df_ohlc[['open','close']].min(axis=1) * (1 - np.random.rand(n) * 0.001) - df_ohlc['volume'] = np.random.rand(n) * 1000 - - # IMPORTANT: Shift before calculating TA features - df_shifted_ohlc = df_ohlc.shift(1) - df_ta = features.add_ta_features(df_shifted_ohlc) - - # Align with the target (requires original non-shifted index) - df_ta = df_ta.loc[y_target.index] - - ta_features_to_test = [col for col in features.minimal_whitelist if col in df_ta.columns and col not in ["return_1m", "return_15m", "return_60m", "hour_sin", "hour_cos"]] - max_abs_corr = 0.0 - leaky_col = "None" - all_corrs = {} - - print(f"\nTesting {len(ta_features_to_test)} TA features for leakage (threshold={leakage_threshold})...") - print(f" Features: {ta_features_to_test}") - - for col in ta_features_to_test: - if pd.api.types.is_numeric_dtype(df_ta[col]): - temp_df = pd.concat([df_ta[col], y_target], axis=1).dropna() - if len(temp_df) < 0.5 * len(y_target): - print(f" Skipping {col} due to excessive NaNs after merging.") - continue - correlation = temp_df[col].corr(temp_df['y_ret']) - all_corrs[col] = correlation - if abs(correlation) > max_abs_corr: - max_abs_corr = abs(correlation) - leaky_col = col - else: - print(f" Skipping non-numeric TA column: {col}") - - print(f"TA Feature Correlations: { {k: round(v, 4) for k, v in all_corrs.items()} }") - print(f"Maximum absolute TA correlation found: {max_abs_corr:.4f} (feature: {leaky_col})") - - assert max_abs_corr < leakage_threshold, \ - f"TA Feature '{leaky_col}' has correlation {max_abs_corr:.4f} > threshold {leakage_threshold}, suggesting leakage from TA calculation." - -# test_label_timing is usually covered by the correlation test, so removed for brevity. \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_metrics.py b/gru_sac_predictor/tests/test_metrics.py deleted file mode 100644 index 5e17e182..00000000 --- a/gru_sac_predictor/tests/test_metrics.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Tests for custom metric functions. - -Ref: revisions.txt Task 6.5 -""" - -import pytest -import numpy as np -import pandas as pd -import sys, os - -# --- Add path for src imports --- # -script_dir = os.path.dirname(os.path.abspath(__file__)) -project_root = os.path.dirname(script_dir) -src_path = os.path.join(project_root, 'src') -if src_path not in sys.path: - sys.path.insert(0, src_path) -# --- End Add path --- # - -from metrics import edge_filtered_accuracy, calculate_sharpe_ratio - -# --- Tests for edge_filtered_accuracy --- # - -def test_edge_filtered_accuracy_basic(): - """Test basic functionality with hard labels and clear edge.""" - y_true = np.array([1, 0, 1, 0, 1, 1, 0, 0]) - p_cal = np.array([0.9, 0.1, 0.8, 0.2, 0.7, 0.6, 0.3, 0.4]) # Edge > 0.1 for all - thr = 0.1 - - accuracy, n_filtered = edge_filtered_accuracy(y_true, p_cal, thr=thr) - - assert n_filtered == 8 - # Predictions: 1, 0, 1, 0, 1, 1, 0, 0. All correct. - assert accuracy == pytest.approx(1.0) - -def test_edge_filtered_accuracy_thresholding(): - """Test that the threshold correctly filters samples.""" - y_true = np.array([1, 0, 1, 0, 1, 1, 0, 0]) - p_cal = np.array([0.9, 0.1, 0.8, 0.2, 0.51, 0.49, 0.55, 0.45]) # Edge: 0.8, 0.8, 0.6, 0.6, 0.02, 0.02, 0.1, 0.1 - - # Test with thr=0.15 (should exclude last 4 samples) - thr1 = 0.15 - accuracy1, n_filtered1 = edge_filtered_accuracy(y_true, p_cal, thr=thr1) - assert n_filtered1 == 4 - # Predictions on first 4: 1, 0, 1, 0. All correct. - assert accuracy1 == pytest.approx(1.0) - - # Test with thr=0.05 (should include all but middle 2) - thr2 = 0.05 - accuracy2, n_filtered2 = edge_filtered_accuracy(y_true, p_cal, thr=thr2) - assert n_filtered2 == 6 - # Included: 1,0,1,0, 1, 0. Correct: 1,0,1,0, ?, ?. Preds: 1,0,1,0, 1, 0. 6/6 correct. - assert accuracy2 == pytest.approx(1.0) - -def test_edge_filtered_accuracy_soft_labels(): - """Test with soft labels.""" - y_true_soft = np.array([0.9, 0.1, 0.8, 0.2, 0.7, 0.6]) # Soft labels - p_cal = np.array([0.8, 0.3, 0.9, 0.1, 0.6, 0.7]) # All edge > 0.1 - thr = 0.1 - - accuracy, n_filtered = edge_filtered_accuracy(y_true_soft, p_cal, thr=thr) - - assert n_filtered == 6 - # y_true_hard: 1, 0, 1, 0, 1, 1 - # y_pred : 1, 0, 1, 0, 1, 1. All correct. - assert accuracy == pytest.approx(1.0) - -def test_edge_filtered_accuracy_no_samples(): - """Test case where no samples meet the edge threshold.""" - y_true = np.array([1, 0, 1, 0]) - p_cal = np.array([0.51, 0.49, 0.52, 0.48]) # All edge < 0.1 - thr = 0.1 - - accuracy, n_filtered = edge_filtered_accuracy(y_true, p_cal, thr=thr) - assert n_filtered == 0 - assert np.isnan(accuracy) - -def test_edge_filtered_accuracy_empty_input(): - """Test with empty input arrays.""" - y_true = np.array([]) - p_cal = np.array([]) - thr = 0.1 - - accuracy, n_filtered = edge_filtered_accuracy(y_true, p_cal, thr=thr) - assert n_filtered == 0 - assert np.isnan(accuracy) - -# --- Tests for calculate_sharpe_ratio --- # - -def test_calculate_sharpe_ratio_basic(): - """Test basic Sharpe calculation.""" - returns = pd.Series([0.01, -0.005, 0.02, 0.005, -0.01]) - # mean = 0.004, std = 0.01166, Sharpe_period = 0.343 - # Annualized (252) = 0.343 * sqrt(252) = 5.44 - expected_sharpe = 5.44441 - sharpe = calculate_sharpe_ratio(returns, benchmark_return=0.0, annualization_factor=252) - assert sharpe == pytest.approx(expected_sharpe, abs=1e-4) - -def test_calculate_sharpe_ratio_different_annualization(): - """Test Sharpe with different annualization factor.""" - returns = pd.Series([0.01, -0.005, 0.02, 0.005, -0.01]) - # Annualized (52) = 0.343 * sqrt(52) = 2.47 - expected_sharpe = 2.4738 - sharpe = calculate_sharpe_ratio(returns, benchmark_return=0.0, annualization_factor=52) - assert sharpe == pytest.approx(expected_sharpe, abs=1e-4) - -def test_calculate_sharpe_ratio_with_benchmark(): - """Test Sharpe with a non-zero benchmark return.""" - returns = pd.Series([0.01, -0.005, 0.02, 0.005, -0.01]) # mean=0.004 - benchmark = 0.001 # Per period - # excess mean = 0.003, std = 0.01166, Sharpe_period = 0.257 - # Annualized (252) = 0.257 * sqrt(252) = 4.08 - expected_sharpe = 4.0833 - sharpe = calculate_sharpe_ratio(returns, benchmark_return=benchmark, annualization_factor=252) - assert sharpe == pytest.approx(expected_sharpe, abs=1e-4) - -def test_calculate_sharpe_ratio_zero_std(): - """Test Sharpe when returns have zero standard deviation.""" - returns_positive = pd.Series([0.01, 0.01, 0.01]) - returns_negative = pd.Series([-0.01, -0.01, -0.01]) - returns_zero = pd.Series([0.0, 0.0, 0.0]) - - assert calculate_sharpe_ratio(returns_positive) == 0.0 # Positive mean, zero std -> 0? - # assert calculate_sharpe_ratio(returns_negative) == -np.inf # Negative mean, zero std -> -inf? - assert calculate_sharpe_ratio(returns_zero) == 0.0 - - # Let's refine zero std handling based on function's logic - # Function returns 0 if mean>0, -inf if mean<0, 0 if mean=0 - assert calculate_sharpe_ratio(returns_positive) == 0.0 - assert calculate_sharpe_ratio(returns_negative) == -np.inf - assert calculate_sharpe_ratio(returns_zero) == 0.0 - -def test_calculate_sharpe_ratio_empty_or_nan(): - """Test Sharpe with empty or all-NaN input.""" - assert np.isnan(calculate_sharpe_ratio(pd.Series([], dtype=float))) - assert np.isnan(calculate_sharpe_ratio(pd.Series([np.nan, np.nan], dtype=float))) \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_model_shapes.py b/gru_sac_predictor/tests/test_model_shapes.py deleted file mode 100644 index 6616a2ca..00000000 --- a/gru_sac_predictor/tests/test_model_shapes.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -Tests for GRU model input/output shapes. - -Ref: revisions.txt Task 3.6 -""" -import pytest -import numpy as np -import sys, os - -# --- Add path for src imports --- # -script_dir = os.path.dirname(os.path.abspath(__file__)) -project_root = os.path.dirname(script_dir) -src_path = os.path.join(project_root, 'src') -if src_path not in sys.path: - sys.path.insert(0, src_path) -# --- End Add path --- # - -# Import the v3 model builder -from model_gru_v3 import build_gru_model_v3 -# TODO: Import v2 model builder if needed for comparison tests -# from model_gru import build_gru_model - -# --- Constants for Testing --- # -LOOKBACK = 60 -N_FEATURES = 25 -BATCH_SIZE = 4 - -# --- Tests --- # - -def test_gru_v3_output_shapes(): - """Verify the output shapes of the GRU v3 model heads.""" - print(f"\nBuilding GRU v3 model for shape test...") - # Build the v3 model with default parameters - model = build_gru_model_v3(lookback=LOOKBACK, n_features=N_FEATURES) - assert model is not None, "Failed to build GRU v3 model" - - # Check number of outputs - assert len(model.outputs) == 2, f"Expected 2 outputs, got {len(model.outputs)}" - - # Check output names and shapes - # Output order in the model definition was [mu, dir3] - mu_output_shape = model.outputs[0].shape.as_list() - dir3_output_shape = model.outputs[1].shape.as_list() - - # Assert shapes (ignoring batch size None) - # mu head should be (None, 1) - assert mu_output_shape == [None, 1], f"Expected mu shape [None, 1], got {mu_output_shape}" - # dir3 head should be (None, 3) - assert dir3_output_shape == [None, 3], f"Expected dir3 shape [None, 3], got {dir3_output_shape}" - - print("GRU v3 output shapes test passed.") - -def test_gru_v3_prediction_shapes(): - """Verify the prediction shapes match the output shapes for a sample batch.""" - model = build_gru_model_v3(lookback=LOOKBACK, n_features=N_FEATURES) - assert model is not None, "Failed to build GRU v3 model" - - # Create dummy input data - dummy_input = np.random.rand(BATCH_SIZE, LOOKBACK, N_FEATURES) - - # Generate predictions - predictions = model.predict(dummy_input) - - # Check prediction structure and shapes - assert isinstance(predictions, list), "Predictions should be a list for multi-output model" - assert len(predictions) == 2, f"Expected 2 prediction arrays, got {len(predictions)}" - - # Predictions order should match model.outputs order [mu, dir3] - mu_preds = predictions[0] - dir3_preds = predictions[1] - - # Assert prediction shapes match expected batch size - assert mu_preds.shape == (BATCH_SIZE, 1), f"Expected mu prediction shape ({BATCH_SIZE}, 1), got {mu_preds.shape}" - assert dir3_preds.shape == (BATCH_SIZE, 3), f"Expected dir3 prediction shape ({BATCH_SIZE}, 3), got {dir3_preds.shape}" - - print("GRU v3 prediction shapes test passed.") - -# TODO: Add tests for GRU v2 model shapes if it's still relevant. - -def test_logits_view_shapes(): - """Test that softmax applied to predict_logits output matches predict output.""" - print(f"\nBuilding GRU v3 model for logits view test...") - model = build_gru_model_v3(lookback=LOOKBACK, n_features=N_FEATURES) - assert model is not None, "Failed to build GRU v3 model" - - # --- Requires GRUModelHandler to run predict_logits --- # - # We need to instantiate the handler to test its methods. - # Mock config and directories needed for handler init. - mock_config = { - 'control': {'use_v3': True}, - 'gru_v3': {} # Use defaults for building - } - mock_run_id = "test_logits_run" - mock_models_dir = "./mock_models/test_logits_run" - os.makedirs(mock_models_dir, exist_ok=True) # Create mock dir - - # Import handler locally for test setup - from gru_model_handler import GRUModelHandler - handler = GRUModelHandler(run_id=mock_run_id, models_dir=mock_models_dir, config=mock_config) - handler.model = model # Assign the already built model to the handler - handler.model_version_used = 'v3' # Set version manually - # --- End Handler Setup --- # - - # Create dummy input data - dummy_input = np.random.rand(BATCH_SIZE, LOOKBACK, N_FEATURES).astype(np.float32) - - # Generate predictions using both methods - logits = handler.predict_logits(dummy_input) - predictions = handler.predict(dummy_input) - - assert logits is not None, "predict_logits returned None" - assert predictions is not None, "predict returned None" - assert isinstance(predictions, list) and len(predictions) == 2, "predict output structure incorrect" - - probs_from_predict = predictions[1] # dir3 is the second output - - # Apply softmax to logits - # Use tf.nn.softmax for consistency with Keras backend - import tensorflow as tf - probs_from_logits = tf.nn.softmax(logits).numpy() - - # Assert shapes match first - assert probs_from_logits.shape == probs_from_predict.shape, \ - f"Shape mismatch: softmax(logits)={probs_from_logits.shape}, predict_probs={probs_from_predict.shape}" - - # Assert values are close - np.testing.assert_allclose( - probs_from_logits, - probs_from_predict, - rtol=1e-6, - atol=1e-6, # Use tighter tolerance for numerical precision check - err_msg="Softmax applied to logits does not match probability output from model.predict()" - ) - - print("Logits view test passed.") - # Clean up mock directory - import shutil - if os.path.exists("./mock_models"): - shutil.rmtree("./mock_models") \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_sac_agent.py b/gru_sac_predictor/tests/test_sac_agent.py deleted file mode 100644 index 9ffd96d0..00000000 --- a/gru_sac_predictor/tests/test_sac_agent.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Tests for the SACTradingAgent class. - -Ref: revisions.txt Task 5.7 -""" -import pytest -import numpy as np -import tensorflow as tf -import sys, os - -# --- Add path for src imports --- # -script_dir = os.path.dirname(os.path.abspath(__file__)) -project_root = os.path.dirname(script_dir) -src_path = os.path.join(project_root, 'src') -if src_path not in sys.path: - sys.path.insert(0, src_path) -# --- End Add path --- # - -from sac_agent import SACTradingAgent - -# --- Constants --- # -STATE_DIM = 5 -ACTION_DIM = 1 -BUFFER_SIZE = 5000 -MIN_BUFFER = 1000 -TRAIN_STEPS = 1500 # Number of training steps for the test -BATCH_SIZE = 64 - -# --- Fixtures --- # - -@pytest.fixture -def sac_agent_fixture() -> SACTradingAgent: - """Provides a default SACTradingAgent instance for testing.""" - agent = SACTradingAgent( - state_dim=STATE_DIM, - action_dim=ACTION_DIM, - buffer_capacity=BUFFER_SIZE, - min_buffer_size=MIN_BUFFER, - alpha_auto_tune=True, # Enable auto-tuning for realistic test - target_entropy=-1.0 * ACTION_DIM # Default target entropy - ) - return agent - -def _populate_buffer(agent: SACTradingAgent, num_samples: int): - """Helper to add random transitions to the agent's buffer.""" - print(f"\nPopulating buffer with {num_samples} random samples...") - for _ in range(num_samples): - state = np.random.randn(STATE_DIM).astype(np.float32) - action = np.random.uniform(-1, 1, size=(ACTION_DIM,)).astype(np.float32) - reward = np.random.randn() - next_state = np.random.randn(STATE_DIM).astype(np.float32) - done = float(np.random.rand() < 0.05) # 5% chance of done - agent.buffer.add(state, action, reward, next_state, done) - print(f"Buffer populated. Size: {len(agent.buffer)}") - -# --- Tests --- # - -def test_sac_training_updates(sac_agent_fixture): - """ - Test 5.7: Run training steps and check for basic health: - a) Q-values are not NaN. - b) Action variance is reasonable (suggests exploration). - """ - agent = sac_agent_fixture - # Populate buffer sufficiently to start training - _populate_buffer(agent, MIN_BUFFER + BATCH_SIZE) - - print(f"\nRunning {TRAIN_STEPS} training steps...") - metrics_history = [] - for i in range(TRAIN_STEPS): - metrics = agent.train(batch_size=BATCH_SIZE) - if metrics: # Train only runs if buffer is full enough - metrics_history.append(metrics) - # Basic check within the loop to fail fast - if i % 100 == 0 and metrics: - assert not np.isnan(metrics['critic1_loss']), f"Critic1 loss is NaN at step {i}" - assert not np.isnan(metrics['critic2_loss']), f"Critic2 loss is NaN at step {i}" - assert not np.isnan(metrics['actor_loss']), f"Actor loss is NaN at step {i}" - if agent.alpha_auto_tune: - assert not np.isnan(metrics['alpha_loss']), f"Alpha loss is NaN at step {i}" - - assert len(metrics_history) > 0, "Training loop did not execute (buffer size issue?)" - print(f"Training steps completed. Last metrics: {metrics_history[-1]}") - - # a) Check final Q-values (indirectly via loss) - last_metrics = metrics_history[-1] - assert not np.isnan(last_metrics['critic1_loss']), "Final Critic1 loss is NaN" - assert not np.isnan(last_metrics['critic2_loss']), "Final Critic2 loss is NaN" - # We assume if losses are not NaN, Q-values involved are also not NaN - print("Check a) Passed: Q-value losses are not NaN.") - - # b) Check action variance after training - num_samples_for_variance = 500 - sampled_actions = [] - dummy_state = np.random.randn(STATE_DIM).astype(np.float32) - for _ in range(num_samples_for_variance): - # Sample non-deterministically to check stochastic policy variance - action = agent.get_action(dummy_state, deterministic=False) - sampled_actions.append(action) - - sampled_actions = np.array(sampled_actions) - action_variance = np.var(sampled_actions, axis=0) - print(f"Action variance after {TRAIN_STEPS} steps: {action_variance}") - - # Check if variance is above a threshold (e.g., 0.2 from revisions.txt) - # This threshold might need tuning based on action space scaling (-1 to 1) - min_variance_threshold = 0.2 - assert np.all(action_variance > min_variance_threshold), \ - f"Action variance ({action_variance}) is below threshold ({min_variance_threshold}). Exploration might be too low." - print(f"Check b) Passed: Action variance ({action_variance.round(3)}) > {min_variance_threshold}.") \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_sac_sanity.py b/gru_sac_predictor/tests/test_sac_sanity.py deleted file mode 100644 index 8d44bf67..00000000 --- a/gru_sac_predictor/tests/test_sac_sanity.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -Sanity checks for the SAC agent (Sec 6 of revisions.txt). -""" -import pytest -import numpy as np -import os - -# Try to import the agent; skip tests if not found -try: - from gru_sac_predictor.src import sac_agent - # Need TF for tensor conversion if testing agent directly - import tensorflow as tf -except ImportError: - sac_agent = None - tf = None - -# --- Fixtures --- -@pytest.fixture(scope="module") -def sac_agent_instance(): - """ - Provides a default SAC agent instance for testing. - Uses standard parameters suitable for basic checks. - """ - if sac_agent is None: - pytest.skip("SAC Agent module not found.") - # Use default params, state_dim=5 as per revisions - # Use fixed seeds for reproducibility in tests if needed inside agent - agent = sac_agent.SACTradingAgent( - state_dim=5, action_dim=1, - initial_lr=1e-4, # Use a common LR for test simplicity - buffer_capacity=1000, # Smaller buffer for testing - min_buffer_size=100, - target_entropy=-1.0 - ) - # Build the models eagerly - try: - agent.actor(tf.zeros((1, 5))) - agent.critic1([tf.zeros((1, 5)), tf.zeros((1, 1))]) - agent.critic2([tf.zeros((1, 5)), tf.zeros((1, 1))]) - # Copy weights to target networks - agent.update_target_networks(tau=1.0) - except Exception as e: - pytest.fail(f"Failed to build SAC agent models: {e}") - return agent - -@pytest.fixture(scope="module") -def sample_sac_inputs(): - """ - Generate sample states and corresponding directional signals. - Simulates states with varying edge and signal-to-noise. - """ - np.random.seed(44) - n_samples = 1500 - # Simulate GRU outputs and position - mu = np.random.randn(n_samples) * 0.0015 # Slightly higher variance - sigma = np.random.uniform(0.0005, 0.0025, n_samples) - # Simulate edge with clearer separation for testing signals - edge_base = np.random.choice([-0.15, -0.05, 0.0, 0.05, 0.15], n_samples, p=[0.2, 0.2, 0.2, 0.2, 0.2]) - edge = np.clip(edge_base + np.random.randn(n_samples) * 0.03, -1.0, 1.0) - z_score = np.abs(mu) / (sigma + 1e-9) - position = np.random.uniform(-1, 1, n_samples) - states = np.vstack([mu, sigma, edge, z_score, position]).T.astype(np.float32) - # Use a small positive/negative threshold for determining signal from edge - signals = np.where(edge > 0.02, 1, np.where(edge < -0.02, -1, 0)) - return states, signals - -# --- Tests --- -@pytest.mark.skipif(sac_agent is None or tf is None, reason="SAC Agent module or TensorFlow not found") -def test_sac_agent_default_min_buffer(sac_agent_instance): - """Verify the default min_buffer_size is at least 10000.""" - agent = sac_agent_instance - # Note: Fixture currently initializes with specific values, overriding default. - # Re-initialize with defaults for this test. - default_agent = sac_agent.SACTradingAgent(state_dim=5, action_dim=1) - min_buffer = default_agent.min_buffer_size - print(f"\nAgent default min_buffer_size: {min_buffer}") - assert min_buffer >= 10000, f"Default min_buffer_size ({min_buffer}) is less than recommended 10000." - -@pytest.mark.skipif(sac_agent is None or tf is None, reason="SAC Agent module or TensorFlow not found") -def test_sac_action_variance(sac_agent_instance, sample_sac_inputs): - """ - Verify that the mean absolute action taken when the signal is non-zero - is >= 0.05. - """ - agent = sac_agent_instance - states, signals = sample_sac_inputs - - actions = [] - for state in states: - # Use deterministic action for this sanity check - action = agent.get_action(state, deterministic=True) - actions.append(action[0]) # get_action returns list/array - actions = np.array(actions) - - # Filter for non-zero signals based on the *simulated* edge - non_zero_signal_idx = signals != 0 - if not np.any(non_zero_signal_idx): - pytest.fail("No non-zero signals generated in fixture for SAC variance test.") - - actions_on_signal = actions[non_zero_signal_idx] - - if len(actions_on_signal) == 0: - # This case should ideally not happen if the above check passed - pytest.fail("Filtered actions array is empty despite non-zero signals.") - - mean_abs_action = np.mean(np.abs(actions_on_signal)) - - print(f"\nSAC Sanity Test: Mean Absolute Action (on signal != 0): {mean_abs_action:.4f}") - - # Check if the agent is outputting actions with sufficient magnitude - assert mean_abs_action >= 0.05, \ - f"Mean absolute action ({mean_abs_action:.4f}) is below threshold (0.05). Agent might be too timid or stuck near zero." - -@pytest.mark.skip(reason="Requires full backtest results which are not available in this unit test setup.") -def test_sac_reward_correlation(): - """ - Optional: Check if actions taken correlate positively with subsequent rewards. - NOTE: This test requires results from a full backtest run (actions vs rewards) - and cannot be reliably simulated or executed in this unit test. - """ - pass # Cannot implement without actual backtest results \ No newline at end of file diff --git a/gru_sac_predictor/tests/test_time_encoding.py b/gru_sac_predictor/tests/test_time_encoding.py deleted file mode 100644 index 728c3172..00000000 --- a/gru_sac_predictor/tests/test_time_encoding.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -Tests for time encoding, specifically DST transitions. -""" -import pytest -import pandas as pd -import numpy as np -import pytz # For timezone handling - -@pytest.fixture(scope="module") -def generate_dst_timeseries(): - """ - Generate a minute-frequency timestamp series crossing DST transitions - for a specific timezone (e.g., US/Eastern). - """ - # Example: US/Eastern DST Start (e.g., March 10, 2024 2:00 AM -> 3:00 AM) - # Example: US/Eastern DST End (e.g., Nov 3, 2024 2:00 AM -> 1:00 AM) - tz = pytz.timezone('US/Eastern') - - # Create timestamps around DST start - dst_start_range = pd.date_range( - start='2024-03-10 01:00:00', end='2024-03-10 04:00:00', freq='T', tz=tz - ) - # Create timestamps around DST end - dst_end_range = pd.date_range( - start='2024-11-03 00:00:00', end='2024-11-03 03:00:00', freq='T', tz=tz - ) - - # Combine and ensure uniqueness/order (though disjoint here) - timestamps = dst_start_range.union(dst_end_range) - df = pd.DataFrame(index=timestamps) - df.index.name = 'timestamp' - return df - -def calculate_cyclical_features(df): - """Helper to calculate sin/cos features from a datetime index.""" - if not isinstance(df.index, pd.DatetimeIndex): - raise TypeError("Input DataFrame must have a DatetimeIndex.") - - # Ensure timezone is present (fixture provides it) - if df.index.tz is None: - print("Warning: Index timezone is None, assuming UTC for calculation.") - timestamp_source = df.index.tz_localize('utc') - else: - timestamp_source = df.index - - # Use UTC hour for consistent calculation if timezone handling upstream is complex - # Or use localized hour if pipeline guarantees consistent local TZ - # Here, let's use the localized hour provided by the fixture - hour_of_day = timestamp_source.hour - # minute_of_day = timestamp_source.hour * 60 + timestamp_source.minute # Alternative - - df['hour_sin'] = np.sin(2 * np.pi * hour_of_day / 24) - df['hour_cos'] = np.cos(2 * np.pi * hour_of_day / 24) - return df - - -def test_cyclical_features_continuity(generate_dst_timeseries): - """ - Check if hour_sin and hour_cos features are continuous (no large jumps) - across DST transitions, assuming calculation uses localized time. - If using UTC hour, continuity is guaranteed, but might not capture - local market patterns intended. - """ - df = generate_dst_timeseries - df = calculate_cyclical_features(df) - - # Check differences between consecutive values - sin_diff = df['hour_sin'].diff().abs() - cos_diff = df['hour_cos'].diff().abs() - - # Define a reasonable threshold for a jump (e.g., difference > value for 15 mins) - # Max change in sin(2*pi*h/24) over 1 minute is small. - # A jump of 1 hour means h changes by 1, argument changes by pi/12. - # Max diff sin(x+pi/12) - sin(x) is approx pi/12 ~ 0.26 - max_allowed_diff = 0.3 # Allow slightly more than 1 hour jump equivalent - - print(f"\nMax Sin Diff: {sin_diff.max():.4f}") - print(f"Max Cos Diff: {cos_diff.max():.4f}") - - assert sin_diff.max() < max_allowed_diff, \ - f"Large jump detected in hour_sin ({sin_diff.max():.4f}) around DST. Check time source/calculation." - assert cos_diff.max() < max_allowed_diff, \ - f"Large jump detected in hour_cos ({cos_diff.max():.4f}) around DST. Check time source/calculation." - - # Optional: Plot to visually inspect - # import matplotlib.pyplot as plt - # plt.figure() - # plt.plot(df.index, df['hour_sin'], '.-.', label='sin') - # plt.plot(df.index, df['hour_cos'], '.-.', label='cos') - # plt.title('Cyclical Features Across DST') - # plt.legend() - # plt.xticks(rotation=45) - # plt.tight_layout() - # plt.show() \ No newline at end of file