Files
Hermes-Skills/openclaw-imports/stock-analysis/scripts/data_source.py
T
Hermes Skills Manager 6770bc9b9d Initial commit: Hermes Agent skills collection
- Trading skills (OKX, dividend, lottery, quantitative)
- Creative skills (ASCII art, diagrams, video)
- Development skills (GitHub, debugging, TDD)
- Research skills (arXiv, blog monitoring)
- Productivity skills (email, documents, notes)
- MCP integration skills
- Custom user skills
2026-07-05 02:31:15 -04:00

384 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Unified data source layer: LongPort (primary) + Yahoo Finance (fallback).
LongPort provides: PE, PB, EPS, BPS, dividend_yield, market_cap, realtime quotes
Yahoo Finance provides: operating margins, ROE, debt ratios, analyst data, earnings history
"""
import os
import sys
from dataclasses import dataclass, field
from typing import Optional, Literal
# Lazy imports to avoid startup cost
_longport_ctx = None
_yf = None
def _get_longport_context():
"""Get or create LongPort QuoteContext (singleton)."""
global _longport_ctx
if _longport_ctx is None:
try:
from longport import openapi
# Map LONGBRIDGE_* to LONGPORT_*
config = {}
with open(os.path.expanduser("~/.bashrc"), "r") as f:
for line in f:
if line.startswith("export LONGBRIDGE_"):
key, value = line.strip().split("=", 1)
config[key.replace("export ", "")] = value
os.environ["LONGPORT_APP_KEY"] = config.get("LONGBRIDGE_APP_KEY", "")
os.environ["LONGPORT_APP_SECRET"] = config.get("LONGBRIDGE_APP_SECRET", "")
os.environ["LONGPORT_ACCESS_TOKEN"] = config.get("LONGBRIDGE_ACCESS_TOKEN", "")
cfg = openapi.Config.from_env()
_longport_ctx = openapi.QuoteContext(config=cfg)
except Exception as e:
print(f"LongPort init failed: {e}", file=sys.stderr)
return _longport_ctx
def _get_yfinance():
"""Lazy import yfinance."""
global _yf
if _yf is None:
try:
import yfinance
_yf = yfinance
except ImportError:
print("yfinance not installed, Yahoo fallback disabled", file=sys.stderr)
return _yf
@dataclass
class UnifiedStockData:
"""Unified stock data from multiple sources."""
# Identity
symbol: str
name: str = ""
market: str = "" # "US", "HK", "CN"
currency: str = ""
# Price data (LongPort primary)
last_price: Optional[float] = None
prev_close: Optional[float] = None
open_price: Optional[float] = None
high: Optional[float] = None
low: Optional[float] = None
volume: Optional[int] = None
turnover: Optional[float] = None
# Valuation metrics (LongPort primary)
pe_ttm: Optional[float] = None
pb: Optional[float] = None
dividend_yield: Optional[float] = None
eps_ttm: Optional[float] = None
bps: Optional[float] = None # Book value per share
market_cap: Optional[float] = None
# Trading metrics (LongPort only)
turnover_rate: Optional[float] = None
volume_ratio: Optional[float] = None
change_rate: Optional[float] = None
# Share info (LongPort)
total_shares: Optional[int] = None
circulating_shares: Optional[int] = None
# Fundamental extras (Yahoo Finance fallback)
operating_margin: Optional[float] = None
profit_margin: Optional[float] = None
roe: Optional[float] = None
roa: Optional[float] = None
revenue_growth: Optional[float] = None
earnings_growth: Optional[float] = None
debt_to_equity: Optional[float] = None
current_ratio: Optional[float] = None
# Analyst data (Yahoo Finance only)
recommendation: Optional[str] = None
target_price: Optional[float] = None
num_analysts: Optional[int] = None
# Earnings (Yahoo Finance only)
earnings_history: Optional[object] = None # DataFrame
# Price history (Yahoo Finance primary, LongPort fallback)
price_history: Optional[object] = None # DataFrame
# Metadata
data_sources: list = field(default_factory=list) # ["longport", "yahoo"]
def _detect_market(symbol: str) -> tuple[str, str]:
"""Detect market from symbol format.
Returns: (normalized_symbol, market)
Examples:
"O.US" -> ("O.US", "US")
"AAPL" -> ("AAPL.US", "US")
"823.HK" -> ("823.HK", "HK")
"000001.SZ" -> ("000001.SZ", "CN")
"""
symbol = symbol.upper().strip()
if symbol.endswith(".US"):
return symbol, "US"
elif symbol.endswith(".HK"):
return symbol, "HK"
elif symbol.endswith(".SZ") or symbol.endswith(".SH"):
return symbol, "CN"
elif symbol.endswith("-USD"):
return symbol, "CRYPTO"
else:
# Assume US stock if no suffix
return f"{symbol}.US", "US"
def fetch_from_longport(symbol: str, verbose: bool = False) -> Optional[UnifiedStockData]:
"""Fetch data from LongPort SDK."""
ctx = _get_longport_context()
if ctx is None:
return None
try:
from longport.openapi import CalcIndex
normalized, market = _detect_market(symbol)
if verbose:
print(f" [LongPort] Fetching {normalized}...", file=sys.stderr)
# Get quote
quotes = ctx.quote([normalized])
if not quotes:
return None
q = quotes[0]
# Get static info (EPS, BPS, shares)
static_infos = ctx.static_info([normalized])
static = static_infos[0] if static_infos else None
# Get calc indexes (PE, PB, dividend yield, market cap)
indexes = [
CalcIndex.PeTtmRatio,
CalcIndex.PbRatio,
CalcIndex.DividendRatioTtm,
CalcIndex.TotalMarketValue,
CalcIndex.TurnoverRate,
CalcIndex.VolumeRatio,
CalcIndex.ChangeRate,
]
calc_results = ctx.calc_indexes([normalized], indexes)
calc = calc_results[0] if calc_results else None
# Build result
data = UnifiedStockData(
symbol=normalized,
name=getattr(static, 'name_en', '') or getattr(static, 'name_cn', ''),
market=market,
currency=getattr(static, 'currency', ''),
# Price
last_price=q.last_done,
prev_close=q.prev_close,
open_price=q.open,
high=q.high,
low=q.low,
volume=q.volume,
turnover=q.turnover,
# Valuation
pe_ttm=getattr(calc, 'pe_ttm_ratio', None),
pb=getattr(calc, 'pb_ratio', None),
dividend_yield=getattr(calc, 'dividend_ratio_ttm', None),
eps_ttm=getattr(static, 'eps_ttm', None),
bps=getattr(static, 'bps', None),
market_cap=getattr(calc, 'total_market_value', None),
# Trading
turnover_rate=getattr(calc, 'turnover_rate', None),
volume_ratio=getattr(calc, 'volume_ratio', None),
change_rate=getattr(calc, 'change_rate', None),
# Shares
total_shares=getattr(static, 'total_shares', None),
circulating_shares=getattr(static, 'circulating_shares', None),
# Source
data_sources=["longport"],
)
return data
except Exception as e:
if verbose:
print(f" [LongPort] Error: {e}", file=sys.stderr)
return None
def fetch_from_yahoo(symbol: str, verbose: bool = False) -> Optional[UnifiedStockData]:
"""Fetch data from Yahoo Finance (fallback for missing fields)."""
yf = _get_yfinance()
if yf is None:
return None
try:
# Convert symbol format for Yahoo
yahoo_symbol = symbol.replace(".US", "")
if symbol.endswith(".HK"):
yahoo_symbol = symbol # Yahoo uses .HK suffix
if verbose:
print(f" [Yahoo] Fetching {yahoo_symbol}...", file=sys.stderr)
stock = yf.Ticker(yahoo_symbol)
info = stock.info
if not info:
return None
# Get price history
try:
price_history = stock.history(period="1y")
except Exception:
price_history = None
# Get earnings history
try:
earnings_history = stock.earnings_dates
except Exception:
earnings_history = None
# Build result with Yahoo data
data = UnifiedStockData(
symbol=symbol,
name=info.get("shortName", "") or info.get("longName", ""),
market="US" if not symbol.endswith(".HK") else "HK",
currency=info.get("currency", ""),
# Price
last_price=info.get("regularMarketPrice") or info.get("currentPrice"),
prev_close=info.get("regularMarketPreviousClose"),
open_price=info.get("regularMarketOpen"),
high=info.get("regularMarketDayHigh"),
low=info.get("regularMarketDayLow"),
volume=info.get("regularMarketVolume"),
# Valuation
pe_ttm=info.get("trailingPE"),
pb=info.get("priceToBook"),
dividend_yield=info.get("dividendYield"),
eps_ttm=info.get("trailingEps"),
market_cap=info.get("marketCap"),
# Fundamentals (Yahoo extras)
operating_margin=info.get("operatingMargins"),
profit_margin=info.get("profitMargins"),
roe=info.get("returnOnEquity"),
roa=info.get("returnOnAssets"),
revenue_growth=info.get("revenueGrowth"),
earnings_growth=info.get("earningsGrowth"),
debt_to_equity=info.get("debtToEquity"),
current_ratio=info.get("currentRatio"),
# Analyst
recommendation=info.get("recommendationKey"),
target_price=info.get("targetMeanPrice"),
num_analysts=info.get("numberOfAnalystOpinions"),
# History
earnings_history=earnings_history,
price_history=price_history,
# Source
data_sources=["yahoo"],
)
return data
except Exception as e:
if verbose:
print(f" [Yahoo] Error: {e}", file=sys.stderr)
return None
def merge_data(longport_data: Optional[UnifiedStockData],
yahoo_data: Optional[UnifiedStockData]) -> Optional[UnifiedStockData]:
"""Merge data from both sources, LongPort takes priority."""
if longport_data is None and yahoo_data is None:
return None
if longport_data is None:
return yahoo_data
if yahoo_data is None:
return longport_data
# Merge: LongPort primary, Yahoo fills gaps
merged = longport_data
# Fill Yahoo extras
for field in ['operating_margin', 'profit_margin', 'roe', 'roa',
'revenue_growth', 'earnings_growth', 'debt_to_equity',
'current_ratio', 'recommendation', 'target_price',
'num_analysts', 'earnings_history', 'price_history']:
if getattr(merged, field) is None:
val = getattr(yahoo_data, field)
if val is not None:
setattr(merged, field, val)
# Track sources
merged.data_sources = ["longport", "yahoo"]
return merged
def fetch_stock_data_unified(symbol: str, verbose: bool = False) -> Optional[UnifiedStockData]:
"""Main entry point: fetch stock data with LongPort primary, Yahoo fallback.
Args:
symbol: Stock symbol (e.g., "O", "O.US", "823.HK", "AAPL")
verbose: Print debug info
Returns:
UnifiedStockData or None
"""
normalized, market = _detect_market(symbol)
if verbose:
print(f"\nFetching {normalized} ({market})...", file=sys.stderr)
# 1. Try LongPort first (for all markets)
longport_data = fetch_from_longport(normalized, verbose)
# 2. For US stocks, also try Yahoo for extras
yahoo_data = None
if market == "US":
yahoo_data = fetch_from_yahoo(normalized, verbose)
# 3. Merge results
result = merge_data(longport_data, yahoo_data)
if result and verbose:
print(f" Sources: {result.data_sources}", file=sys.stderr)
print(f" PE: {result.pe_ttm}, PB: {result.pb}, Yield: {result.dividend_yield}%", file=sys.stderr)
return result
# Convenience function for backward compatibility
def fetch_stock_data(symbol: str, verbose: bool = False):
"""Backward compatible wrapper."""
return fetch_stock_data_unified(symbol, verbose)
if __name__ == "__main__":
# Test
symbols = ["O", "823.HK", "MAIN"]
for sym in symbols:
data = fetch_stock_data_unified(sym, verbose=True)
if data:
print(f"\n{data.symbol}:")
print(f" Name: {data.name}")
print(f" Price: {data.last_price} {data.currency}")
print(f" PE: {data.pe_ttm}, PB: {data.pb}")
print(f" Dividend Yield: {data.dividend_yield}%")
print(f" EPS: {data.eps_ttm}, BPS: {data.bps}")
print(f" Market Cap: {data.market_cap:,.0f}" if data.market_cap else "")
print(f" Sources: {data.data_sources}")