indiciumforge-cli 2.0.0__py3-none-any.whl

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.
@@ -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,14 @@
1
+ indiciumforge_cli/__init__.py,sha256=21pWuZ3WTTEHKu94yAtymHSnRMBHkJwlrBlDHuihcHE,39
2
+ indiciumforge_cli/artifact.py,sha256=vBP8ZRox0J7ZHlQe9kcNg2iYWm6uP_YDCmZRv0NEYqQ,4684
3
+ indiciumforge_cli/daily_review.py,sha256=m4ExnbSy8EHu4UptCvAoISjlSbMUXFRT7EjBDzVR-_Q,945
4
+ indiciumforge_cli/factor.py,sha256=r2YCG4r3Ty3_wAIfGmOMfITyhfLNC3MphvKfqGsxWrk,3628
5
+ indiciumforge_cli/main.py,sha256=aWDb875TdANPAEw8Fk9UUiMUouz_uTwNCoN64kUmba4,8018
6
+ indiciumforge_cli/parity.py,sha256=WOApkAia5wMxt7LBwL87LNYgqXj6pAlmf5SrbG34Uqo,1800
7
+ indiciumforge_cli/provider.py,sha256=Wxjw4RjLpOo0KtJl5_vavLPKv7mGPt4cNgv2LR7YOY4,5975
8
+ indiciumforge_cli/synthetic_e2e.py,sha256=X05e2MNT91S9Xa8rcSqBlUNrhrYHyKjQzUb2Il20gPw,1760
9
+ indiciumforge_cli/workflow_chain.py,sha256=HYbmuAzGouUV2QfED6LiXqUR-x4igfo8oP-Jp0C7TUI,5903
10
+ indiciumforge_cli-2.0.0.dist-info/METADATA,sha256=Z4gf9wpfRLPMOnyk2P01iVGtSfIZrwho65EwZJxos-E,1955
11
+ indiciumforge_cli-2.0.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
12
+ indiciumforge_cli-2.0.0.dist-info/entry_points.txt,sha256=5avLhPwpgZvM3G5rDxwmctIRiOlGr2hGuNKoxipuSNk,61
13
+ indiciumforge_cli-2.0.0.dist-info/top_level.txt,sha256=qMRjR2PXPf11HfVPJe8gz8mDRP7xiETFjJzIBeZPsSA,18
14
+ indiciumforge_cli-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ indiciumforge = indiciumforge_cli.main:app
@@ -0,0 +1 @@
1
+ indiciumforge_cli