diff --git a/scripts/live/live_trend.py b/scripts/live/live_trend.py new file mode 100644 index 0000000..164dbce --- /dev/null +++ b/scripts/live/live_trend.py @@ -0,0 +1,78 @@ +"""TP01 LIVE — SHADOW MODE (Deribit mainnet, SOLA LETTURA, nessun ordine inviato). + +Valida l'esecuzione di TP01 a RISCHIO ZERO: gira il loop live completo contro dati/conto/posizioni +REALI del mainnet, calcola i target causali (stesso codice del backtest/paper), costruisce gli ordini +di ribilancio esatti — e li STAMPA invece di inviarli. Confronta i target col paper trader (parita'). + +Perche' non testnet: il testnet Cerbero/Deribit e' la causa del reset v2.0.0 (feed farlocco). La +validazione a rischio zero qui e' "shadow su mainnet reale in sola lettura"; il fill (slippage/fee) +si valida solo col micro-test mainnet a size minima, in un passo successivo. + +Logica condivisa con la dashboard in src/live/shadow.py (un solo codice, niente drift). + + uv run python scripts/live/live_trend.py # shadow su mainnet reale + uv run python scripts/live/live_trend.py --equity 2000 # forza la base di sizing + uv run python scripts/live/live_trend.py --no-net # offline: solo matematica + parita' +""" +from __future__ import annotations + +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.live.deribit import notional_to_amount +from src.live.shadow import shadow_report + + +def main(): + argv = sys.argv[1:] + offline = "--no-net" in argv + equity_override = float(argv[argv.index("--equity") + 1]) if "--equity" in argv else None + r = shadow_report(offline=offline, equity_override=equity_override) + + print("=" * 84) + print(" TP01 LIVE — SHADOW MODE (Deribit mainnet, SOLA LETTURA — NESSUN ORDINE INVIATO)") + print("=" * 84) + real_eq = r["real_equity"] + conto = f"${real_eq:,.2f}" if real_eq else r["eq_basis"] + print(f" ultima barra 1d chiusa : {r['last_data']}") + print(f" rete : {'mainnet via Cerbero MCP' if r['online'] else 'OFFLINE / fallback close'}") + print(f" prezzi mark : " + " | ".join(f"{a['asset']} ${a['mark']:,.1f} ({a['mark_src']})" for a in r["assets"])) + print(f" conto reale : {conto}") + print(f" posizioni reali : " + ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in r["assets"]) + f" ({r['pos_src']})") + print(f" base di sizing : ${r['equity']:,.2f} [{r['eq_basis']}]") + + print("\n PER ASSET (target causale @ ultima barra chiusa):") + for a in r["assets"]: + state = "FLAT" if abs(a["target"]) < 1e-9 else ("LONG" if a["target"] > 0 else "SHORT") + line = (f" {a['asset']:<3} {state:<5} target {a['target']:+.3f}x -> notional ${a['target_notional']:,.0f}" + f" (pos reale ${a['position_usd']:,.0f})") + o = a["order"] + if o: + print(line + f"\n -> ORDINE: {o['side'].upper()} {o['amount']:.0f} {a['instrument']} " + f"(market{', reduce_only' if o['reduce_only'] else ''}, delta ${o['delta_notional']:,.0f})") + else: + print(line + " -> nessun ordine (gia' a target / sotto-soglia)") + + print("\n PARITA' vs paper trader (target = current_target):") + if all(a["paper"] is None for a in r["assets"]): + print(" (paper non inizializzato: avvia scripts/live/paper_trend.py)") + else: + for a in r["assets"]: + print(f" {a['asset']}: paper {a['paper']:+.3f}x shadow {a['target']:+.3f}x -> {'OK' if a['parity'] else 'DIFFERISCE'}") + if not r["paper_aligned"]: + print(" NB paper non all'ultima barra -> avanzalo se i target differiscono") + + print("\n VERIFICA costruttore ordini (quantizzazione step/minimo):") + for inst, samples in (("BTC-PERPETUAL", [1000, 1005, 7, 250.4]), ("ETH-PERPETUAL", [1000, 0.4, 33.7])): + got = ", ".join(f"${s}->{notional_to_amount(inst, s):.0f}" for s in samples) + print(f" {inst}: {got}") + + print("\n => NESSUN ORDINE INVIATO (shadow). " + + (f"{len(r['orders'])} ordine/i costruito/i sopra." if r["orders"] else "Target flat: 0 ordini.")) + + +if __name__ == "__main__": + main() diff --git a/src/live/dashboard.py b/src/live/dashboard.py index 2304219..be670f0 100644 --- a/src/live/dashboard.py +++ b/src/live/dashboard.py @@ -15,6 +15,7 @@ sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np, pandas as pd from src.portfolio.portfolio import StrategyPortfolio, metrics, HOLDOUT from src.portfolio.sleeves import active_sleeves +from src.live.shadow import shadow_report from src.version import APP_VERSION PAPER = PROJECT_ROOT / "data" / "paper_portfolio" / "state.json" @@ -32,12 +33,16 @@ def build(): step = max(1, len(eq) // 400) spark = [(str(idx[i].date()), float(eq[i])) for i in range(0, len(eq), step)] paper = json.loads(PAPER.read_text()) if PAPER.exists() else None + try: + shadow = shadow_report() # mainnet sola lettura, best-effort + except Exception as e: + shadow = {"error": f"{type(e).__name__}: {e}"} data = dict( version=APP_VERSION, last_data=str(idx[-1].date()), full=bt["full"], holdout=bt["holdout"], weights=bt["weights"], per_sleeve=bt["per_sleeve"], yearly=bt["yearly"], - positions=pf.current_positions(), spark=spark, paper=paper, + positions=pf.current_positions(), spark=spark, paper=paper, shadow=shadow, bh=None, ) _CACHE.update(t=time.time(), data=data) @@ -77,6 +82,23 @@ def html(): f"{pp['last'][:10]}, {days}g)   ret {ret*100:+.2f}%   maxDD {pp['max_dd']*100:.1f}%") else: paper_html = "non inizializzato (gira paper_portfolio.py)" + sh = d.get("shadow") + if sh and "error" not in sh: + bits = "  ·  ".join( + f"{a['asset']} {'FLAT' if abs(a['target'])<1e-9 else 'LONG' if a['target']>0 else 'SHORT'} " + f"{a['target']:+.2f}x" for a in sh["assets"]) + if sh.get("online"): + eq = f"${sh['real_equity']:,.2f}" if sh.get("real_equity") else sh.get("eq_basis", "?") + pos = ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in sh["assets"]) + ordtxt = ("; ".join(f"{o['side'].upper()} {o['amount']:.0f} {o['instrument']}" for o in sh["orders"]) + if sh.get("orders") else "nessuno (target flat / gia' a target)") + shadow_html = (f"mainnet · sola lettura · conto reale {eq} · pos {pos} · dato {sh['last_data']}
" + f"TP01 target: {bits}
ordini-che-invierebbe (NON inviati): {ordtxt}") + else: + shadow_html = (f"conto reale non leggibile dal container (token solo su host) · dato {sh['last_data']}
" + f"TP01 target: {bits}
→ per gli ordini reali: uv run python scripts/live/live_trend.py (host)") + else: + shadow_html = "non disponibile" + (f" — {sh['error']}" if sh and sh.get('error') else "") return f""" PythagorasGoal — Portafoglio -

PythagorasGoal — Portafoglio attivo (TP01 + XS01)

-
monitor PAPER · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE disabilitata
+

PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01)

