Isolate legacy range-grid features
This commit is contained in:
@@ -144,6 +144,8 @@ SR_RESEARCH_VARIANTS = {
|
|||||||
"legacy_geometry_neutral",
|
"legacy_geometry_neutral",
|
||||||
"legacy_pivots_only",
|
"legacy_pivots_only",
|
||||||
"legacy_traffic_grid_only",
|
"legacy_traffic_grid_only",
|
||||||
|
"legacy_range_grid_touch",
|
||||||
|
"legacy_range_grid_neutral",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -158,18 +160,7 @@ def _sr_research_variant() -> str:
|
|||||||
|
|
||||||
def _apply_zone_strength_variant(zone_levels: list[Any], sr_variant: str) -> list[Any]:
|
def _apply_zone_strength_variant(zone_levels: list[Any], sr_variant: str) -> list[Any]:
|
||||||
"""Apply post-cluster research controls without changing zone geometry."""
|
"""Apply post-cluster research controls without changing zone geometry."""
|
||||||
if sr_variant == "legacy_geometry_neutral":
|
if sr_variant in {"legacy_geometry_neutral", "legacy_range_grid_neutral"}:
|
||||||
# Neutrality must be enforced after the shared zone cluster: its legacy
|
|
||||||
# sum mode can otherwise turn two 50-strength constituents back into a
|
|
||||||
# 100-strength target and silently invalidate the ablation.
|
|
||||||
for level in zone_levels:
|
|
||||||
level.strength = 50
|
|
||||||
return zone_levels
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_zone_strength_variant(zone_levels: list[Any], sr_variant: str) -> list[Any]:
|
|
||||||
"""Apply post-cluster research controls without changing zone geometry."""
|
|
||||||
if sr_variant == "legacy_geometry_neutral":
|
|
||||||
# Neutrality must be enforced after the shared zone cluster: its legacy
|
# Neutrality must be enforced after the shared zone cluster: its legacy
|
||||||
# sum mode can otherwise turn two 50-strength constituents back into a
|
# sum mode can otherwise turn two 50-strength constituents back into a
|
||||||
# 100-strength target and silently invalidate the ablation.
|
# 100-strength target and silently invalidate the ablation.
|
||||||
@@ -310,6 +301,16 @@ def _window_setups(
|
|||||||
detected_levels = detect_sr_levels_legacy(
|
detected_levels = detect_sr_levels_legacy(
|
||||||
highs, lows, closes, volumes, include_pivots=False
|
highs, lows, closes, volumes, include_pivots=False
|
||||||
)
|
)
|
||||||
|
elif sr_variant in {"legacy_range_grid_touch", "legacy_range_grid_neutral"}:
|
||||||
|
detected_levels = detect_sr_levels_legacy(
|
||||||
|
highs,
|
||||||
|
lows,
|
||||||
|
closes,
|
||||||
|
volumes,
|
||||||
|
include_pivots=False,
|
||||||
|
neutral_strength=sr_variant == "legacy_range_grid_neutral",
|
||||||
|
explicit_range_grid=True,
|
||||||
|
)
|
||||||
elif detector_variant in {"production_control", "rr_aligned_control"}:
|
elif detector_variant in {"production_control", "rr_aligned_control"}:
|
||||||
detected_levels = detect_sr_levels_legacy(highs, lows, closes, volumes)
|
detected_levels = detect_sr_levels_legacy(highs, lows, closes, volumes)
|
||||||
else:
|
else:
|
||||||
@@ -351,7 +352,6 @@ def _window_setups(
|
|||||||
strength_mode=zone_strength_mode,
|
strength_mode=zone_strength_mode,
|
||||||
)
|
)
|
||||||
zone_levels = _apply_zone_strength_variant(zone_levels, sr_variant)
|
zone_levels = _apply_zone_strength_variant(zone_levels, sr_variant)
|
||||||
zone_levels = _apply_zone_strength_variant(zone_levels, sr_variant)
|
|
||||||
targets = target_generator.generate_targets(direction, entry, stop, zone_levels, atr)
|
targets = target_generator.generate_targets(direction, entry, stop, zone_levels, atr)
|
||||||
if not targets:
|
if not targets:
|
||||||
fallback_k = _atr_target_fallback_k()
|
fallback_k = _atr_target_fallback_k()
|
||||||
|
|||||||
@@ -394,6 +394,34 @@ def _legacy_volume_profile_nodes(
|
|||||||
return hvn + lvn
|
return hvn + lvn
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_range_grid_nodes(
|
||||||
|
highs: list[float],
|
||||||
|
lows: list[float],
|
||||||
|
closes: list[float],
|
||||||
|
num_bins: int = 20,
|
||||||
|
) -> list[float]:
|
||||||
|
"""Return every deployed grid center without the irrelevant volume pass.
|
||||||
|
|
||||||
|
The legacy profile returns both its above-average (HVN) and below-average
|
||||||
|
(LVN) bins. Their union is therefore the complete range grid except for the
|
||||||
|
rare bin whose traffic equals the average exactly. This explicit helper
|
||||||
|
isolates that geometry from the misleading volume-profile label.
|
||||||
|
"""
|
||||||
|
if len(closes) < 20:
|
||||||
|
raise ValidationError(
|
||||||
|
f"Range grid requires at least 20 bars, got {len(closes)}"
|
||||||
|
)
|
||||||
|
price_min = min(lows)
|
||||||
|
price_max = max(highs)
|
||||||
|
if price_max == price_min:
|
||||||
|
price_max = price_min + 1.0
|
||||||
|
bin_width = (price_max - price_min) / num_bins
|
||||||
|
return [
|
||||||
|
round(price_min + (i + 0.5) * bin_width, 4)
|
||||||
|
for i in range(num_bins)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def detect_sr_levels_legacy(
|
def detect_sr_levels_legacy(
|
||||||
highs: list[float],
|
highs: list[float],
|
||||||
lows: list[float],
|
lows: list[float],
|
||||||
@@ -404,6 +432,7 @@ def detect_sr_levels_legacy(
|
|||||||
include_volume_profile: bool = True,
|
include_volume_profile: bool = True,
|
||||||
include_pivots: bool = True,
|
include_pivots: bool = True,
|
||||||
neutral_strength: bool = False,
|
neutral_strength: bool = False,
|
||||||
|
explicit_range_grid: bool = False,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Deployed detector plus source-isolation controls for local research.
|
"""Deployed detector plus source-isolation controls for local research.
|
||||||
|
|
||||||
@@ -417,7 +446,12 @@ def detect_sr_levels_legacy(
|
|||||||
candidates: list[tuple[float, str]] = []
|
candidates: list[tuple[float, str]] = []
|
||||||
if include_volume_profile:
|
if include_volume_profile:
|
||||||
try:
|
try:
|
||||||
for price in _legacy_volume_profile_nodes(highs, lows, closes, volumes):
|
nodes = (
|
||||||
|
_legacy_range_grid_nodes(highs, lows, closes)
|
||||||
|
if explicit_range_grid
|
||||||
|
else _legacy_volume_profile_nodes(highs, lows, closes, volumes)
|
||||||
|
)
|
||||||
|
for price in nodes:
|
||||||
candidates.append((float(price), "volume_profile"))
|
candidates.append((float(price), "volume_profile"))
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -481,13 +481,13 @@ into production as-is.
|
|||||||
|
|
||||||
### Hidden-feature isolation
|
### Hidden-feature isolation
|
||||||
|
|
||||||
The remaining hypothesis is that the deployed detector accidentally measures
|
The deployed detector accidentally measures long-memory historical price traffic
|
||||||
long-memory historical price traffic rather than genuine S/R. A dedicated matrix
|
rather than genuine S/R. A dedicated matrix holds the complete gate fixed and
|
||||||
holds the complete gate fixed and changes one legacy component at a time:
|
changes one legacy component at a time:
|
||||||
|
|
||||||
- `legacy_geometry_neutral`: old locations, every merged strength fixed at 50;
|
- `legacy_geometry_neutral`: old locations, every merged strength fixed at 50;
|
||||||
- `legacy_pivots_only`: unfiltered full-history pivots, no VP grid;
|
- `legacy_pivots_only`: unfiltered full-history pivots, no VP grid;
|
||||||
- `legacy_traffic_grid_only`: old range-volume price grid, no pivots.
|
- `legacy_traffic_grid_only`: deployed HVN+LVN grid, no pivots.
|
||||||
|
|
||||||
Run on macOS:
|
Run on macOS:
|
||||||
|
|
||||||
@@ -498,29 +498,54 @@ Run on macOS:
|
|||||||
Do not validate any traffic arm yet. First establish whether geometry, pivots, or
|
Do not validate any traffic arm yet. First establish whether geometry, pivots, or
|
||||||
the range-occupancy grid reproduces production on pre-2024 training data.
|
the range-occupancy grid reproduces production on pre-2024 training data.
|
||||||
|
|
||||||
Initial result:
|
Corrected training result:
|
||||||
|
|
||||||
| arm | Sharpe | CAGR | MaxDD | net avg R | ex-top-5% |
|
| arm | qualified | Sharpe | CAGR | MaxDD | net avg R | ex-top-5% |
|
||||||
|---|---:|---:|---:|---:|---:|
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
| production control | 1.28 | 28.8% | 21.4% | 0.230 | 0.066 |
|
| production control | 676 | 1.28 | 28.8% | 21.4% | 0.230 | 0.066 |
|
||||||
| pivots only | 1.38 | 32.9% | 25.2% | 0.236 | 0.076 |
|
| corrected neutral geometry | 505 | 1.48 | 33.5% | 18.2% | 0.230 | 0.087 |
|
||||||
| traffic grid only | **1.72** | **41.8%** | **16.8%** | **0.271** | **0.136** |
|
| pivots only | 717 | 1.38 | 32.9% | 25.2% | 0.236 | 0.076 |
|
||||||
|
| traffic grid only | 504 | **1.72** | **41.8%** | **16.8%** | **0.271** | **0.136** |
|
||||||
|
|
||||||
The traffic grid is the first research arm to beat control simultaneously on
|
The traffic grid is the first research arm to beat control simultaneously on
|
||||||
Sharpe, CAGR, drawdown, and robust expectancy. It retains 264 production setups,
|
Sharpe, CAGR, drawdown, and robust expectancy. It retains 264 production setups
|
||||||
adds 240 positive-expectancy setups, and removes 412 weaker setups. This supports
|
at +0.214R ex-top-5%, adds 240 at +0.051R, and removes 412 at only +0.010R.
|
||||||
the hypothesis that the useful legacy feature is the volume-weighted range-
|
Against corrected neutral geometry, only 239 qualified setups overlap; the 265
|
||||||
occupancy grid, not pivots or visually meaningful S/R.
|
traffic-only setups return +0.140R ex-top-5% versus +0.073R for the 266
|
||||||
|
neutral-only setups. This is different selection, not merely a lower trade count.
|
||||||
|
|
||||||
The first geometry-neutral result is not interpretable: neutral strength was set
|
The old `volume_profile` name is misleading. Its helper returns both HVN and LVN
|
||||||
before the shared zone cluster, which summed multiple 50-strength constituents
|
bins, so their union retains almost every one of the 20 evenly spaced centers over
|
||||||
back to 100. Neutrality is now enforced after clustering. Rerun only that arm:
|
the expanding historical high-low range (19.987 levels on average in the audit).
|
||||||
|
The later strength calculation does not use volume; it counts bars whose ranges
|
||||||
|
cross each center. The candidate feature is therefore:
|
||||||
|
|
||||||
|
1. a normalized, expanding 20-bin price-range grid;
|
||||||
|
2. range-touch occupancy strength;
|
||||||
|
3. no full-history pivot ladder or pivot/grid strength saturation.
|
||||||
|
|
||||||
|
Two final training arms isolate the first two items explicitly:
|
||||||
|
|
||||||
|
- `legacy_range_grid_touch`: all 20 range centers, no volume calculation, legacy
|
||||||
|
touch strength;
|
||||||
|
- `legacy_range_grid_neutral`: identical centers, strength fixed at 50 after
|
||||||
|
clustering.
|
||||||
|
|
||||||
|
Run only these new arms on macOS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/python scripts/run_sr_v2_matrix.py traffic \
|
.venv/bin/python scripts/run_sr_v2_matrix.py traffic \
|
||||||
--only-arm legacy_geometry_neutral --workers 14
|
--only-arm legacy_range_grid_touch --workers 14
|
||||||
|
|
||||||
|
.venv/bin/python scripts/run_sr_v2_matrix.py traffic \
|
||||||
|
--only-arm legacy_range_grid_neutral --workers 14
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The touch arm should closely reproduce `legacy_traffic_grid_only`; that is the
|
||||||
|
volume-removal parity check. The touch-versus-neutral comparison then attributes
|
||||||
|
any remaining difference to occupancy strength. Freeze the winner before running
|
||||||
|
the post-2024-06-30 validation command; do not tune the bin count on training data.
|
||||||
|
|
||||||
**Next runs, if picked back up:**
|
**Next runs, if picked back up:**
|
||||||
|
|
||||||
- A **per-name target model** for clear-air setups instead of a constant k×ATR. This
|
- A **per-name target model** for clear-air setups instead of a constant k×ATR. This
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ def _parse_args() -> argparse.Namespace:
|
|||||||
"rewrite_legacy_primary", "soft_zones_legacy_primary",
|
"rewrite_legacy_primary", "soft_zones_legacy_primary",
|
||||||
"confirmed_rounds_legacy_primary", "gate_v2_legacy_primary",
|
"confirmed_rounds_legacy_primary", "gate_v2_legacy_primary",
|
||||||
"legacy_geometry_neutral", "legacy_pivots_only",
|
"legacy_geometry_neutral", "legacy_pivots_only",
|
||||||
"legacy_traffic_grid_only",
|
"legacy_traffic_grid_only", "legacy_range_grid_touch",
|
||||||
|
"legacy_range_grid_neutral",
|
||||||
),
|
),
|
||||||
default=None,
|
default=None,
|
||||||
help="Research-only S/R detector/gate arm.",
|
help="Research-only S/R detector/gate arm.",
|
||||||
|
|||||||
@@ -22,12 +22,17 @@ TRAINING_ARMS = (
|
|||||||
"confirmed_rounds_legacy_primary",
|
"confirmed_rounds_legacy_primary",
|
||||||
"gate_v2_legacy_primary",
|
"gate_v2_legacy_primary",
|
||||||
)
|
)
|
||||||
LOCKABLE_ARMS = TRAINING_ARMS[1:]
|
RANGE_GRID_ARMS = (
|
||||||
|
"legacy_traffic_grid_only",
|
||||||
|
"legacy_range_grid_touch",
|
||||||
|
"legacy_range_grid_neutral",
|
||||||
|
)
|
||||||
|
LOCKABLE_ARMS = TRAINING_ARMS[1:] + RANGE_GRID_ARMS
|
||||||
TRAFFIC_ARMS = (
|
TRAFFIC_ARMS = (
|
||||||
"production_control",
|
"production_control",
|
||||||
"legacy_geometry_neutral",
|
"legacy_geometry_neutral",
|
||||||
"legacy_pivots_only",
|
"legacy_pivots_only",
|
||||||
"legacy_traffic_grid_only",
|
*RANGE_GRID_ARMS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +52,7 @@ def _args() -> argparse.Namespace:
|
|||||||
_add_common(train)
|
_add_common(train)
|
||||||
traffic = commands.add_parser(
|
traffic = commands.add_parser(
|
||||||
"traffic",
|
"traffic",
|
||||||
help="Isolate legacy geometry, pivots, and price-traffic grid on training data.",
|
help="Isolate pivots, range-grid geometry, and touch strength on training data.",
|
||||||
)
|
)
|
||||||
_add_common(traffic)
|
_add_common(traffic)
|
||||||
traffic.add_argument(
|
traffic.add_argument(
|
||||||
|
|||||||
@@ -759,22 +759,76 @@ def test_window_setups_too_short_returns_empty():
|
|||||||
|
|
||||||
|
|
||||||
def test_sr_research_variant_is_explicit_and_validated(monkeypatch):
|
def test_sr_research_variant_is_explicit_and_validated(monkeypatch):
|
||||||
monkeypatch.setenv("BACKTEST_SR_VARIANT", "production_control")
|
for variant in (
|
||||||
assert bt._sr_research_variant() == "production_control"
|
"production_control",
|
||||||
|
"legacy_range_grid_touch",
|
||||||
|
"legacy_range_grid_neutral",
|
||||||
|
):
|
||||||
|
monkeypatch.setenv("BACKTEST_SR_VARIANT", variant)
|
||||||
|
assert bt._sr_research_variant() == variant
|
||||||
monkeypatch.setenv("BACKTEST_SR_VARIANT", "not-a-variant")
|
monkeypatch.setenv("BACKTEST_SR_VARIANT", "not-a-variant")
|
||||||
with pytest.raises(ValueError, match="Unknown BACKTEST_SR_VARIANT"):
|
with pytest.raises(ValueError, match="Unknown BACKTEST_SR_VARIANT"):
|
||||||
bt._sr_research_variant()
|
bt._sr_research_variant()
|
||||||
|
|
||||||
|
|
||||||
def test_geometry_neutral_strength_is_enforced_after_zone_clustering():
|
@pytest.mark.parametrize(
|
||||||
|
("variant", "neutral_strength"),
|
||||||
|
[
|
||||||
|
("legacy_range_grid_touch", False),
|
||||||
|
("legacy_range_grid_neutral", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_window_setups_routes_explicit_range_grid(
|
||||||
|
monkeypatch,
|
||||||
|
variant,
|
||||||
|
neutral_strength,
|
||||||
|
):
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_detector(*args, **kwargs):
|
||||||
|
captured.update(kwargs)
|
||||||
|
return []
|
||||||
|
|
||||||
|
monkeypatch.setenv("BACKTEST_SR_VARIANT", variant)
|
||||||
|
monkeypatch.setattr(bt, "detect_sr_levels_legacy", fake_detector)
|
||||||
|
records = [
|
||||||
|
SimpleNamespace(
|
||||||
|
date=date(2024, 1, 1) + timedelta(days=i),
|
||||||
|
open=100.0,
|
||||||
|
high=101.0,
|
||||||
|
low=99.0,
|
||||||
|
close=100.0,
|
||||||
|
volume=1_000_000,
|
||||||
|
)
|
||||||
|
for i in range(bt.MIN_LOOKBACK)
|
||||||
|
]
|
||||||
|
assert bt._window_setups(records, {}, {}) == []
|
||||||
|
assert captured == {
|
||||||
|
"include_pivots": False,
|
||||||
|
"neutral_strength": neutral_strength,
|
||||||
|
"explicit_range_grid": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"variant",
|
||||||
|
["legacy_geometry_neutral", "legacy_range_grid_neutral"],
|
||||||
|
)
|
||||||
|
def test_neutral_strength_is_enforced_after_zone_clustering(variant):
|
||||||
levels = [
|
levels = [
|
||||||
SimpleNamespace(strength=100),
|
SimpleNamespace(strength=100),
|
||||||
SimpleNamespace(strength=75),
|
SimpleNamespace(strength=75),
|
||||||
]
|
]
|
||||||
result = bt._apply_zone_strength_variant(levels, "legacy_geometry_neutral")
|
result = bt._apply_zone_strength_variant(levels, variant)
|
||||||
assert [level.strength for level in result] == [50, 50]
|
assert [level.strength for level in result] == [50, 50]
|
||||||
|
|
||||||
|
|
||||||
|
def test_touch_strength_survives_post_cluster_research_hook():
|
||||||
|
levels = [SimpleNamespace(strength=82), SimpleNamespace(strength=37)]
|
||||||
|
result = bt._apply_zone_strength_variant(levels, "legacy_range_grid_touch")
|
||||||
|
assert [level.strength for level in result] == [82, 37]
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_entry_bounds_validate_dates(monkeypatch):
|
def test_backtest_entry_bounds_validate_dates(monkeypatch):
|
||||||
monkeypatch.setenv("BACKTEST_ENTRY_START", "2024-07-01")
|
monkeypatch.setenv("BACKTEST_ENTRY_START", "2024-07-01")
|
||||||
monkeypatch.setenv("BACKTEST_ENTRY_END", "2024-12-31")
|
monkeypatch.setenv("BACKTEST_ENTRY_END", "2024-12-31")
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from app.services.sr_service import (
|
|||||||
MAX_LEVELS,
|
MAX_LEVELS,
|
||||||
_bar_respect_weight,
|
_bar_respect_weight,
|
||||||
_cap_levels,
|
_cap_levels,
|
||||||
|
_legacy_range_grid_nodes,
|
||||||
|
_legacy_volume_profile_nodes,
|
||||||
_merge_levels,
|
_merge_levels,
|
||||||
_round_number_candidates,
|
_round_number_candidates,
|
||||||
_strength_from_respects,
|
_strength_from_respects,
|
||||||
@@ -267,3 +269,65 @@ class TestDetectSrLevels:
|
|||||||
assert pivots and traffic
|
assert pivots and traffic
|
||||||
assert all(level["sources"] == ["pivot_point"] for level in pivots)
|
assert all(level["sources"] == ["pivot_point"] for level in pivots)
|
||||||
assert all(level["sources"] == ["volume_profile"] for level in traffic)
|
assert all(level["sources"] == ["volume_profile"] for level in traffic)
|
||||||
|
|
||||||
|
def test_legacy_profile_union_matches_explicit_range_grid(self):
|
||||||
|
highs, lows, closes, volumes = _make_series(n=500)
|
||||||
|
deployed = _legacy_volume_profile_nodes(highs, lows, closes, volumes)
|
||||||
|
explicit = _legacy_range_grid_nodes(highs, lows, closes)
|
||||||
|
assert sorted(deployed) == explicit
|
||||||
|
assert len(explicit) == 20
|
||||||
|
|
||||||
|
def test_explicit_range_grid_is_volume_independent(self):
|
||||||
|
highs, lows, closes, volumes = _make_series(n=500)
|
||||||
|
shifted_volumes = [volume * (i + 1) for i, volume in enumerate(volumes)]
|
||||||
|
baseline = detect_sr_levels_legacy(
|
||||||
|
highs,
|
||||||
|
lows,
|
||||||
|
closes,
|
||||||
|
volumes,
|
||||||
|
include_pivots=False,
|
||||||
|
explicit_range_grid=True,
|
||||||
|
)
|
||||||
|
shifted = detect_sr_levels_legacy(
|
||||||
|
highs,
|
||||||
|
lows,
|
||||||
|
closes,
|
||||||
|
shifted_volumes,
|
||||||
|
include_pivots=False,
|
||||||
|
explicit_range_grid=True,
|
||||||
|
)
|
||||||
|
assert shifted == baseline
|
||||||
|
|
||||||
|
def test_explicit_range_grid_neutral_changes_only_strength(self):
|
||||||
|
highs, lows, closes, volumes = _make_series(n=500)
|
||||||
|
touch = detect_sr_levels_legacy(
|
||||||
|
highs,
|
||||||
|
lows,
|
||||||
|
closes,
|
||||||
|
volumes,
|
||||||
|
include_pivots=False,
|
||||||
|
explicit_range_grid=True,
|
||||||
|
)
|
||||||
|
neutral = detect_sr_levels_legacy(
|
||||||
|
highs,
|
||||||
|
lows,
|
||||||
|
closes,
|
||||||
|
volumes,
|
||||||
|
include_pivots=False,
|
||||||
|
neutral_strength=True,
|
||||||
|
explicit_range_grid=True,
|
||||||
|
)
|
||||||
|
touch_by_price = {level["price_level"]: level for level in touch}
|
||||||
|
neutral_by_price = {level["price_level"]: level for level in neutral}
|
||||||
|
assert neutral_by_price.keys() == touch_by_price.keys()
|
||||||
|
for price, neutral_level in neutral_by_price.items():
|
||||||
|
assert neutral_level["strength"] == 50
|
||||||
|
assert {
|
||||||
|
key: value
|
||||||
|
for key, value in neutral_level.items()
|
||||||
|
if key != "strength"
|
||||||
|
} == {
|
||||||
|
key: value
|
||||||
|
for key, value in touch_by_price[price].items()
|
||||||
|
if key != "strength"
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user