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