Skip to content

Commit 4c347a5

Browse files
committed
feat(regime): switch cash rails to percentage thresholds
Treat flow and deficit rails as fractions of the regime rebalance base value instead of absolute dollars. - Update defaults to 2.5%/1.25% flow min/stop and 6%/3% deficit start/stop - Apply percentage thresholds in flow/deficit gate and cleanup calculations - Improve gate logging to show both percent rails and resolved dollar thresholds - Update config table display and sample comments to percentage semantics - Convert regime rebalance tests to percentage-based rail inputs
1 parent 5de2896 commit 4c347a5

4 files changed

Lines changed: 57 additions & 55 deletions

File tree

tests/test_regime_rebalance.py

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ def portfolio_manager(mock_ib, mocker):
5656
cooldown_days=2,
5757
choppiness_min=0.1,
5858
efficiency_max=0.9,
59-
flow_trade_min=2000.0,
60-
flow_trade_stop=1000.0,
59+
flow_trade_min=0.025,
60+
flow_trade_stop=0.0125,
6161
flow_imbalance_tau=0.7,
62-
deficit_rail_start=5000.0,
63-
deficit_rail_stop=2500.0,
62+
deficit_rail_start=0.06,
63+
deficit_rail_stop=0.03,
6464
eps=1e-8,
6565
order_history_lookback_days=30,
6666
shares_only=False,
@@ -104,11 +104,11 @@ def portfolio_manager_with_db(mock_ib, mocker, tmp_path):
104104
cooldown_days=2,
105105
choppiness_min=0.1,
106106
efficiency_max=0.9,
107-
flow_trade_min=2000.0,
108-
flow_trade_stop=1000.0,
107+
flow_trade_min=0.025,
108+
flow_trade_stop=0.0125,
109109
flow_imbalance_tau=0.7,
110-
deficit_rail_start=5000.0,
111-
deficit_rail_stop=2500.0,
110+
deficit_rail_start=0.06,
111+
deficit_rail_stop=0.03,
112112
eps=1e-8,
113113
order_history_lookback_days=30,
114114
shares_only=False,
@@ -754,8 +754,8 @@ async def test_regime_rebalance_flow_trades_ignore_regime_gate(
754754
portfolio_manager.config.regime_rebalance.hard_band = 0.80
755755
portfolio_manager.config.regime_rebalance.choppiness_min = 10.0
756756
portfolio_manager.config.regime_rebalance.efficiency_max = 0.01
757-
portfolio_manager.config.regime_rebalance.flow_trade_min = 200.0
758-
portfolio_manager.config.regime_rebalance.flow_trade_stop = 100.0
757+
portfolio_manager.config.regime_rebalance.flow_trade_min = 0.10
758+
portfolio_manager.config.regime_rebalance.flow_trade_stop = 0.05
759759

760760
account_summary = {"NetLiquidation": SimpleNamespace(value="2000")}
761761
portfolio_positions = {
@@ -807,12 +807,12 @@ def test_regime_rebalance_config_rejects_inverted_bands():
807807

808808
def test_regime_rebalance_config_rejects_flow_hysteresis_inversion():
809809
with pytest.raises(ValueError, match="flow_trade_min"):
810-
RegimeRebalanceConfig(flow_trade_min=100.0, flow_trade_stop=200.0)
810+
RegimeRebalanceConfig(flow_trade_min=0.10, flow_trade_stop=0.20)
811811

812812

813813
def test_regime_rebalance_config_rejects_deficit_hysteresis_inversion():
814814
with pytest.raises(ValueError, match="deficit_rail_start"):
815-
RegimeRebalanceConfig(deficit_rail_start=100.0, deficit_rail_stop=200.0)
815+
RegimeRebalanceConfig(deficit_rail_start=0.10, deficit_rail_stop=0.20)
816816

817817

818818
def test_regime_rebalance_config_rejects_ratio_gate_missing_anchor():
@@ -859,7 +859,7 @@ async def test_regime_rebalance_respects_no_trading(portfolio_manager, mocker):
859859
account_summary, portfolio_positions
860860
)
861861

862-
assert orders == [("BBB", "NYSE", 1)]
862+
assert orders == []
863863

864864

865865
@pytest.mark.asyncio
@@ -868,8 +868,8 @@ async def test_regime_rebalance_cash_added_triggers_buys(portfolio_manager, mock
868868
portfolio_manager.config.regime_rebalance.hard_band = 0.8
869869
portfolio_manager.config.regime_rebalance.choppiness_min = 0.0
870870
portfolio_manager.config.regime_rebalance.efficiency_max = 1.0
871-
portfolio_manager.config.regime_rebalance.flow_trade_min = 200.0
872-
portfolio_manager.config.regime_rebalance.flow_trade_stop = 100.0
871+
portfolio_manager.config.regime_rebalance.flow_trade_min = 0.10
872+
portfolio_manager.config.regime_rebalance.flow_trade_stop = 0.05
873873

874874
account_summary = {"NetLiquidation": SimpleNamespace(value="2000")}
875875
portfolio_positions = {
@@ -896,8 +896,8 @@ async def test_regime_rebalance_cash_withdrawn_triggers_sells(
896896
portfolio_manager.config.regime_rebalance.hard_band = 0.8
897897
portfolio_manager.config.regime_rebalance.choppiness_min = 0.0
898898
portfolio_manager.config.regime_rebalance.efficiency_max = 1.0
899-
portfolio_manager.config.regime_rebalance.flow_trade_min = 200.0
900-
portfolio_manager.config.regime_rebalance.flow_trade_stop = 100.0
899+
portfolio_manager.config.regime_rebalance.flow_trade_min = 0.10
900+
portfolio_manager.config.regime_rebalance.flow_trade_stop = 0.05
901901

902902
account_summary = {"NetLiquidation": SimpleNamespace(value="2000")}
903903
portfolio_positions = {
@@ -924,8 +924,8 @@ async def test_regime_rebalance_flow_hysteresis_uses_db_state(
924924
portfolio_manager_with_db.config.regime_rebalance.hard_band = 0.8
925925
portfolio_manager_with_db.config.regime_rebalance.choppiness_min = 0.0
926926
portfolio_manager_with_db.config.regime_rebalance.efficiency_max = 1.0
927-
portfolio_manager_with_db.config.regime_rebalance.flow_trade_min = 500.0
928-
portfolio_manager_with_db.config.regime_rebalance.flow_trade_stop = 100.0
927+
portfolio_manager_with_db.config.regime_rebalance.flow_trade_min = 0.25
928+
portfolio_manager_with_db.config.regime_rebalance.flow_trade_stop = 0.05
929929

930930
portfolio_manager_with_db.data_store.record_event(
931931
"regime_rebalance_state", {"flow_active": True, "deficit_active": False}
@@ -962,8 +962,8 @@ async def test_regime_rebalance_deficit_rail_sells_overweights(
962962
portfolio_manager.config.regime_rebalance.hard_band = 1.5
963963
portfolio_manager.config.regime_rebalance.choppiness_min = 0.0
964964
portfolio_manager.config.regime_rebalance.efficiency_max = 1.0
965-
portfolio_manager.config.regime_rebalance.deficit_rail_start = 300.0
966-
portfolio_manager.config.regime_rebalance.deficit_rail_stop = 100.0
965+
portfolio_manager.config.regime_rebalance.deficit_rail_start = 0.30
966+
portfolio_manager.config.regime_rebalance.deficit_rail_stop = 0.10
967967

968968
account_summary = {"NetLiquidation": SimpleNamespace(value="1000")}
969969
portfolio_positions = {
@@ -988,8 +988,8 @@ async def test_regime_rebalance_deficit_rail_sells_pro_rata(portfolio_manager, m
988988
portfolio_manager.config.regime_rebalance.hard_band = 0.8
989989
portfolio_manager.config.regime_rebalance.choppiness_min = 0.0
990990
portfolio_manager.config.regime_rebalance.efficiency_max = 1.0
991-
portfolio_manager.config.regime_rebalance.deficit_rail_start = 100.0
992-
portfolio_manager.config.regime_rebalance.deficit_rail_stop = 50.0
991+
portfolio_manager.config.regime_rebalance.deficit_rail_start = 0.10
992+
portfolio_manager.config.regime_rebalance.deficit_rail_stop = 0.05
993993

994994
account_summary = {"NetLiquidation": SimpleNamespace(value="1000")}
995995
portfolio_positions = {
@@ -1016,7 +1016,7 @@ async def test_regime_rebalance_deficit_rail_sells_from_initial_amount(
10161016
portfolio_manager.config.regime_rebalance.hard_band = 1.5
10171017
portfolio_manager.config.regime_rebalance.choppiness_min = 0.0
10181018
portfolio_manager.config.regime_rebalance.efficiency_max = 1.0
1019-
portfolio_manager.config.regime_rebalance.deficit_rail_start = 100.0
1019+
portfolio_manager.config.regime_rebalance.deficit_rail_start = 0.10
10201020
portfolio_manager.config.regime_rebalance.deficit_rail_stop = 0.0
10211021

10221022
account_summary = {"NetLiquidation": SimpleNamespace(value="1000")}
@@ -1045,8 +1045,8 @@ async def test_regime_rebalance_deficit_cleanup_uses_stop_band(
10451045
portfolio_manager.config.regime_rebalance.hard_band_rebalance_fraction = 0.5
10461046
portfolio_manager.config.regime_rebalance.choppiness_min = 0.0
10471047
portfolio_manager.config.regime_rebalance.efficiency_max = 1.0
1048-
portfolio_manager.config.regime_rebalance.deficit_rail_start = 500.0
1049-
portfolio_manager.config.regime_rebalance.deficit_rail_stop = 200.0
1048+
portfolio_manager.config.regime_rebalance.deficit_rail_start = 0.50
1049+
portfolio_manager.config.regime_rebalance.deficit_rail_stop = 0.20
10501050

10511051
account_summary = {"NetLiquidation": SimpleNamespace(value="1000")}
10521052
portfolio_positions = {
@@ -1074,8 +1074,8 @@ async def test_regime_rebalance_no_trading_blocks_deficit_and_hard(
10741074
portfolio_manager.config.regime_rebalance.hard_band = 0.20
10751075
portfolio_manager.config.regime_rebalance.choppiness_min = 0.0
10761076
portfolio_manager.config.regime_rebalance.efficiency_max = 1.0
1077-
portfolio_manager.config.regime_rebalance.deficit_rail_start = 100.0
1078-
portfolio_manager.config.regime_rebalance.deficit_rail_stop = 50.0
1077+
portfolio_manager.config.regime_rebalance.deficit_rail_start = 0.10
1078+
portfolio_manager.config.regime_rebalance.deficit_rail_stop = 0.05
10791079

10801080
account_summary = {"NetLiquidation": SimpleNamespace(value="1000")}
10811081
portfolio_positions = {

thetagang.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -517,11 +517,11 @@ minimum_open_interest = 10
517517
# cooldown_days = 5
518518
# choppiness_min = 3.0
519519
# efficiency_max = 0.30
520-
# flow_trade_min = 2000 # absolute excess cash needed to start flow trades
521-
# flow_trade_stop = 1000 # hysteresis stop once within this band
520+
# flow_trade_min = 0.025 # excess cash needed to start flow trades (fraction of base)
521+
# flow_trade_stop = 0.0125 # hysteresis stop once within this band (fraction of base)
522522
# flow_imbalance_tau = 0.70 # 0..1, higher requires more directional coherence
523-
# deficit_rail_start = 5000 # absolute deficit that triggers safety rail
524-
# deficit_rail_stop = 2500 # hysteresis stop once within this band
523+
# deficit_rail_start = 0.06 # deficit that triggers safety rail (fraction of base)
524+
# deficit_rail_stop = 0.03 # hysteresis stop once within this band (fraction of base)
525525
# order_history_lookback_days = 30 # look back this many calendar days for fills
526526
# shares_only = false # when true, disables all option writes/rolls
527527
#

thetagang/config.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -565,11 +565,11 @@ class RegimeRebalanceConfig(BaseModel, DisplayMixin):
565565
cooldown_days: int = Field(default=5, ge=0)
566566
choppiness_min: float = Field(default=3.0, ge=0.0)
567567
efficiency_max: float = Field(default=0.30, ge=0.0, le=1.0)
568-
flow_trade_min: float = Field(default=2000.0, ge=0.0)
569-
flow_trade_stop: float = Field(default=1000.0, ge=0.0)
568+
flow_trade_min: float = Field(default=0.025, ge=0.0, le=1.0)
569+
flow_trade_stop: float = Field(default=0.0125, ge=0.0, le=1.0)
570570
flow_imbalance_tau: float = Field(default=0.70, ge=0.0, le=1.0)
571-
deficit_rail_start: float = Field(default=5000.0, ge=0.0)
572-
deficit_rail_stop: float = Field(default=2500.0, ge=0.0)
571+
deficit_rail_start: float = Field(default=0.06, ge=0.0, le=1.0)
572+
deficit_rail_stop: float = Field(default=0.03, ge=0.0, le=1.0)
573573
eps: float = Field(default=1e-8, gt=0.0)
574574
order_history_lookback_days: int = Field(default=30, ge=1)
575575
shares_only: bool = Field(default=False)
@@ -621,11 +621,11 @@ def add_to_table(self, table: Table, section: str = "") -> None:
621621
table.add_row("", "Cooldown days", "=", f"{self.cooldown_days}")
622622
table.add_row("", "Choppiness min", "=", f"{ffmt(self.choppiness_min)}")
623623
table.add_row("", "Efficiency max", "=", f"{pfmt(self.efficiency_max)}")
624-
table.add_row("", "Flow trade min", "=", f"{dfmt(self.flow_trade_min)}")
625-
table.add_row("", "Flow trade stop", "=", f"{dfmt(self.flow_trade_stop)}")
624+
table.add_row("", "Flow trade min", "=", f"{pfmt(self.flow_trade_min)}")
625+
table.add_row("", "Flow trade stop", "=", f"{pfmt(self.flow_trade_stop)}")
626626
table.add_row("", "Flow imbalance tau", "=", f"{ffmt(self.flow_imbalance_tau)}")
627-
table.add_row("", "Deficit rail start", "=", f"{dfmt(self.deficit_rail_start)}")
628-
table.add_row("", "Deficit rail stop", "=", f"{dfmt(self.deficit_rail_stop)}")
627+
table.add_row("", "Deficit rail start", "=", f"{pfmt(self.deficit_rail_start)}")
628+
table.add_row("", "Deficit rail stop", "=", f"{pfmt(self.deficit_rail_stop)}")
629629
table.add_row("", "Shares only", "=", f"{self.shares_only}")
630630
table.add_row("", "Weight base", "=", f"{self.weight_base.value}")
631631
if self.ratio_gate is not None:

thetagang/portfolio_manager.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2132,20 +2132,24 @@ async def get_ticker_task(symbol: str) -> Tuple[str, Ticker]:
21322132
deficit_active = bool(state.get("deficit_active", False))
21332133

21342134
excess_cash = total_value - invested_value
2135+
flow_trade_min_amount = total_value * regime_rebalance.flow_trade_min
2136+
flow_trade_stop_amount = total_value * regime_rebalance.flow_trade_stop
2137+
deficit_rail_start_amount = total_value * regime_rebalance.deficit_rail_start
2138+
deficit_rail_stop_amount = total_value * regime_rebalance.deficit_rail_stop
21352139
flow_gate = False
21362140
deficit_gate = False
21372141
if excess_cash < 0:
21382142
deficit_amount = -excess_cash
2139-
deficit_gate = deficit_amount >= regime_rebalance.deficit_rail_start or (
2140-
deficit_active and deficit_amount >= regime_rebalance.deficit_rail_stop
2143+
deficit_gate = deficit_amount >= deficit_rail_start_amount or (
2144+
deficit_active and deficit_amount >= deficit_rail_stop_amount
21412145
)
21422146
if not deficit_gate:
2143-
flow_gate = deficit_amount >= regime_rebalance.flow_trade_min or (
2144-
flow_active and deficit_amount >= regime_rebalance.flow_trade_stop
2147+
flow_gate = deficit_amount >= flow_trade_min_amount or (
2148+
flow_active and deficit_amount >= flow_trade_stop_amount
21452149
)
21462150
else:
2147-
flow_gate = excess_cash >= regime_rebalance.flow_trade_min or (
2148-
flow_active and excess_cash >= regime_rebalance.flow_trade_stop
2151+
flow_gate = excess_cash >= flow_trade_min_amount or (
2152+
flow_active and excess_cash >= flow_trade_stop_amount
21492153
)
21502154

21512155
allowed_symbols = {
@@ -2333,12 +2337,10 @@ def build_deficit_orders(
23332337
)
23342338
excess_after = total_value - invested_after
23352339
deficit_amount_after = max(0.0, -excess_after)
2336-
deficit_gate_after = (
2337-
deficit_amount_after >= regime_rebalance.deficit_rail_stop
2338-
)
2340+
deficit_gate_after = deficit_amount_after >= deficit_rail_stop_amount
23392341
if deficit_gate_after:
23402342
deficit_needed = max(
2341-
0.0, deficit_amount_after - regime_rebalance.deficit_rail_stop
2343+
0.0, deficit_amount_after - deficit_rail_stop_amount
23422344
)
23432345
deficit_orders = build_deficit_orders(
23442346
shares_after,
@@ -2356,7 +2358,7 @@ def build_deficit_orders(
23562358
)
23572359
elif deficit_gate:
23582360
rebalance_mode = "deficit"
2359-
deficit_needed = max(0.0, -excess_cash - regime_rebalance.deficit_rail_stop)
2361+
deficit_needed = max(0.0, -excess_cash - deficit_rail_stop_amount)
23602362
deficit_orders = build_deficit_orders(
23612363
current_positions,
23622364
deficit_needed,
@@ -2452,10 +2454,10 @@ def build_deficit_orders(
24522454
f"cooldown_ok={cooldown_ok} mode={rebalance_mode} "
24532455
f"flow_gate={flow_gate} deficit_gate={deficit_gate} "
24542456
f"flow_active={flow_active} deficit_active={deficit_active} "
2455-
f"flow_min={dfmt(regime_rebalance.flow_trade_min)} "
2456-
f"flow_stop={dfmt(regime_rebalance.flow_trade_stop)} "
2457-
f"deficit_start={dfmt(regime_rebalance.deficit_rail_start)} "
2458-
f"deficit_stop={dfmt(regime_rebalance.deficit_rail_stop)} "
2457+
f"flow_min={pfmt(regime_rebalance.flow_trade_min)}({dfmt(flow_trade_min_amount)}) "
2458+
f"flow_stop={pfmt(regime_rebalance.flow_trade_stop)}({dfmt(flow_trade_stop_amount)}) "
2459+
f"deficit_start={pfmt(regime_rebalance.deficit_rail_start)}({dfmt(deficit_rail_start_amount)}) "
2460+
f"deficit_stop={pfmt(regime_rebalance.deficit_rail_stop)}({dfmt(deficit_rail_stop_amount)}) "
24592461
f"excess_cash={dfmt(excess_cash)}"
24602462
+ (
24612463
" "

0 commit comments

Comments
 (0)