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
97 changes: 75 additions & 22 deletions scripts/run_continuous_paper_cycle_v1.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,86 @@
#!/usr/bin/env python3
"""Manual, offline-safe entry point for one continuous paper cycle."""

from __future__ import annotations

import argparse
import os
from pathlib import Path
from quantpilot_core.continuous_paper import ActiveShadowConfig, ContinuousPaperCycle, ContinuousPaperCycleConfig, InMemoryReportingStore, PostgreSQLReportingStore
from quantpilot_core.production_candidate import load_runtime_manifest
from quantpilot_core.real_candidate_pipeline import RealCandidatePipelineConfig

from quantpilot_core.continuous_paper import (
ActiveShadowConfig,
ContinuousPaperCycle,
ContinuousPaperCycleConfig,
InMemoryReportingStore,
PostgreSQLReportingStore,
load_production_input_payload,
load_production_pipeline_config,
normalize_active_shadow_roles,
)


def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--production-manifest", required=True); p.add_argument("--decision-session", required=True)
p.add_argument("--state-path", required=True); p.add_argument("--report-path", required=True)
p.add_argument("--dsn-env-var", default="QUANTPILOT_POSTGRES_DSN"); p.add_argument("--test-store", action="store_true")
p.add_argument("--cache-path", default=".cache/quantpilot/active_shadow"); p.add_argument("--timeout", type=float, default=30.0)
p.add_argument("--allowed-roles", default=""); p.add_argument("--enable-active-shadow", action="store_true"); p.add_argument("--enable-live-ai", action="store_true")
p.add_argument("--max-physical-model-calls", type=int, default=0); p.add_argument("--max-estimated-api-cost", type=float, default=0.0); p.add_argument("--estimated-cost-per-call", type=float, default=0.0)
args = p.parse_args()
if args.enable_live_ai and (not args.enable_active_shadow or args.max_physical_model_calls <= 0 or args.max_estimated_api_cost <= 0 or args.estimated_cost_per_call <= 0):
p.error("live AI requires active shadow and positive explicit call, cycle-cost, and per-call cost limits")
parser = argparse.ArgumentParser()
parser.add_argument("--production-manifest", required=True)
parser.add_argument("--decision-session", required=True)
parser.add_argument("--state-path", required=True)
parser.add_argument("--report-path", required=True)
parser.add_argument("--input-json", default=None)
parser.add_argument("--live-market-data", action="store_true")
parser.add_argument("--dsn-env-var", default="QUANTPILOT_POSTGRES_DSN")
parser.add_argument("--test-store", action="store_true")
parser.add_argument("--cache-path", default=".cache/quantpilot/active_shadow")
parser.add_argument("--timeout", type=float, default=30.0)
parser.add_argument("--allowed-roles", default="")
parser.add_argument("--enable-active-shadow", action="store_true")
parser.add_argument("--enable-live-ai", action="store_true")
parser.add_argument("--max-physical-model-calls", type=int, default=0)
parser.add_argument("--max-estimated-api-cost", type=float, default=0.0)
parser.add_argument("--estimated-cost-per-call", type=float, default=0.0)
args = parser.parse_args()

try:
shadow = ActiveShadowConfig(
enabled=args.enable_active_shadow,
enable_live_calls=args.enable_live_ai,
max_physical_model_calls_per_cycle=args.max_physical_model_calls,
max_estimated_cost_per_cycle=args.max_estimated_api_cost,
estimated_cost_per_call=args.estimated_cost_per_call,
cache_path=args.cache_path,
allow_roles=normalize_active_shadow_roles(args.allowed_roles),
timeout=args.timeout,
)
except ValueError as exc:
parser.error(str(exc))

dsn = os.environ.get(args.dsn_env_var)
if not args.test_store and not dsn: p.error("set the DSN environment variable or use explicit --test-store")
manifest = load_runtime_manifest(Path(args.production_manifest))
pipeline = RealCandidatePipelineConfig(decision_session=args.decision_session, state_path=args.state_path, report_path=args.report_path, production_manifest=manifest)
store = InMemoryReportingStore() if args.test_store else PostgreSQLReportingStore(dsn)
shadow = ActiveShadowConfig(enabled=args.enable_active_shadow, enable_live_calls=args.enable_live_ai, max_physical_model_calls_per_cycle=args.max_physical_model_calls, max_estimated_cost_per_cycle=args.max_estimated_api_cost, estimated_cost_per_call=args.estimated_cost_per_call, cache_path=args.cache_path, timeout=args.timeout)
result = ContinuousPaperCycle(store).run_once(ContinuousPaperCycleConfig(pipeline, run_id=f"manual-{args.decision_session}", active_shadow=shadow))
print(f"session_id={result.session_id} status={result.status} run_id={result.run_id}")
if not args.test_store and not dsn:
parser.error("set the DSN environment variable or use explicit --test-store")

