import full eventflow project
This commit is contained in:
108
backend/fastapi_app/services/market_moves_qfr.py
Normal file
108
backend/fastapi_app/services/market_moves_qfr.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import json
|
||||
import os
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
@dataclass
|
||||
class Move:
|
||||
symbol: str
|
||||
trade_date: str
|
||||
prev_trade_date: str
|
||||
close: float
|
||||
prev_close: float
|
||||
ret_1d: float
|
||||
vol_20d: float
|
||||
z_1d: float
|
||||
|
||||
|
||||
def _read_one(rawdir: str, symbol: str) -> pd.DataFrame:
|
||||
# QFR stores parquet as e.g. 510300SH.parquet / 159915SZ.parquet
|
||||
fn = symbol.replace(".", "") + ".parquet"
|
||||
p = os.path.join(rawdir, fn)
|
||||
df = pd.read_parquet(p)
|
||||
# Standardize
|
||||
if "trade_date" not in df.columns or "close" not in df.columns:
|
||||
raise RuntimeError(f"unexpected parquet schema for {symbol}: {df.columns.tolist()}")
|
||||
df = df.sort_values("trade_date").reset_index(drop=True)
|
||||
return df
|
||||
|
||||
|
||||
def _calc_move(df: pd.DataFrame, symbol: str, trade_date: str | None) -> Move | None:
|
||||
if df.empty:
|
||||
return None
|
||||
# pick last available <= trade_date if given
|
||||
if trade_date:
|
||||
df2 = df[df["trade_date"] <= trade_date]
|
||||
if df2.empty:
|
||||
return None
|
||||
df = df2
|
||||
|
||||
if len(df) < 2:
|
||||
return None
|
||||
|
||||
# compute returns
|
||||
close = df["close"].astype(float)
|
||||
ret = close.pct_change()
|
||||
|
||||
i = len(df) - 1
|
||||
prev_i = i - 1
|
||||
|
||||
td = str(df.iloc[i]["trade_date"])
|
||||
ptd = str(df.iloc[prev_i]["trade_date"])
|
||||
|
||||
close_i = float(close.iloc[i])
|
||||
prev_close_i = float(close.iloc[prev_i])
|
||||
ret_1d = float(ret.iloc[i])
|
||||
|
||||
# vol over last 20 returns (excluding NaN)
|
||||
vol_20 = float(ret.iloc[max(0, i - 20 + 1) : i + 1].std(skipna=True))
|
||||
if not (vol_20 > 0):
|
||||
vol_20 = 0.0
|
||||
z = float(ret_1d / vol_20) if vol_20 > 0 else 0.0
|
||||
|
||||
return Move(
|
||||
symbol=symbol,
|
||||
trade_date=td,
|
||||
prev_trade_date=ptd,
|
||||
close=close_i,
|
||||
prev_close=prev_close_i,
|
||||
ret_1d=ret_1d,
|
||||
vol_20d=vol_20,
|
||||
z_1d=z,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
rawdir = os.environ.get("QFR_RAWDIR") or "/home/openclaw/projects/quant-factor-research/data/raw"
|
||||
symbols = (os.environ.get("QFR_SYMBOLS") or "").strip()
|
||||
trade_date = (os.environ.get("QFR_TRADE_DATE") or "").strip() or None
|
||||
|
||||
if not symbols:
|
||||
raise SystemExit("QFR_SYMBOLS is required")
|
||||
|
||||
out: dict[str, Any] = {
|
||||
"rawdir": rawdir,
|
||||
"trade_date": trade_date,
|
||||
"moves": [],
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
for sym in [s.strip() for s in symbols.split(",") if s.strip()]:
|
||||
try:
|
||||
df = _read_one(rawdir, sym)
|
||||
mv = _calc_move(df, sym, trade_date)
|
||||
if mv is None:
|
||||
out["errors"].append({"symbol": sym, "error": "no_data"})
|
||||
continue
|
||||
out["moves"].append(asdict(mv))
|
||||
except Exception as e:
|
||||
out["errors"].append({"symbol": sym, "error": str(e)})
|
||||
|
||||
print(json.dumps(out, ensure_ascii=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user