| `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 |
| 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
)
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(