rebuild(stage-9): deliver deterministic intelligent screening
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
|
||||
from backend.features.screener.factor_math import number, ratio, rounded
|
||||
|
||||
|
||||
def group(rows: Any, field: str) -> dict[str, list[dict[str, Any]]]:
|
||||
result: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for row in rows:
|
||||
key = str(row.get(field) or "")
|
||||
if key:
|
||||
result[key].append(dict(row))
|
||||
return result
|
||||
|
||||
|
||||
def latest_by_code(rows: Any, through: str) -> dict[str, dict[str, Any]]:
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
identifier = str(row.get("ts_code") or "")
|
||||
row_date = str(row.get("trade_date") or "")
|
||||
if (
|
||||
identifier
|
||||
and row_date <= through
|
||||
and (
|
||||
identifier not in result
|
||||
or row_date > str(result[identifier].get("trade_date") or "")
|
||||
)
|
||||
):
|
||||
result[identifier] = dict(row)
|
||||
return result
|
||||
|
||||
|
||||
def point_in_time(rows: Any, through: str) -> dict[str, dict[str, Any]]:
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
identifier = str(row.get("ts_code") or "")
|
||||
announced = str(row.get("ann_date") or "")
|
||||
if (
|
||||
identifier
|
||||
and announced
|
||||
and announced <= through
|
||||
and (
|
||||
identifier not in result
|
||||
or announced > str(result[identifier].get("ann_date") or "")
|
||||
)
|
||||
):
|
||||
result[identifier] = dict(row)
|
||||
return result
|
||||
|
||||
|
||||
def limit_events(rows: Any) -> dict[str, dict[str, str]]:
|
||||
result: dict[str, dict[str, str]] = defaultdict(dict)
|
||||
for row in rows:
|
||||
date_value = str(row.get("trade_date") or "")
|
||||
identifier = str(row.get("ts_code") or "")
|
||||
event = str(row.get("limit_type") or "")
|
||||
if date_value and identifier and event in {"U", "D", "Z"}:
|
||||
result[date_value][identifier] = event
|
||||
return result
|
||||
|
||||
|
||||
def listed_days(value: Any, through: str) -> int:
|
||||
try:
|
||||
listed = str(value or "").replace("-", "")
|
||||
start = date(int(listed[:4]), int(listed[4:6]), int(listed[6:]))
|
||||
return (date.fromisoformat(through) - start).days
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
|
||||
def dividend_years(rows: list[dict[str, Any]], through: str) -> int:
|
||||
return len(
|
||||
{
|
||||
str(row.get("end_date") or "")[:4]
|
||||
for row in rows
|
||||
if str(row.get("ann_date") or row.get("ex_date") or "") <= through
|
||||
and (number(row.get("cash_div_tax")) or 0) > 0
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def large_flow(row: dict[str, Any]) -> float | None:
|
||||
if not row:
|
||||
return None
|
||||
direct = number(row.get("large_net_amount"))
|
||||
if direct is not None:
|
||||
return rounded(direct / 100, 2)
|
||||
buys = [number(row.get(field)) for field in ("buy_lg_amount", "buy_elg_amount")]
|
||||
sells = [number(row.get(field)) for field in ("sell_lg_amount", "sell_elg_amount")]
|
||||
if any(value is None for value in buys + sells):
|
||||
return None
|
||||
return rounded(
|
||||
(sum(float(value) for value in buys) - sum(float(value) for value in sells)) / 100,
|
||||
2,
|
||||
)
|
||||
|
||||
|
||||
def ending_streak(flags: list[bool], end: int | None = None) -> int:
|
||||
index = len(flags) - 1 if end is None else end
|
||||
count = 0
|
||||
while index >= 0 and flags[index]:
|
||||
count += 1
|
||||
index -= 1
|
||||
return count
|
||||
|
||||
|
||||
def max_streak(flags: list[bool]) -> int:
|
||||
best = current = 0
|
||||
for flag in flags:
|
||||
current = current + 1 if flag else 0
|
||||
best = max(best, current)
|
||||
return best
|
||||
|
||||
|
||||
def broken_metrics(bars: list[dict[str, Any]], up_flags: list[bool]) -> dict[str, Any]:
|
||||
if len(bars) < 3:
|
||||
return {"signal": None, "days": None, "recovered": None, "volume_ratio": None}
|
||||
last_limit = next((index for index in range(len(up_flags) - 2, -1, -1) if up_flags[index]), -1)
|
||||
if last_limit < 0:
|
||||
return {"signal": False, "days": None, "recovered": False, "volume_ratio": None}
|
||||
days = len(bars) - 1 - last_limit
|
||||
broken_high = number(bars[last_limit].get("high"))
|
||||
recovered = (
|
||||
number(bars[-1].get("close")) is not None
|
||||
and broken_high is not None
|
||||
and float(number(bars[-1]["close"]) or 0) > broken_high
|
||||
)
|
||||
volume_ratio = ratio(number(bars[-1].get("vol")), number(bars[last_limit].get("vol")))
|
||||
return {
|
||||
"signal": 1 <= days <= 3 and recovered and volume_ratio is not None and volume_ratio >= 1,
|
||||
"days": days,
|
||||
"recovered": recovered,
|
||||
"volume_ratio": rounded(volume_ratio, 3),
|
||||
}
|
||||
Reference in New Issue
Block a user