Architecture: - Restructure into 5 subsystems: data/, features/, market/, signals/, execution/, apps/ - Unified ts_code conversion in core/codes.py (idempotent, kills 4 duplicate copies) - analytics_conn() + kline_glob() — zero hardcoded DB/Parquet paths - Fixed double-suffix bug (.SZ.SZ) in backfill pipeline root cause Signal Intelligence (the moat): - 14 signal types: EMA52, Vegas, Chan, ORB, Gap, NR7, Inside Bar - 640K+ historical signal instances across 8 backfilled types - Multi-signal Expectancy Engine with breadth-similarity matching - Signal backfill CLI: ashare-dp backfill signals Market Intelligence: - 8 engines: State, Leadership, Opportunity, Flow, Sentiment, Memory, Knowledge Graph, Recommendations - Real limit-up/down sentiment via akshare (108 ZT, 19 DT, 52 broken board) - Knowledge Graph: 8 themes × 30+ concepts with keyword matching - Money-flow stock recommendations with entry/stop/target trade plans Dashboard Command Center: - Decision-first layout: COMMAND → WHERE → WHY → RISK → EXPECTANCY - Multi-signal Expectancy comparison table (8 types ranked by WR) - Theme Map visualization with rotation detection - Intraday Replay infrastructure (30min state snapshots) - RECOMMENDATIONS card with actionable trade plans Trading Memory: - trade_log table + POST/GET/PUT API for trade recording - Performance stats aggregation Code Quality: - 0 hardcoded DB paths, 0 REPLACE hacks, 0 dead ts_code copies - EMA52 screening deduplicated (CLI + scheduler share one function) - read_parquet_sql() helper for 28 duplicate patterns - 6 bugs fixed from code review (NR7 window, theme matching, column indices, etc.) Co-Authored-By: Claude <noreply@anthropic.com>
129 lines
4.3 KiB
Python
129 lines
4.3 KiB
Python
"""Feature Registry — centralized management of all market features.
|
|
|
|
Engine code only knows about the registry; individual feature functions
|
|
are looked up by name. Adding a new feature = register it, no engine changes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from typing import Any, Callable
|
|
|
|
from ashare_dp.domain.features import FeatureDefinition
|
|
|
|
|
|
class FeatureRegistry:
|
|
"""Central registry for all market features.
|
|
|
|
Usage:
|
|
from ashare_dp.features.registry import registry
|
|
|
|
# Register a feature
|
|
registry.register(FeatureDefinition(
|
|
name="breadth_vector",
|
|
category="breadth",
|
|
description="Advance/decline ratio and related breadth metrics",
|
|
dependencies=[],
|
|
compute=_compute_breadth_vector,
|
|
))
|
|
|
|
# Compute all features for a date
|
|
features = registry.compute_all(db, trade_date)
|
|
|
|
# Get features by category
|
|
breadth_features = registry.get_by_category("breadth")
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._features: dict[str, FeatureDefinition] = {}
|
|
|
|
def register(self, feature: FeatureDefinition) -> None:
|
|
"""Register a feature definition. Overwrites if name exists."""
|
|
self._features[feature.name] = feature
|
|
|
|
def get(self, name: str) -> FeatureDefinition | None:
|
|
"""Get a feature definition by name."""
|
|
return self._features.get(name)
|
|
|
|
def get_by_category(self, category: str) -> list[FeatureDefinition]:
|
|
"""Get all features in a category."""
|
|
return [f for f in self._features.values() if f.category == category]
|
|
|
|
def compute_all(self, db: Any, trade_date: date) -> dict[str, dict[str, Any]]:
|
|
"""Compute all registered features for a given trading date.
|
|
|
|
Args:
|
|
db: Database instance (read_only DuckDB connection).
|
|
trade_date: The trading date to compute features for.
|
|
|
|
Returns:
|
|
Dict mapping feature name → feature value dict.
|
|
Features are computed in dependency order (simple topological sort).
|
|
"""
|
|
results: dict[str, dict[str, Any]] = {}
|
|
computed: set[str] = set()
|
|
pending: set[str] = set(self._features.keys())
|
|
|
|
while pending:
|
|
ready = [
|
|
name for name in pending
|
|
if all(dep in computed for dep in self._features[name].dependencies)
|
|
]
|
|
if not ready:
|
|
# Circular dependency or missing dependency
|
|
remaining = ", ".join(sorted(pending))
|
|
raise RuntimeError(
|
|
f"Cannot resolve feature dependencies. "
|
|
f"Remaining: {remaining}. Computed: {computed}"
|
|
)
|
|
|
|
for name in ready:
|
|
feature = self._features[name]
|
|
if feature.compute is not None:
|
|
results[name] = feature.compute(db, trade_date, results)
|
|
computed.add(name)
|
|
pending.discard(name)
|
|
|
|
return results
|
|
|
|
def compute_category(
|
|
self, db: Any, trade_date: date, category: str
|
|
) -> dict[str, dict[str, Any]]:
|
|
"""Compute all features in a specific category."""
|
|
features = self.get_by_category(category)
|
|
# Compute dependencies first
|
|
all_needed: set[str] = set()
|
|
for f in features:
|
|
all_needed.add(f.name)
|
|
all_needed.update(f.dependencies)
|
|
|
|
full_results = self.compute_all(db, trade_date)
|
|
return {k: v for k, v in full_results.items() if k in all_needed}
|
|
|
|
def list_categories(self) -> list[str]:
|
|
"""List all unique feature categories."""
|
|
return sorted({f.category for f in self._features.values()})
|
|
|
|
def list_features(self) -> list[dict]:
|
|
"""List all registered features with metadata."""
|
|
return [
|
|
{
|
|
"name": f.name,
|
|
"category": f.category,
|
|
"description": f.description,
|
|
"dependencies": f.dependencies,
|
|
"version": f.version,
|
|
}
|
|
for f in sorted(self._features.values(), key=lambda x: x.name)
|
|
]
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._features)
|
|
|
|
def __contains__(self, name: str) -> bool:
|
|
return name in self._features
|
|
|
|
|
|
# Module-level singleton
|
|
registry = FeatureRegistry()
|