The long-only crypto_tstrend sleeve ignored the perp funding a long book pays (~4.1%/yr drag in a positive-funding bull), overstating the deploy book. TrendRunner now takes an optional funding panel and nets Σ position·funding[nxt] per booked day; weights_and_contributions() nets it per-symbol too so the reconciliation invariant (Σ weight·return == sleeve return) still holds — no live-book/returns-book drift. Both callers (bybit_book_eval._tstrend_series, bybit_edges_eval._tstrend_metrics) and the paper-weights reconciliation now pass store.crypto_funding_panel(). Turnover stays a separate downstream haircut; funding is per-holding so it belongs in the per-symbol contribution. Book Sharpe 1.93 -> 1.85 (honest); long-only still beats long-short (1.49). 1807 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
424 lines
20 KiB
Python
424 lines
20 KiB
Python
"""READ-ONLY, MEMORY-BOUNDED risk-managed BOOK evaluator on the Bybit liquid sleeves.
|
|
|
|
Seeds a synthetic in-memory `TimescaleFeatureStore("sqlite://", table="bybit_features")` and asserts:
|
|
* `sleeve_returns_from_store(store, "tstrend")` yields a per-day return series; a `universe` restriction
|
|
limits the symbols that load (the panel READ is restricted to the liquid set, not the whole table);
|
|
* `risk_managed_book_from_store` on a multi-sleeve fixture returns SANE book metrics, and the LIVE overlay
|
|
changes the result vs the naive equal-weight (leverage != 1 / a kill or a different curve);
|
|
* the evaluator is READ-ONLY — it never calls a write/persist method on the store (write-tripwire proxy);
|
|
* MEMORY-BOUND: the universe-restricted store reads via `read_panel(liquid_symbols, ...)`, NOT the
|
|
full-universe `crypto_close_panel()` — proven by a tripwire that fails if the full-table panel is read;
|
|
* CLI smoke (mocked store) prints the risk-managed book table.
|
|
NO network — the store is in-memory SQLite; external inputs are injected.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
import pytest
|
|
|
|
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
|
|
from fxhnt.application.bybit_book_eval import (
|
|
risk_managed_book_from_store,
|
|
sleeve_returns_from_store,
|
|
)
|
|
|
|
_DAY = 86_400
|
|
|
|
|
|
def _seed_momentum(store: TimescaleFeatureStore, *, n_symbols: int = 8, days: int = 260,
|
|
drift: float = 0.01) -> None:
|
|
"""Daily closes for `n_symbols` with a persistent up/down momentum pattern + small wobble (finite vol),
|
|
>= 130 closes so the TSMOM min-history gate clears and the long/flat book books returns."""
|
|
for idx in range(n_symbols):
|
|
sign = 1.0 if idx % 2 == 0 else -1.0
|
|
px = 100.0
|
|
rows = []
|
|
for d in range(days):
|
|
wobble = 0.003 * math.cos(0.4 * d + idx)
|
|
px *= (1.0 + sign * drift + wobble)
|
|
rows.append((d * _DAY, {"close": px}))
|
|
store.write_features(f"SYM{idx:02d}USDT", rows)
|
|
|
|
|
|
def _seed_carry(store: TimescaleFeatureStore, *, days: int = 260) -> None:
|
|
"""Funding dispersion + flat perp/spot closes → a positive carry curve for the xsfunding sleeve."""
|
|
funding = {"AAAUSDT": 0.001, "BBBUSDT": 0.001, "CCCUSDT": 0.001,
|
|
"XXXUSDT": -0.001, "YYYUSDT": -0.001, "ZZZUSDT": -0.001}
|
|
for idx, (sym, fund) in enumerate(funding.items()):
|
|
rows = []
|
|
for d in range(days):
|
|
f = fund + 0.0002 * math.cos(0.5 * d + idx)
|
|
rows.append((d * _DAY, {"funding": f, "close": 100.0, "spot_close": 100.0}))
|
|
store.write_features(sym, rows)
|
|
|
|
|
|
def _seed_turnover(store: TimescaleFeatureStore, symbols: list[str], *, days: int = 260,
|
|
dollar_vol: float = 50_000_000.0) -> None:
|
|
"""A flat turnover (USD) feature so `liquid_universe` admits these symbols (median >= floor)."""
|
|
for sym in symbols:
|
|
store.write_features(sym, [(d * _DAY, {"turnover": dollar_vol}) for d in range(days)])
|
|
|
|
|
|
def _seed_positioning(store: TimescaleFeatureStore, *, days: int = 80) -> None:
|
|
"""Seed `long_ratio` + `close` so the CONTRARIAN positioning edge is profitable: the over-LONG coin
|
|
(high buyRatio) FALLS next day (crowd wrong → fade pays). Two coins, oscillating positioning. The
|
|
positioning sleeve reads `long_ratio` + `close` (memory-bounded via read_panel)."""
|
|
px = {"PAAUSDT": 100.0, "PBBUSDT": 100.0}
|
|
for d in range(days):
|
|
crowd_long_a = (d % 2 == 0)
|
|
ra = 0.70 if crowd_long_a else 0.30
|
|
rb = 0.30 if crowd_long_a else 0.70
|
|
store.write_features("PAAUSDT", [(d * _DAY, {"long_ratio": ra, "close": px["PAAUSDT"]})])
|
|
store.write_features("PBBUSDT", [(d * _DAY, {"long_ratio": rb, "close": px["PBBUSDT"]})])
|
|
px["PAAUSDT"] *= (1.0 - 0.004) if crowd_long_a else (1.0 + 0.004)
|
|
px["PBBUSDT"] *= (1.0 + 0.004) if crowd_long_a else (1.0 - 0.004)
|
|
d = days
|
|
store.write_features("PAAUSDT", [(d * _DAY, {"long_ratio": 0.5, "close": px["PAAUSDT"]})])
|
|
store.write_features("PBBUSDT", [(d * _DAY, {"long_ratio": 0.5, "close": px["PBBUSDT"]})])
|
|
|
|
|
|
class _WriteTripwireStore:
|
|
"""Read-only proxy: forwards reads, but ANY write/persist call trips. Proves the READ-ONLY guarantee."""
|
|
|
|
_FORBIDDEN = frozenset({
|
|
"write_features", "write_features_bulk", "upsert_feature_rows", "upsert_membership",
|
|
"replace_positions", "upsert_nav", "upsert_sleeve_ret", "replace_shadow_positions",
|
|
"replace_trades", "_create_schema", "_bulk_upsert",
|
|
})
|
|
|
|
def __init__(self, inner: TimescaleFeatureStore) -> None:
|
|
object.__setattr__(self, "_inner", inner)
|
|
|
|
def __getattr__(self, name: str):
|
|
if name in _WriteTripwireStore._FORBIDDEN:
|
|
raise AssertionError(f"READ-ONLY violation: evaluator called write method {name!r}")
|
|
return getattr(self._inner, name)
|
|
|
|
|
|
class _FullPanelTripwireStore:
|
|
"""Read-only proxy that FAILS if the full-table panel reads (`crypto_close_panel` etc.) are called, but
|
|
forwards the symbol-restricted `read_panel`. Proves the universe path NEVER materialises the whole
|
|
universe — the memory-bound guarantee. Records which symbol sets `read_panel` was asked for."""
|
|
|
|
_FULL_PANELS = frozenset({"crypto_close_panel", "crypto_funding_panel", "crypto_spot_panel"})
|
|
|
|
def __init__(self, inner: TimescaleFeatureStore) -> None:
|
|
object.__setattr__(self, "_inner", inner)
|
|
object.__setattr__(self, "read_panel_calls", [])
|
|
|
|
def read_panel(self, symbols, feature): # noqa: ANN001, ANN201
|
|
self.read_panel_calls.append((tuple(symbols), feature))
|
|
return self._inner.read_panel(symbols, feature)
|
|
|
|
def __getattr__(self, name: str):
|
|
if name in _FullPanelTripwireStore._FULL_PANELS:
|
|
raise AssertionError(
|
|
f"MEMORY-BOUND violation: evaluator read the FULL-universe panel {name!r} "
|
|
"instead of a universe-restricted read_panel")
|
|
return getattr(self._inner, name)
|
|
|
|
|
|
# --- sleeve_returns_from_store ------------------------------------------------------------------
|
|
|
|
def test_sleeve_returns_tstrend_is_a_per_day_series() -> None:
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(store)
|
|
series = sleeve_returns_from_store(store, "tstrend", cost_bps=0.0)
|
|
store.close()
|
|
|
|
assert isinstance(series, dict)
|
|
assert len(series) > 0
|
|
# keys are epoch-days (ints), values finite floats.
|
|
for d, r in series.items():
|
|
assert isinstance(d, int)
|
|
assert math.isfinite(r)
|
|
|
|
|
|
def test_tstrend_sleeve_is_net_of_perp_funding() -> None:
|
|
"""The crypto_tstrend sleeve charges the perp funding a LONG book pays. Seeding positive funding on the
|
|
(long-held) trending symbols lowers the sleeve's cumulative return vs. the identical panel with none —
|
|
the honest deploy accounting: a long-biased perp trend book bleeds funding, it isn't free price-trend."""
|
|
def _series(with_funding: bool) -> dict[int, float]:
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(store)
|
|
if with_funding:
|
|
for idx in range(8): # +10 bp/day funding on every seeded symbol (longs pay it)
|
|
store.write_features(f"SYM{idx:02d}USDT",
|
|
[(d * _DAY, {"funding": 0.001}) for d in range(260)])
|
|
s = sleeve_returns_from_store(store, "tstrend", cost_bps=0.0)
|
|
store.close()
|
|
return s
|
|
|
|
plain, funded = _series(False), _series(True)
|
|
assert sum(plain.values()) > sum(funded.values()) # funding is a strict net drag on the long book
|
|
|
|
|
|
def test_sleeve_returns_accepts_sleeve_name_alias() -> None:
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(store)
|
|
by_edge = sleeve_returns_from_store(store, "tstrend")
|
|
by_sleeve = sleeve_returns_from_store(store, "crypto_tstrend") # paper_sleeves vocabulary
|
|
store.close()
|
|
assert by_edge == by_sleeve
|
|
|
|
|
|
def test_sleeve_returns_universe_restriction_limits_symbols_loaded() -> None:
|
|
"""The universe path reads via read_panel(liquid_symbols, ...) and NEVER the full-table panel."""
|
|
inner = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(inner, n_symbols=8)
|
|
guarded = _FullPanelTripwireStore(inner)
|
|
|
|
universe = {"SYM00USDT", "SYM02USDT", "SYM04USDT"}
|
|
series = sleeve_returns_from_store(guarded, "tstrend", universe=universe)
|
|
inner.close()
|
|
|
|
# The series computed (universe non-empty) AND the only reads were symbol-restricted read_panel calls.
|
|
assert isinstance(series, dict)
|
|
assert guarded.read_panel_calls, "expected a universe-restricted read_panel, none happened"
|
|
for symbols, _feature in guarded.read_panel_calls:
|
|
# Every read_panel was asked for a SUBSET of the liquid universe — never the full 8-symbol table.
|
|
assert set(symbols) <= universe
|
|
assert set(symbols), "read_panel must be given the liquid symbol set, not empty/all"
|
|
|
|
|
|
def test_sleeve_returns_xsfunding_series() -> None:
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_carry(store)
|
|
series = sleeve_returns_from_store(store, "xsfunding", cost_bps=5.5)
|
|
store.close()
|
|
assert len(series) > 0
|
|
for d, r in series.items():
|
|
assert isinstance(d, int)
|
|
assert math.isfinite(r)
|
|
|
|
|
|
def test_sleeve_returns_positioning_is_a_per_day_series() -> None:
|
|
"""positioning is a known edge: a `long_ratio`+`close` fixture yields a sane per-day return series."""
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_positioning(store)
|
|
series = sleeve_returns_from_store(store, "positioning", cost_bps=0.0)
|
|
store.close()
|
|
assert isinstance(series, dict)
|
|
assert len(series) > 0
|
|
for d, r in series.items():
|
|
assert isinstance(d, int)
|
|
assert math.isfinite(r)
|
|
|
|
|
|
def test_sleeve_returns_positioning_universe_restricted_reads_only() -> None:
|
|
"""The positioning sleeve reads via read_panel(liquid_symbols, ...) — NEVER the full-table panel."""
|
|
inner = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_positioning(inner)
|
|
guarded = _FullPanelTripwireStore(inner)
|
|
universe = {"PAAUSDT", "PBBUSDT"}
|
|
series = sleeve_returns_from_store(guarded, "positioning", universe=universe, cost_bps=0.0)
|
|
inner.close()
|
|
assert isinstance(series, dict)
|
|
assert len(series) > 0
|
|
assert guarded.read_panel_calls, "expected a universe-restricted read_panel, none happened"
|
|
for symbols, _feature in guarded.read_panel_calls:
|
|
assert set(symbols) <= universe
|
|
assert set(symbols)
|
|
|
|
|
|
def test_sleeve_returns_positioning_empty_when_no_ratio_data() -> None:
|
|
"""No `long_ratio` (close-only fixture) → positioning is uncomputable → empty series, never fabricated."""
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(store, n_symbols=4)
|
|
series = sleeve_returns_from_store(store, "positioning")
|
|
store.close()
|
|
assert series == {}
|
|
|
|
|
|
def test_sleeve_returns_unknown_edge_raises() -> None:
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
with pytest.raises(ValueError):
|
|
sleeve_returns_from_store(store, "not_an_edge")
|
|
store.close()
|
|
|
|
|
|
# --- risk_managed_book_from_store ---------------------------------------------------------------
|
|
|
|
def test_risk_managed_book_has_sane_metrics() -> None:
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(store, n_symbols=8, days=260)
|
|
_seed_carry(store, days=260)
|
|
book = risk_managed_book_from_store(store, capital=100_000.0, cost_bps=5.5)
|
|
store.close()
|
|
|
|
assert book["days"] > 0
|
|
assert set(book["sleeves"]) <= {"crypto_tstrend", "unlock", "xsfunding"}
|
|
assert "crypto_tstrend" in book["sleeves"]
|
|
assert "xsfunding" in book["sleeves"]
|
|
assert math.isfinite(book["sharpe"])
|
|
assert math.isfinite(book["cagr"])
|
|
assert book["max_dd"] <= 0.0
|
|
# leverage stats present + finite; killed_days a count.
|
|
for k in ("avg", "min", "max"):
|
|
assert math.isfinite(book["leverage_stats"][k])
|
|
assert book["killed_days"] >= 0
|
|
# standalone Sharpe reported per active sleeve.
|
|
assert set(book["per_sleeve_standalone_sharpe"]) == set(book["sleeves"])
|
|
# final-day post-overlay weights present for the active sleeves.
|
|
assert book["per_sleeve_weights"]
|
|
|
|
|
|
def test_overlay_changes_result_vs_naive_equal_weight() -> None:
|
|
"""The LIVE overlay (vol-target leverage + ISV weights + killswitch) must move the book away from the
|
|
naive equal-weight baseline — same returns, different curve (leverage != 1 and/or different total)."""
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(store, n_symbols=8, days=260)
|
|
_seed_carry(store, days=260)
|
|
book = risk_managed_book_from_store(store, capital=100_000.0, cost_bps=0.0)
|
|
store.close()
|
|
|
|
naive = book["naive_equal_weight"]
|
|
assert naive["days"] > 0
|
|
# The overlay applies leverage (vol-target) so the average leverage is not pinned at exactly 1.0, OR
|
|
# the risk-managed total return differs from the equal-weight baseline. Either proves the overlay ran.
|
|
overlay_moved = (abs(book["leverage_stats"]["avg"] - 1.0) > 1e-6
|
|
or abs(book["total_return"] - naive["total_return"]) > 1e-9)
|
|
assert overlay_moved, (book["leverage_stats"], book["total_return"], naive["total_return"])
|
|
|
|
|
|
def test_risk_managed_book_includes_positioning_as_fourth_sleeve() -> None:
|
|
"""With tstrend + xsfunding + positioning data present the book carries all three (the default sleeve set
|
|
now includes positioning), and the correlation matrix is reported for the active set."""
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(store, n_symbols=8, days=260)
|
|
_seed_carry(store, days=260)
|
|
_seed_positioning(store, days=260)
|
|
book = risk_managed_book_from_store(store, capital=100_000.0, cost_bps=5.5)
|
|
store.close()
|
|
|
|
assert "positioning" in book["sleeves"]
|
|
assert {"crypto_tstrend", "xsfunding", "positioning"} <= set(book["sleeves"])
|
|
# correlation matrix is square over the active sleeves, diagonal 1.0.
|
|
cm = book["correlation_matrix"]
|
|
assert set(cm) == set(book["sleeves"])
|
|
for si in book["sleeves"]:
|
|
assert set(cm[si]) == set(book["sleeves"])
|
|
assert cm[si][si] == 1.0
|
|
for sj in book["sleeves"]:
|
|
v = cm[si][sj]
|
|
assert v is None or (-1.0 - 1e-9 <= v <= 1.0 + 1e-9)
|
|
# standalone Sharpe + final weights reported for every active sleeve incl. positioning.
|
|
assert set(book["per_sleeve_standalone_sharpe"]) == set(book["sleeves"])
|
|
|
|
|
|
def test_naive_eqwt_sharpe_improves_with_uncorrelated_positive_fourth_sleeve() -> None:
|
|
"""Diversification: adding an UNCORRELATED, positive 4th sleeve must NOT lower the naive equal-weight
|
|
Sharpe (it should help). Compare the 3-sleeve book vs the 4-sleeve book on the SAME fixture."""
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(store, n_symbols=8, days=260)
|
|
_seed_carry(store, days=260)
|
|
_seed_positioning(store, days=260)
|
|
|
|
three = risk_managed_book_from_store(
|
|
store, sleeves=["crypto_tstrend", "xsfunding"], capital=100_000.0, cost_bps=0.0)
|
|
four = risk_managed_book_from_store(
|
|
store, sleeves=["crypto_tstrend", "xsfunding", "positioning"], capital=100_000.0, cost_bps=0.0)
|
|
store.close()
|
|
|
|
# positioning is genuinely the added sleeve.
|
|
assert "positioning" in four["sleeves"] and "positioning" not in three["sleeves"]
|
|
# positioning is uncorrelated with the others (|corr| small) — the diversification premise.
|
|
cm = four["correlation_matrix"]
|
|
for other in ("crypto_tstrend", "xsfunding"):
|
|
c = cm["positioning"][other]
|
|
assert c is None or abs(c) < 0.5, (other, c)
|
|
# And positioning is positive standalone (a real positive sleeve, not noise).
|
|
assert four["per_sleeve_standalone_sharpe"]["positioning"] > 0.0
|
|
# The naive equal-weight Sharpe does not DROP when the uncorrelated positive sleeve joins.
|
|
assert four["naive_equal_weight"]["sharpe"] >= three["naive_equal_weight"]["sharpe"] - 1e-9
|
|
|
|
|
|
def test_risk_managed_book_skips_positioning_gracefully_when_absent() -> None:
|
|
"""No `long_ratio` data → positioning is omitted, the book is the 3 (or fewer) available sleeves."""
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(store, n_symbols=8, days=260)
|
|
_seed_carry(store, days=260)
|
|
book = risk_managed_book_from_store(store, capital=100_000.0, cost_bps=5.5)
|
|
store.close()
|
|
assert "positioning" not in book["sleeves"]
|
|
assert book["days"] > 0 # the rest of the book still builds
|
|
|
|
|
|
def test_risk_managed_book_is_read_only() -> None:
|
|
inner = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(inner, n_symbols=8, days=260)
|
|
_seed_carry(inner, days=260)
|
|
guarded = _WriteTripwireStore(inner)
|
|
book = risk_managed_book_from_store(guarded, capital=100_000.0, cost_bps=5.5)
|
|
inner.close()
|
|
assert book["days"] > 0 # ran without tripping the write tripwire
|
|
|
|
|
|
def test_risk_managed_book_empty_store_reports_reason() -> None:
|
|
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
book = risk_managed_book_from_store(store)
|
|
store.close()
|
|
assert book["days"] == 0
|
|
assert book["sleeves"] == []
|
|
assert "reason" in book
|
|
|
|
|
|
def test_risk_managed_book_universe_restricted_reads_only() -> None:
|
|
"""End-to-end memory bound: with a universe, the WHOLE book build never reads a full-table panel."""
|
|
inner = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(inner, n_symbols=8, days=260)
|
|
guarded = _FullPanelTripwireStore(inner)
|
|
universe = {"SYM00USDT", "SYM02USDT", "SYM04USDT", "SYM06USDT"}
|
|
# Only tstrend (close-only fixture); must not raise the full-panel tripwire.
|
|
risk_managed_book_from_store(guarded, universe=universe, sleeves=["crypto_tstrend"])
|
|
inner.close()
|
|
assert guarded.read_panel_calls
|
|
for symbols, _feature in guarded.read_panel_calls:
|
|
assert set(symbols) <= universe
|
|
|
|
|
|
# --- CLI smoke (mocked store) -------------------------------------------------------------------
|
|
|
|
def test_cli_bybit_book_eval_prints_table(monkeypatch) -> None:
|
|
"""CLI smoke: a seeded in-memory store stands in for the live Bybit warehouse (no network); the command
|
|
prints the standalone-Sharpe rows + the RISK-MANAGED book table + the naive comparison."""
|
|
from typer.testing import CliRunner
|
|
|
|
import fxhnt.cli as cli
|
|
|
|
seeded = TimescaleFeatureStore("sqlite://", table="bybit_features")
|
|
_seed_momentum(seeded, n_symbols=8, days=260)
|
|
_seed_carry(seeded, days=260)
|
|
_seed_positioning(seeded, days=260) # the 4th sleeve (long_ratio + close)
|
|
# turnover so --liquid admits the seeded symbols (median >= floor).
|
|
syms = ([f"SYM{i:02d}USDT" for i in range(8)] + ["AAAUSDT", "BBBUSDT", "CCCUSDT",
|
|
"XXXUSDT", "YYYUSDT", "ZZZUSDT"]
|
|
+ ["PAAUSDT", "PBBUSDT"])
|
|
_seed_turnover(seeded, syms, days=260)
|
|
|
|
monkeypatch.setattr(
|
|
"fxhnt.adapters.warehouse.timescale_feature_store.TimescaleFeatureStore",
|
|
lambda *a, **k: seeded, raising=False)
|
|
|
|
class _NoSpot:
|
|
def daily_close(self, sym): # noqa: ANN001, ANN201
|
|
raise RuntimeError("no network in tests")
|
|
|
|
monkeypatch.setattr(
|
|
"fxhnt.adapters.data.binance_spot_history.BinanceSpotHistory", _NoSpot, raising=False)
|
|
|
|
result = CliRunner().invoke(cli.app, ["bybit-book-eval", "--liquid", "--min-dollar-vol", "10000000"])
|
|
seeded.close()
|
|
|
|
assert result.exit_code == 0, result.output
|
|
out_lower = result.output.lower()
|
|
assert "risk-managed book" in out_lower
|
|
assert "Sharpe" in result.output
|
|
assert "naive" in out_lower
|
|
# the positioning sleeve appears + the correlation matrix is printed (the diversification view).
|
|
# unlock has no calendar in this smoke, so the active book is tstrend + xsfunding + positioning (3).
|
|
assert "positioning" in out_lower
|
|
assert "correlation" in out_lower
|
|
assert "sleeves=3" in result.output
|