pipeline = load_production_pipeline_config(
manifest_path=args.production_manifest,
decision_session=args.decision_session,
state_path=args.state_path,
report_path=args.report_path,
live_market_data=args.live_market_data,
input_payload=load_production_input_payload(args.input_json),
)

store = (
InMemoryReportingStore()
if args.test_store
else PostgreSQLReportingStore(dsn)
)
result = ContinuousPaperCycle(store).run_once(
ContinuousPaperCycleConfig(
pipeline,
run_id=f"manual-{args.decision_session}",
active_shadow=shadow,
)
)
print(
f"session_id={result.session_id} status={result.status} run_id={result.run_id}"
)


if __name__ == "__main__": main()
if __name__ == "__main__":
main()
98 changes: 80 additions & 18 deletions scripts/serve_continuous_paper_v1.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,97 @@
#!/usr/bin/env python3
"""Explicit Prefect deployment helper; no schedule starts during import."""

from __future__ import annotations

import argparse
from typing import Any


def parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser()
p.add_argument("--production-manifest", required=True)
p.add_argument("--decision-session", required=True)
p.add_argument("--state-path", required=True)
p.add_argument("--report-path", required=True)
p.add_argument("--run-id", required=True)
p.add_argument("--dsn-env-var", default="QUANTPILOT_POSTGRES_DSN")
p.add_argument("--cron", default="30 16 * * 1-5")
p.add_argument("--timezone", default="Asia/Shanghai")
p.add_argument("--max-physical-model-calls", type=int, default=0)
p.add_argument("--max-estimated-api-cost", type=float, default=0.0)
p.add_argument("--estimated-cost-per-call", type=float, default=0.0)
return p
value = argparse.ArgumentParser()
value.add_argument("--production-manifest", required=True)
value.add_argument("--decision-session", required=True)
value.add_argument("--state-path", required=True)
value.add_argument("--report-path", required=True)
value.add_argument("--run-id", required=True)
value.add_argument("--input-json", default=None)
value.add_argument("--live-market-data", action="store_true")
value.add_argument("--dsn-env-var", default="QUANTPILOT_POSTGRES_DSN")
value.add_argument("--cron", default="30 16 * * 1-5")
value.add_argument("--timezone", default="Asia/Shanghai")
value.add_argument("--cache-path", default=".cache/quantpilot/active_shadow")
value.add_argument("--timeout", type=float, default=30.0)
value.add_argument("--allowed-roles", default="")
value.add_argument("--enable-active-shadow", action="store_true")
value.add_argument("--enable-live-ai", action="store_true")
value.add_argument("--max-physical-model-calls", type=int, default=0)
value.add_argument("--max-estimated-api-cost", type=float, default=0.0)
value.add_argument("--estimated-cost-per-call", type=float, default=0.0)
return value


def main() -> None:
args = parser().parse_args()
argument_parser = parser()
args = argument_parser.parse_args()

# Deliberately lazy: --help stays useful in minimal/offline environments.
from quantpilot_core.continuous_paper import (
ActiveShadowConfig,
load_production_input_payload,
normalize_active_shadow_roles,
)

try:
shadow = ActiveShadowConfig(
enabled=args.enable_active_shadow,
enable_live_calls=args.enable_live_ai,
max_physical_model_calls_per_cycle=args.max_physical_model_calls,
max_estimated_cost_per_cycle=args.max_estimated_api_cost,
estimated_cost_per_call=args.estimated_cost_per_call,
cache_path=args.cache_path,
allow_roles=normalize_active_shadow_roles(args.allowed_roles),
timeout=args.timeout,
)
except ValueError as exc:
argument_parser.error(str(exc))

from quantpilot_core.continuous_paper.prefect_flow import continuous_paper_flow

if not hasattr(continuous_paper_flow, "serve"):
raise RuntimeError("install quantpilot-ai-next[continuous-paper] to serve a Prefect deployment")
raise RuntimeError(
"install quantpilot-ai-next[continuous-paper] to serve a Prefect deployment"
)
from prefect.schedules import Cron

