
Edge Pipeline Orchestrator
- 805 installs
- 2.6k repo stars
- Updated August 4, 2026
- tradermonty/claude-trading-skills
edge-pipeline-orchestrator is a Claude Code skill that runs the full quantitative edge research pipeline from market data through strategy generation, review, revision, and export for developers automating systematic tra
About
edge-pipeline-orchestrator is a workflow skill from tradermonty/claude-trading-skills that chains edge research stages into one automated run. It loads pipeline configuration from CLI arguments, processes tickets or OHLCV inputs, moves through candidate detection, strategy design, review, and revision loops, then exports finalized strategies. Developers can resume partially completed runs from the drafts stage or dry-run to preview outputs without exporting. Reach for edge-pipeline-orchestrator when quant research steps are already scripted separately but need reliable end-to-end coordination with feedback loops instead of manual handoffs between notebooks and scripts.
- Orchestrates 7-stage edge research pipeline end-to-end
- Supports full run from tickets or OHLCV data, resume from drafts, and dry-run preview
- Implements review-revision feedback loop with verdict accumulation (PASS/REJECT/REVISE)
- Exports only PASS + export_ready_v1 strategies with full pipeline_run_manifest.json trace
- Max 2 review iterations before downgrading remaining REVISE verdicts to research_probe
Edge Pipeline Orchestrator by the numbers
- 805 all-time installs (skills.sh)
- +91 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,320 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/tradermonty/claude-trading-skills --skill edge-pipeline-orchestratorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 805 |
|---|---|
| repo stars | ★ 2.6k |
| Last updated | August 4, 2026 |
| Repository | tradermonty/claude-trading-skills ↗ |
How do you orchestrate quant edge research end to end?
Automatically coordinate an entire multi-step quantitative trading research pipeline from raw market data through strategy generation, review, revision, and final expor
Who is it for?
Quant developers with modular edge-research scripts who need one command to run detection, design, review, revision, and export with resume support.
Skip if: Developers placing live orders or teams wanting a single-indicator backtest without multi-stage strategy review and export gates.
When should I use this skill?
User wants to run the full edge pipeline, resume from drafts, dry-run strategy export, or coordinate multi-stage quant research end to end.
What you get
Reviewed strategy drafts, revision history, and exported strategy artifacts from a coordinated pipeline run.
- exported strategies
- strategy drafts
- pipeline run preview
Files
Edge Pipeline Orchestrator
Coordinate all edge research stages into a single automated pipeline run.
When to Use
- Run the full edge pipeline from tickets (or OHLCV) to exported strategies
- Resume a partially completed pipeline from the drafts stage
- Review and revise existing strategy drafts with feedback loop
- Dry-run the pipeline to preview results without exporting
Workflow
1. Load pipeline configuration from CLI arguments 2. Run auto_detect stage if --from-ohlcv is provided (generates tickets from raw OHLCV data) 3. Run hints stage to extract edge hints from market summary and anomalies 4. Run concepts stage to synthesize abstract edge concepts from tickets and hints 5. Run drafts stage to design strategy drafts from concepts 6. Run review-revision feedback loop:
- Review all drafts (max 2 iterations)
- PASS verdicts accumulated; REJECT verdicts accumulated
- REVISE verdicts trigger apply_revisions and re-review
- Remaining REVISE after max iterations downgraded to research_probe
7. Export eligible drafts (PASS + export_ready_v1 + exportable entry_family) 8. Write pipeline_run_manifest.json with full execution trace
CLI Usage
# Full pipeline from tickets
python3 scripts/orchestrate_edge_pipeline.py \
--tickets-dir path/to/tickets/ \
--output-dir reports/edge_pipeline/
# Full pipeline from OHLCV
python3 scripts/orchestrate_edge_pipeline.py \
--from-ohlcv path/to/ohlcv.csv \
--output-dir reports/edge_pipeline/
# Resume from drafts stage
python3 scripts/orchestrate_edge_pipeline.py \
--resume-from drafts \
--drafts-dir path/to/drafts/ \
--output-dir reports/edge_pipeline/
# Review-only mode
python3 scripts/orchestrate_edge_pipeline.py \
--review-only \
--drafts-dir path/to/drafts/ \
--output-dir reports/edge_pipeline/
# Dry run (no export)
python3 scripts/orchestrate_edge_pipeline.py \
--tickets-dir path/to/tickets/ \
--output-dir reports/edge_pipeline/ \
--dry-runOutput
All artifacts are written to --output-dir:
output-dir/
├── pipeline_run_manifest.json
├── tickets/ (from auto_detect)
├── hints/hints.yaml (from hints)
├── concepts/edge_concepts.yaml
├── drafts/*.yaml
├── exportable_tickets/*.yaml
├── reviews_iter_0/*.yaml
├── reviews_iter_1/*.yaml (if needed)
└── strategies/<candidate_id>/
├── strategy.yaml
└── metadata.jsonClaude Code LLM-Augmented Workflow
Run the LLM-augmented pipeline entirely within Claude Code:
1. Run auto_detect to produce market_summary.json + anomalies.json 2. Claude Code analyzes data and generates edge hints 3. Save hints to a YAML file:
- title: Sector rotation into industrials
observation: Tech underperforming while industrials show relative strength
symbols: [CAT, DE, GE]
regime_bias: Neutral
mechanism_tag: flow
preferred_entry_family: pivot_breakout
hypothesis_type: sector_x_stock4. Run orchestrator with --llm-ideas-file and --promote-hints:
python3 scripts/orchestrate_edge_pipeline.py \
--tickets-dir path/to/tickets/ \
--llm-ideas-file llm_hints.yaml \
--promote-hints \
--as-of 2026-02-28 \
--max-synthetic-ratio 1.5 \
--strict-export \
--output-dir reports/edge_pipeline/Optional Flags
--as-of YYYY-MM-DD— forwarded to hints stage for date filtering--strict-export— export-eligible drafts with any warn finding get REVISE instead of PASS--max-synthetic-ratio N— cap synthetic tickets to N × real ticket count (floor: 3)--overlap-threshold F— condition overlap threshold for concept deduplication (default: 0.75)--no-dedup— disable concept deduplication
Note: --llm-ideas-file and --promote-hints are effective only during full pipeline runs. --resume-from drafts and --review-only skip hints/concepts stages, so these flags are ignored.
Resources
references/pipeline_flow.md— Pipeline stages, data contracts, and architecturereferences/revision_loop_rules.md— Review-revision feedback loop rules and heuristics
Edge Pipeline Flow
Pipeline Stages
OHLCV / Tickets
|
v
[auto_detect] ───> tickets/
|
v
[hints] ───> hints.yaml
|
v
[concepts] ───> edge_concepts.yaml
|
v
[drafts] ───> drafts/*.yaml + exportable_tickets/*.yaml
|
v
[review] <──────────────────────┐
| |
├── PASS → accumulated |
├── REJECT → accumulated |
└── REVISE → [revision] ───┘ (max 2 iterations)
|
remaining REVISE → research_probe downgrade
|
v
[export] ───> strategies/<candidate_id>/Stage Script Mapping
| Stage | Script Path |
|---|---|
| auto_detect | skills/edge-candidate-agent/scripts/auto_detect_candidates.py |
| hints | skills/edge-hint-extractor/scripts/build_hints.py |
| concepts | skills/edge-concept-synthesizer/scripts/synthesize_edge_concepts.py |
| drafts | skills/edge-strategy-designer/scripts/design_strategy_drafts.py |
| review | skills/edge-strategy-reviewer/scripts/review_strategy_drafts.py |
| export | skills/edge-candidate-agent/scripts/export_candidate.py |
Data Contracts
auto_detect output (tickets/)
Each ticket is a YAML file with at minimum:
id: unique ticket identifierhypothesis_type: breakout, earnings_drift, etc.entry_family: pivot_breakout, gap_up_continuation, or research_onlypriority_score: 0-100 numeric score
hints output (hints.yaml)
generated_at_utc: "2026-01-01T00:00:00+00:00"
hints:
- title: "Breadth-supported breakout regime"
observation: "..."
symbols: [AAPL, MSFT]
regime_bias: "RiskOn"
mechanism_tag: "behavior"concepts output (edge_concepts.yaml)
concept_count: 3
concepts:
- id: edge_concept_breakout_behavior_riskon
hypothesis_type: breakout
strategy_design:
recommended_entry_family: pivot_breakout
export_ready_v1: truedrafts output (drafts/*.yaml)
Each draft YAML file contains:
id: draft identifier (draft_<concept_id>_<variant>)concept_id: source conceptvariant: core, conservative, or research_probeentry_family: pivot_breakout, gap_up_continuation, or research_onlyexport_ready_v1: booleanentry.conditions: list of condition stringsentry.trend_filter: list of trend filter stringsexit: stop_loss_pct, take_profit_rrrisk: position sizing parameters
review output (reviews/*.yaml)
Each review YAML contains:
draft_id: matching draft identifierverdict: PASS, REVISE, or REJECTrevision_instructions: list of strings (for REVISE verdicts)confidence_score: 0-100
export output (strategies/<candidate_id>/)
strategy.yaml: Phase I-compatible strategy specificationmetadata.json: provenance and research context
Exportable Entry Families
Only these families can be exported to the trade-strategy-pipeline:
pivot_breakoutgap_up_continuation
All other families (research_only, etc.) remain as research probes.
Revision Loop Rules
Overview
The review-revision feedback loop ensures strategy drafts meet quality standards before export. Drafts cycle through review and optional revision up to a maximum number of iterations.
Verdict Categories
| Verdict | Meaning | Next Action |
|---|---|---|
| PASS | Draft meets quality standards | Accumulated; eligible for export |
| REJECT | Draft has fundamental flaws | Accumulated; no further action |
| REVISE | Draft needs specific improvements | Apply revisions, re-review |
Loop Mechanics
1. All drafts enter the first review iteration (iter 0). 2. After each review:
- PASS drafts are accumulated in the passed list (never re-reviewed).
- REJECT drafts are accumulated in the rejected list (never re-reviewed).
- REVISE drafts proceed to the revision stage.
3. Revised drafts re-enter the review stage for the next iteration. 4. After max_review_iterations (default: 2), any remaining REVISE drafts are downgraded to research_probe variant and marked not export-ready.
Accumulation Rules
- PASS and REJECT lists are append-only across iterations.
- A draft that PASSed in iteration 0 remains passed even if iteration 1 runs.
- A draft that was REJECTed in iteration 0 is never revisited.
- Only REVISE drafts flow into the next iteration.
Revision Heuristics (apply_revisions)
When a draft receives REVISE verdict with revision instructions:
| Instruction Pattern | Action |
|---|---|
| "Reduce entry conditions" | Keep only the first 5 entry conditions |
| "Add volume filter" | Append "avg_volume > 500000" to conditions |
| "Round precise thresholds" | Round decimal numbers in conditions to integers |
After revision:
variantremains unchangedexport_ready_v1remains unchanged
Downgrade Rules (downgrade_to_research_probe)
When a REVISE draft exhausts all iterations:
- Set
variant= "research_probe" - Set
export_ready_v1= False - Record draft_id in the downgraded list
Export Eligibility
A draft is eligible for export when ALL conditions are met: 1. Verdict is PASS 2. export_ready_v1 is True 3. entry_family is in EXPORTABLE_FAMILIES (pivot_breakout, gap_up_continuation)
Ticket Generation for Export
- Pre-generated tickets: The designer stage can write exportable ticket YAMLs
to --exportable-tickets-dir. These are preferred when available.
- Revised draft tickets: If a draft was revised (no pre-generated ticket),
build_export_ticket() generates a ticket from the draft data.
- Export uses
--ticket PATH --strategies-dir DIRCLI interface.
#!/usr/bin/env python3
"""Orchestrate the full edge research pipeline from detection to export."""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from copy import deepcopy
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import yaml
DEFAULT_EXPORTABLE_FAMILIES = {
"pivot_breakout",
"gap_up_continuation",
"panic_reversal",
"news_reaction",
}
# Resolve script paths relative to the skills project root
# (3 levels up from this script: scripts/ -> edge-pipeline-orchestrator/ -> skills/ -> project root)
_SKILLS_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
SCRIPT_PATHS = {
"auto_detect": str(
_SKILLS_PROJECT_ROOT / "skills/edge-candidate-agent/scripts/auto_detect_candidates.py"
),
"hints": str(_SKILLS_PROJECT_ROOT / "skills/edge-hint-extractor/scripts/build_hints.py"),
"concepts": str(
_SKILLS_PROJECT_ROOT / "skills/edge-concept-synthesizer/scripts/synthesize_edge_concepts.py"
),
"drafts": str(
_SKILLS_PROJECT_ROOT / "skills/edge-strategy-designer/scripts/design_strategy_drafts.py"
),
"review": str(
_SKILLS_PROJECT_ROOT / "skills/edge-strategy-reviewer/scripts/review_strategy_drafts.py"
),
"export": str(_SKILLS_PROJECT_ROOT / "skills/edge-candidate-agent/scripts/export_candidate.py"),
}
MAX_REVIEW_ITERATIONS = 2
class EdgePipelineError(Exception):
"""Raised when the pipeline cannot proceed."""
@dataclass
class TrackedDraft:
"""Track a draft through the review-revision loop."""
draft_id: str
file_path: Path
verdict: str
export_eligible: bool
confidence_score: int
@dataclass
class ReviewLoopResult:
"""Accumulated results from the review-revision loop."""
passed: list[TrackedDraft] = field(default_factory=list)
rejected: list[TrackedDraft] = field(default_factory=list)
downgraded: list[str] = field(default_factory=list)
# ---------------------------------------------------------------------------
# Subprocess helper
# ---------------------------------------------------------------------------
def run_stage(stage: str, args: list[str], cwd: str | None = None) -> subprocess.CompletedProcess:
"""Run an upstream skill script via subprocess."""
if stage not in SCRIPT_PATHS:
raise EdgePipelineError(f"unknown stage: {stage}")
script_path = SCRIPT_PATHS[stage]
cmd = [sys.executable, script_path] + args
result = subprocess.run(
cmd,
text=True,
capture_output=True,
cwd=cwd,
)
if result.returncode != 0:
detail = (result.stderr or result.stdout).strip()
raise EdgePipelineError(f"stage '{stage}' failed (exit {result.returncode}): {detail}")
return result
# ---------------------------------------------------------------------------
# File I/O helpers
# ---------------------------------------------------------------------------
def load_drafts_from_dir(drafts_dir: Path) -> list[dict[str, Any]]:
"""Load all draft YAML files from a directory."""
drafts: list[dict[str, Any]] = []
for path in sorted(drafts_dir.glob("*.yaml")):
payload = yaml.safe_load(path.read_text())
if isinstance(payload, dict) and payload.get("id"):
drafts.append(payload)
return drafts
def load_reviews_from_dir(reviews_dir: Path) -> list[dict[str, Any]]:
"""Load review results from a directory.
Supports two formats:
1. Consolidated: review.yaml / review.json with a top-level ``reviews`` list
(output of edge-strategy-reviewer).
2. Per-draft: individual {draft_id}_review.yaml files.
"""
reviews: list[dict[str, Any]] = []
# Format 1: consolidated review.yaml / review.json
for name in ("review.yaml", "review.json"):
consolidated = reviews_dir / name
if consolidated.exists():
payload = yaml.safe_load(consolidated.read_text())
if isinstance(payload, dict) and isinstance(payload.get("reviews"), list):
for r in payload["reviews"]:
if isinstance(r, dict) and r.get("draft_id"):
reviews.append(r)
return reviews
# Format 2: per-draft *_review.yaml files
for path in sorted(reviews_dir.glob("*_review.yaml")):
payload = yaml.safe_load(path.read_text())
if isinstance(payload, dict) and payload.get("draft_id"):
reviews.append(payload)
return reviews
# ---------------------------------------------------------------------------
# Export logic
# ---------------------------------------------------------------------------
def should_export(
draft: dict[str, Any],
exportable_families: set[str] | None = None,
) -> bool:
"""Check if a draft is eligible for export."""
families = (
exportable_families if exportable_families is not None else DEFAULT_EXPORTABLE_FAMILIES
)
return bool(draft.get("export_ready_v1")) and draft.get("entry_family", "") in families
def build_export_ticket(draft: dict[str, Any]) -> dict[str, Any]:
"""Build exportable ticket from strategy draft."""
ticket_id = draft["id"].replace("draft_", "edge_")
return {
"id": ticket_id,
"name": draft["name"],
"description": f"Draft-derived ticket from concept {draft['concept_id']} ({draft['variant']}).",
"hypothesis_type": draft.get("hypothesis_type", "unknown"),
"entry_family": draft["entry_family"],
"mechanism_tag": draft.get("mechanism_tag", "uncertain"),
"regime": draft.get("regime", "Neutral"),
"holding_horizon": "20D",
"entry": {
"conditions": draft.get("entry", {}).get("conditions", []),
"trend_filter": draft.get("entry", {}).get("trend_filter", []),
},
"risk": draft.get("risk", {}),
"exit": {
"stop_loss_pct": draft.get("exit", {}).get("stop_loss_pct", 0.07),
"take_profit_rr": draft.get("exit", {}).get("take_profit_rr", 3.0),
},
"cost_model": {
"commission_per_share": 0.0,
"slippage_bps": 5,
},
}
def export_draft(
draft: dict[str, Any],
draft_path: Path,
strategies_dir: Path,
exportable_tickets_dir: Path | None,
dry_run: bool,
) -> str | None:
"""Export a single draft via the export_candidate script. Returns ticket_id or None."""
ticket_id = draft["id"].replace("draft_", "edge_")
# Try pre-generated ticket first
ticket_path: Path | None = None
if exportable_tickets_dir is not None:
candidate_path = exportable_tickets_dir / f"{ticket_id}.yaml"
if candidate_path.exists():
ticket_path = candidate_path
# Fall back to generating ticket from draft
if ticket_path is None:
ticket = build_export_ticket(draft)
generated_path = draft_path.parent / f"{ticket_id}_export_ticket.yaml"
generated_path.write_text(yaml.safe_dump(ticket, sort_keys=False))
ticket_path = generated_path
if dry_run:
return ticket_id
export_args = [
"--ticket",
str(ticket_path),
"--strategies-dir",
str(strategies_dir),
"--force",
]
run_stage("export", export_args)
return ticket_id
# ---------------------------------------------------------------------------
# Revision logic
# ---------------------------------------------------------------------------
def apply_revisions(draft: dict[str, Any], instructions: list[str]) -> dict[str, Any]:
"""Apply heuristic revisions based on reviewer instructions."""
revised = deepcopy(draft)
entry = revised.get("entry", {})
conditions = list(entry.get("conditions", []))
for instruction in instructions:
lower = instruction.lower()
if "reduce entry conditions" in lower:
conditions = conditions[:5]
elif "add volume filter" in lower:
if "avg_volume > 500000" not in conditions:
conditions.append("avg_volume > 500000")
elif "round precise thresholds" in lower:
rounded: list[str] = []
for cond in conditions:
rounded.append(
re.sub(
r"(\d+)\.(\d+)",
lambda m: str(round(float(m.group(0)))),
cond,
)
)
conditions = rounded
entry["conditions"] = conditions
revised["entry"] = entry
# variant and export_ready_v1 remain unchanged
return revised
def downgrade_to_research_probe(draft: dict[str, Any]) -> dict[str, Any]:
"""Downgrade a draft to research_probe variant."""
downgraded = deepcopy(draft)
downgraded["variant"] = "research_probe"
downgraded["export_ready_v1"] = False
return downgraded
# ---------------------------------------------------------------------------
# Review-revision feedback loop
# ---------------------------------------------------------------------------
def run_review_loop(
drafts_dir: Path,
review_output_base: Path,
max_iterations: int = MAX_REVIEW_ITERATIONS,
strict_export: bool = False,
exportable_families: set[str] | None = None,
) -> ReviewLoopResult:
"""Run the review-revision feedback loop."""
result = ReviewLoopResult()
# Build initial draft map: draft_id → (draft_data, file_path)
current_drafts: dict[str, tuple[dict[str, Any], Path]] = {}
for path in sorted(drafts_dir.glob("*.yaml")):
payload = yaml.safe_load(path.read_text())
if isinstance(payload, dict) and payload.get("id"):
current_drafts[payload["id"]] = (payload, path)
for iteration in range(max_iterations):
if not current_drafts:
break
# Prepare drafts dir for this iteration's review
iter_drafts_dir = review_output_base / f"review_input_iter_{iteration}"
iter_drafts_dir.mkdir(parents=True, exist_ok=True)
for draft_id, (draft_data, orig_path) in current_drafts.items():
iter_draft_path = iter_drafts_dir / f"{draft_id}.yaml"
iter_draft_path.write_text(yaml.safe_dump(draft_data, sort_keys=False))
# Run review stage
review_dir = review_output_base / f"reviews_iter_{iteration}"
review_args = [
"--drafts-dir",
str(iter_drafts_dir),
"--output-dir",
str(review_dir),
]
if strict_export:
review_args.append("--strict-export")
if exportable_families is not None:
review_args += ["--exportable-families", ",".join(sorted(exportable_families))]
run_stage("review", review_args)
# Load reviews
reviews = load_reviews_from_dir(review_dir)
review_map = {r["draft_id"]: r for r in reviews}
next_drafts: dict[str, tuple[dict[str, Any], Path]] = {}
for draft_id, (draft_data, file_path) in current_drafts.items():
review = review_map.get(draft_id)
if review is None:
# No review found, treat as REVISE to retry
next_drafts[draft_id] = (draft_data, file_path)
continue
verdict = review.get("verdict", "REJECT")
confidence = int(review.get("confidence_score", 0))
if verdict == "PASS":
result.passed.append(
TrackedDraft(
draft_id=draft_id,
file_path=file_path,
verdict="PASS",
export_eligible=should_export(draft_data, exportable_families),
confidence_score=confidence,
)
)
elif verdict == "REJECT":
result.rejected.append(
TrackedDraft(
draft_id=draft_id,
file_path=file_path,
verdict="REJECT",
export_eligible=False,
confidence_score=confidence,
)
)
elif verdict == "REVISE":
instructions = review.get("revision_instructions", [])
revised = apply_revisions(draft_data, instructions)
# Write revised draft back to file
file_path.write_text(yaml.safe_dump(revised, sort_keys=False))
next_drafts[draft_id] = (revised, file_path)
current_drafts = next_drafts
# Downgrade remaining REVISE drafts
for draft_id, (draft_data, file_path) in current_drafts.items():
downgraded = downgrade_to_research_probe(draft_data)
file_path.write_text(yaml.safe_dump(downgraded, sort_keys=False))
result.downgraded.append(draft_id)
return result
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def parse_args() -> argparse.Namespace:
"""Parse CLI arguments."""
parser = argparse.ArgumentParser(
description="Orchestrate the full edge research pipeline.",
)
parser.add_argument(
"--tickets-dir",
default=None,
help="Directory containing edge ticket YAML files",
)
parser.add_argument(
"--market-summary",
default=None,
help="Path to market_summary.json",
)
parser.add_argument(
"--anomalies",
default=None,
help="Path to anomalies.json",
)
parser.add_argument(
"--from-ohlcv",
default=None,
help="Path to OHLCV CSV for auto_detect stage",
)
parser.add_argument(
"--resume-from",
default=None,
choices=["drafts"],
help="Resume pipeline from a specific stage",
)
parser.add_argument(
"--review-only",
action="store_true",
help="Only run review loop on existing drafts",
)
parser.add_argument(
"--drafts-dir",
default=None,
help="Path to existing drafts directory (for --resume-from or --review-only)",
)
parser.add_argument(
"--output-dir",
default="reports/edge_pipeline",
help="Output directory for pipeline artifacts",
)
parser.add_argument(
"--risk-profile",
default="balanced",
choices=["conservative", "balanced", "aggressive"],
help="Risk profile for strategy design",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Run pipeline without export stage",
)
parser.add_argument(
"--max-review-iterations",
type=int,
default=MAX_REVIEW_ITERATIONS,
help="Maximum review-revision iterations",
)
parser.add_argument(
"--llm-ideas-file",
default=None,
metavar="PATH",
help="YAML file of LLM hints, forwarded to hints stage",
)
parser.add_argument(
"--promote-hints",
action="store_true",
default=False,
help="Forward --promote-hints to concepts stage (full pipeline only)",
)
parser.add_argument(
"--as-of",
default=None,
help="Target date YYYY-MM-DD forwarded to hints stage",
)
parser.add_argument(
"--strict-export",
action="store_true",
default=False,
help="Forward --strict-export to review stage (warn on export-eligible → REVISE)",
)
parser.add_argument(
"--max-synthetic-ratio",
type=float,
default=None,
help="Forward --max-synthetic-ratio to concepts stage (cap synthetic tickets)",
)
parser.add_argument(
"--overlap-threshold",
type=float,
default=None,
help="Forward --overlap-threshold to concepts stage (dedup threshold)",
)
parser.add_argument(
"--no-dedup",
action="store_true",
default=False,
help="Forward --no-dedup to concepts stage (disable deduplication)",
)
parser.add_argument(
"--exportable-families",
default=None,
help="Comma-separated list of exportable entry families (overrides module default)",
)
return parser.parse_args()
def main() -> int:
"""CLI entrypoint."""
args = parse_args()
exportable_families: set[str] | None = None
if args.exportable_families:
exportable_families = {f.strip() for f in args.exportable_families.split(",") if f.strip()}
output_dir = Path(args.output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
run_id = f"edge_pipeline_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}"
started_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
manifest: dict[str, Any] = {
"run_id": run_id,
"started_at_utc": started_at,
"status": "running",
"input": {
"tickets_dir": str(args.tickets_dir) if args.tickets_dir else None,
"from_ohlcv": str(args.from_ohlcv) if args.from_ohlcv else None,
"resume_from": args.resume_from,
"review_only": args.review_only,
"risk_profile": args.risk_profile,
"dry_run": args.dry_run,
"llm_ideas_file": str(args.llm_ideas_file) if args.llm_ideas_file else None,
"promote_hints": args.promote_hints,
},
"stages": {},
}
try:
tickets_dir = Path(args.tickets_dir).resolve() if args.tickets_dir else None
drafts_dir: Path | None = Path(args.drafts_dir).resolve() if args.drafts_dir else None
auto_detect_output: Path | None = None
# --- auto_detect stage ---
if args.from_ohlcv and not args.resume_from and not args.review_only:
ohlcv_path = Path(args.from_ohlcv).resolve()
auto_detect_output = output_dir / "tickets"
auto_detect_args = [
"--ohlcv",
str(ohlcv_path),
"--output-dir",
str(auto_detect_output),
]
run_stage("auto_detect", auto_detect_args)
tickets_dir = auto_detect_output
manifest["stages"]["auto_detect"] = {
"status": "completed",
"output": str(auto_detect_output),
}
# --- hints stage ---
hints_output: Path | None = None
if not args.resume_from and not args.review_only:
hints_args: list[str] = []
# Resolve market_summary / anomalies paths:
# 1. Explicit CLI args take priority
# 2. Fall back to auto_detect output when --from-ohlcv was used
market_summary_path = (
Path(args.market_summary).resolve()
if args.market_summary
else (auto_detect_output / "market_summary.json" if auto_detect_output else None)
)
anomalies_path = (
Path(args.anomalies).resolve()
if args.anomalies
else (auto_detect_output / "anomalies.json" if auto_detect_output else None)
)
if market_summary_path and market_summary_path.exists():
hints_args += ["--market-summary", str(market_summary_path)]
if anomalies_path and anomalies_path.exists():
hints_args += ["--anomalies", str(anomalies_path)]
if args.llm_ideas_file:
hints_args += ["--llm-ideas-file", str(Path(args.llm_ideas_file).resolve())]
if args.as_of:
hints_args += ["--as-of", args.as_of]
hints_output_path = output_dir / "hints" / "hints.yaml"
hints_args += ["--output", str(hints_output_path)]
run_stage("hints", hints_args)
hints_output = hints_output_path
manifest["stages"]["hints"] = {"status": "completed", "output": str(hints_output_path)}
# --- concepts stage ---
concepts_output: Path | None = None
if not args.resume_from and not args.review_only:
if tickets_dir is None:
raise EdgePipelineError("--tickets-dir or --from-ohlcv required for concepts stage")
concepts_output_path = output_dir / "concepts" / "edge_concepts.yaml"
concepts_args = [
"--tickets-dir",
str(tickets_dir),
"--output",
str(concepts_output_path),
]
if hints_output and hints_output.exists():
concepts_args += ["--hints", str(hints_output)]
if args.promote_hints:
concepts_args += ["--promote-hints"]
if args.max_synthetic_ratio is not None:
concepts_args += ["--max-synthetic-ratio", str(args.max_synthetic_ratio)]
if args.overlap_threshold is not None:
concepts_args += ["--overlap-threshold", str(args.overlap_threshold)]
if args.no_dedup:
concepts_args += ["--no-dedup"]
if args.exportable_families:
concepts_args += ["--exportable-families", args.exportable_families]
run_stage("concepts", concepts_args)
concepts_output = concepts_output_path
manifest["stages"]["concepts"] = {
"status": "completed",
"output": str(concepts_output_path),
}
# --- drafts stage ---
exportable_tickets_dir: Path | None = None
if not args.resume_from and not args.review_only:
if concepts_output is None:
raise EdgePipelineError("concepts output required for drafts stage")
drafts_output = output_dir / "drafts"
exportable_tickets_dir = output_dir / "exportable_tickets"
drafts_args = [
"--concepts",
str(concepts_output),
"--output-dir",
str(drafts_output),
"--risk-profile",
args.risk_profile,
"--exportable-tickets-dir",
str(exportable_tickets_dir),
]
if args.exportable_families:
drafts_args += ["--exportable-families", args.exportable_families]
run_stage("drafts", drafts_args)
drafts_dir = drafts_output
manifest["stages"]["drafts"] = {"status": "completed", "output": str(drafts_output)}
# --- review-revision loop ---
if drafts_dir is None:
raise EdgePipelineError(
"--drafts-dir required when using --resume-from or --review-only"
)
review_result = run_review_loop(
drafts_dir=drafts_dir,
review_output_base=output_dir,
max_iterations=args.max_review_iterations,
strict_export=args.strict_export,
exportable_families=exportable_families,
)
manifest["stages"]["review_loop"] = {
"status": "completed",
"passed_count": len(review_result.passed),
"rejected_count": len(review_result.rejected),
"downgraded_count": len(review_result.downgraded),
"passed_ids": [td.draft_id for td in review_result.passed],
"rejected_ids": [td.draft_id for td in review_result.rejected],
"downgraded_ids": review_result.downgraded,
}
# --- export stage ---
exported: list[str] = []
skipped: list[str] = []
if not args.dry_run:
strategies_dir = output_dir / "strategies"
strategies_dir.mkdir(parents=True, exist_ok=True)
for td in review_result.passed:
draft_data = yaml.safe_load(td.file_path.read_text())
if not isinstance(draft_data, dict):
skipped.append(td.draft_id)
continue
if should_export(draft_data, exportable_families):
ticket_id = export_draft(
draft=draft_data,
draft_path=td.file_path,
strategies_dir=strategies_dir,
exportable_tickets_dir=exportable_tickets_dir,
dry_run=False,
)
if ticket_id:
exported.append(ticket_id)
else:
skipped.append(td.draft_id)
else:
for td in review_result.passed:
draft_data = (
yaml.safe_load(td.file_path.read_text()) if td.file_path.exists() else {}
)
if isinstance(draft_data, dict) and should_export(draft_data, exportable_families):
exported.append(td.draft_id)
else:
skipped.append(td.draft_id)
manifest["stages"]["export"] = {
"status": "completed" if not args.dry_run else "skipped_dry_run",
"exported": exported,
"skipped_not_eligible": skipped,
}
manifest["status"] = "completed"
except EdgePipelineError as exc:
manifest["status"] = "failed"
manifest["error"] = str(exc)
print(f"[ERROR] {exc}", file=sys.stderr)
manifest_path = output_dir / "pipeline_run_manifest.json"
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
return 1
manifest_path = output_dir / "pipeline_run_manifest.json"
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
print(
f"[OK] pipeline={run_id} "
f"passed={len(review_result.passed)} "
f"rejected={len(review_result.rejected)} "
f"downgraded={len(review_result.downgraded)} "
f"exported={len(exported)}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
"""Shared fixtures for edge-pipeline-orchestrator tests."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
import yaml
@pytest.fixture()
def sample_draft_pass() -> dict[str, Any]:
"""A draft that should PASS review and be export-eligible."""
return {
"id": "draft_edge_concept_breakout_behavior_riskon_core",
"concept_id": "edge_concept_breakout_behavior_riskon",
"variant": "core",
"risk_profile": "balanced",
"name": "Participation-backed trend breakout (core)",
"hypothesis_type": "breakout",
"mechanism_tag": "behavior",
"regime": "RiskOn",
"export_ready_v1": True,
"entry_family": "pivot_breakout",
"entry": {
"conditions": [
"close > high20_prev",
"rel_volume >= 1.5",
"close > ma50 > ma200",
],
"trend_filter": ["price > sma_200", "price > sma_50"],
"note": "Use baseline confirmation and trend filter.",
},
"exit": {"stop_loss_pct": 0.07, "take_profit_rr": 3.0, "time_stop_days": 20},
"risk": {
"position_sizing": "fixed_risk",
"risk_per_trade": 0.01,
"max_positions": 5,
"max_sector_exposure": 0.3,
},
}
@pytest.fixture()
def sample_draft_revise() -> dict[str, Any]:
"""A draft that receives REVISE verdict."""
return {
"id": "draft_edge_concept_breakout_behavior_riskon_conservative",
"concept_id": "edge_concept_breakout_behavior_riskon",
"variant": "conservative",
"risk_profile": "balanced",
"name": "Participation-backed trend breakout (conservative)",
"hypothesis_type": "breakout",
"mechanism_tag": "behavior",
"regime": "RiskOn",
"export_ready_v1": True,
"entry_family": "pivot_breakout",
"entry": {
"conditions": [
"close > high20_prev",
"rel_volume >= 1.5",
"close > ma50 > ma200",
"rsi_14 > 50",
"macd_histogram > 0",
"adx > 25",
"obv_rising",
],
"trend_filter": ["price > sma_200", "price > sma_50"],
"note": "Require stricter confirmation before entry.",
},
"exit": {"stop_loss_pct": 0.07, "take_profit_rr": 3.0, "time_stop_days": 20},
"risk": {
"position_sizing": "fixed_risk",
"risk_per_trade": 0.0075,
"max_positions": 4,
"max_sector_exposure": 0.3,
},
}
@pytest.fixture()
def sample_draft_reject() -> dict[str, Any]:
"""A draft that receives REJECT verdict."""
return {
"id": "draft_edge_concept_panic_reversal_risk_premium_riskoff_research_probe",
"concept_id": "edge_concept_panic_reversal_risk_premium_riskoff",
"variant": "research_probe",
"risk_profile": "balanced",
"name": "Shock overshoot mean reversion (research_probe)",
"hypothesis_type": "panic_reversal",
"mechanism_tag": "risk_premium",
"regime": "RiskOff",
"export_ready_v1": False,
"entry_family": "research_only",
"entry": {"conditions": [], "trend_filter": []},
"exit": {"stop_loss_pct": 0.07, "take_profit_rr": 3.0},
"risk": {
"position_sizing": "fixed_risk",
"risk_per_trade": 0.005,
"max_positions": 3,
"max_sector_exposure": 0.3,
},
}
@pytest.fixture()
def sample_draft_gap_up() -> dict[str, Any]:
"""A draft with gap_up_continuation family."""
return {
"id": "draft_edge_concept_earnings_drift_behavior_neutral_core",
"concept_id": "edge_concept_earnings_drift_behavior_neutral",
"variant": "core",
"risk_profile": "balanced",
"name": "Event-driven continuation drift (core)",
"hypothesis_type": "earnings_drift",
"mechanism_tag": "behavior",
"regime": "Neutral",
"export_ready_v1": True,
"entry_family": "gap_up_continuation",
"entry": {
"conditions": [
"gap_up_detected",
"close_above_gap_day_high",
"volume > 2.0 * avg_volume_50",
],
"trend_filter": ["price > sma_200", "price > sma_50"],
},
"exit": {"stop_loss_pct": 0.07, "take_profit_rr": 3.0},
"risk": {
"position_sizing": "fixed_risk",
"risk_per_trade": 0.01,
"max_positions": 5,
"max_sector_exposure": 0.3,
},
}
@pytest.fixture()
def sample_review_pass() -> dict[str, Any]:
"""Review result with PASS verdict."""
return {
"draft_id": "draft_edge_concept_breakout_behavior_riskon_core",
"verdict": "PASS",
"confidence_score": 82,
"revision_instructions": [],
}
@pytest.fixture()
def sample_review_revise() -> dict[str, Any]:
"""Review result with REVISE verdict."""
return {
"draft_id": "draft_edge_concept_breakout_behavior_riskon_conservative",
"verdict": "REVISE",
"confidence_score": 55,
"revision_instructions": ["Reduce entry conditions", "Add volume filter"],
}
@pytest.fixture()
def sample_review_reject() -> dict[str, Any]:
"""Review result with REJECT verdict."""
return {
"draft_id": "draft_edge_concept_panic_reversal_risk_premium_riskoff_research_probe",
"verdict": "REJECT",
"confidence_score": 20,
"revision_instructions": [],
}
@pytest.fixture()
def drafts_dir(
tmp_path: Path, sample_draft_pass: dict, sample_draft_revise: dict, sample_draft_reject: dict
) -> Path:
"""Create a temporary drafts directory with sample draft YAML files."""
d = tmp_path / "drafts"
d.mkdir()
for draft in [sample_draft_pass, sample_draft_revise, sample_draft_reject]:
(d / f"{draft['id']}.yaml").write_text(yaml.safe_dump(draft, sort_keys=False))
return d
@pytest.fixture()
def reviews_dir(
tmp_path: Path, sample_review_pass: dict, sample_review_revise: dict, sample_review_reject: dict
) -> Path:
"""Create a temporary reviews directory with sample review YAML files."""
d = tmp_path / "reviews"
d.mkdir()
for review in [sample_review_pass, sample_review_revise, sample_review_reject]:
(d / f"{review['draft_id']}_review.yaml").write_text(
yaml.safe_dump(review, sort_keys=False)
)
return d
@pytest.fixture()
def tickets_dir(tmp_path: Path) -> Path:
"""Create a temporary tickets directory with sample ticket YAML files."""
d = tmp_path / "tickets"
d.mkdir()
ticket = {
"id": "edge_breakout_AAPL_20260101",
"hypothesis_type": "breakout",
"entry_family": "pivot_breakout",
"mechanism_tag": "behavior",
"regime": "RiskOn",
"priority_score": 75,
"observation": {"symbol": "AAPL", "date": "2026-01-01"},
}
(d / "edge_breakout_AAPL_20260101.yaml").write_text(yaml.safe_dump(ticket, sort_keys=False))
return d
@pytest.fixture()
def hints_file(tmp_path: Path) -> Path:
"""Create a sample hints YAML file."""
path = tmp_path / "hints.yaml"
payload = {
"generated_at_utc": "2026-01-01T00:00:00+00:00",
"hints": [
{
"title": "Breadth-supported breakout regime",
"observation": "Risk-on regime with pct_above_ma50=0.650",
"regime_bias": "RiskOn",
"mechanism_tag": "behavior",
}
],
}
path.write_text(yaml.safe_dump(payload, sort_keys=False))
return path
@pytest.fixture()
def concepts_file(tmp_path: Path) -> Path:
"""Create a sample concepts YAML file."""
path = tmp_path / "edge_concepts.yaml"
payload = {
"concept_count": 1,
"concepts": [
{
"id": "edge_concept_breakout_behavior_riskon",
"hypothesis_type": "breakout",
"mechanism_tag": "behavior",
"regime": "RiskOn",
"strategy_design": {
"recommended_entry_family": "pivot_breakout",
"export_ready_v1": True,
},
}
],
}
path.write_text(yaml.safe_dump(payload, sort_keys=False))
return path
@pytest.fixture()
def market_summary_file(tmp_path: Path) -> Path:
"""Create a sample market summary JSON file."""
path = tmp_path / "market_summary.json"
payload = {
"regime_label": "RiskOn",
"pct_above_ma50": 0.65,
"vol_trend": 0.85,
"risk_on_score": 70,
"risk_off_score": 30,
}
path.write_text(json.dumps(payload))
return path
@pytest.fixture()
def anomalies_file(tmp_path: Path) -> Path:
"""Create a sample anomalies JSON file."""
path = tmp_path / "anomalies.json"
payload = [
{"symbol": "AAPL", "metric": "gap", "z": 3.2},
{"symbol": "MSFT", "metric": "rel_volume", "z": 2.8},
]
path.write_text(json.dumps(payload))
return path
@pytest.fixture()
def exportable_tickets_dir(tmp_path: Path, sample_draft_pass: dict) -> Path:
"""Create a directory with pre-generated exportable ticket YAML files."""
d = tmp_path / "exportable_tickets"
d.mkdir()
ticket_id = sample_draft_pass["id"].replace("draft_", "edge_")
ticket = {
"id": ticket_id,
"name": sample_draft_pass["name"],
"description": f"Draft-derived ticket from concept {sample_draft_pass['concept_id']} ({sample_draft_pass['variant']}).",
"hypothesis_type": sample_draft_pass["hypothesis_type"],
"entry_family": sample_draft_pass["entry_family"],
"mechanism_tag": sample_draft_pass.get("mechanism_tag", "uncertain"),
"regime": sample_draft_pass.get("regime", "Neutral"),
"holding_horizon": "20D",
"entry": sample_draft_pass["entry"],
"risk": sample_draft_pass["risk"],
"exit": {
"stop_loss_pct": sample_draft_pass["exit"]["stop_loss_pct"],
"take_profit_rr": sample_draft_pass["exit"]["take_profit_rr"],
},
"cost_model": {"commission_per_share": 0.0, "slippage_bps": 5},
}
(d / f"{ticket_id}.yaml").write_text(yaml.safe_dump(ticket, sort_keys=False))
return d
Related skills
How it compares
Use edge-pipeline-orchestrator when multiple edge-research stages already exist and you need orchestration, resume, and dry-run gates rather than a single backtest script.
FAQ
Can edge-pipeline-orchestrator resume a partial run?
edge-pipeline-orchestrator supports resuming from the drafts stage when a prior pipeline run stopped mid-workflow. Load CLI configuration and continue review, revision, and export without restarting candidate detection.
What inputs does edge-pipeline-orchestrator accept?
edge-pipeline-orchestrator runs from tickets or OHLCV market data through candidate detection, strategy design, review, revision, and export. Pipeline behavior is controlled through CLI-loaded configuration.