]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
fix(reforcexy): report marginal unmasked action probabilities
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 23 Sep 2026 22:49:11 +0000 (00:49 +0200)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 23 Sep 2026 22:49:11 +0000 (00:49 +0200)
ReforceXY/reward_space_analysis/README.md
ReforceXY/reward_space_analysis/reward_space_analysis.py
ReforceXY/reward_space_analysis/tests/README.md
ReforceXY/reward_space_analysis/tests/api/test_api_helpers.py

index 7d55875b555d41c99eae10e6b9660a79808cdca5..0df5b6fb47e2857e284f2167e78b67ed448fbc63 100644 (file)
@@ -555,6 +555,11 @@ descriptive.
 | `partial_dependence_*.csv` | Partial dependence data                              |
 | `manifest.json`            | Runtime manifest (simulation + reward params + hash) |
 
+The `sample_entry_prob`, `sample_exit_prob`, and `sample_neutral_prob` columns in
+`reward_samples.csv` report marginal probabilities of valid actions when applicable.
+With masking disabled, these probabilities include the 90% valid-action mass;
+they are not conditional on drawing a valid action.
+
 ### Manifest (`manifest.json`)
 
 | Field                   | Type              | Description                                                                                             |
index f056d4d69dc43f0e1b068c3b383844d8457f120b..ed6b312fd754f1a2bec6697387ad0cc1f75cb3ac 100644 (file)
@@ -1633,6 +1633,9 @@ def _sample_action(
         weights.extend(
             [_SAMPLE_INVALID_ACTION_PROBABILITY / len(invalid_choices)] * len(invalid_choices)
         )
+        entry_prob *= valid_mass
+        exit_prob *= valid_mass
+        neutral_prob *= valid_mass
 
     action = rng.choices(choices, weights=weights, k=1)[0]
     return action, entry_prob, exit_prob, neutral_prob
index 48f04536f8f61b83cc6b9c2fe5b68345a8fb34c5..474849e90129891b71170e7152fd828e321f5624 100644 (file)
@@ -227,6 +227,7 @@ Columns:
 | statistics-rank-direction-141                 | statistics  | Rank-biserial effect follows the named first-group advantage                         | statistics/test_statistics.py::test_pnl_rank_biserial_direction_matches_named_first_group                         | Checks both directions                                                                                                                                                         |
 | statistics-bootstrap-count-142                | statistics  | Zero and negative resample counts fail for variable and constant data                | statistics/test_statistics.py::test_bootstrap_rejects_nonpositive_resample_count                                  | Rejects missing bootstrap                                                                                                                                                      |
 | integration-finite-report-143                 | integration | Reports distinguish unusable real values from missing real episodes                  | integration/test_report_formatting.py::test_report_distinguishes_missing_real_episodes_from_unusable_observations | Section and summary agree                                                                                                                                                      |
+| api-unmasked-sample-probabilities-144         | api         | Unmasked sample probabilities match marginal valid-action frequencies               | api/test_api_helpers.py::test_unmasked_sampling_probabilities_match_action_frequencies                             | Spot/futures entries, long/short exits, and neutral probability                                                                                                               |
 
 ### Non-Owning Smoke / Reference Checks
 
index e1e976e2089b4ec94b6332fbd79c4f43ac30716d..2b76b1911ac40d79da01d1999a2dfd63ebab26be 100644 (file)
@@ -83,6 +83,41 @@ class TestAPIAndHelpers(RewardSpaceTestBase):
         )
         self.assertGreater(high_idle_rate_spot, low_idle_rate_spot)
 
+    def test_unmasked_sampling_probabilities_match_action_frequencies(self):
+        """Reported probabilities describe valid actions, not their conditional hazards."""
+        draws = SCENARIOS.API_ENTRY_RATE_DRAWS
+        # Four standard errors using the maximum Bernoulli variance.
+        tolerance = 4 * math.sqrt(0.25 / draws)
+        cases = (
+            (Positions.Neutral, False, (Actions.Long_enter,), 1),
+            (Positions.Neutral, True, (Actions.Long_enter, Actions.Short_enter), 1),
+            (Positions.Long, True, (Actions.Long_exit,), 2),
+            (Positions.Short, True, (Actions.Short_exit,), 2),
+        )
+        for position, short_allowed, actions, probability_index in cases:
+            with self.subTest(position=position, short_allowed=short_allowed):
+                rng = random.Random(SEEDS.REPRODUCIBILITY)
+                samples = [
+                    _sample_action(
+                        position,
+                        rng,
+                        short_allowed=short_allowed,
+                        trade_duration=SCENARIOS.API_IDLE_DURATION_HIGH,
+                        max_trade_duration_candles=SCENARIOS.API_MAX_IDLE_DURATION_CANDLES,
+                        idle_duration=SCENARIOS.API_IDLE_DURATION_HIGH,
+                        max_idle_duration_candles=SCENARIOS.API_MAX_IDLE_DURATION_CANDLES,
+                        action_masking=False,
+                    )
+                    for _ in range(draws)
+                ]
+                observed = sum(sample[0] in actions for sample in samples) / draws
+                self.assertAlmostEqual(samples[0][probability_index], observed, delta=tolerance)
+                if position == Positions.Neutral:
+                    observed_neutral = (
+                        sum(sample[0] == Actions.Neutral for sample in samples) / draws
+                    )
+                    self.assertAlmostEqual(samples[0][3], observed_neutral, delta=tolerance)
+
     def test_parse_overrides(self):
         """Overrides accept canonical keys and reject unsupported keys."""
         result = parse_overrides(