indiciumforge-cli 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- indiciumforge_cli-2.0.0/PKG-INFO +44 -0
- indiciumforge_cli-2.0.0/README.md +17 -0
- indiciumforge_cli-2.0.0/pyproject.toml +42 -0
- indiciumforge_cli-2.0.0/setup.cfg +4 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli/__init__.py +1 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli/artifact.py +123 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli/daily_review.py +27 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli/factor.py +93 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli/main.py +196 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli/parity.py +51 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli/provider.py +156 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli/synthetic_e2e.py +47 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli/workflow_chain.py +148 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli.egg-info/PKG-INFO +44 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli.egg-info/SOURCES.txt +17 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli.egg-info/dependency_links.txt +1 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli.egg-info/entry_points.txt +2 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli.egg-info/requires.txt +2 -0
- indiciumforge_cli-2.0.0/src/indiciumforge_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: indiciumforge-cli
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Reference CLI for IndiciumForge research audit workflows (indiciumforge command).
|
|
5
|
+
Author: IndiciumForge contributors
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Cavaradossi/indiciumforge
|
|
8
|
+
Project-URL: Repository, https://github.com/Cavaradossi/indiciumforge
|
|
9
|
+
Project-URL: Documentation, https://github.com/Cavaradossi/indiciumforge/tree/master/docs
|
|
10
|
+
Project-URL: Issues, https://github.com/Cavaradossi/indiciumforge/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/Cavaradossi/indiciumforge/blob/master/RELEASE_NOTES.md
|
|
12
|
+
Keywords: cli,workflow,research,audit,open-core
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: indiciumforge-workflow<3.0.0,>=2.0.0
|
|
26
|
+
Requires-Dist: typer>=0.9
|
|
27
|
+
|
|
28
|
+
# indiciumforge-cli
|
|
29
|
+
|
|
30
|
+
Reference CLI for [IndiciumForge](https://github.com/Cavaradossi/indiciumforge).
|
|
31
|
+
|
|
32
|
+
Entry point: `indiciumforge` — workflow, artifact audit, factor scan, provider inspect, parity harness.
|
|
33
|
+
|
|
34
|
+
Research audit only — not investment advice, not broker execution.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
PyPI publish is pending. Install from the monorepo:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e packages/indiciumforge-core -e packages/indiciumforge-workflow -e packages/indiciumforge-cli
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
See [docs/PYPI_RELEASE_CHECKLIST.md](https://github.com/Cavaradossi/indiciumforge/blob/master/docs/PYPI_RELEASE_CHECKLIST.md).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# indiciumforge-cli
|
|
2
|
+
|
|
3
|
+
Reference CLI for [IndiciumForge](https://github.com/Cavaradossi/indiciumforge).
|
|
4
|
+
|
|
5
|
+
Entry point: `indiciumforge` — workflow, artifact audit, factor scan, provider inspect, parity harness.
|
|
6
|
+
|
|
7
|
+
Research audit only — not investment advice, not broker execution.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
PyPI publish is pending. Install from the monorepo:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e packages/indiciumforge-core -e packages/indiciumforge-workflow -e packages/indiciumforge-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
See [docs/PYPI_RELEASE_CHECKLIST.md](https://github.com/Cavaradossi/indiciumforge/blob/master/docs/PYPI_RELEASE_CHECKLIST.md).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "indiciumforge-cli"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Reference CLI for IndiciumForge research audit workflows (indiciumforge command)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "IndiciumForge contributors" }]
|
|
13
|
+
keywords = ["cli", "workflow", "research", "audit", "open-core"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Scientific/Engineering",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"indiciumforge-workflow>=2.0.0,<3.0.0",
|
|
28
|
+
"typer>=0.9",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
indiciumforge = "indiciumforge_cli.main:app"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/Cavaradossi/indiciumforge"
|
|
36
|
+
Repository = "https://github.com/Cavaradossi/indiciumforge"
|
|
37
|
+
Documentation = "https://github.com/Cavaradossi/indiciumforge/tree/master/docs"
|
|
38
|
+
Issues = "https://github.com/Cavaradossi/indiciumforge/issues"
|
|
39
|
+
Changelog = "https://github.com/Cavaradossi/indiciumforge/blob/master/RELEASE_NOTES.md"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reference CLI for IndiciumForge."""
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from indiciumforge_core.artifacts.comparator import GATE_ARTIFACTS
|
|
8
|
+
from indiciumforge_core.artifacts.manifest import (
|
|
9
|
+
DAILY_REVIEW_REQUIRED_FILES,
|
|
10
|
+
format_audit_report,
|
|
11
|
+
is_daily_review_stage_dir,
|
|
12
|
+
is_factor_scan_stage_dir,
|
|
13
|
+
list_daily_review_stages,
|
|
14
|
+
list_market_gate_stages,
|
|
15
|
+
resolve_daily_review_audit_target,
|
|
16
|
+
resolve_factor_scan_audit_target,
|
|
17
|
+
resolve_market_gate_audit_target,
|
|
18
|
+
validate_daily_review_stage,
|
|
19
|
+
validate_factor_scan_stage,
|
|
20
|
+
validate_market_gate_stage,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
artifact_app = typer.Typer(help="Artifact commands.")
|
|
24
|
+
|
|
25
|
+
ARTIFACT_ROOT_OPTION = typer.Option(..., "--artifact-root")
|
|
26
|
+
TRADE_DATE_OPTION = typer.Option(None, "--trade-date")
|
|
27
|
+
STAGE_DIR_OPTION = typer.Option(None, "--stage-dir")
|
|
28
|
+
META_PATH_OPTION = typer.Option(None, "--meta-path")
|
|
29
|
+
STAGE_TYPE_OPTION = typer.Option(
|
|
30
|
+
"market_gate",
|
|
31
|
+
"--stage-type",
|
|
32
|
+
help="Stage domain: market_gate, daily_review, or factor_scan.",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@artifact_app.command("list")
|
|
37
|
+
def artifact_list(artifact_root: Path = ARTIFACT_ROOT_OPTION) -> None:
|
|
38
|
+
gate_stages = list_market_gate_stages(artifact_root)
|
|
39
|
+
review_stages = list_daily_review_stages(artifact_root)
|
|
40
|
+
if not gate_stages and not review_stages:
|
|
41
|
+
typer.echo(f"No artifact stages found under {artifact_root}")
|
|
42
|
+
raise typer.Exit(code=0)
|
|
43
|
+
|
|
44
|
+
for ref in gate_stages:
|
|
45
|
+
typer.echo(
|
|
46
|
+
f"market_gate\t{ref.trade_date}\t"
|
|
47
|
+
f"{ref.core_artifact_count}/{len(GATE_ARTIFACTS)}\t{ref.stage_dir}"
|
|
48
|
+
)
|
|
49
|
+
for ref in review_stages:
|
|
50
|
+
typer.echo(
|
|
51
|
+
f"daily_review\t{ref.trade_date}\t"
|
|
52
|
+
f"{ref.core_artifact_count}/{len(DAILY_REVIEW_REQUIRED_FILES)}\t{ref.stage_dir}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@artifact_app.command("audit")
|
|
57
|
+
def artifact_audit(
|
|
58
|
+
artifact_root: Path | None = typer.Option(None, "--artifact-root"),
|
|
59
|
+
trade_date: str | None = TRADE_DATE_OPTION,
|
|
60
|
+
stage_dir: Path | None = STAGE_DIR_OPTION,
|
|
61
|
+
meta_path: Path | None = META_PATH_OPTION,
|
|
62
|
+
stage_type: str = STAGE_TYPE_OPTION,
|
|
63
|
+
) -> None:
|
|
64
|
+
parsed_trade_date = None
|
|
65
|
+
if trade_date is not None:
|
|
66
|
+
parsed_trade_date = datetime.strptime(trade_date, "%Y-%m-%d").date()
|
|
67
|
+
|
|
68
|
+
if stage_dir is None and (artifact_root is None or parsed_trade_date is None):
|
|
69
|
+
typer.echo("Provide --stage-dir or both --artifact-root and --trade-date.", err=True)
|
|
70
|
+
raise typer.Exit(code=2)
|
|
71
|
+
|
|
72
|
+
use_daily_review = stage_dir is not None and is_daily_review_stage_dir(stage_dir)
|
|
73
|
+
use_factor_scan = stage_dir is not None and is_factor_scan_stage_dir(stage_dir)
|
|
74
|
+
if stage_dir is None and stage_type == "daily_review":
|
|
75
|
+
use_daily_review = True
|
|
76
|
+
if stage_dir is None and stage_type == "factor_scan":
|
|
77
|
+
use_factor_scan = True
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
if use_factor_scan:
|
|
81
|
+
target_dir, expected_trade_date = resolve_factor_scan_audit_target(
|
|
82
|
+
artifact_root=artifact_root,
|
|
83
|
+
trade_date=parsed_trade_date,
|
|
84
|
+
stage_dir=stage_dir,
|
|
85
|
+
)
|
|
86
|
+
if stage_dir is not None and parsed_trade_date is not None:
|
|
87
|
+
expected_trade_date = parsed_trade_date.isoformat()
|
|
88
|
+
manifest = validate_factor_scan_stage(
|
|
89
|
+
target_dir,
|
|
90
|
+
expected_trade_date=expected_trade_date,
|
|
91
|
+
)
|
|
92
|
+
elif use_daily_review:
|
|
93
|
+
target_dir, expected_trade_date = resolve_daily_review_audit_target(
|
|
94
|
+
artifact_root=artifact_root,
|
|
95
|
+
trade_date=parsed_trade_date,
|
|
96
|
+
stage_dir=stage_dir,
|
|
97
|
+
)
|
|
98
|
+
if stage_dir is not None and parsed_trade_date is not None:
|
|
99
|
+
expected_trade_date = parsed_trade_date.isoformat()
|
|
100
|
+
manifest = validate_daily_review_stage(
|
|
101
|
+
target_dir,
|
|
102
|
+
expected_trade_date=expected_trade_date,
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
target_dir, expected_trade_date = resolve_market_gate_audit_target(
|
|
106
|
+
artifact_root=artifact_root,
|
|
107
|
+
trade_date=parsed_trade_date,
|
|
108
|
+
stage_dir=stage_dir,
|
|
109
|
+
)
|
|
110
|
+
if stage_dir is not None and parsed_trade_date is not None:
|
|
111
|
+
expected_trade_date = parsed_trade_date.isoformat()
|
|
112
|
+
manifest = validate_market_gate_stage(
|
|
113
|
+
target_dir,
|
|
114
|
+
expected_trade_date=expected_trade_date,
|
|
115
|
+
meta_path=meta_path,
|
|
116
|
+
)
|
|
117
|
+
except ValueError as exc:
|
|
118
|
+
typer.echo(str(exc), err=True)
|
|
119
|
+
raise typer.Exit(code=2) from exc
|
|
120
|
+
|
|
121
|
+
typer.echo(format_audit_report(manifest))
|
|
122
|
+
if not manifest.ok:
|
|
123
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from indiciumforge_workflow.market_awareness.runner import run_daily_review_skeleton
|
|
8
|
+
|
|
9
|
+
TRADE_DATE_OPTION = typer.Option(..., "--trade-date")
|
|
10
|
+
ARTIFACT_ROOT_OPTION = typer.Option(..., "--artifact-root")
|
|
11
|
+
FIXTURE_PATH_OPTION = typer.Option(..., "--fixture-path")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def workflow_daily_review(
|
|
15
|
+
trade_date: str = TRADE_DATE_OPTION,
|
|
16
|
+
artifact_root: Path = ARTIFACT_ROOT_OPTION,
|
|
17
|
+
fixture_path: Path = FIXTURE_PATH_OPTION,
|
|
18
|
+
) -> None:
|
|
19
|
+
parsed = datetime.strptime(trade_date, "%Y-%m-%d").date()
|
|
20
|
+
result = run_daily_review_skeleton(
|
|
21
|
+
trade_date=parsed,
|
|
22
|
+
artifact_root=artifact_root,
|
|
23
|
+
fixture_path=fixture_path,
|
|
24
|
+
)
|
|
25
|
+
typer.echo(f"Wrote daily-review artifacts: {result.state_path.parent}")
|
|
26
|
+
typer.echo(f" theme_state_ranking: {result.theme_state_ranking_path}")
|
|
27
|
+
typer.echo(f" state: {result.state_path}")
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from indiciumforge_core.artifacts.manifest import validate_factor_scan_stage
|
|
8
|
+
from indiciumforge_core.factors.loading import DetectorLoadError
|
|
9
|
+
from indiciumforge_core.factors.universe import load_assets_from_fixture_list, parse_asset_codes
|
|
10
|
+
from indiciumforge_workflow.factor_scan.runner import FactorScanStageConfig, run_factor_scan_stage
|
|
11
|
+
|
|
12
|
+
TRADE_DATE_OPTION = typer.Option(..., "--trade-date")
|
|
13
|
+
ARTIFACT_ROOT_OPTION = typer.Option(..., "--artifact-root")
|
|
14
|
+
OHLCV_FIXTURE_ROOT_OPTION = typer.Option(
|
|
15
|
+
...,
|
|
16
|
+
"--ohlcv-fixture-root",
|
|
17
|
+
help="Directory with synthetic OHLCV CSV fixtures.",
|
|
18
|
+
)
|
|
19
|
+
ASSET_FIXTURE_LIST_OPTION = typer.Option(
|
|
20
|
+
None,
|
|
21
|
+
"--asset-fixture-list",
|
|
22
|
+
help="YAML asset universe list (default when --codes omitted).",
|
|
23
|
+
)
|
|
24
|
+
CODES_OPTION = typer.Option(
|
|
25
|
+
None,
|
|
26
|
+
"--codes",
|
|
27
|
+
help="Local convenience: comma-separated asset codes (not a production universe).",
|
|
28
|
+
)
|
|
29
|
+
FACTOR_PACK_OPTION = typer.Option(None, "--factor-pack")
|
|
30
|
+
DETECTORS_CONFIG_OPTION = typer.Option(None, "--detectors-config")
|
|
31
|
+
INCLUDE_ENTRY_POINTS_OPTION = typer.Option(
|
|
32
|
+
False,
|
|
33
|
+
"--include-entry-points",
|
|
34
|
+
help="Also load detectors from installed indiciumforge.factor_detectors entry points.",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def factor_scan(
|
|
39
|
+
trade_date: str = TRADE_DATE_OPTION,
|
|
40
|
+
artifact_root: Path = ARTIFACT_ROOT_OPTION,
|
|
41
|
+
ohlcv_fixture_root: Path = OHLCV_FIXTURE_ROOT_OPTION,
|
|
42
|
+
asset_fixture_list: Path | None = ASSET_FIXTURE_LIST_OPTION,
|
|
43
|
+
codes: str | None = CODES_OPTION,
|
|
44
|
+
factor_pack: Path | None = FACTOR_PACK_OPTION,
|
|
45
|
+
detectors_config: Path | None = DETECTORS_CONFIG_OPTION,
|
|
46
|
+
include_entry_points: bool = INCLUDE_ENTRY_POINTS_OPTION,
|
|
47
|
+
) -> None:
|
|
48
|
+
if factor_pack is None and detectors_config is None and not include_entry_points:
|
|
49
|
+
typer.echo(
|
|
50
|
+
"Provide --factor-pack, --detectors-config, or --include-entry-points.",
|
|
51
|
+
err=True,
|
|
52
|
+
)
|
|
53
|
+
raise typer.Exit(code=2)
|
|
54
|
+
|
|
55
|
+
parsed = datetime.strptime(trade_date, "%Y-%m-%d").date()
|
|
56
|
+
|
|
57
|
+
if codes is not None:
|
|
58
|
+
assets = tuple(parse_asset_codes(codes))
|
|
59
|
+
asset_universe_source = "cli_codes"
|
|
60
|
+
elif asset_fixture_list is not None:
|
|
61
|
+
assets = tuple(load_assets_from_fixture_list(asset_fixture_list))
|
|
62
|
+
asset_universe_source = "fixture_asset_list"
|
|
63
|
+
else:
|
|
64
|
+
typer.echo("Provide --asset-fixture-list or --codes.", err=True)
|
|
65
|
+
raise typer.Exit(code=2)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
result = run_factor_scan_stage(
|
|
69
|
+
trade_date=parsed,
|
|
70
|
+
artifact_root=artifact_root,
|
|
71
|
+
config=FactorScanStageConfig(
|
|
72
|
+
pack_config=factor_pack,
|
|
73
|
+
detectors_config=detectors_config,
|
|
74
|
+
include_entry_points=include_entry_points,
|
|
75
|
+
ohlcv_fixture_root=ohlcv_fixture_root,
|
|
76
|
+
asset_fixture_list=asset_fixture_list,
|
|
77
|
+
assets=assets,
|
|
78
|
+
asset_universe_source=asset_universe_source,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
except (ValueError, OSError, DetectorLoadError) as exc:
|
|
82
|
+
typer.echo(str(exc), err=True)
|
|
83
|
+
raise typer.Exit(code=2) from exc
|
|
84
|
+
|
|
85
|
+
audit = validate_factor_scan_stage(result.stage_dir, expected_trade_date=parsed.isoformat())
|
|
86
|
+
typer.echo(f"factor-scan stage: {result.stage_dir}")
|
|
87
|
+
typer.echo(f"pack_id: {result.pack.pack_id or '(none)'}")
|
|
88
|
+
typer.echo(f"detectors: {', '.join(result.pack.registry.list_detectors())}")
|
|
89
|
+
typer.echo(f"signals: {result.signal_count}")
|
|
90
|
+
typer.echo(f"factor_scan audit: {'ok' if audit.ok else 'failed'}")
|
|
91
|
+
|
|
92
|
+
if not audit.ok:
|
|
93
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from indiciumforge_workflow.market_gate.runner import run_market_gate
|
|
8
|
+
|
|
9
|
+
from indiciumforge_cli.artifact import artifact_app
|
|
10
|
+
from indiciumforge_cli.daily_review import workflow_daily_review
|
|
11
|
+
from indiciumforge_cli.factor import factor_scan
|
|
12
|
+
from indiciumforge_cli.parity import parity_report, parity_run
|
|
13
|
+
from indiciumforge_cli.provider import provider_fetch, provider_inspect
|
|
14
|
+
from indiciumforge_cli.synthetic_e2e import workflow_synthetic_e2e
|
|
15
|
+
from indiciumforge_cli.workflow_chain import workflow_chain
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(help="IndiciumForge reference CLI.")
|
|
18
|
+
workflow_app = typer.Typer(help="Workflow commands.")
|
|
19
|
+
factor_app = typer.Typer(help="Factor commands.")
|
|
20
|
+
provider_app = typer.Typer(help="Data provider commands.")
|
|
21
|
+
parity_app = typer.Typer(help="Private-local parity harness (research audit only).")
|
|
22
|
+
app.add_typer(workflow_app, name="workflow")
|
|
23
|
+
app.add_typer(artifact_app, name="artifact")
|
|
24
|
+
app.add_typer(factor_app, name="factor")
|
|
25
|
+
app.add_typer(provider_app, name="provider")
|
|
26
|
+
app.add_typer(parity_app, name="parity")
|
|
27
|
+
|
|
28
|
+
TRADE_DATE_OPTION = typer.Option(..., "--trade-date")
|
|
29
|
+
ARTIFACT_ROOT_OPTION = typer.Option(..., "--artifact-root")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@workflow_app.command("market-gate")
|
|
33
|
+
def workflow_market_gate(
|
|
34
|
+
trade_date: str = TRADE_DATE_OPTION,
|
|
35
|
+
artifact_root: Path = ARTIFACT_ROOT_OPTION,
|
|
36
|
+
) -> None:
|
|
37
|
+
parsed = datetime.strptime(trade_date, "%Y-%m-%d").date()
|
|
38
|
+
result = run_market_gate(trade_date=parsed, artifact_root=artifact_root)
|
|
39
|
+
typer.echo(f"Wrote market-gate artifacts: {result.stage_dir}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@workflow_app.command("daily-review", help="Run skeleton daily-review from a synthetic fixture.")
|
|
43
|
+
def workflow_daily_review_command(
|
|
44
|
+
trade_date: str = TRADE_DATE_OPTION,
|
|
45
|
+
artifact_root: Path = ARTIFACT_ROOT_OPTION,
|
|
46
|
+
fixture_path: Path = typer.Option(..., "--fixture-path"),
|
|
47
|
+
) -> None:
|
|
48
|
+
workflow_daily_review(
|
|
49
|
+
trade_date=trade_date,
|
|
50
|
+
artifact_root=artifact_root,
|
|
51
|
+
fixture_path=fixture_path,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@workflow_app.command(
|
|
56
|
+
"synthetic-e2e",
|
|
57
|
+
help="Run synthetic end-to-end workflow: daily-review -> market-gate -> audit summary.",
|
|
58
|
+
)
|
|
59
|
+
def workflow_synthetic_e2e_command(
|
|
60
|
+
trade_date: str = TRADE_DATE_OPTION,
|
|
61
|
+
artifact_root: Path = ARTIFACT_ROOT_OPTION,
|
|
62
|
+
daily_review_fixture: Path = typer.Option(..., "--daily-review-fixture"),
|
|
63
|
+
preopen_review_fixture: Path = typer.Option(..., "--preopen-review-fixture"),
|
|
64
|
+
) -> None:
|
|
65
|
+
workflow_synthetic_e2e(
|
|
66
|
+
trade_date=trade_date,
|
|
67
|
+
artifact_root=artifact_root,
|
|
68
|
+
daily_review_fixture=daily_review_fixture,
|
|
69
|
+
preopen_review_fixture=preopen_review_fixture,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@workflow_app.command(
|
|
74
|
+
"chain",
|
|
75
|
+
help="Run workflow chain: skeleton fixtures or recipe-driven private extension pack.",
|
|
76
|
+
)
|
|
77
|
+
def workflow_chain_command(
|
|
78
|
+
trade_date: str = TRADE_DATE_OPTION,
|
|
79
|
+
artifact_root: Path = ARTIFACT_ROOT_OPTION,
|
|
80
|
+
daily_review_fixture: Path = typer.Option(..., "--daily-review-fixture"),
|
|
81
|
+
post_close_review_fixture: Path | None = typer.Option(
|
|
82
|
+
None, "--post-close-review-fixture"
|
|
83
|
+
),
|
|
84
|
+
preopen_review_fixture: Path | None = typer.Option(None, "--preopen-review-fixture"),
|
|
85
|
+
recipe: Path | None = typer.Option(None, "--recipe"),
|
|
86
|
+
recipe_extension_pack: Path | None = typer.Option(None, "--recipe-extension-pack"),
|
|
87
|
+
factor_pack: Path | None = typer.Option(None, "--factor-pack"),
|
|
88
|
+
detectors_config: Path | None = typer.Option(None, "--detectors-config"),
|
|
89
|
+
include_entry_points: bool = typer.Option(False, "--include-entry-points"),
|
|
90
|
+
ohlcv_fixture_root: Path | None = typer.Option(None, "--ohlcv-fixture-root"),
|
|
91
|
+
asset_fixture_list: Path | None = typer.Option(None, "--asset-fixture-list"),
|
|
92
|
+
codes: str | None = typer.Option(
|
|
93
|
+
None,
|
|
94
|
+
"--codes",
|
|
95
|
+
help="Local convenience only; not a production universe mechanism.",
|
|
96
|
+
),
|
|
97
|
+
) -> None:
|
|
98
|
+
workflow_chain(
|
|
99
|
+
trade_date=trade_date,
|
|
100
|
+
artifact_root=artifact_root,
|
|
101
|
+
daily_review_fixture=daily_review_fixture,
|
|
102
|
+
post_close_review_fixture=post_close_review_fixture,
|
|
103
|
+
preopen_review_fixture=preopen_review_fixture,
|
|
104
|
+
recipe=recipe,
|
|
105
|
+
recipe_extension_pack=recipe_extension_pack,
|
|
106
|
+
factor_pack=factor_pack,
|
|
107
|
+
detectors_config=detectors_config,
|
|
108
|
+
include_entry_points=include_entry_points,
|
|
109
|
+
ohlcv_fixture_root=ohlcv_fixture_root,
|
|
110
|
+
asset_fixture_list=asset_fixture_list,
|
|
111
|
+
codes=codes,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@provider_app.command("inspect", help="List loaded providers and capability matrix.")
|
|
116
|
+
def provider_inspect_command(
|
|
117
|
+
provider_pack: Path | None = typer.Option(None, "--provider-pack"),
|
|
118
|
+
providers_config: Path | None = typer.Option(None, "--providers-config"),
|
|
119
|
+
include_entry_points: bool = typer.Option(False, "--include-entry-points"),
|
|
120
|
+
ohlcv_fixture_root: Path | None = typer.Option(None, "--ohlcv-fixture-root"),
|
|
121
|
+
) -> None:
|
|
122
|
+
provider_inspect(
|
|
123
|
+
provider_pack=provider_pack,
|
|
124
|
+
providers_config=providers_config,
|
|
125
|
+
include_entry_points=include_entry_points,
|
|
126
|
+
ohlcv_fixture_root=ohlcv_fixture_root,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@provider_app.command("fetch", help="Single-query provider smoke (fixture/fake only).")
|
|
131
|
+
def provider_fetch_command(
|
|
132
|
+
trade_date: str = TRADE_DATE_OPTION,
|
|
133
|
+
code: str = typer.Option(..., "--code"),
|
|
134
|
+
data_kind: str = typer.Option("ohlcv", "--data-kind"),
|
|
135
|
+
provider_pack: Path | None = typer.Option(None, "--provider-pack"),
|
|
136
|
+
providers_config: Path | None = typer.Option(None, "--providers-config"),
|
|
137
|
+
include_entry_points: bool = typer.Option(False, "--include-entry-points"),
|
|
138
|
+
ohlcv_fixture_root: Path | None = typer.Option(None, "--ohlcv-fixture-root"),
|
|
139
|
+
recipe_id: str = typer.Option("indiciumforge.recipe.ashare_daily_research.v1", "--recipe-id"),
|
|
140
|
+
cycle_id: str | None = typer.Option(None, "--cycle-id"),
|
|
141
|
+
checkpoint_id: str | None = typer.Option(None, "--checkpoint-id"),
|
|
142
|
+
) -> None:
|
|
143
|
+
provider_fetch(
|
|
144
|
+
trade_date=trade_date,
|
|
145
|
+
code=code,
|
|
146
|
+
data_kind=data_kind,
|
|
147
|
+
provider_pack=provider_pack,
|
|
148
|
+
providers_config=providers_config,
|
|
149
|
+
include_entry_points=include_entry_points,
|
|
150
|
+
ohlcv_fixture_root=ohlcv_fixture_root,
|
|
151
|
+
recipe_id=recipe_id,
|
|
152
|
+
cycle_id=cycle_id,
|
|
153
|
+
checkpoint_id=checkpoint_id,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@factor_app.command("scan", help="Run factor scan from a local private pack or detectors config.")
|
|
158
|
+
def factor_scan_command(
|
|
159
|
+
trade_date: str = TRADE_DATE_OPTION,
|
|
160
|
+
artifact_root: Path = ARTIFACT_ROOT_OPTION,
|
|
161
|
+
ohlcv_fixture_root: Path = typer.Option(..., "--ohlcv-fixture-root"),
|
|
162
|
+
asset_fixture_list: Path | None = typer.Option(None, "--asset-fixture-list"),
|
|
163
|
+
codes: str | None = typer.Option(None, "--codes"),
|
|
164
|
+
factor_pack: Path | None = typer.Option(None, "--factor-pack"),
|
|
165
|
+
detectors_config: Path | None = typer.Option(None, "--detectors-config"),
|
|
166
|
+
include_entry_points: bool = typer.Option(False, "--include-entry-points"),
|
|
167
|
+
) -> None:
|
|
168
|
+
factor_scan(
|
|
169
|
+
trade_date=trade_date,
|
|
170
|
+
artifact_root=artifact_root,
|
|
171
|
+
ohlcv_fixture_root=ohlcv_fixture_root,
|
|
172
|
+
asset_fixture_list=asset_fixture_list,
|
|
173
|
+
codes=codes,
|
|
174
|
+
factor_pack=factor_pack,
|
|
175
|
+
detectors_config=detectors_config,
|
|
176
|
+
include_entry_points=include_entry_points,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@parity_app.command(
|
|
181
|
+
"run",
|
|
182
|
+
help="Run recipe chain then compare against a local reference artifact root.",
|
|
183
|
+
)
|
|
184
|
+
def parity_run_command(
|
|
185
|
+
parity_config: Path = typer.Option(..., "--parity-config"),
|
|
186
|
+
artifact_root: Path | None = typer.Option(None, "--artifact-root"),
|
|
187
|
+
) -> None:
|
|
188
|
+
parity_run(parity_config=parity_config, artifact_root=artifact_root)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@parity_app.command("report", help="Summarize an existing parity_run_report.json.")
|
|
192
|
+
def parity_report_command(
|
|
193
|
+
report: Path = typer.Option(..., "--report"),
|
|
194
|
+
output_format: str = typer.Option("table", "--format", help="table or json"),
|
|
195
|
+
) -> None:
|
|
196
|
+
parity_report(report_path=report, output_format=output_format)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from indiciumforge_core.parity.config import ParityConfigError, load_parity_config
|
|
8
|
+
from indiciumforge_workflow.parity.runner import run_parity_after_recipe_chain
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parity_run(
|
|
12
|
+
*,
|
|
13
|
+
parity_config: Path,
|
|
14
|
+
artifact_root: Path | None = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
try:
|
|
17
|
+
config = load_parity_config(parity_config)
|
|
18
|
+
result = run_parity_after_recipe_chain(config=config, artifact_root=artifact_root)
|
|
19
|
+
except (ParityConfigError, FileNotFoundError, ValueError, OSError) as exc:
|
|
20
|
+
typer.echo(str(exc), err=True)
|
|
21
|
+
raise typer.Exit(code=2) from exc
|
|
22
|
+
|
|
23
|
+
typer.echo(f"recipe chain summary: {result.chain.summary_path}")
|
|
24
|
+
typer.echo(f"parity report: {result.report.report_path}")
|
|
25
|
+
for check in result.report.results:
|
|
26
|
+
typer.echo(f"{check.dimension.value}: {check.verdict.value} - {check.message}")
|
|
27
|
+
|
|
28
|
+
if not result.report.all_match:
|
|
29
|
+
raise typer.Exit(code=1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parity_report(*, report_path: Path, output_format: str = "table") -> None:
|
|
33
|
+
if not report_path.is_file():
|
|
34
|
+
typer.echo(f"report not found: {report_path}", err=True)
|
|
35
|
+
raise typer.Exit(code=2)
|
|
36
|
+
|
|
37
|
+
payload = json.loads(report_path.read_text(encoding="utf-8-sig"))
|
|
38
|
+
if output_format == "json":
|
|
39
|
+
typer.echo(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
typer.echo(f"trade_date: {payload.get('trade_date')}")
|
|
43
|
+
typer.echo(f"all_match: {payload.get('all_match')}")
|
|
44
|
+
typer.echo(f"disclaimer: {payload.get('disclaimer')}")
|
|
45
|
+
for item in payload.get("results", []):
|
|
46
|
+
typer.echo(
|
|
47
|
+
f"- {item.get('dimension')}: {item.get('verdict')} ({item.get('message')})"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if not payload.get("all_match"):
|
|
51
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from indiciumforge_core.domain.models import AssetID, AssetType, Exchange
|
|
9
|
+
from indiciumforge_core.providers.capabilities import DataKind
|
|
10
|
+
from indiciumforge_core.providers.config import ProviderLoadError, load_provider_pack
|
|
11
|
+
from indiciumforge_core.providers.local_fixture_v2 import LocalFixtureProviderV2
|
|
12
|
+
from indiciumforge_core.providers.query import DataQuery
|
|
13
|
+
from indiciumforge_core.providers.registry_v2 import ProviderRegistryV2
|
|
14
|
+
from indiciumforge_core.workflow.model import (
|
|
15
|
+
AssetDomain,
|
|
16
|
+
SessionModel,
|
|
17
|
+
WorkflowSessionMetadata,
|
|
18
|
+
ashare_cycle_id,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
PROVIDER_PACK_OPTION = typer.Option(None, "--provider-pack")
|
|
22
|
+
PROVIDERS_CONFIG_OPTION = typer.Option(None, "--providers-config")
|
|
23
|
+
INCLUDE_ENTRY_POINTS_OPTION = typer.Option(
|
|
24
|
+
False,
|
|
25
|
+
"--include-entry-points",
|
|
26
|
+
help="Also load providers from installed indiciumforge.data_providers entry points.",
|
|
27
|
+
)
|
|
28
|
+
OHLCV_FIXTURE_ROOT_OPTION = typer.Option(
|
|
29
|
+
None,
|
|
30
|
+
"--ohlcv-fixture-root",
|
|
31
|
+
help="Fixture root when no pack/config is supplied (local_fixture only).",
|
|
32
|
+
)
|
|
33
|
+
TRADE_DATE_OPTION = typer.Option(..., "--trade-date")
|
|
34
|
+
CODE_OPTION = typer.Option(..., "--code", help="Asset code (SSE stock for smoke tests).")
|
|
35
|
+
DATA_KIND_OPTION = typer.Option("ohlcv", "--data-kind")
|
|
36
|
+
RECIPE_ID_OPTION = typer.Option(
|
|
37
|
+
"indiciumforge.recipe.ashare_daily_research.v1",
|
|
38
|
+
"--recipe-id",
|
|
39
|
+
)
|
|
40
|
+
CYCLE_ID_OPTION = typer.Option(None, "--cycle-id")
|
|
41
|
+
CHECKPOINT_ID_OPTION = typer.Option(None, "--checkpoint-id")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _load_registry(
|
|
45
|
+
*,
|
|
46
|
+
provider_pack: Path | None,
|
|
47
|
+
providers_config: Path | None,
|
|
48
|
+
include_entry_points: bool,
|
|
49
|
+
ohlcv_fixture_root: Path | None,
|
|
50
|
+
) -> ProviderRegistryV2:
|
|
51
|
+
if provider_pack is not None or providers_config is not None or include_entry_points:
|
|
52
|
+
loaded = load_provider_pack(
|
|
53
|
+
pack_config=provider_pack,
|
|
54
|
+
providers_config=providers_config,
|
|
55
|
+
include_entry_points=include_entry_points,
|
|
56
|
+
)
|
|
57
|
+
return ProviderRegistryV2(list(loaded.providers))
|
|
58
|
+
|
|
59
|
+
if ohlcv_fixture_root is None:
|
|
60
|
+
raise ProviderLoadError(
|
|
61
|
+
"provide --provider-pack, --providers-config, --include-entry-points, "
|
|
62
|
+
"or --ohlcv-fixture-root"
|
|
63
|
+
)
|
|
64
|
+
return ProviderRegistryV2([LocalFixtureProviderV2(ohlcv_fixture_root)])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def provider_inspect(
|
|
68
|
+
provider_pack: Path | None = PROVIDER_PACK_OPTION,
|
|
69
|
+
providers_config: Path | None = PROVIDERS_CONFIG_OPTION,
|
|
70
|
+
include_entry_points: bool = INCLUDE_ENTRY_POINTS_OPTION,
|
|
71
|
+
ohlcv_fixture_root: Path | None = OHLCV_FIXTURE_ROOT_OPTION,
|
|
72
|
+
) -> None:
|
|
73
|
+
try:
|
|
74
|
+
if provider_pack is not None or providers_config is not None or include_entry_points:
|
|
75
|
+
loaded = load_provider_pack(
|
|
76
|
+
pack_config=provider_pack,
|
|
77
|
+
providers_config=providers_config,
|
|
78
|
+
include_entry_points=include_entry_points,
|
|
79
|
+
)
|
|
80
|
+
if loaded.pack_id:
|
|
81
|
+
typer.echo(f"pack_id: {loaded.pack_id}")
|
|
82
|
+
if loaded.version:
|
|
83
|
+
typer.echo(f"version: {loaded.version}")
|
|
84
|
+
typer.echo(f"sources: {', '.join(loaded.sources)}")
|
|
85
|
+
providers = loaded.providers
|
|
86
|
+
elif ohlcv_fixture_root is not None:
|
|
87
|
+
providers = (LocalFixtureProviderV2(ohlcv_fixture_root),)
|
|
88
|
+
else:
|
|
89
|
+
typer.echo(
|
|
90
|
+
"Provide --provider-pack, --providers-config, --include-entry-points, "
|
|
91
|
+
"or --ohlcv-fixture-root.",
|
|
92
|
+
err=True,
|
|
93
|
+
)
|
|
94
|
+
raise typer.Exit(code=2)
|
|
95
|
+
except ProviderLoadError as exc:
|
|
96
|
+
typer.echo(str(exc), err=True)
|
|
97
|
+
raise typer.Exit(code=2) from exc
|
|
98
|
+
|
|
99
|
+
for provider in providers:
|
|
100
|
+
typer.echo(f"provider_id: {provider.provider_id}")
|
|
101
|
+
typer.echo(f" authority_level: {provider.authority_level.value}")
|
|
102
|
+
for capability in provider.capabilities:
|
|
103
|
+
venues = ",".join(capability.venues) if capability.venues else "-"
|
|
104
|
+
typer.echo(
|
|
105
|
+
" capability: "
|
|
106
|
+
f"{capability.asset_domain.value}/"
|
|
107
|
+
f"{capability.data_kind.value}/"
|
|
108
|
+
f"{capability.latency_profile.value} venues={venues}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def provider_fetch(
|
|
113
|
+
trade_date: str = TRADE_DATE_OPTION,
|
|
114
|
+
code: str = CODE_OPTION,
|
|
115
|
+
data_kind: str = DATA_KIND_OPTION,
|
|
116
|
+
provider_pack: Path | None = PROVIDER_PACK_OPTION,
|
|
117
|
+
providers_config: Path | None = PROVIDERS_CONFIG_OPTION,
|
|
118
|
+
include_entry_points: bool = INCLUDE_ENTRY_POINTS_OPTION,
|
|
119
|
+
ohlcv_fixture_root: Path | None = OHLCV_FIXTURE_ROOT_OPTION,
|
|
120
|
+
recipe_id: str = RECIPE_ID_OPTION,
|
|
121
|
+
cycle_id: str | None = CYCLE_ID_OPTION,
|
|
122
|
+
checkpoint_id: str | None = CHECKPOINT_ID_OPTION,
|
|
123
|
+
) -> None:
|
|
124
|
+
try:
|
|
125
|
+
parsed = datetime.strptime(trade_date, "%Y-%m-%d").date()
|
|
126
|
+
kind = DataKind(data_kind)
|
|
127
|
+
registry = _load_registry(
|
|
128
|
+
provider_pack=provider_pack,
|
|
129
|
+
providers_config=providers_config,
|
|
130
|
+
include_entry_points=include_entry_points,
|
|
131
|
+
ohlcv_fixture_root=ohlcv_fixture_root,
|
|
132
|
+
)
|
|
133
|
+
except (ProviderLoadError, ValueError) as exc:
|
|
134
|
+
typer.echo(str(exc), err=True)
|
|
135
|
+
raise typer.Exit(code=2) from exc
|
|
136
|
+
|
|
137
|
+
session = WorkflowSessionMetadata(
|
|
138
|
+
recipe_id=recipe_id,
|
|
139
|
+
asset_domain=AssetDomain.CHINA_A_SHARE,
|
|
140
|
+
session_model=SessionModel.CALENDAR_DAY_CYCLE,
|
|
141
|
+
cycle_id=cycle_id or ashare_cycle_id(parsed),
|
|
142
|
+
)
|
|
143
|
+
query = DataQuery.from_session(
|
|
144
|
+
asset=AssetID(code, Exchange.SSE, AssetType.STOCK),
|
|
145
|
+
data_kind=kind,
|
|
146
|
+
session=session,
|
|
147
|
+
as_of=parsed,
|
|
148
|
+
checkpoint_id=checkpoint_id,
|
|
149
|
+
)
|
|
150
|
+
result = registry.fetch(query)
|
|
151
|
+
payload = {
|
|
152
|
+
"rows": len(result.frame),
|
|
153
|
+
"provenance": result.provenance.to_payload(),
|
|
154
|
+
"attempted_providers": list(result.attempted_providers),
|
|
155
|
+
}
|
|
156
|
+
typer.echo(json.dumps(payload, indent=2, default=str))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from indiciumforge_workflow.e2e.synthetic import run_synthetic_e2e
|
|
8
|
+
|
|
9
|
+
TRADE_DATE_OPTION = typer.Option(..., "--trade-date")
|
|
10
|
+
ARTIFACT_ROOT_OPTION = typer.Option(..., "--artifact-root")
|
|
11
|
+
DAILY_REVIEW_FIXTURE_OPTION = typer.Option(..., "--daily-review-fixture")
|
|
12
|
+
PREOPEN_REVIEW_FIXTURE_OPTION = typer.Option(..., "--preopen-review-fixture")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def workflow_synthetic_e2e(
|
|
16
|
+
trade_date: str = TRADE_DATE_OPTION,
|
|
17
|
+
artifact_root: Path = ARTIFACT_ROOT_OPTION,
|
|
18
|
+
daily_review_fixture: Path = DAILY_REVIEW_FIXTURE_OPTION,
|
|
19
|
+
preopen_review_fixture: Path = PREOPEN_REVIEW_FIXTURE_OPTION,
|
|
20
|
+
) -> None:
|
|
21
|
+
parsed = datetime.strptime(trade_date, "%Y-%m-%d").date()
|
|
22
|
+
try:
|
|
23
|
+
result = run_synthetic_e2e(
|
|
24
|
+
trade_date=parsed,
|
|
25
|
+
artifact_root=artifact_root,
|
|
26
|
+
daily_review_fixture=daily_review_fixture,
|
|
27
|
+
preopen_review_fixture=preopen_review_fixture,
|
|
28
|
+
)
|
|
29
|
+
except FileNotFoundError as exc:
|
|
30
|
+
typer.echo(str(exc), err=True)
|
|
31
|
+
raise typer.Exit(code=2) from exc
|
|
32
|
+
|
|
33
|
+
typer.echo(f"daily-review stage: {result.daily_review_stage_dir}")
|
|
34
|
+
typer.echo(
|
|
35
|
+
" theme_state_ranking: "
|
|
36
|
+
f"{result.daily_review_stage_dir / 'theme_state_ranking.csv'}"
|
|
37
|
+
)
|
|
38
|
+
typer.echo(f"market-gate stage: {result.market_gate_stage_dir}")
|
|
39
|
+
typer.echo(
|
|
40
|
+
f"daily_review audit: {'ok' if result.daily_review_audit_ok else 'failed'}"
|
|
41
|
+
)
|
|
42
|
+
typer.echo(f"market_gate audit: {'ok' if result.market_gate_audit_ok else 'failed'}")
|
|
43
|
+
typer.echo(f"audit: {'ok' if result.audit_ok else 'failed'}")
|
|
44
|
+
typer.echo(f"summary: {result.summary_path}")
|
|
45
|
+
|
|
46
|
+
if not result.audit_ok:
|
|
47
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from indiciumforge_core.factors.loading import DetectorLoadError
|
|
8
|
+
from indiciumforge_core.factors.universe import load_assets_from_fixture_list, parse_asset_codes
|
|
9
|
+
from indiciumforge_core.recipes.pack import RecipeExtensionLoadError
|
|
10
|
+
from indiciumforge_workflow.factor_scan.runner import FactorScanStageConfig
|
|
11
|
+
from indiciumforge_workflow.workflow_chain.runner import (
|
|
12
|
+
WorkflowChainRecipeConfig,
|
|
13
|
+
run_workflow_chain_recipe,
|
|
14
|
+
run_workflow_chain_skeleton,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_factor_scan_config(
|
|
19
|
+
*,
|
|
20
|
+
factor_pack: Path | None,
|
|
21
|
+
detectors_config: Path | None,
|
|
22
|
+
include_entry_points: bool,
|
|
23
|
+
ohlcv_fixture_root: Path | None,
|
|
24
|
+
asset_fixture_list: Path | None,
|
|
25
|
+
codes: str | None,
|
|
26
|
+
) -> FactorScanStageConfig | None:
|
|
27
|
+
if factor_pack is None and detectors_config is None and not include_entry_points:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
if ohlcv_fixture_root is None:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"--ohlcv-fixture-root is required when factor scan flags are set."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if codes is not None:
|
|
36
|
+
assets = tuple(parse_asset_codes(codes))
|
|
37
|
+
asset_universe_source = "cli_codes"
|
|
38
|
+
elif asset_fixture_list is not None:
|
|
39
|
+
assets = tuple(load_assets_from_fixture_list(asset_fixture_list))
|
|
40
|
+
asset_universe_source = "fixture_asset_list"
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Provide --asset-fixture-list or --codes when enabling factor scan."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return FactorScanStageConfig(
|
|
47
|
+
pack_config=factor_pack,
|
|
48
|
+
detectors_config=detectors_config,
|
|
49
|
+
include_entry_points=include_entry_points,
|
|
50
|
+
ohlcv_fixture_root=ohlcv_fixture_root,
|
|
51
|
+
asset_fixture_list=asset_fixture_list,
|
|
52
|
+
assets=assets,
|
|
53
|
+
asset_universe_source=asset_universe_source,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _echo_chain_result(result) -> None: # noqa: ANN001
|
|
58
|
+
typer.echo(f"daily-review stage: {result.daily_review_stage_dir}")
|
|
59
|
+
if result.factor_scan_enabled and result.factor_scan_stage_dir is not None:
|
|
60
|
+
typer.echo(f"factor-scan stage: {result.factor_scan_stage_dir}")
|
|
61
|
+
typer.echo(f"factor signals: {result.signal_count}")
|
|
62
|
+
typer.echo(
|
|
63
|
+
"factor_scan audit: "
|
|
64
|
+
f"{'ok' if result.factor_scan_audit_ok else 'failed'}"
|
|
65
|
+
)
|
|
66
|
+
typer.echo(f"post-close stage: {result.post_close_stage_dir}")
|
|
67
|
+
typer.echo(f"preopen stage: {result.preopen_stage_dir}")
|
|
68
|
+
typer.echo(f"market-gate stage: {result.market_gate_stage_dir}")
|
|
69
|
+
if result.recipe_id is not None:
|
|
70
|
+
typer.echo(f"recipe_id: {result.recipe_id}")
|
|
71
|
+
if result.extension_pack_id is not None:
|
|
72
|
+
typer.echo(f"extension_pack_id: {result.extension_pack_id}")
|
|
73
|
+
if result.recipe_run_summary_path is not None:
|
|
74
|
+
typer.echo(f"recipe_run_summary: {result.recipe_run_summary_path}")
|
|
75
|
+
typer.echo(f"workflow_review_source_stage: {result.workflow_review_source_stage}")
|
|
76
|
+
typer.echo(f"strict_count: {result.strict_count}")
|
|
77
|
+
typer.echo(
|
|
78
|
+
f"daily_review audit: {'ok' if result.daily_review_audit_ok else 'failed'}"
|
|
79
|
+
)
|
|
80
|
+
typer.echo(f"market_gate audit: {'ok' if result.market_gate_audit_ok else 'failed'}")
|
|
81
|
+
typer.echo(f"chain: {'ok' if result.chain_ok else 'failed'}")
|
|
82
|
+
typer.echo(f"summary: {result.summary_path}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def workflow_chain(
|
|
86
|
+
*,
|
|
87
|
+
trade_date: str,
|
|
88
|
+
artifact_root: Path,
|
|
89
|
+
daily_review_fixture: Path,
|
|
90
|
+
post_close_review_fixture: Path | None = None,
|
|
91
|
+
preopen_review_fixture: Path | None = None,
|
|
92
|
+
recipe: Path | None = None,
|
|
93
|
+
recipe_extension_pack: Path | None = None,
|
|
94
|
+
factor_pack: Path | None = None,
|
|
95
|
+
detectors_config: Path | None = None,
|
|
96
|
+
include_entry_points: bool = False,
|
|
97
|
+
ohlcv_fixture_root: Path | None = None,
|
|
98
|
+
asset_fixture_list: Path | None = None,
|
|
99
|
+
codes: str | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
parsed = datetime.strptime(trade_date, "%Y-%m-%d").date()
|
|
102
|
+
try:
|
|
103
|
+
factor_scan_config = _build_factor_scan_config(
|
|
104
|
+
factor_pack=factor_pack,
|
|
105
|
+
detectors_config=detectors_config,
|
|
106
|
+
include_entry_points=include_entry_points,
|
|
107
|
+
ohlcv_fixture_root=ohlcv_fixture_root,
|
|
108
|
+
asset_fixture_list=asset_fixture_list,
|
|
109
|
+
codes=codes,
|
|
110
|
+
)
|
|
111
|
+
if recipe is not None:
|
|
112
|
+
if recipe_extension_pack is None:
|
|
113
|
+
raise ValueError("--recipe-extension-pack is required with --recipe")
|
|
114
|
+
result = run_workflow_chain_recipe(
|
|
115
|
+
trade_date=parsed,
|
|
116
|
+
artifact_root=artifact_root,
|
|
117
|
+
config=WorkflowChainRecipeConfig(
|
|
118
|
+
recipe_path=recipe,
|
|
119
|
+
recipe_extension_pack=recipe_extension_pack,
|
|
120
|
+
daily_review_fixture=daily_review_fixture,
|
|
121
|
+
factor_scan_config=factor_scan_config,
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
if post_close_review_fixture is None or preopen_review_fixture is None:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
"skeleton chain requires --post-close-review-fixture and "
|
|
128
|
+
"--preopen-review-fixture (or use --recipe)"
|
|
129
|
+
)
|
|
130
|
+
result = run_workflow_chain_skeleton(
|
|
131
|
+
trade_date=parsed,
|
|
132
|
+
artifact_root=artifact_root,
|
|
133
|
+
daily_review_fixture=daily_review_fixture,
|
|
134
|
+
post_close_review_fixture=post_close_review_fixture,
|
|
135
|
+
preopen_review_fixture=preopen_review_fixture,
|
|
136
|
+
factor_scan_config=factor_scan_config,
|
|
137
|
+
)
|
|
138
|
+
except FileNotFoundError as exc:
|
|
139
|
+
typer.echo(str(exc), err=True)
|
|
140
|
+
raise typer.Exit(code=2) from exc
|
|
141
|
+
except (ValueError, OSError, DetectorLoadError, RecipeExtensionLoadError) as exc:
|
|
142
|
+
typer.echo(str(exc), err=True)
|
|
143
|
+
raise typer.Exit(code=2) from exc
|
|
144
|
+
|
|
145
|
+
_echo_chain_result(result)
|
|
146
|
+
|
|
147
|
+
if not result.chain_ok:
|
|
148
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: indiciumforge-cli
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Reference CLI for IndiciumForge research audit workflows (indiciumforge command).
|
|
5
|
+
Author: IndiciumForge contributors
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/Cavaradossi/indiciumforge
|
|
8
|
+
Project-URL: Repository, https://github.com/Cavaradossi/indiciumforge
|
|
9
|
+
Project-URL: Documentation, https://github.com/Cavaradossi/indiciumforge/tree/master/docs
|
|
10
|
+
Project-URL: Issues, https://github.com/Cavaradossi/indiciumforge/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/Cavaradossi/indiciumforge/blob/master/RELEASE_NOTES.md
|
|
12
|
+
Keywords: cli,workflow,research,audit,open-core
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: indiciumforge-workflow<3.0.0,>=2.0.0
|
|
26
|
+
Requires-Dist: typer>=0.9
|
|
27
|
+
|
|
28
|
+
# indiciumforge-cli
|
|
29
|
+
|
|
30
|
+
Reference CLI for [IndiciumForge](https://github.com/Cavaradossi/indiciumforge).
|
|
31
|
+
|
|
32
|
+
Entry point: `indiciumforge` — workflow, artifact audit, factor scan, provider inspect, parity harness.
|
|
33
|
+
|
|
34
|
+
Research audit only — not investment advice, not broker execution.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
PyPI publish is pending. Install from the monorepo:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e packages/indiciumforge-core -e packages/indiciumforge-workflow -e packages/indiciumforge-cli
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
See [docs/PYPI_RELEASE_CHECKLIST.md](https://github.com/Cavaradossi/indiciumforge/blob/master/docs/PYPI_RELEASE_CHECKLIST.md).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/indiciumforge_cli/__init__.py
|
|
4
|
+
src/indiciumforge_cli/artifact.py
|
|
5
|
+
src/indiciumforge_cli/daily_review.py
|
|
6
|
+
src/indiciumforge_cli/factor.py
|
|
7
|
+
src/indiciumforge_cli/main.py
|
|
8
|
+
src/indiciumforge_cli/parity.py
|
|
9
|
+
src/indiciumforge_cli/provider.py
|
|
10
|
+
src/indiciumforge_cli/synthetic_e2e.py
|
|
11
|
+
src/indiciumforge_cli/workflow_chain.py
|
|
12
|
+
src/indiciumforge_cli.egg-info/PKG-INFO
|
|
13
|
+
src/indiciumforge_cli.egg-info/SOURCES.txt
|
|
14
|
+
src/indiciumforge_cli.egg-info/dependency_links.txt
|
|
15
|
+
src/indiciumforge_cli.egg-info/entry_points.txt
|
|
16
|
+
src/indiciumforge_cli.egg-info/requires.txt
|
|
17
|
+
src/indiciumforge_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
indiciumforge_cli
|