Two execute-CLI gaps from the C3 port:
- `execute_multistrat`'s generic `except Exception` caught the ValueError `plan_and_record` raises
when a paper-envelope is refused (account not paper) and printed it as a connectivity failure
("broker error: ... reachable? ... creds/gateway set?") — misleading for a deliberate safety
refusal. `execute_bybit` had no try/except at all, so the same refusal from `run_bybit_testnet`
surfaced as a raw traceback. Both commands now catch `ValueError` distinctly, print
"paper-envelope refused: <msg>", and exit 1 — same exit code and zero orders placed, just an
honest message.
- `_resolve_paper_envelope` silently fell back to the config default whenever the flag was <= 0.0,
so `--paper-envelope -5` looked identical to "unset" and hid an operator typo. A negative flag
now raises `typer.BadParameter` instead of falling through.
Extends test_cli_execute_helpers.py: the negative-envelope rejection, and two new tests (no live
broker — YahooDataProvider/AlpacaBroker faked, run_bybit_testnet faked) proving each command's
refuse path prints the distinct message and never reaches order placement.
110 lines
4.6 KiB
Python
110 lines
4.6 KiB
Python
def test_venue_string():
|
|
from fxhnt.cli import _exec_venue_string
|
|
assert _exec_venue_string("alpaca", True) == "alpaca-paper"
|
|
assert _exec_venue_string("ibkr", True) == "ibkr-paper"
|
|
assert _exec_venue_string("alpaca", False) == "alpaca-live"
|
|
|
|
|
|
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 AlpacaBroker 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 = "alpaca"
|
|
supports_fractional = True
|
|
|
|
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("fxhnt.adapters.broker.alpaca.AlpacaBroker", 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, broker_name="alpaca",
|
|
paper_envelope=100.0)
|
|
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
|