Alpaca's rebalancer CronJob has been suspended with 401'd keys since Task 4's venue consolidation; IBKR paper is already the live forward-track venue for the multi-strat book. This removes the dead code/config/manifests instead of leaving them to rot, and relabels the multistrat/multistrat_exec registry venues from the vestigial "alpaca-paper" to "ibkr-equity" (they execute on IBKR paper, not Alpaca — exec_venue itself is still computed dynamically at runtime). - Delete src/fxhnt/adapters/broker/alpaca.py, AlpacaSettings + the `alpaca` config field; move the one real setting it carried (min_order_notional, a generic "skip sub-min-notional dust" floor for any future fractional-capable broker) onto ExecutionSettings. - Remove the `--broker` option and its alpaca branches from `execute` and `execute-multistrat` (cli.py) — IBKR is now implicit; IbkrBroker was already the module-level import, so the branch was pure dead code once Alpaca is gone. - Delete infra/k8s/jobs/fxhnt-alpaca-rebalancer.yaml and infra/k8s/secrets/alpaca-credentials.yaml; delete the superseded infra/k8s/services/multistrat-rebalancer.yaml (PVC + NetworkPolicy + CronJob), replaced by fxhnt-ibkr-rebalancer. Drop the now-invalid `--broker ibkr` argument from fxhnt-ibkr-rebalancer.yaml and fxhnt-ucits-rebalancer.yaml (the flag no longer exists). - Delete tests/integration/test_alpaca_broker.py (tested the deleted adapter directly); add tests/unit/test_no_alpaca.py as a standing guard (no "alpaca" substring anywhere in src/fxhnt + IbkrBroker still imports). - Update test_registry_multistrat_exec.py + test_cockpit_ssot.py (venue label) and test_cli_execute_helpers.py (drop the broker_name kwarg and AlpacaBroker fake, use IbkrBroker instead) to match. MANUAL cluster ops (not run by CI, record only): kubectl delete cronjob/fxhnt-alpaca-rebalancer secret/alpaca-credentials -n foxhunt kubectl delete cronjob/multistrat-rebalancer networkpolicy/multistrat-rebalancer pvc/multistrat-state -n foxhunt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
4.5 KiB
Python
110 lines
4.5 KiB
Python
def test_venue_string():
|
|
from fxhnt.cli import _exec_venue_string
|
|
assert _exec_venue_string("ibkr", True) == "ibkr-paper"
|
|
assert _exec_venue_string("ibkr", False) == "ibkr-live"
|
|
assert _exec_venue_string("bybit", True) == "bybit-paper"
|
|
|
|
|
|
def test_resolve_paper_envelope():
|
|
import pytest
|
|
import typer
|
|
|
|
from fxhnt.cli import _resolve_paper_envelope
|
|
assert _resolve_paper_envelope(1_000_000.0, 0.0) == 1_000_000.0 # flag wins
|
|
assert _resolve_paper_envelope(0.0, 500_000.0) == 500_000.0 # config fallback
|
|
assert _resolve_paper_envelope(0.0, 0.0) == 0.0 # off
|
|
# a negative flag is an operator typo, not "unset" — must be rejected, not silently fall back to the
|
|
# config default (which would hide the typo behind a seemingly-valid run).
|
|
with pytest.raises(typer.BadParameter):
|
|
_resolve_paper_envelope(-5.0, 500_000.0)
|
|
|
|
|
|
def test_execute_bybit_command_registered():
|
|
from fxhnt.cli import app
|
|
names = {c.name for c in app.registered_commands}
|
|
assert "execute-bybit" in names
|
|
|
|
|
|
def test_execute_bybit_paper_envelope_refused_is_distinct_from_traceback(monkeypatch, tmp_path):
|
|
"""A paper-envelope ValueError refusal from `run_bybit_testnet` must surface as a clear, distinct
|
|
message + exit code 1 — NOT a raw traceback (the pre-fix behavior: `execute_bybit` had no try/except
|
|
at all, so the ValueError propagated uncaught)."""
|
|
from typer.testing import CliRunner
|
|
|
|
import fxhnt.cli as climod
|
|
|
|
dsn = f"sqlite:///{tmp_path / 'op.db'}"
|
|
monkeypatch.setenv("FXHNT_OPERATIONAL_DSN", dsn)
|
|
climod.get_settings.cache_clear()
|
|
|
|
def _boom(settings, *, paper_envelope, execute, log):
|
|
raise ValueError("paper-envelope refused: bybit account is not testnet/paper")
|
|
|
|
monkeypatch.setattr("fxhnt.application.bybit_testnet_run.run_bybit_testnet", _boom)
|
|
|
|
res = CliRunner().invoke(climod.app, ["execute-bybit", "--execute", "--paper-envelope", "5"])
|
|
climod.get_settings.cache_clear()
|
|
assert res.exit_code == 1, res.output
|
|
assert "paper-envelope refused" in res.output
|
|
assert "Traceback" not in res.output
|
|
|
|
|
|
def test_execute_multistrat_paper_envelope_refused_is_distinct_from_broker_error(monkeypatch, tmp_path, capsys):
|
|
"""A paper-envelope ValueError refusal from `plan_and_record` must be caught distinctly (BEFORE the
|
|
generic `except Exception`) and printed as a clear safety-refusal message — NOT the misleading
|
|
connectivity framing ('broker error: ... reachable? ... creds/gateway set?') the generic handler prints
|
|
for every other exception. No live broker/network: YahooDataProvider and IbkrBroker are faked."""
|
|
import numpy as np
|
|
import pytest
|
|
import typer
|
|
|
|
import fxhnt.cli as climod
|
|
from fxhnt.domain.models import PriceSeries
|
|
from fxhnt.ports.broker import AccountState
|
|
|
|
dsn = f"sqlite:///{tmp_path / 'op.db'}"
|
|
monkeypatch.setenv("FXHNT_OPERATIONAL_DSN", dsn)
|
|
climod.get_settings.cache_clear()
|
|
|
|
# 120 synthetic daily bars per fund instrument (needs >= 66 for target_weights to be non-empty).
|
|
rng = np.random.default_rng(7)
|
|
n = 120
|
|
dates = tuple(f"2026-{1 + i // 28:02d}-{1 + i % 28:02d}" for i in range(n))
|
|
|
|
class _FakeData:
|
|
def fetch(self, market): # noqa: ANN001, ANN201
|
|
close = 100.0 * np.cumprod(1 + rng.normal(0, 0.01, size=n))
|
|
return PriceSeries(market=market, dates=dates, close=close)
|
|
|
|
monkeypatch.setattr(climod, "YahooDataProvider", lambda: _FakeData())
|
|
|
|
class _FakeBroker:
|
|
name = "ibkr"
|
|
supports_fractional = False
|
|
|
|
def __enter__(self): # noqa: ANN201
|
|
return self
|
|
|
|
def __exit__(self, *a): # noqa: ANN002, ANN201
|
|
return False
|
|
|
|
def account_state(self): # noqa: ANN201
|
|
return AccountState(nlv=100_000.0, cash=100_000.0, is_paper=False)
|
|
|
|
monkeypatch.setattr(climod, "IbkrBroker", lambda *a, **k: _FakeBroker())
|
|
|
|
def _boom(**kwargs): # noqa: ANN003, ANN201
|
|
raise ValueError("paper-envelope refused: account is not paper (real-capital exposure guard)")
|
|
|
|
monkeypatch.setattr("fxhnt.application.multistrat_exec_record.plan_and_record", _boom)
|
|
|
|
with pytest.raises(typer.Exit) as ei:
|
|
climod.execute_multistrat(do_execute=True, live=False, max_gross=2.0,
|
|
paper_envelope=100.0, venue_kind="us")
|
|
climod.get_settings.cache_clear()
|
|
assert ei.value.exit_code == 1
|
|
out = capsys.readouterr().out
|
|
assert "paper-envelope refused" in out
|
|
assert "broker error" not in out
|
|
assert "reachable?" not in out
|