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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ jobs:
--source examples/price_history.example.csv \
--target /tmp/qqq_overlay_prices.csv \
--symbols QQQ
- name: Build example context bundle
run: |
python scripts/build_context_bundle.py \
--prices examples/price_history.example.csv \
--symbols QQQ \
--output /tmp/context_bundle.json
13 changes: 13 additions & 0 deletions .github/workflows/dispatch_shadow_signal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ jobs:
- name: Validate existing latest signal if present
run: python scripts/validate_latest_signal.py --allow-missing

- name: Build point-in-time context bundle
run: |
python scripts/build_context_bundle.py \
--output data/output/context_bundle/latest_context_bundle.json \
--allow-download-errors

- name: Create or update shadow signal issue
id: shadow_issue
env:
Expand All @@ -56,6 +62,7 @@ jobs:
--source-ref "${SOURCE_REF}" \
--provider "${BRIDGE_PROVIDER}" \
--bridge-repository "${BRIDGE_REPOSITORY}" \
--context-file data/output/context_bundle/latest_context_bundle.json \
--label "${SHADOW_SIGNAL_LABEL}"

- name: Append shadow signal job summary
Expand Down Expand Up @@ -155,3 +162,9 @@ jobs:
raise RuntimeError(f"unexpected dispatch status: {status}")
print(f"Dispatched {bridge_repo} for issue #{os.environ['ISSUE_NUMBER']}")
PY

- name: Upload context bundle
uses: actions/upload-artifact@v7
with:
name: long-horizon-context-bundle
path: data/output/context_bundle/latest_context_bundle.json
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ This repo does not own:

## Operating Model

1. A weekly workflow creates or updates a dated long-horizon shadow-signal issue.
2. The issue is dispatched to `QuantStrategyLab/CodexAuditBridge` with task
1. A weekly workflow builds a point-in-time context bundle from current market
prices.
2. The workflow creates or updates a dated long-horizon shadow-signal issue and
embeds the context bundle as review evidence.
3. The issue is dispatched to `QuantStrategyLab/CodexAuditBridge` with task
`long_horizon_signal_shadow`.
3. `CodexAuditBridge` tries self-hosted Codex first and uses its own OpenAI or
4. `CodexAuditBridge` tries self-hosted Codex first and uses its own OpenAI or
Anthropic API fallback only when configured.
4. Any AI-generated artifact must remain `mode=shadow` and pass local schema
5. Any AI-generated artifact must remain `mode=shadow` and pass local schema
validation.
5. Downstream runtimes must treat the artifact as advisory context only until a
6. Downstream runtimes must treat the artifact as advisory context only until a
separate deterministic policy engine explicitly consumes it.

## GitHub Configuration
Expand Down Expand Up @@ -76,6 +79,21 @@ Validate the example artifact:
python scripts/validate_latest_signal.py examples/latest_signal.example.json
```

Build a context bundle from a local price file:

```bash
python scripts/build_context_bundle.py \
--prices examples/price_history.example.csv \
--symbols QQQ \
--output data/output/context_bundle/latest_context_bundle.json
```

Without `--prices`, the script downloads recent daily prices for the default
universe through Yahoo's chart endpoint and writes a point-in-time context bundle
for the weekly shadow issue. The scheduled workflow uses
`--allow-download-errors`, so external data-source failures still create an
operator issue with the failure recorded instead of silently skipping the run.

Validate the promoted latest artifact when it exists:

```bash
Expand Down
3 changes: 3 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ order routing.
- `CodexAuditBridge` owns provider routing and API keys.
- GitHub Issues are the first operator notification layer for scheduled shadow
signal runs.
- The scheduled workflow builds the market context bundle before dispatching the
bridge and embeds that bundle into the issue, because the bridge reads the
source repository ref plus issue content.
- `QuantStrategyPlugins` may later read promoted artifacts as sidecar context.
- Platform repositories remain unchanged.

Expand Down
71 changes: 71 additions & 0 deletions scripts/build_context_bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))

from ai_long_horizon_signal_pipelines.context_bundle import ( # noqa: E402
DEFAULT_UNIVERSE,
build_context_from_source,
build_error_context_bundle,
normalize_symbols,
write_context_bundle,
)


def main() -> int:
parser = argparse.ArgumentParser(description="Build point-in-time context for long-horizon AI review.")
parser.add_argument("--symbols", default=",".join(DEFAULT_UNIVERSE), help="Comma-separated symbol universe")
parser.add_argument("--prices", help="Optional local CSV with symbol,close and date or as_of")
parser.add_argument("--start-date", help="Optional inclusive YYYY-MM-DD lower bound")
parser.add_argument("--end-date", help="Optional inclusive YYYY-MM-DD upper bound")
parser.add_argument("--lookback-days", type=int, default=420)
parser.add_argument(
"--allow-download-errors",
action="store_true",
help="Write a degraded operator-notification context bundle instead of failing on download errors",
)
parser.add_argument(
"--output",
default="data/output/context_bundle/latest_context_bundle.json",
help="Output JSON context bundle path",
)
args = parser.parse_args()

