Skip to content

Commit 5c6944e

Browse files
committed
fix(migration): enforce v2 strategy semantics
1 parent 6a8d889 commit 5c6944e

2 files changed

Lines changed: 82 additions & 99 deletions

File tree

tests/test_config_migration.py

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ def test_migration_report_contains_mapping_and_warnings_sections(
628628
assert "`symbols` -> `portfolio.symbols`" in report
629629

630630

631-
def test_migration_regime_enabled_non_shares_only_uses_explicit_stage_plan() -> None:
631+
def test_migration_regime_enabled_non_shares_only_uses_strategy_plan() -> None:
632632
raw = """
633633
[account]
634634
number = "DUX"
@@ -658,24 +658,13 @@ def test_migration_regime_enabled_non_shares_only_uses_explicit_stage_plan() ->
658658
migrated = migrate_v1_to_v2(raw)
659659
parsed = tomlkit.parse(migrated.migrated_text).unwrap()
660660
run = parsed["run"]
661-
assert "stages" in run
662-
assert "strategies" not in run
663-
stage_ids = [stage["id"] for stage in run["stages"]]
664-
assert stage_ids == [
665-
"options_write_puts",
666-
"options_write_calls",
667-
"equity_regime_rebalance",
668-
"equity_buy_rebalance",
669-
"equity_sell_rebalance",
670-
"options_roll_positions",
671-
"options_close_positions",
672-
]
673-
assert any("explicit run.stages" in warning for warning in migrated.warnings), (
674-
migrated.warnings
675-
)
661+
assert "strategies" in run
662+
assert "stages" not in run
663+
assert run["strategies"] == ["regime_rebalance"]
664+
assert not any("explicit run.stages" in warning for warning in migrated.warnings)
676665

677666

678-
def test_migration_regime_shares_only_excludes_option_stages() -> None:
667+
def test_migration_regime_shares_only_uses_strategy_plan() -> None:
679668
raw = """
680669
[account]
681670
number = "DUX"
@@ -704,16 +693,74 @@ def test_migration_regime_shares_only_excludes_option_stages() -> None:
704693
"""
705694
migrated = migrate_v1_to_v2(raw)
706695
parsed = tomlkit.parse(migrated.migrated_text).unwrap()
707-
stage_ids = [stage["id"] for stage in parsed["run"]["stages"]]
708-
assert "options_write_puts" not in stage_ids
709-
assert "options_write_calls" not in stage_ids
710-
assert "options_roll_positions" not in stage_ids
711-
assert "options_close_positions" not in stage_ids
712-
assert stage_ids == [
713-
"equity_regime_rebalance",
714-
"equity_buy_rebalance",
715-
"equity_sell_rebalance",
716-
]
696+
run = parsed["run"]
697+
assert "strategies" in run
698+
assert "stages" not in run
699+
assert run["strategies"] == ["regime_rebalance"]
700+
701+
702+
def test_migration_cash_management_enabled_does_not_force_wheel_strategy() -> None:
703+
raw = """
704+
[account]
705+
number = "DUX"
706+
margin_usage = 0.5
707+
708+
[option_chains]
709+
expirations = 4
710+
strikes = 10
711+
712+
[target]
713+
dte = 30
714+
minimum_open_interest = 5
715+
716+
[roll_when]
717+
dte = 7
718+
719+
[ibc]
720+
721+
[cash_management]
722+
enabled = true
723+
cash_fund = "SGOV"
724+
target_cash_balance = 1000
725+
726+
[symbols.AAA]
727+
weight = 1.0
728+
"""
729+
migrated = migrate_v1_to_v2(raw)
730+
parsed = tomlkit.parse(migrated.migrated_text).unwrap()
731+
run = parsed["run"]
732+
assert run["strategies"] == ["cash_management"]
733+
734+
735+
def test_migration_vix_enabled_does_not_force_wheel_strategy() -> None:
736+
raw = """
737+
[account]
738+
number = "DUX"
739+
margin_usage = 0.5
740+
741+
[option_chains]
742+
expirations = 4
743+
strikes = 10
744+
745+
[target]
746+
dte = 30
747+
minimum_open_interest = 5
748+
749+
[roll_when]
750+
dte = 7
751+
752+
[ibc]
753+
754+
[vix_call_hedge]
755+
enabled = true
756+
757+
[symbols.AAA]
758+
weight = 1.0
759+
"""
760+
migrated = migrate_v1_to_v2(raw)
761+
parsed = tomlkit.parse(migrated.migrated_text).unwrap()
762+
run = parsed["run"]
763+
assert run["strategies"] == ["vix_call_hedge"]
717764

718765

719766
def test_golden_migration_output_subset_for_stable_shape() -> None:

thetagang/config_migration/migrate_v1_to_v2.py

Lines changed: 8 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,7 @@
66

77
import tomlkit
88

9-
from thetagang.config import (
10-
CANONICAL_STAGE_ORDER,
11-
DEFAULT_RUN_STRATEGIES,
12-
STAGE_KIND_BY_ID,
13-
)
9+
from thetagang.config import DEFAULT_RUN_STRATEGIES
1410
from thetagang.legacy_config import LegacyConfig, normalize_config
1511

1612
RUNTIME_KEYS = [
@@ -336,25 +332,8 @@ def _strip_legacy_symbol_strategy_keys(symbols_doc: Any) -> None:
336332

337333

338334
def _infer_run_plan(
339-
validated_legacy: Dict[str, Any], warnings: List[str]
335+
validated_legacy: Dict[str, Any], _warnings: List[str]
340336
) -> Dict[str, Any]:
341-
regime = validated_legacy.get("regime_rebalance", {})
342-
regime_enabled = (
343-
bool(regime.get("enabled", False)) if isinstance(regime, dict) else False
344-
)
345-
shares_only = (
346-
bool(regime.get("shares_only", False)) if isinstance(regime, dict) else False
347-
)
348-
349-
if regime_enabled:
350-
stage_ids = _legacy_stage_ids(
351-
regime_shares_only=shares_only, config=validated_legacy
352-
)
353-
warnings.append(
354-
"regime_rebalance.enabled=true migrated to explicit run.stages to preserve legacy execution order."
355-
)
356-
return {"stages": _build_stage_plan(stage_ids)}
357-
358337
return {"strategies": _infer_run_strategies(validated_legacy)}
359338

360339

@@ -366,7 +345,8 @@ def _infer_run_strategies(validated_legacy: Dict[str, Any]) -> List[str]:
366345
)
367346
if regime_enabled:
368347
out.append("regime_rebalance")
369-
else:
348+
wheel_enabled = not regime_enabled
349+
if wheel_enabled:
370350
out.append("wheel")
371351

372352
vix = validated_legacy.get("vix_call_hedge", {})
@@ -377,51 +357,7 @@ def _infer_run_strategies(validated_legacy: Dict[str, Any]) -> List[str]:
377357
if isinstance(cash, dict) and bool(cash.get("enabled", False)):
378358
out.append("cash_management")
379359

380-
if not out:
381-
return list(DEFAULT_RUN_STRATEGIES)
382-
return out
383-
384-
385-
def _legacy_stage_ids(*, regime_shares_only: bool, config: Dict[str, Any]) -> List[str]:
386-
enabled = set()
387-
if not regime_shares_only:
388-
enabled.update(
389-
{
390-
"options_write_puts",
391-
"options_write_calls",
392-
"options_roll_positions",
393-
"options_close_positions",
394-
}
395-
)
396-
enabled.update(
397-
{
398-
"equity_regime_rebalance",
399-
"equity_buy_rebalance",
400-
"equity_sell_rebalance",
401-
}
402-
)
403-
vix = config.get("vix_call_hedge", {})
404-
if isinstance(vix, dict) and bool(vix.get("enabled", False)):
405-
enabled.add("post_vix_call_hedge")
406-
407-
cash = config.get("cash_management", {})
408-
if isinstance(cash, dict) and bool(cash.get("enabled", False)):
409-
enabled.add("post_cash_management")
410-
411-
return [stage_id for stage_id in CANONICAL_STAGE_ORDER if stage_id in enabled]
412-
413-
414-
def _build_stage_plan(stage_ids: List[str]) -> List[Dict[str, Any]]:
415-
plan: List[Dict[str, Any]] = []
416-
previous_id: str | None = None
417-
for stage_id in stage_ids:
418-
stage = {
419-
"id": stage_id,
420-
"kind": STAGE_KIND_BY_ID[stage_id],
421-
"enabled": True,
422-
}
423-
if previous_id is not None:
424-
stage["depends_on"] = [previous_id]
425-
plan.append(stage)
426-
previous_id = stage_id
427-
return plan
360+
explicitly_enabled = [strategy_id for strategy_id in out if strategy_id != "wheel"]
361+
if explicitly_enabled:
362+
return explicitly_enabled
363+
return list(DEFAULT_RUN_STRATEGIES)

0 commit comments

Comments
 (0)