""" Taiwan block trades (鉅額交易) data via FinMind API. Source: TaiwanStockBlockTrade Fields: volume (block trade volume), close (block trade price), premium (vs market %) """ from __future__ import annotations import logging from datetime import date, timedelta import pandas as pd import requests from cachetools import TTLCache logger = logging.getLogger(__name__) FINMIND_URL = "https://api.finmindtrade.com/api/v4/data" _BT_CACHE: TTLCache = TTLCache(maxsize=200, ttl=6 * 60 * 60) _HEADERS = {"User-Agent": "Mozilla/5.0"} _BT_COLS = ["block_vol_ratio", "block_premium"] def _normalize_code(stock_no: str) -> str: return stock_no.replace(".TW", "").replace(".TWO", "").strip() def fetch_block_trades(stock_no: str, months: int = 24) -> pd.DataFrame: bare = _normalize_code(stock_no) cache_key = f"bt:{bare}:{months}" cached = _BT_CACHE.get(cache_key) if cached is not None: return cached.copy() start = (date.today().replace(day=1) - timedelta(days=months * 31)).strftime("%Y-%m-%d") try: resp = requests.get( FINMIND_URL, params={"dataset": "TaiwanStockBlockTrade", "data_id": bare, "start_date": start, "token": ""}, headers=_HEADERS, timeout=15, ) resp.raise_for_status() records = resp.json().get("data", []) except Exception as exc: logger.warning("FinMind block trade fetch failed for %s: %s", bare, exc) return pd.DataFrame() if not records: return pd.DataFrame() try: df = pd.DataFrame(records) needed = [c for c in ("date", "volume", "close") if c in df.columns] if "date" not in needed: return pd.DataFrame() df = df[needed].copy() for col in ("volume", "close"): if col in df.columns: df[col] = pd.to_numeric(df[col], errors="coerce").fillna(0.0) # Aggregate multiple block trades on same day df = df.groupby("date", as_index=False).agg( block_volume=("volume", "sum"), block_close=("close", "mean"), ) df = df.sort_values("date").reset_index(drop=True) _BT_CACHE[cache_key] = df.copy() return df except Exception as exc: logger.warning("Block trade parse failed for %s: %s", bare, exc) return pd.DataFrame() def add_block_trade_features(df: pd.DataFrame, stock_no: str) -> pd.DataFrame: out = df.copy() def _zero_fill(): for col in _BT_COLS: out[col] = 0.0 return out if out.empty or "date" not in out.columns: return _zero_fill() bt = fetch_block_trades(stock_no) if bt.empty: return _zero_fill() out["_bdate"] = pd.to_datetime(out["date"]).dt.strftime("%Y-%m-%d") merged = out.merge(bt, how="left", left_on="_bdate", right_on="date", suffixes=("", "_bt")) merged = merged.drop(columns=["_bdate"] + [c for c in ["date_bt"] if c in merged.columns]) block_vol = pd.to_numeric(merged.get("block_volume", 0), errors="coerce").fillna(0.0) volume = pd.to_numeric(out.get("volume", pd.Series(0.0, index=out.index)), errors="coerce").replace(0, float("nan")) close = pd.to_numeric(out.get("close", pd.Series(0.0, index=out.index)), errors="coerce").replace(0, float("nan")) block_close = pd.to_numeric(merged.get("block_close", float("nan")), errors="coerce") out["block_vol_ratio"] = (block_vol / volume).fillna(0.0).clip(upper=1.0) # Negative premium = block trade below market price = distribution signal out["block_premium"] = ((block_close - close) / close).fillna(0.0).clip(-0.1, 0.1) return out