symbols = normalize_symbols(args.symbols)
try:
bundle = build_context_from_source(
symbols=symbols,
prices_path=Path(args.prices) if args.prices else None,
start_date=args.start_date,
end_date=args.end_date,
lookback_days=args.lookback_days,
)
except Exception as exc:
if not args.allow_download_errors:
raise
bundle = build_error_context_bundle(symbols=symbols, error=f"{type(exc).__name__}: {exc}")
output_path = Path(args.output)
write_context_bundle(bundle, output_path)
print(
json.dumps(
{
"output": str(output_path),
"as_of": bundle["as_of"],
"symbols": bundle["universe"],
"price_context_symbols": sorted(bundle.get("price_context", {})),
"warnings": bundle.get("data_quality", {}).get("warnings", []),
"errors": bundle.get("data_quality", {}).get("errors", []),
}
)
)
return 0


if __name__ == "__main__":
raise SystemExit(main())
68 changes: 60 additions & 8 deletions scripts/post_shadow_signal_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import datetime as dt
import json
import os
from pathlib import Path
import sys
import urllib.error
import urllib.parse
Expand Down Expand Up @@ -67,8 +68,53 @@ def build_issue_title(as_of_date: str) -> str:
return f"Long-horizon AI shadow signal: {as_of_date}"


def build_issue_body(*, as_of_date: str, source_ref: str, provider: str, bridge_repository: str) -> str:
def load_context_bundle(path: str | None) -> dict[str, Any] | None:
if not path:
return None
return json.loads(Path(path).read_text(encoding="utf-8"))


def resolve_as_of_date(raw_as_of_date: str | None, context_bundle: Mapping[str, Any] | None) -> str:
if raw_as_of_date:
return raw_as_of_date
if context_bundle and str(context_bundle.get("as_of") or "").strip():
return str(context_bundle["as_of"]).strip()
return dt.date.today().isoformat()


def context_markdown(context_bundle: Mapping[str, Any] | None) -> str:
if not context_bundle:
return "\n".join(
[
"## Context",
"",
"No generated context bundle was attached to this request.",
"If evidence is insufficient, report findings and leave artifacts unchanged.",
]
)
context_json = json.dumps(context_bundle, ensure_ascii=True, indent=2, sort_keys=True)
return "\n".join(
[
"## Context Bundle",
"",
"Use this point-in-time context bundle as the primary evidence for the shadow signal review.",
"",
"```json",
context_json,
"```",
]
)


def build_issue_body(
*,
as_of_date: str,
source_ref: str,
provider: str,
bridge_repository: str,
context_bundle: Mapping[str, Any] | None = None,
) -> str:
sections = [
[
"## Long-Horizon Shadow Signal Request",
"",
Expand All @@ -92,12 +138,14 @@ def build_issue_body(*, as_of_date: str, source_ref: str, provider: str, bridge_
"- Any downstream use must remain advisory until a deterministic policy consumes the artifact.",
"- If evidence is insufficient, report findings and leave artifacts unchanged.",
"",
"## Context",
],
[context_markdown(context_bundle)],
[
"",
"Use committed examples, context bundles, and existing shadow artifacts as evidence.",
"Do not infer historical AI signals that were not generated point-in-time.",
]
)
],
]
return "\n".join(line for section in sections for line in section)


def upsert_issue(
Expand Down Expand Up @@ -125,7 +173,8 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--source-ref", default="main")
parser.add_argument("--provider", default="auto")
parser.add_argument("--bridge-repository", default="QuantStrategyLab/CodexAuditBridge")
parser.add_argument("--as-of-date", default=dt.date.today().isoformat())
parser.add_argument("--as-of-date")
parser.add_argument("--context-file", help="Optional JSON context bundle to embed in the issue body")
parser.add_argument("--label", default=DEFAULT_LABEL)
parser.add_argument("--api-url", default=DEFAULT_API_URL)
return parser.parse_args()
Expand All @@ -152,12 +201,15 @@ def main() -> int:
print("GITHUB_TOKEN is required", file=sys.stderr)
return 1

title = build_issue_title(args.as_of_date)
context_bundle = load_context_bundle(args.context_file)
as_of_date = resolve_as_of_date(args.as_of_date, context_bundle)
title = build_issue_title(as_of_date)
body = build_issue_body(
as_of_date=args.as_of_date,
as_of_date=as_of_date,
source_ref=args.source_ref,
provider=args.provider,
bridge_repository=args.bridge_repository,
context_bundle=context_bundle,
)
try:
action, issue_number, issue_url = upsert_issue(
Expand Down
4 changes: 4 additions & 0 deletions src/ai_long_horizon_signal_pipelines/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"""Shadow-only long-horizon AI signal artifact helpers."""

from .overlay_backtest import OverlayPolicy, backtest_overlay
from .context_bundle import DEFAULT_UNIVERSE, build_context_bundle, build_context_from_source
from .price_history import PriceExtractionSummary, write_filtered_price_history
from .schema import SignalValidationError, validate_signal

__all__ = [
"OverlayPolicy",
"PriceExtractionSummary",
"SignalValidationError",
"DEFAULT_UNIVERSE",
"backtest_overlay",
"build_context_bundle",
"build_context_from_source",
"validate_signal",
"write_filtered_price_history",
]
Loading