rebuild(stage-9): deliver deterministic intelligent screening
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from statistics import fmean
|
||||
from typing import Any
|
||||
|
||||
|
||||
def number(value: Any) -> float | None:
|
||||
try:
|
||||
result = float(value)
|
||||
return result if math.isfinite(result) else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def change(current: float | None, previous: float | None) -> float | None:
|
||||
if current is None or previous in (None, 0):
|
||||
return None
|
||||
return (current / previous - 1) * 100
|
||||
|
||||
|
||||
def mean(values: list[float | None]) -> float | None:
|
||||
valid = [value for value in values if value is not None]
|
||||
return fmean(valid) if valid else None
|
||||
|
||||
|
||||
def ratio(numerator: float | None, denominator: float | None) -> float | None:
|
||||
if numerator is None or denominator in (None, 0):
|
||||
return None
|
||||
return numerator / denominator
|
||||
|
||||
|
||||
def rsi(closes: list[float], period: int = 6) -> float | None:
|
||||
if len(closes) <= period:
|
||||
return None
|
||||
differences = [closes[index] - closes[index - 1] for index in range(1, len(closes))]
|
||||
recent = differences[-period:]
|
||||
gains = sum(max(value, 0) for value in recent) / period
|
||||
losses = sum(max(-value, 0) for value in recent) / period
|
||||
if losses == 0:
|
||||
return 100.0 if gains > 0 else 50.0
|
||||
return 100 - 100 / (1 + gains / losses)
|
||||
|
||||
|
||||
def ema(values: list[float], period: int) -> list[float]:
|
||||
if not values:
|
||||
return []
|
||||
alpha = 2 / (period + 1)
|
||||
result = [values[0]]
|
||||
for value in values[1:]:
|
||||
result.append(value * alpha + result[-1] * (1 - alpha))
|
||||
return result
|
||||
|
||||
|
||||
def macd(values: list[float]) -> tuple[list[float], list[float]]:
|
||||
fast = ema(values, 12)
|
||||
slow = ema(values, 26)
|
||||
difference = [left - right for left, right in zip(fast, slow, strict=True)]
|
||||
return difference, ema(difference, 9)
|
||||
|
||||
|
||||
def weekly_series(rows: list[dict[str, Any]]) -> tuple[list[float], list[float]]:
|
||||
weeks: dict[str, dict[str, float]] = {}
|
||||
for row in rows:
|
||||
date = str(row.get("trade_date") or "")
|
||||
if len(date) != 10:
|
||||
continue
|
||||
from datetime import date as date_type
|
||||
|
||||
parsed = date_type.fromisoformat(date)
|
||||
key = f"{parsed.isocalendar().year}-{parsed.isocalendar().week:02d}"
|
||||
weeks.setdefault(key, {"close": 0.0, "amount": 0.0})
|
||||
close = number(row.get("close"))
|
||||
amount = number(row.get("amount"))
|
||||
if close is not None:
|
||||
weeks[key]["close"] = close
|
||||
if amount is not None:
|
||||
weeks[key]["amount"] += amount
|
||||
values = list(weeks.values())
|
||||
return [item["close"] for item in values], [item["amount"] for item in values]
|
||||
|
||||
|
||||
def percentile_map(rows: list[dict[str, Any]], field: str, direction: str) -> dict[str, float]:
|
||||
valid = [row for row in rows if number(row.get(field)) is not None]
|
||||
ordered = sorted(
|
||||
valid,
|
||||
key=lambda row: (
|
||||
number(row[field]) if direction == "asc" else -float(number(row[field]) or 0),
|
||||
str(row["identifier"]),
|
||||
),
|
||||
)
|
||||
if len(ordered) == 1:
|
||||
return {str(ordered[0]["identifier"]): 1.0}
|
||||
return {
|
||||
str(row["identifier"]): 1 - index / (len(ordered) - 1) for index, row in enumerate(ordered)
|
||||
}
|
||||
|
||||
|
||||
def pearson(left: list[float], right: list[float]) -> float:
|
||||
if len(left) < 3 or len(left) != len(right):
|
||||
return 0.0
|
||||
left_mean = fmean(left)
|
||||
right_mean = fmean(right)
|
||||
numerator = sum(
|
||||
(first - left_mean) * (second - right_mean)
|
||||
for first, second in zip(left, right, strict=True)
|
||||
)
|
||||
left_scale = math.sqrt(sum((value - left_mean) ** 2 for value in left))
|
||||
right_scale = math.sqrt(sum((value - right_mean) ** 2 for value in right))
|
||||
return numerator / (left_scale * right_scale) if left_scale and right_scale else 0.0
|
||||
|
||||
|
||||
def rounded(value: float | None, digits: int = 4) -> float | None:
|
||||
return round(value, digits) if value is not None else None
|
||||
|
||||
|
||||
def calculate_earnings_quality(bars: list[dict[str, Any]], announcement_date: str) -> bool | None:
|
||||
index = next(
|
||||
(
|
||||
offset
|
||||
for offset, row in enumerate(bars)
|
||||
if str(row.get("trade_date") or "") == announcement_date
|
||||
),
|
||||
-1,
|
||||
)
|
||||
if index < 0:
|
||||
return None
|
||||
prior = [
|
||||
number(row.get("vol"))
|
||||
for row in bars[max(0, index - 5) : index]
|
||||
if number(row.get("vol")) is not None
|
||||
]
|
||||
baseline = mean(prior)
|
||||
current = bars[index]
|
||||
volume_ratio = ratio(number(current.get("vol")), baseline)
|
||||
bad = (
|
||||
number(current.get("close")) is not None
|
||||
and number(current.get("open")) is not None
|
||||
and float(number(current["close"]) or 0) < float(number(current["open"]) or 0)
|
||||
and float(number(current.get("pct_chg")) or 0) < 0
|
||||
and volume_ratio is not None
|
||||
and volume_ratio >= 1.8
|
||||
)
|
||||
return not bad
|
||||
Reference in New Issue
Block a user