parameters = {"production_manifest_path": args.production_manifest, "decision_session": args.decision_session, "state_path": args.state_path, "report_path": args.report_path, "run_id": args.run_id, "dsn_env_var": args.dsn_env_var, "active_shadow": {"max_physical_model_calls_per_cycle": args.max_physical_model_calls, "max_estimated_cost_per_cycle": args.max_estimated_api_cost, "estimated_cost_per_call": args.estimated_cost_per_call}}
continuous_paper_flow.serve(name="quantpilot-continuous-paper-v1", schedules=[Cron(args.cron, timezone=args.timezone)], parameters=parameters)
parameters: dict[str, Any] = {
"production_manifest_path": args.production_manifest,
"decision_session": args.decision_session,
"state_path": args.state_path,
"report_path": args.report_path,
"run_id": args.run_id,
"dsn_env_var": args.dsn_env_var,
"live_market_data": args.live_market_data,
"input_payload": load_production_input_payload(args.input_json),
"active_shadow": {
"enabled": shadow.enabled,
"enable_live_calls": shadow.enable_live_calls,
"max_physical_model_calls_per_cycle": (
shadow.max_physical_model_calls_per_cycle
),
"max_estimated_cost_per_cycle": shadow.max_estimated_cost_per_cycle,
"estimated_cost_per_call": shadow.estimated_cost_per_call,
"cache_path": str(shadow.cache_path),
"allow_roles": [role.value for role in shadow.allow_roles],
"timeout": shadow.timeout,
"failure_policy": shadow.failure_policy,
},
}
continuous_paper_flow.serve(
name="quantpilot-continuous-paper-v1",
schedules=[Cron(args.cron, timezone=args.timezone)],
parameters=parameters,
)


if __name__ == "__main__": main()
if __name__ == "__main__":
main()
15 changes: 13 additions & 2 deletions src/quantpilot_core/continuous_paper/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
"""One-cycle, advisory-only continuous paper orchestration."""

from .active_shadow import ActiveShadowConfig, ActiveShadowRunner
from .active_shadow import (
ActiveShadowConfig,
ActiveShadowRunner,
normalize_active_shadow_roles,
)
from .contracts import ContinuousPaperCycleConfig, ContinuousPaperCycleResult, ReportingCycleBundle
from .manifest_bridge import (
load_production_input_payload,
load_production_pipeline_config,
normalize_production_input_payload,
)
from .service import ContinuousPaperCycle
from .store import InMemoryReportingStore, PostgreSQLReportingStore, ReportingConflictError

__all__ = [
"ActiveShadowConfig", "ActiveShadowRunner", "ContinuousPaperCycle",
"ContinuousPaperCycleConfig", "ContinuousPaperCycleResult", "InMemoryReportingStore",
"PostgreSQLReportingStore", "ReportingConflictError",
"ReportingCycleBundle",
"ReportingCycleBundle", "load_production_input_payload",
"load_production_pipeline_config", "normalize_active_shadow_roles",
"normalize_production_input_payload",
]
25 changes: 25 additions & 0 deletions src/quantpilot_core/continuous_paper/active_shadow.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,39 @@ class ActiveShadowConfig:
failure_policy: str = "abstain"

def __post_init__(self) -> None:
object.__setattr__(self, "allow_roles", normalize_active_shadow_roles(self.allow_roles))
if self.failure_policy != "abstain":
raise ValueError("active shadow supports only failure_policy='abstain'")
if self.timeout <= 0:
raise ValueError("timeout must be positive")
if self.enable_live_calls and not self.enabled:
raise ValueError("live AI requires active shadow to be enabled")
if self.enable_live_calls and (self.max_physical_model_calls_per_cycle <= 0 or self.max_estimated_cost_per_cycle <= 0 or self.estimated_cost_per_call <= 0):
raise ValueError("live active shadow requires positive call, cycle-cost, and per-call cost limits")


def normalize_active_shadow_roles(
roles: str | tuple[DeepSeekAdvisoryRole | str, ...] | list[str],
) -> tuple[DeepSeekAdvisoryRole, ...]:
"""Normalize CLI or serialized role values to unique runtime enums."""
if isinstance(roles, str):
values = tuple(item.strip() for item in roles.split(",") if item.strip())
if not values:
return tuple(DeepSeekAdvisoryRole)
else:
values = tuple(roles)

normalized: list[DeepSeekAdvisoryRole] = []
for value in values:
if isinstance(value, DeepSeekAdvisoryRole):
role = value
else:
role = DeepSeekAdvisoryRole(str(value).strip().lower().replace("-", "_"))
if role not in normalized:
normalized.append(role)
return tuple(normalized)


class ActiveShadowRunner:
def __init__(self, config: ActiveShadowConfig, *, agent: Any | None = None) -> None:
self.config = config
Expand Down
Loading
Loading