Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,13 @@ platform marks unified signals as required, failure to resolve a valid artifact
should fail fast; only validated `last_valid` artifacts inside the configured
stale window are an acceptable fallback.

The scheduled IBIT BTC daily handoff uses a 48-hour freshness limit because
the UTC daily candle carries its opening timestamp while the completed candle
is published the next day and consumed during the US trading afternoon. This
limit still rejects a missed daily publication before the following US session;
other bundle builders retain their 36-hour default. Do not advance the source
timestamp to the publish time or reuse an older candle to pass this check.

Platform CI can then validate one handoff manifest before loading the linked
bundle:

Expand Down
5 changes: 5 additions & 0 deletions scripts/publish_ibit_btc_platform_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
DEFAULT_CONSUMER = "us_equity:ibit_smart_dca"
DEFAULT_STRATEGY = "ibit_smart_dca"
DEFAULT_GCS_PREFIX = "gs://qsl-runtime-logs-shared/platform_handoffs"
# BTC UTC daily bars are timestamped at their opening midnight. The next
# completed bar is published before the following US trading afternoon.
IBIT_BTC_DAILY_MAX_AGE_HOURS = 48


def default_as_of(*, today: date | None = None) -> str:
Expand Down Expand Up @@ -136,6 +139,8 @@ def build_ibit_btc_platform_handoff(
code_commit,
"--generated-at",
generated_at,
"--max-age-hours",
str(IBIT_BTC_DAILY_MAX_AGE_HOURS),
]
)
if build_exit != 0:
Expand Down
2 changes: 2 additions & 0 deletions src/market_signal_sources/cli/build_btc_cycle_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def main(argv: Sequence[str] | None = None) -> int:
generated_at=args.generated_at,
provider_timestamp=provider_metadata.provider_timestamp,
freshness_status=args.freshness_status,
max_age_hours=args.max_age_hours,
license_scope=provider_metadata.license_scope,
generated_by=provider_metadata.generated_by,
)
Expand Down Expand Up @@ -117,6 +118,7 @@ def _build_parser() -> argparse.ArgumentParser:
)
parser.add_argument("--generated-at", required=True)
parser.add_argument("--freshness-status", default="fresh")
parser.add_argument("--max-age-hours", type=int, default=36)
parser.add_argument("--date-column", default="date")
parser.add_argument("--close-column", default="close")
parser.add_argument("--high-column", default="high")
Expand Down
27 changes: 27 additions & 0 deletions tests/test_publish_ibit_btc_platform_handoff.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import json
from datetime import date
from pathlib import Path

Expand All @@ -9,9 +10,14 @@
from scripts.publish_ibit_btc_platform_handoff import (
BINANCE_BTCUSDT_DAILY_URLS,
_provider_name_for_url,
build_ibit_btc_platform_handoff,
default_as_of,
resolve_as_of,
)
from market_signal_sources.artifacts.validation import (
SignalBundleValidationError,
validate_signal_bundle,
)


def _write_btc_csv(path: Path, dates: list[str]) -> None:
Expand Down Expand Up @@ -62,3 +68,24 @@ def test_provider_name_for_url() -> None:
assert _provider_name_for_url(BINANCE_BTCUSDT_DAILY_URLS[0]) == "binance_vision_public"
assert _provider_name_for_url(BINANCE_BTCUSDT_DAILY_URLS[1]) == "binance_us_public"
assert _provider_name_for_url(BINANCE_BTCUSDT_DAILY_URLS[2]) == "binance_public"


def test_ibit_daily_handoff_remains_fresh_through_next_us_trading_afternoon(tmp_path: Path) -> None:
csv_path = tmp_path / "btc.csv"
dates = pd.date_range(end="2026-09-24", periods=800, freq="D")
_write_btc_csv(csv_path, [day.date().isoformat() for day in dates])
artifacts = build_ibit_btc_platform_handoff(
work_dir=tmp_path / "output",
input_csv=csv_path,
as_of="2026-09-24",
code_commit="a" * 40,
source_version="0.1.1",
)
bundle = json.loads(
(artifacts["publication_dir"] / "bundle" / "signal_bundle.json").read_text(encoding="utf-8")
)
assert bundle["freshness"]["provider_timestamp"] == "2026-09-24T00:00:00Z"
assert bundle["freshness"]["max_age_hours"] == 48
validate_signal_bundle(bundle, now="2026-09-25T19:45:00Z")
with pytest.raises(SignalBundleValidationError, match="freshness"):
validate_signal_bundle(bundle, now="2026-09-26T00:00:01Z")
Loading