indiciumforge-workflow 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_workflow-2.0.0/PKG-INFO +44 -0
- indiciumforge_workflow-2.0.0/README.md +15 -0
- indiciumforge_workflow-2.0.0/pyproject.toml +41 -0
- indiciumforge_workflow-2.0.0/setup.cfg +4 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/__init__.py +1 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/e2e/__init__.py +1 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/e2e/synthetic.py +155 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/factor_scan/__init__.py +11 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/factor_scan/runner.py +77 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_awareness/__init__.py +35 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_awareness/artifacts.py +70 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_awareness/classifier.py +121 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_awareness/fixtures.py +57 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_awareness/models.py +39 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_awareness/runner.py +51 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_gate/__init__.py +1 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_gate/active_watch.py +85 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_gate/calibration.py +74 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_gate/helpers.py +92 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_gate/kernel.py +173 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_gate/render.py +110 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_gate/resolver.py +51 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/market_gate/runner.py +157 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/parity/__init__.py +1 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/parity/runner.py +54 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/workflow_chain/__init__.py +1 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/workflow_chain/runner.py +533 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow/workflow_chain/skeleton.py +76 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow.egg-info/PKG-INFO +44 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow.egg-info/SOURCES.txt +31 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow.egg-info/dependency_links.txt +1 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow.egg-info/requires.txt +4 -0
- indiciumforge_workflow-2.0.0/src/indiciumforge_workflow.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: indiciumforge-workflow
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Workflow services for IndiciumForge: market gate, daily review, workflow chain.
|
|
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: workflow,market-gate,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
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: indiciumforge-core<3.0.0,>=2.0.0
|
|
26
|
+
Requires-Dist: pandas>=2.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: tabulate>=0.9
|
|
29
|
+
|
|
30
|
+
# indiciumforge-workflow
|
|
31
|
+
|
|
32
|
+
Workflow services (market gate, daily review, workflow chain) for [IndiciumForge](https://github.com/Cavaradossi/indiciumforge).
|
|
33
|
+
|
|
34
|
+
Research audit workflows only — not investment advice, not live trading.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
PyPI publish is pending. Install from the monorepo (requires `indiciumforge-core`):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e packages/indiciumforge-core -e packages/indiciumforge-workflow
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
See [docs/PYPI_RELEASE_CHECKLIST.md](https://github.com/Cavaradossi/indiciumforge/blob/master/docs/PYPI_RELEASE_CHECKLIST.md).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# indiciumforge-workflow
|
|
2
|
+
|
|
3
|
+
Workflow services (market gate, daily review, workflow chain) for [IndiciumForge](https://github.com/Cavaradossi/indiciumforge).
|
|
4
|
+
|
|
5
|
+
Research audit workflows only — not investment advice, not live trading.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
PyPI publish is pending. Install from the monorepo (requires `indiciumforge-core`):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install -e packages/indiciumforge-core -e packages/indiciumforge-workflow
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
See [docs/PYPI_RELEASE_CHECKLIST.md](https://github.com/Cavaradossi/indiciumforge/blob/master/docs/PYPI_RELEASE_CHECKLIST.md).
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "indiciumforge-workflow"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "Workflow services for IndiciumForge: market gate, daily review, workflow chain."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "IndiciumForge contributors" }]
|
|
13
|
+
keywords = ["workflow", "market-gate", "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",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"indiciumforge-core>=2.0.0,<3.0.0",
|
|
28
|
+
"pandas>=2.0",
|
|
29
|
+
"pyyaml>=6.0",
|
|
30
|
+
"tabulate>=0.9",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/Cavaradossi/indiciumforge"
|
|
35
|
+
Repository = "https://github.com/Cavaradossi/indiciumforge"
|
|
36
|
+
Documentation = "https://github.com/Cavaradossi/indiciumforge/tree/master/docs"
|
|
37
|
+
Issues = "https://github.com/Cavaradossi/indiciumforge/issues"
|
|
38
|
+
Changelog = "https://github.com/Cavaradossi/indiciumforge/blob/master/RELEASE_NOTES.md"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Workflow services for IndiciumForge."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Synthetic end-to-end workflow orchestration."""
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import date
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from indiciumforge_core.artifacts.manifest import (
|
|
11
|
+
validate_daily_review_stage,
|
|
12
|
+
validate_market_gate_stage,
|
|
13
|
+
)
|
|
14
|
+
from indiciumforge_core.artifacts.paths import (
|
|
15
|
+
daily_review_dir,
|
|
16
|
+
market_gate_stage_dir,
|
|
17
|
+
synthetic_e2e_summary_path,
|
|
18
|
+
workflow_root,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from indiciumforge_workflow.market_awareness.runner import run_daily_review_skeleton
|
|
22
|
+
from indiciumforge_workflow.market_gate.runner import run_market_gate
|
|
23
|
+
|
|
24
|
+
SYNTHETIC_E2E_SUMMARY_SCHEMA = "indiciumforge.synthetic_e2e_summary.v1"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class SyntheticE2EResult:
|
|
29
|
+
trade_date: date
|
|
30
|
+
daily_review_stage_dir: Path
|
|
31
|
+
market_gate_stage_dir: Path
|
|
32
|
+
summary_path: Path
|
|
33
|
+
daily_review_audit_ok: bool
|
|
34
|
+
market_gate_audit_ok: bool
|
|
35
|
+
audit_ok: bool
|
|
36
|
+
warnings: tuple[str, ...]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _seed_preopen_review(
|
|
40
|
+
artifact_root: Path,
|
|
41
|
+
trade_date: date,
|
|
42
|
+
preopen_review_fixture: Path,
|
|
43
|
+
) -> Path:
|
|
44
|
+
target_dir = workflow_root(artifact_root, trade_date) / "preopen"
|
|
45
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
target_path = target_dir / "buy_point_review_internal.csv"
|
|
47
|
+
shutil.copy2(preopen_review_fixture, target_path)
|
|
48
|
+
return target_path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _write_summary(
|
|
52
|
+
path: Path,
|
|
53
|
+
*,
|
|
54
|
+
trade_date: date,
|
|
55
|
+
daily_review_stage_dir: Path,
|
|
56
|
+
market_gate_stage_dir: Path,
|
|
57
|
+
daily_review_audit_ok: bool,
|
|
58
|
+
market_gate_audit_ok: bool,
|
|
59
|
+
daily_review_fixture: Path,
|
|
60
|
+
preopen_review_fixture: Path,
|
|
61
|
+
warnings: list[str],
|
|
62
|
+
) -> None:
|
|
63
|
+
payload: dict[str, Any] = {
|
|
64
|
+
"schema": SYNTHETIC_E2E_SUMMARY_SCHEMA,
|
|
65
|
+
"trade_date": trade_date.isoformat(),
|
|
66
|
+
"provenance": {
|
|
67
|
+
"mode": "synthetic_e2e",
|
|
68
|
+
"fixtures": {
|
|
69
|
+
"daily_review": str(daily_review_fixture),
|
|
70
|
+
"preopen_review": str(preopen_review_fixture),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
"stages": {
|
|
74
|
+
"daily_review": {
|
|
75
|
+
"dir": str(daily_review_stage_dir),
|
|
76
|
+
"audit_ok": daily_review_audit_ok,
|
|
77
|
+
},
|
|
78
|
+
"market_gate": {
|
|
79
|
+
"dir": str(market_gate_stage_dir),
|
|
80
|
+
"audit_ok": market_gate_audit_ok,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
"audit_ok": daily_review_audit_ok and market_gate_audit_ok,
|
|
84
|
+
"warnings": warnings,
|
|
85
|
+
}
|
|
86
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8-sig")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_synthetic_e2e(
|
|
91
|
+
*,
|
|
92
|
+
trade_date: date,
|
|
93
|
+
artifact_root: Path,
|
|
94
|
+
daily_review_fixture: Path,
|
|
95
|
+
preopen_review_fixture: Path,
|
|
96
|
+
) -> SyntheticE2EResult:
|
|
97
|
+
if not daily_review_fixture.is_file():
|
|
98
|
+
raise FileNotFoundError(f"missing daily-review fixture: {daily_review_fixture}")
|
|
99
|
+
if not preopen_review_fixture.is_file():
|
|
100
|
+
raise FileNotFoundError(f"missing preopen review fixture: {preopen_review_fixture}")
|
|
101
|
+
|
|
102
|
+
warnings: list[str] = []
|
|
103
|
+
trade_date_iso = trade_date.isoformat()
|
|
104
|
+
|
|
105
|
+
_seed_preopen_review(artifact_root, trade_date, preopen_review_fixture)
|
|
106
|
+
|
|
107
|
+
daily_result = run_daily_review_skeleton(
|
|
108
|
+
trade_date=trade_date,
|
|
109
|
+
artifact_root=artifact_root,
|
|
110
|
+
fixture_path=daily_review_fixture,
|
|
111
|
+
)
|
|
112
|
+
warnings.extend(daily_result.warnings)
|
|
113
|
+
|
|
114
|
+
gate_result = run_market_gate(trade_date=trade_date, artifact_root=artifact_root)
|
|
115
|
+
warnings.extend(gate_result.warnings)
|
|
116
|
+
|
|
117
|
+
dr_stage_dir = daily_review_dir(artifact_root, trade_date)
|
|
118
|
+
mg_stage_dir = market_gate_stage_dir(artifact_root, trade_date)
|
|
119
|
+
|
|
120
|
+
daily_manifest = validate_daily_review_stage(
|
|
121
|
+
dr_stage_dir,
|
|
122
|
+
expected_trade_date=trade_date_iso,
|
|
123
|
+
)
|
|
124
|
+
gate_manifest = validate_market_gate_stage(
|
|
125
|
+
mg_stage_dir,
|
|
126
|
+
expected_trade_date=trade_date_iso,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
daily_review_audit_ok = daily_manifest.ok
|
|
130
|
+
market_gate_audit_ok = gate_manifest.ok
|
|
131
|
+
audit_ok = daily_review_audit_ok and market_gate_audit_ok
|
|
132
|
+
|
|
133
|
+
summary_path = synthetic_e2e_summary_path(artifact_root, trade_date)
|
|
134
|
+
_write_summary(
|
|
135
|
+
summary_path,
|
|
136
|
+
trade_date=trade_date,
|
|
137
|
+
daily_review_stage_dir=dr_stage_dir,
|
|
138
|
+
market_gate_stage_dir=mg_stage_dir,
|
|
139
|
+
daily_review_audit_ok=daily_review_audit_ok,
|
|
140
|
+
market_gate_audit_ok=market_gate_audit_ok,
|
|
141
|
+
daily_review_fixture=daily_review_fixture,
|
|
142
|
+
preopen_review_fixture=preopen_review_fixture,
|
|
143
|
+
warnings=warnings,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return SyntheticE2EResult(
|
|
147
|
+
trade_date=trade_date,
|
|
148
|
+
daily_review_stage_dir=dr_stage_dir,
|
|
149
|
+
market_gate_stage_dir=mg_stage_dir,
|
|
150
|
+
summary_path=summary_path,
|
|
151
|
+
daily_review_audit_ok=daily_review_audit_ok,
|
|
152
|
+
market_gate_audit_ok=market_gate_audit_ok,
|
|
153
|
+
audit_ok=audit_ok,
|
|
154
|
+
warnings=tuple(warnings),
|
|
155
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import date
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from indiciumforge_core.artifacts.paths import factor_scan_dir
|
|
8
|
+
from indiciumforge_core.domain.models import AssetID
|
|
9
|
+
from indiciumforge_core.factors.artifacts import write_factor_scan_bundle, write_factor_scan_state
|
|
10
|
+
from indiciumforge_core.factors.pack import LoadedFactorPack, load_factor_pack
|
|
11
|
+
from indiciumforge_core.factors.scan import FactorScanRunner
|
|
12
|
+
from indiciumforge_core.providers.local_fixture import LocalFixtureProvider
|
|
13
|
+
from indiciumforge_core.providers.registry import ProviderRegistry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class FactorScanStageConfig:
|
|
18
|
+
pack_config: Path | None = None
|
|
19
|
+
detectors_config: Path | None = None
|
|
20
|
+
include_entry_points: bool = False
|
|
21
|
+
ohlcv_fixture_root: Path | None = None
|
|
22
|
+
asset_fixture_list: Path | None = None
|
|
23
|
+
assets: tuple[AssetID, ...] = ()
|
|
24
|
+
asset_universe_source: str = "fixture_asset_list"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class FactorScanStageResult:
|
|
29
|
+
stage_dir: Path
|
|
30
|
+
pack: LoadedFactorPack
|
|
31
|
+
signal_count: int
|
|
32
|
+
detector_count: int
|
|
33
|
+
warnings: tuple[str, ...]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_factor_scan_stage(
|
|
37
|
+
*,
|
|
38
|
+
trade_date: date,
|
|
39
|
+
artifact_root: Path,
|
|
40
|
+
config: FactorScanStageConfig,
|
|
41
|
+
) -> FactorScanStageResult:
|
|
42
|
+
if not config.assets:
|
|
43
|
+
raise ValueError("factor scan stage requires at least one asset")
|
|
44
|
+
|
|
45
|
+
if config.ohlcv_fixture_root is None:
|
|
46
|
+
raise ValueError("factor scan stage requires ohlcv_fixture_root")
|
|
47
|
+
|
|
48
|
+
loaded = load_factor_pack(
|
|
49
|
+
pack_config=config.pack_config,
|
|
50
|
+
detectors_config=config.detectors_config,
|
|
51
|
+
include_entry_points=config.include_entry_points,
|
|
52
|
+
)
|
|
53
|
+
provider_registry = ProviderRegistry(
|
|
54
|
+
[LocalFixtureProvider(config.ohlcv_fixture_root)]
|
|
55
|
+
)
|
|
56
|
+
runner = FactorScanRunner(provider_registry, loaded.registry)
|
|
57
|
+
result = runner.scan(list(config.assets), trade_date)
|
|
58
|
+
|
|
59
|
+
stage_dir = factor_scan_dir(artifact_root, trade_date)
|
|
60
|
+
write_factor_scan_bundle(stage_dir, trade_date, result)
|
|
61
|
+
write_factor_scan_state(
|
|
62
|
+
stage_dir,
|
|
63
|
+
trade_date=trade_date,
|
|
64
|
+
pack_id=loaded.pack_id,
|
|
65
|
+
detector_names=loaded.registry.list_detectors(),
|
|
66
|
+
asset_universe_source=config.asset_universe_source,
|
|
67
|
+
signal_count=len(result.signals),
|
|
68
|
+
warning_count=len(result.warnings),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return FactorScanStageResult(
|
|
72
|
+
stage_dir=stage_dir,
|
|
73
|
+
pack=loaded,
|
|
74
|
+
signal_count=len(result.signals),
|
|
75
|
+
detector_count=len(loaded.registry.list_detectors()),
|
|
76
|
+
warnings=result.warnings,
|
|
77
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from indiciumforge_workflow.market_awareness.artifacts import (
|
|
2
|
+
THEME_STATE_RANKING_COLUMNS,
|
|
3
|
+
theme_state_rows_to_frame,
|
|
4
|
+
write_daily_review_state,
|
|
5
|
+
write_theme_state_ranking,
|
|
6
|
+
)
|
|
7
|
+
from indiciumforge_workflow.market_awareness.classifier import (
|
|
8
|
+
classify_theme_state,
|
|
9
|
+
classify_theme_states,
|
|
10
|
+
)
|
|
11
|
+
from indiciumforge_workflow.market_awareness.fixtures import (
|
|
12
|
+
ThemeFixtureLoadError,
|
|
13
|
+
load_theme_sector_fixture,
|
|
14
|
+
)
|
|
15
|
+
from indiciumforge_workflow.market_awareness.models import (
|
|
16
|
+
DailyReviewResult,
|
|
17
|
+
ThemeSectorMetrics,
|
|
18
|
+
ThemeStateRow,
|
|
19
|
+
)
|
|
20
|
+
from indiciumforge_workflow.market_awareness.runner import run_daily_review_skeleton
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"DailyReviewResult",
|
|
24
|
+
"THEME_STATE_RANKING_COLUMNS",
|
|
25
|
+
"ThemeFixtureLoadError",
|
|
26
|
+
"ThemeSectorMetrics",
|
|
27
|
+
"ThemeStateRow",
|
|
28
|
+
"classify_theme_state",
|
|
29
|
+
"classify_theme_states",
|
|
30
|
+
"load_theme_sector_fixture",
|
|
31
|
+
"run_daily_review_skeleton",
|
|
32
|
+
"theme_state_rows_to_frame",
|
|
33
|
+
"write_daily_review_state",
|
|
34
|
+
"write_theme_state_ranking",
|
|
35
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import date
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from indiciumforge_core.labels.market_gate import MARKET_DAILY, MARKET_ZH
|
|
10
|
+
from indiciumforge_core.market.theme_rules import THEME_STATE_RULE_VERSION, THEME_STATE_RULES
|
|
11
|
+
|
|
12
|
+
from indiciumforge_workflow.market_awareness.models import ThemeStateRow
|
|
13
|
+
|
|
14
|
+
THEME_STATE_RANKING_COLUMNS: tuple[str, ...] = (
|
|
15
|
+
MARKET_ZH["theme_name"],
|
|
16
|
+
MARKET_DAILY["status"],
|
|
17
|
+
MARKET_DAILY["daily_state"],
|
|
18
|
+
MARKET_DAILY["mid_state"],
|
|
19
|
+
MARKET_DAILY["risk_state"],
|
|
20
|
+
MARKET_DAILY["divergence_state"],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def theme_state_rows_to_frame(rows: list[ThemeStateRow]) -> pd.DataFrame:
|
|
25
|
+
records = [
|
|
26
|
+
{
|
|
27
|
+
MARKET_ZH["theme_name"]: row.theme_name,
|
|
28
|
+
MARKET_DAILY["status"]: row.status,
|
|
29
|
+
MARKET_DAILY["daily_state"]: row.daily_state,
|
|
30
|
+
MARKET_DAILY["mid_state"]: row.mid_state,
|
|
31
|
+
MARKET_DAILY["risk_state"]: row.risk_state,
|
|
32
|
+
MARKET_DAILY["divergence_state"]: row.divergence_state,
|
|
33
|
+
}
|
|
34
|
+
for row in rows
|
|
35
|
+
]
|
|
36
|
+
if not records:
|
|
37
|
+
return pd.DataFrame(columns=list(THEME_STATE_RANKING_COLUMNS))
|
|
38
|
+
return pd.DataFrame(records)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def write_theme_state_ranking(path: Path, rows: list[ThemeStateRow]) -> Path:
|
|
42
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
frame = theme_state_rows_to_frame(rows)
|
|
44
|
+
frame.to_csv(path, index=False, encoding="utf-8-sig")
|
|
45
|
+
return path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_daily_review_state(
|
|
49
|
+
path: Path,
|
|
50
|
+
*,
|
|
51
|
+
trade_date: date,
|
|
52
|
+
theme_state_ranking_path: Path,
|
|
53
|
+
fixture_name: str,
|
|
54
|
+
warnings: list[str] | None = None,
|
|
55
|
+
) -> Path:
|
|
56
|
+
payload: dict[str, Any] = {
|
|
57
|
+
"schema": "indiciumforge.market_daily_review_state.v1",
|
|
58
|
+
"trade_date": trade_date.isoformat(),
|
|
59
|
+
"theme_state_rule_version": THEME_STATE_RULE_VERSION,
|
|
60
|
+
"theme_state_rules": dict(THEME_STATE_RULES),
|
|
61
|
+
"paths": {"theme_state_ranking": str(theme_state_ranking_path)},
|
|
62
|
+
"warnings": warnings or [],
|
|
63
|
+
"provenance": {
|
|
64
|
+
"source": "synthetic_fixture",
|
|
65
|
+
"fixture": fixture_name,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8-sig")
|
|
70
|
+
return path
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from indiciumforge_core.labels.market_gate import MARKET_DAILY, MARKET_ZH
|
|
4
|
+
from indiciumforge_core.market.theme_rules import THEME_STATE_RULES
|
|
5
|
+
|
|
6
|
+
from indiciumforge_workflow.market_awareness.models import ThemeSectorMetrics, ThemeStateRow
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def classify_theme_state(
|
|
10
|
+
metrics: ThemeSectorMetrics,
|
|
11
|
+
rules: dict[str, float | int] | None = None,
|
|
12
|
+
) -> ThemeStateRow:
|
|
13
|
+
rule_set = rules or THEME_STATE_RULES
|
|
14
|
+
neutral = MARKET_ZH["neutral_theme"]
|
|
15
|
+
no_risk = MARKET_DAILY["normal_risk"]
|
|
16
|
+
no_div = MARKET_DAILY["normal_divergence"]
|
|
17
|
+
|
|
18
|
+
if _is_hard_weak(metrics, rule_set):
|
|
19
|
+
hard_weak = MARKET_DAILY["hard_weak"]
|
|
20
|
+
return ThemeStateRow(
|
|
21
|
+
theme_name=metrics.theme_name,
|
|
22
|
+
status=hard_weak,
|
|
23
|
+
daily_state=MARKET_DAILY["daily_weak"],
|
|
24
|
+
mid_state=neutral,
|
|
25
|
+
risk_state=hard_weak,
|
|
26
|
+
divergence_state=no_div,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if _is_divergent(metrics, rule_set):
|
|
30
|
+
return ThemeStateRow(
|
|
31
|
+
theme_name=metrics.theme_name,
|
|
32
|
+
status=MARKET_DAILY["divergent"],
|
|
33
|
+
daily_state=MARKET_DAILY["daily_weak"],
|
|
34
|
+
mid_state=neutral,
|
|
35
|
+
risk_state=no_risk,
|
|
36
|
+
divergence_state=MARKET_DAILY["divergent"],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if _is_turn_weak(metrics, rule_set):
|
|
40
|
+
return ThemeStateRow(
|
|
41
|
+
theme_name=metrics.theme_name,
|
|
42
|
+
status=MARKET_DAILY["mid_strong_daily_weak"],
|
|
43
|
+
daily_state=MARKET_DAILY["daily_weak"],
|
|
44
|
+
mid_state=MARKET_DAILY["mid_strong"],
|
|
45
|
+
risk_state=no_risk,
|
|
46
|
+
divergence_state=no_div,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if _is_daily_strong(metrics, rule_set):
|
|
50
|
+
daily_strong = MARKET_DAILY["daily_strong"]
|
|
51
|
+
return ThemeStateRow(
|
|
52
|
+
theme_name=metrics.theme_name,
|
|
53
|
+
status=daily_strong,
|
|
54
|
+
daily_state=daily_strong,
|
|
55
|
+
mid_state=neutral,
|
|
56
|
+
risk_state=no_risk,
|
|
57
|
+
divergence_state=no_div,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return ThemeStateRow(
|
|
61
|
+
theme_name=metrics.theme_name,
|
|
62
|
+
status=neutral,
|
|
63
|
+
daily_state=MARKET_DAILY["daily_weak"],
|
|
64
|
+
mid_state=neutral,
|
|
65
|
+
risk_state=no_risk,
|
|
66
|
+
divergence_state=no_div,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def classify_theme_states(
|
|
71
|
+
metrics_list: list[ThemeSectorMetrics],
|
|
72
|
+
rules: dict[str, float | int] | None = None,
|
|
73
|
+
) -> list[ThemeStateRow]:
|
|
74
|
+
return [classify_theme_state(metrics, rules) for metrics in metrics_list]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_hard_weak(metrics: ThemeSectorMetrics, rules: dict[str, float | int]) -> bool:
|
|
78
|
+
if metrics.sample_count < int(rules["min_sample"]):
|
|
79
|
+
return False
|
|
80
|
+
return (
|
|
81
|
+
metrics.median_1d <= float(rules["hard_weak_max_1d_median"])
|
|
82
|
+
and metrics.up_rate <= float(rules["hard_weak_max_up_rate"])
|
|
83
|
+
and metrics.le_minus5_ratio >= float(rules["hard_weak_min_le_minus5_ratio"])
|
|
84
|
+
and metrics.limit_down_count >= int(rules["hard_weak_min_limit_down_count"])
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _is_divergent(metrics: ThemeSectorMetrics, rules: dict[str, float | int]) -> bool:
|
|
89
|
+
if metrics.sample_count < int(rules["min_sample"]):
|
|
90
|
+
return False
|
|
91
|
+
return (
|
|
92
|
+
metrics.limit_up_count >= int(rules["divergent_min_limit_up_count"])
|
|
93
|
+
and metrics.ge5_ratio >= float(rules["divergent_min_ge5_ratio"])
|
|
94
|
+
and metrics.median_1d <= float(rules["divergent_max_1d_median"])
|
|
95
|
+
and metrics.up_rate <= float(rules["divergent_max_up_rate"])
|
|
96
|
+
and metrics.le_minus5_ratio >= float(rules["divergent_min_le_minus5_ratio"])
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _is_turn_weak(metrics: ThemeSectorMetrics, rules: dict[str, float | int]) -> bool:
|
|
101
|
+
if metrics.sample_count < int(rules["min_sample"]):
|
|
102
|
+
return False
|
|
103
|
+
mid_strong = metrics.median_3d >= float(rules["daily_strong_min_1d_median"])
|
|
104
|
+
return mid_strong and (
|
|
105
|
+
metrics.median_1d <= float(rules["turn_weak_max_1d_median"])
|
|
106
|
+
and metrics.up_rate <= float(rules["turn_weak_max_up_rate"])
|
|
107
|
+
and metrics.le_minus5_ratio >= float(rules["turn_weak_min_le_minus5_ratio"])
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _is_daily_strong(metrics: ThemeSectorMetrics, rules: dict[str, float | int]) -> bool:
|
|
112
|
+
min_sample = int(rules["strong_min_sample"])
|
|
113
|
+
if metrics.sample_count < min_sample:
|
|
114
|
+
return False
|
|
115
|
+
return (
|
|
116
|
+
metrics.median_1d >= float(rules["daily_strong_min_1d_median"])
|
|
117
|
+
and metrics.relative_1d >= float(rules["daily_strong_min_relative_1d"])
|
|
118
|
+
and metrics.up_rate >= float(rules["daily_strong_min_up_rate"])
|
|
119
|
+
and metrics.ge5_ratio >= float(rules["daily_strong_min_ge5_ratio"])
|
|
120
|
+
and metrics.limit_up_count >= int(rules["daily_strong_min_limit_up_count"])
|
|
121
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from indiciumforge_workflow.market_awareness.models import ThemeSectorMetrics
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ThemeFixtureLoadError(ValueError):
|
|
13
|
+
"""Raised when a theme sector fixture cannot be parsed."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_theme_sector_fixture(path: Path) -> tuple[date, list[ThemeSectorMetrics]]:
|
|
17
|
+
payload = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
18
|
+
if not isinstance(payload, dict):
|
|
19
|
+
raise ThemeFixtureLoadError("fixture root must be a mapping")
|
|
20
|
+
|
|
21
|
+
trade_date_raw = payload.get("trade_date")
|
|
22
|
+
if not trade_date_raw:
|
|
23
|
+
raise ThemeFixtureLoadError("fixture requires trade_date")
|
|
24
|
+
trade_date = date.fromisoformat(str(trade_date_raw))
|
|
25
|
+
|
|
26
|
+
themes = payload.get("themes")
|
|
27
|
+
if not isinstance(themes, list) or not themes:
|
|
28
|
+
raise ThemeFixtureLoadError("fixture requires non-empty themes list")
|
|
29
|
+
|
|
30
|
+
metrics: list[ThemeSectorMetrics] = []
|
|
31
|
+
for index, entry in enumerate(themes):
|
|
32
|
+
if not isinstance(entry, dict):
|
|
33
|
+
raise ThemeFixtureLoadError(f"theme entry {index} must be a mapping")
|
|
34
|
+
metrics.append(_parse_theme_entry(entry, index))
|
|
35
|
+
return trade_date, metrics
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_theme_entry(entry: dict[str, Any], index: int) -> ThemeSectorMetrics:
|
|
39
|
+
theme_name = entry.get("theme_name")
|
|
40
|
+
if not theme_name:
|
|
41
|
+
raise ThemeFixtureLoadError(f"theme entry {index} missing theme_name")
|
|
42
|
+
required = ("sample_count", "median_1d", "median_3d", "up_rate")
|
|
43
|
+
for field in required:
|
|
44
|
+
if field not in entry:
|
|
45
|
+
raise ThemeFixtureLoadError(f"theme entry {index} missing {field}")
|
|
46
|
+
return ThemeSectorMetrics(
|
|
47
|
+
theme_name=str(theme_name),
|
|
48
|
+
sample_count=int(entry["sample_count"]),
|
|
49
|
+
median_1d=float(entry["median_1d"]),
|
|
50
|
+
median_3d=float(entry["median_3d"]),
|
|
51
|
+
up_rate=float(entry["up_rate"]),
|
|
52
|
+
relative_1d=float(entry.get("relative_1d", 0.0)),
|
|
53
|
+
ge5_ratio=float(entry.get("ge5_ratio", 0.0)),
|
|
54
|
+
le_minus5_ratio=float(entry.get("le_minus5_ratio", 0.0)),
|
|
55
|
+
limit_up_count=int(entry.get("limit_up_count", 0)),
|
|
56
|
+
limit_down_count=int(entry.get("limit_down_count", 0)),
|
|
57
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import date
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class ThemeSectorMetrics:
|
|
10
|
+
theme_name: str
|
|
11
|
+
sample_count: int
|
|
12
|
+
median_1d: float
|
|
13
|
+
median_3d: float
|
|
14
|
+
up_rate: float
|
|
15
|
+
relative_1d: float = 0.0
|
|
16
|
+
ge5_ratio: float = 0.0
|
|
17
|
+
le_minus5_ratio: float = 0.0
|
|
18
|
+
limit_up_count: int = 0
|
|
19
|
+
limit_down_count: int = 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ThemeStateRow:
|
|
24
|
+
theme_name: str
|
|
25
|
+
status: str
|
|
26
|
+
daily_state: str
|
|
27
|
+
mid_state: str
|
|
28
|
+
risk_state: str
|
|
29
|
+
divergence_state: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class DailyReviewResult:
|
|
34
|
+
trade_date: date
|
|
35
|
+
rows: tuple[ThemeStateRow, ...]
|
|
36
|
+
warnings: tuple[str, ...] = ()
|
|
37
|
+
fixture_path: Path | None = None
|
|
38
|
+
theme_state_ranking_path: Path | None = None
|
|
39
|
+
state_path: Path | None = None
|