+
monitor PAPER + SHADOW · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE disabilitata
FULL Sharpe
{f['sharpe']:.2f}
HOLD-OUT Sharpe (2025-26)
{ho['sharpe']:.2f}
@@ -99,12 +121,13 @@ th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;b
EQUITY backtest (2019→oggi, €2k)
{svg_spark(d['spark'])}
Paper forward-only: {paper_html}
+
Shadow live — TP01 su Deribit (sola lettura, nessun ordine inviato):
{shadow_html}

Sleeve

{rows}
sleevepesoFULL ShDDHOLD Sh

Posizioni correnti (ultima barra chiusa)

{pos}
{yrs}
-

⚠️ Paper/monitor. XS01 e' STAT-MODE (book a 19 gambe market-neutral, non eseguibile a €2k). Storia XS ~2.5 anni.

+

⚠️ Paper/monitor. XS01 e' STAT-MODE (book a 19 gambe market-neutral, non eseguibile a €2k, storia ~2.5 anni). VRP01 = lead short-vol MODELLATO (non deploy pieno). TP01 e' l'unico deployable pieno: lo "Shadow live" mostra cosa farebbe sul mainnet, ma NON invia ordini.

""" diff --git a/src/live/deribit.py b/src/live/deribit.py new file mode 100644 index 0000000..39c6414 --- /dev/null +++ b/src/live/deribit.py @@ -0,0 +1,139 @@ +"""Accesso Deribit MAINNET in SOLA LETTURA (via Cerbero MCP) + costruttore ordini deterministico. + +Serve lo SHADOW MODE di TP01 (`scripts/live/live_trend.py`): legge prezzi / conto / posizioni REALI +dal mainnet (token `.env.mainnet`) e costruisce gli ordini di ribilancio **senza inviarli**. Qui NON +esiste alcun metodo di trading — by design: l'unica via per piazzare ordini sara' un modulo separato, +abilitato esplicitamente, dopo la validazione shadow + micro-test. + +Disciplina del progetto: **testnet FUORI** (feed farlocco, causa del reset v2.0.0). Solo mainnet reale, +e in questa fase solo in lettura. I contratti sono ristretti a BTC/ETH-PERPETUAL (inverse: `amount` +in USD notional, step verificato su Deribit: BTC $10, ETH $1). +""" +from __future__ import annotations + +import os +from decimal import Decimal +from pathlib import Path + +import requests + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +BASE_URL = os.environ.get("CERBERO_BASE_URL", "https://cerbero-mcp.tielogic.xyz") +TIMEOUT = 15 + +# Inverse perp: amount = USD notional, step in USD. settle = base-coin (per get_positions/fee). +_CONTRACT = { + "BTC-PERPETUAL": {"min": 10.0, "step": 10.0, "tick": 0.5, "settle": "BTC"}, + "ETH-PERPETUAL": {"min": 1.0, "step": 1.0, "tick": 0.05, "settle": "ETH"}, +} +INSTRUMENT = {"BTC": "BTC-PERPETUAL", "ETH": "ETH-PERPETUAL"} + + +# ----------------------------- costruzione ordini (pura, testabile, NIENTE rete) ----------------------------- + +def _quantize_step(value: float, step: float, mn: float) -> float: + """Arrotonda al multiplo di `step` (Decimal, niente artefatti float), clampa al minimo.""" + n = round(value / step) + return float(max(n * Decimal(str(step)), Decimal(str(mn)))) + + +def notional_to_amount(instrument: str, notional_usd: float) -> float: + """USD notional -> `amount` Deribit (inverse: amount in USD), arrotondato allo step e clampato + al minimo. Ritorna 0.0 se |notional| < mezzo step (sotto-soglia: niente ordine).""" + spec = _CONTRACT[instrument] + step, mn = spec["step"], spec["min"] + if abs(notional_usd) < step / 2: + return 0.0 + return _quantize_step(abs(notional_usd), step, mn) + + +def target_notional_usd(target_fraction: float, weight: float, equity_usd: float) -> float: + """Notional bersaglio (USD) di un asset = peso nel book * frazione-di-equity TP01 * equity. + Coerente col paper trader (esposizione asset = WEIGHT * target * equity).""" + return weight * target_fraction * equity_usd + + +def build_rebalance_order(instrument: str, target_fraction: float, weight: float, + equity_usd: float, current_pos_usd: float) -> dict | None: + """COSTRUISCE (non invia) l'ordine di ribilancio verso il target. Ritorna un dict-ordine o None + se sotto-soglia. Long-only TP01 -> target_notional >= 0; delta = target - posizione corrente.""" + tgt = target_notional_usd(target_fraction, weight, equity_usd) + delta = tgt - current_pos_usd + amount = notional_to_amount(instrument, delta) + if amount == 0.0: + return None + is_exit = abs(tgt) < 1e-9 and abs(current_pos_usd) > 0 + return dict( + instrument=instrument, + side="buy" if delta > 0 else "sell", + amount=amount, + type="market", + reduce_only=is_exit, + target_notional=round(tgt, 2), + current_notional=round(current_pos_usd, 2), + delta_notional=round(delta, 2), + ) + + +# ----------------------------- lettura mainnet (Cerbero MCP) — SOLA LETTURA ----------------------------- + +def _load_mainnet_token() -> tuple[str, str]: + """Legge CERBERO_TOKEN (mainnet) + bot-tag da .env.mainnet. Il token NON viene mai stampato.""" + env: dict[str, str] = {} + for ln in (PROJECT_ROOT / ".env.mainnet").read_text().splitlines(): + ln = ln.strip() + if ln and not ln.startswith("#") and "=" in ln: + k, v = ln.split("=", 1) + env[k] = v.strip() + if "CERBERO_TOKEN" not in env: + raise RuntimeError("CERBERO_TOKEN assente in .env.mainnet") + return env["CERBERO_TOKEN"], env.get("CERBERO_BOT_TAG", "pythagoras-shadow") + + +class DeribitRead: + """Accesso Deribit mainnet in SOLA LETTURA via Cerbero MCP. Nessun metodo di trading (by design).""" + + def __init__(self) -> None: + self._token, self._tag = _load_mainnet_token() + + def _post(self, path: str, payload: dict) -> dict | list: + r = requests.post( + f"{BASE_URL}{path}", + headers={"Authorization": f"Bearer {self._token}", "X-Bot-Tag": self._tag, + "Content-Type": "application/json"}, + json=payload, timeout=TIMEOUT, + ) + r.raise_for_status() + return r.json() + + @staticmethod + def _unwrap(resp: dict | list) -> dict | list: + return resp.get("result", resp) if isinstance(resp, dict) else resp + + def ticker(self, instrument: str) -> dict: + return self._unwrap(self._post("/mcp-deribit/tools/get_ticker", {"instrument": instrument})) or {} + + def mark_price(self, instrument: str) -> float: + t = self.ticker(instrument) + for k in ("mark_price", "index_price", "last_price", "last"): + v = t.get(k) + if v: + return float(v) + raise ValueError(f"prezzo assente nel ticker {instrument}: chiavi={list(t)[:8]}") + + def account_summary(self, currency: str) -> dict: + return self._unwrap(self._post("/mcp-deribit/tools/get_account_summary", {"currency": currency})) or {} + + def positions(self, currency: str) -> list[dict]: + out = self._unwrap(self._post("/mcp-deribit/tools/get_positions", {"currency": currency})) + if isinstance(out, list): + return out + return out.get("positions", []) if isinstance(out, dict) else [] + + def position_usd(self, instrument: str) -> float: + """Size netta (USD notional, segno = direzione) della posizione su `instrument`. 0 se flat.""" + cur = _CONTRACT[instrument]["settle"] + for p in self.positions(cur): + if p.get("instrument_name") == instrument or p.get("instrument") == instrument: + return float(p.get("size") or p.get("size_currency") or 0.0) + return 0.0 diff --git a/src/live/shadow.py b/src/live/shadow.py new file mode 100644 index 0000000..0cc5a15 --- /dev/null +++ b/src/live/shadow.py @@ -0,0 +1,120 @@ +"""Stato SHADOW di TP01 (Deribit mainnet, SOLA LETTURA): target causali + conto/posizioni REALI + +ordini di ribilancio COSTRUITI (mai inviati). Modulo condiviso da `scripts/live/live_trend.py` (CLI) +e dalla dashboard, cosi' i due non divergono. Robusto ai fallimenti di rete: degrada a offline/flat +senza sollevare eccezioni (la dashboard non deve crashare se il mainnet non risponde).""" +from __future__ import annotations + +import json +from pathlib import Path + +import pandas as pd + +from src.backtest.harness import load +from src.live.deribit import INSTRUMENT, DeribitRead, build_rebalance_order +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +ASSETS = ["BTC", "ETH"] +WEIGHT = 0.5 +FALLBACK_CAPITAL = 2000.0 +PAPER_STATE = PROJECT_ROOT / "data" / "paper_trend" / "state.json" + + +def _safe_client() -> DeribitRead | None: + try: + return DeribitRead() + except Exception: + return None + + +def _marks(client, dfs): + marks, src = {}, {} + for a in ASSETS: + if client is None: + marks[a], src[a] = float(dfs[a]["close"].iloc[-1]), "close certificata" + continue + try: + marks[a], src[a] = client.mark_price(INSTRUMENT[a]), "mainnet" + except Exception as e: + marks[a], src[a] = float(dfs[a]["close"].iloc[-1]), f"fallback close ({type(e).__name__})" + return marks, src + + +def _positions(client): + if client is None: + return {a: 0.0 for a in ASSETS}, "offline -> assunto flat" + pos, note = {}, "mainnet" + for a in ASSETS: + try: + pos[a] = client.position_usd(INSTRUMENT[a]) + except Exception as e: + pos[a], note = 0.0, f"read fallito ({type(e).__name__}) -> assunto flat" + return pos, note + + +def _equity(client, marks): + if client is None: + return None, "offline" + try: + eq = float(client.account_summary("USDC").get("equity") or 0) + if eq > 1: + return eq, "mainnet USDC" + except Exception: + pass + tot, any_ok = 0.0, False + for a in ASSETS: + try: + eq = float(client.account_summary(a).get("equity") or 0) + tot += eq * marks[a] + any_ok = True + except Exception: + pass + if any_ok and tot > 1: + return tot, "mainnet coin-margined" + return None, "conto flat / non finanziato" + + +def shadow_report(offline: bool = False, equity_override: float | None = None) -> dict: + """Calcola lo stato shadow completo. NON invia nulla. Ritorna un dict serializzabile.""" + dfs = {a: resample_1d(load(a, "1h")) for a in ASSETS} + tp = TrendPortfolio(**CANONICAL) + targets = {a: tp.current_target(dfs[a]) for a in ASSETS} + last_ts = min(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS) + + client = None if offline else _safe_client() + marks, marks_src = _marks(client, dfs) + positions, pos_src = _positions(client) + real_eq, eq_src = _equity(client, marks) + + paper = json.loads(PAPER_STATE.read_text()) if PAPER_STATE.exists() else None + paper_cap = float(paper["capital"]) if paper else FALLBACK_CAPITAL + paper_pos = paper.get("positions") if paper else None + paper_ts = int(paper["last_ts"]) if paper else 0 + + equity = equity_override if equity_override is not None else (real_eq if real_eq else paper_cap) + eq_basis = ("override" if equity_override is not None + else eq_src if real_eq else "paper capital (ipotetico: conto non finanziato)") + + assets, orders = [], [] + for a in ASSETS: + inst = INSTRUMENT[a] + order = build_rebalance_order(inst, targets[a], WEIGHT, equity, positions[a]) + if order: + orders.append(order) + parity = None + if paper_pos is not None: + parity = abs(float(paper_pos.get(a, 0.0)) - targets[a]) < 1e-9 + assets.append(dict( + asset=a, instrument=inst, target=targets[a], + target_notional=WEIGHT * targets[a] * equity, + position_usd=positions[a], mark=marks[a], mark_src=marks_src[a], + order=order, paper=(float(paper_pos.get(a, 0.0)) if paper_pos else None), parity=parity, + )) + return dict( + last_data=str(pd.Timestamp(last_ts, unit="ms", tz="UTC").date()), + online=(client is not None and marks_src.get("BTC") == "mainnet"), + real_equity=real_eq, equity=equity, eq_basis=eq_basis, + pos_src=pos_src, assets=assets, orders=orders, + flat=all(abs(targets[a]) < 1e-9 for a in ASSETS), + paper_aligned=(paper_ts == last_ts), + ) diff --git a/tests/test_live_shadow.py b/tests/test_live_shadow.py new file mode 100644 index 0000000..a99041e --- /dev/null +++ b/tests/test_live_shadow.py @@ -0,0 +1,81 @@ +"""Test deterministici dello SHADOW MODE di TP01 (src/live/deribit.py). + +Coprono la logica a rischio zero che NON tocca la rete: quantizzazione notional->contratti, sizing +target, costruzione ordine di ribilancio (buy/sell/exit/None), e PARITA' col backtest (il target +live = ultimo target della serie causale). Il fill reale (slippage/fee) NON e' qui: si valida solo +col micro-test mainnet. +""" +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.live.deribit import (build_rebalance_order, notional_to_amount, target_notional_usd) +from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d + + +def test_notional_to_amount_step_and_min(): + # BTC step/min $10 + assert notional_to_amount("BTC-PERPETUAL", 1000) == 1000 + assert notional_to_amount("BTC-PERPETUAL", 1006) == 1010 # round allo step + assert notional_to_amount("BTC-PERPETUAL", 7) == 10 # clamp al minimo (>= mezzo step) + assert notional_to_amount("BTC-PERPETUAL", 3) == 0.0 # < mezzo step -> niente ordine + # ETH step/min $1 + assert notional_to_amount("ETH-PERPETUAL", 33.7) == 34 + assert notional_to_amount("ETH-PERPETUAL", 0.4) == 0.0 + # usa il valore assoluto (il segno lo decide il delta a monte) + assert notional_to_amount("BTC-PERPETUAL", -1000) == 1000 + + +def test_no_float_artifacts(): + # _quantize_step usa Decimal: nessun 0.07200000000000001 & co. + v = notional_to_amount("ETH-PERPETUAL", 72.0) + assert v == 72 and float(v).is_integer() + + +def test_target_notional_5050_weight(): + # 50/50 book: notional asset = 0.5 * frazione * equity + assert target_notional_usd(1.0, 0.5, 2000) == 1000 + assert target_notional_usd(2.0, 0.5, 2000) == 2000 # leva-cap 2x -> piena equity sull'asset + assert target_notional_usd(0.0, 0.5, 2000) == 0.0 + + +def test_build_order_entry(): + o = build_rebalance_order("BTC-PERPETUAL", target_fraction=1.0, weight=0.5, + equity_usd=2000, current_pos_usd=0.0) + assert o["side"] == "buy" and o["amount"] == 1000 and o["reduce_only"] is False + assert o["target_notional"] == 1000 and o["delta_notional"] == 1000 + + +def test_build_order_exit_is_reduce_only(): + o = build_rebalance_order("ETH-PERPETUAL", target_fraction=0.0, weight=0.5, + equity_usd=2000, current_pos_usd=1000.0) + assert o["side"] == "sell" and o["reduce_only"] is True and o["amount"] == 1000 + + +def test_build_order_already_at_target_is_none(): + o = build_rebalance_order("BTC-PERPETUAL", 1.0, 0.5, 2000, current_pos_usd=1000.0) + assert o is None # delta 0 -> nessun ordine + + +def test_build_order_subthreshold_is_none(): + # delta $3 su BTC (< mezzo step $5) -> niente ordine + o = build_rebalance_order("BTC-PERPETUAL", 0.5015, 0.5, 2000, current_pos_usd=498.5) + assert o is None + + +def test_partial_rebalance_direction(): + # target $1000, ho $600 -> compro $400; ho $1400 -> vendo $400 + up = build_rebalance_order("BTC-PERPETUAL", 1.0, 0.5, 2000, 600.0) + dn = build_rebalance_order("BTC-PERPETUAL", 1.0, 0.5, 2000, 1400.0) + assert up["side"] == "buy" and up["amount"] == 400 + assert dn["side"] == "sell" and dn["amount"] == 400 and dn["reduce_only"] is False + + +def test_parity_live_target_equals_backtest(): + # il target live (current_target) DEVE essere l'ultimo della serie causale del backtest + from src.backtest.harness import load + tp = TrendPortfolio(**CANONICAL) + df = resample_1d(load("BTC", "1h")) + assert tp.current_target(df) == tp.target_series(df)[-1]