Files
xiaobaifupan/next/backend/features/screener/technical_support.py
T

138 lines
4.5 KiB
Python

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),
}