145 lines
4.7 KiB
Python
145 lines
4.7 KiB
Python
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
|