Files
finance-talk/成交量分析/fetcher.py
T

393 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""五大期货交易所前20会员成交持仓排名数据获取工具。
交易所对照:
- CZCE 郑商所 → get_rank_table_czce (玻璃FG、纯碱SA、甲醇MA等)
- SHFE 上期所 → get_shfe_rank_table (铜CU、铝AL、螺纹RB、原油SC等)
- DCE 大商所 → get_dce_rank_table (铁矿石I、豆粕M、棕榈P等)
- CFFEX 中金所 → get_cffex_rank_table (IF、IC、IH、T等)
- GFEX 广期所 → futures_gfex_position_rank (工业硅SI、碳酸锂LC等)
交易所公布规则:
- CZCE: 同时公布品种汇总 + 各合约明细
- SHFE/CFFEX: 只公布合约明细,品种汇总由函数自行加总
- DCE: 只公布品种排名
- GFEX: 公布合约明细
"""
import akshare as ak
import pandas as pd
from datetime import datetime, timedelta
EXCHANGE_CODE = {
"CZCE": "CZCE",
"SHFE": "SHFE",
"DCE": "DCE",
"CFFEX": "CFFEX",
"GFEX": "GFEX",
}
# 常见品种 -> 交易所映射
VARIETY_EXCHANGE = {
# 郑商所 CZCE
"FG": "CZCE", "SA": "CZCE", "MA": "CZCE", "TA": "CZCE",
"RM": "CZCE", "OI": "CZCE", "ZC": "CZCE", "SF": "CZCE",
"SM": "CZCE", "UR": "CZCE", "PF": "CZCE", "PX": "CZCE",
"SH": "CZCE", "PR": "CZCE", "PL": "CZCE", "AP": "CZCE",
"CJ": "CZCE", "CY": "CZCE", "PK": "CZCE", "CF": "CZCE",
"SR": "CZCE",
# 上期所 SHFE
"CU": "SHFE", "AL": "SHFE", "ZN": "SHFE", "PB": "SHFE",
"NI": "SHFE", "SN": "SHFE", "AU": "SHFE", "AG": "SHFE",
"RB": "SHFE", "HC": "SHFE", "WR": "SHFE", "FU": "SHFE",
"BU": "SHFE", "RU": "SHFE", "SC": "SHFE", "NR": "SHFE",
"SP": "SHFE", "SS": "SHFE", "LU": "SHFE", "BC": "SHFE",
"AO": "SHFE", "BR": "SHFE", "EC": "SHFE",
# 大商所 DCE
"C": "DCE", "CS": "DCE", "A": "DCE", "B": "DCE",
"M": "DCE", "Y": "DCE", "P": "DCE", "FB": "DCE",
"BB": "DCE", "JD": "DCE", "L": "DCE", "V": "DCE",
"PP": "DCE", "J": "DCE", "JM": "DCE", "I": "DCE",
"EG": "DCE", "RR": "DCE", "EB": "DCE", "PG": "DCE",
"LH": "DCE", "LG": "DCE", "BZ": "DCE",
# 中金所 CFFEX
"IF": "CFFEX", "IC": "CFFEX", "IH": "CFFEX", "IM": "CFFEX",
"T": "CFFEX", "TF": "CFFEX", "TS": "CFFEX", "TL": "CFFEX",
# 广期所 GFEX
"SI": "GFEX", "LC": "GFEX", "PS": "GFEX",
}
# 各交易所的品种代码列表(用于 get_rank_sum
ALL_VARS_CZCE = ["WH","PM","CF","SR","TA","OI","RI","MA","FG","RS","RM","ZC",
"JR","LR","SF","SM","WT","TC","GN","RO","ER","SRX","SRY",
"WSX","WSY","CY","AP","UR","CJ","SA","PK","PF","PX","SH","PR","PL"]
ALL_VARS_DCE = ["C","CS","A","B","M","Y","P","FB","BB","JD","L","V","PP",
"J","JM","I","EG","RR","EB","PG","LH","LG","BZ"]
ALL_VARS_SHFE = ["CU","AL","ZN","PB","NI","SN","AU","AG","RB","WR","HC",
"FU","BU","RU","SC","NR","SP","SS","LU","BC","AO","BR","EC"]
ALL_VARS_CFFEX = ["IF","IC","IM","IH","T","TF","TS","TL"]
ALL_VARS_GFEX = ["SI","LC","PS"]
# 没有统一字段名映射时用的 to_int 辅助
def _to_int(x):
if isinstance(x, str):
return int(x.replace(",", ""))
return int(x)
# ---------------------------------------------------------------------------
# 底层接口,按交易所获取当日前20排名
# ---------------------------------------------------------------------------
def fetch_top20(date: str = None, exchange: str = None, variety: str = None) -> pd.DataFrame:
"""获取指定交易日该品种的前20会员成交持仓排名。
主力合约检测调用 :func:`main_contract`,本函数自动定位。
Parameters
----------
date : str or None
交易日,格式 YYYY-MM-DD 或 YYYYMMDD。None 表示最新。
exchange : str or None
交易所代码(CZCE/SHFE/DCE/CFFEX/GFEX)。None 时从 variety 推算。
variety : str
品种代码(如 "FG", "RB", "I", "IF")。如果为 None 则返回全品种。
Returns
-------
pd.DataFrame
前20会员排名数据。
列:rank, party_name, vol, vol_chg,
long_party_name, long_open_interest, long_open_interest_chg,
short_party_name, short_open_interest, short_open_interest_chg,
symbol, exchange
"""
if exchange is None and variety is not None:
exchange = VARIETY_EXCHANGE.get(variety.upper())
if exchange is None:
raise ValueError(f"未知品种 {variety},请指定 exchange 参数")
if exchange == "CZCE":
return _fetch_czce(date, variety)
elif exchange == "SHFE":
return _fetch_shfe(date, variety)
elif exchange == "DCE":
return _fetch_dce(date, variety)
elif exchange == "CFFEX":
return _fetch_cffex(date, variety)
elif exchange == "GFEX":
return _fetch_gfex(date, variety)
else:
raise ValueError(f"不支持的交易所: {exchange}")
def _fetch_czce(date=None, variety=None):
rank = ak.get_rank_table_czce(date=date)
if variety:
variety = variety.upper()
if variety in rank:
# 品种级别汇总
return _normalize_czce(rank[variety], variety, "CZCE")
# 合约级别: 找到该品种的所有合约
contract_keys = [k for k in rank if k.startswith(variety) and k != variety]
if not contract_keys:
raise ValueError(f"未找到品种 {variety} 的数据")
# 返回汇总版
return _normalize_czce(rank.get(variety, pd.DataFrame()), variety, "CZCE")
else:
# 全部品种 - 返回汇总
return rank # dict, 调用方自己处理
def _fetch_shfe(date=None, variety=None):
vars_list = [variety.upper()] if variety else None
rank = ak.get_shfe_rank_table(date=date, vars_list=vars_list)
# SHFE 返回 dict: {合约代码: DataFrame}
rows = []
for symbol, df in rank.items():
for _, r in df.iterrows():
rows.append({
"rank": int(r["名次"]),
"party_name": r["会员简称"],
"vol": r["成交量"],
"vol_chg": r["成交量增减"],
"long_party_name": r["持买单量-会员简称"],
"long_open_interest": r["持买单量-合计"],
"long_open_interest_chg": r["持买单量-增减"],
"short_party_name": r["持卖单量-会员简称"],
"short_open_interest": r["持卖单量-合计"],
"short_open_interest_chg": r["持卖单量-增减"],
"symbol": symbol,
})
df = pd.DataFrame(rows)
if variety:
df = df[df["symbol"].str.startswith(variety.upper())]
return df
def _fetch_dce(date=None, variety=None):
vars_list = [variety.upper()] if variety else None
rank = ak.get_dce_rank_table(date=date, vars_list=vars_list)
rows = []
for symbol, df in rank.items():
for _, r in df.iterrows():
rows.append({
"rank": int(r["名次"]),
"party_name": r["会员简称"],
"vol": r["成交量"],
"vol_chg": r["成交量增减"],
"long_party_name": r["持买单-会员简称"],
"long_open_interest": r["持买单"],
"long_open_interest_chg": r["持买单增减"],
"short_party_name": r["持卖单-会员简称"],
"short_open_interest": r["持卖单"],
"short_open_interest_chg": r["持卖单增减"],
"symbol": symbol,
})
return pd.DataFrame(rows)
def _fetch_cffex(date=None, variety=None):
vars_list = [variety.upper()] if variety else None
rank = ak.get_cffex_rank_table(date=date, vars_list=vars_list)
rows = []
for symbol, df in rank.items():
for _, r in df.iterrows():
rows.append({
"rank": int(r["名次"]),
"party_name": r["会员简称"],
"vol": r["成交量"],
"vol_chg": r["成交量增减"],
"long_party_name": r["持买单-会员简称"],
"long_open_interest": r["持买单-合计"],
"long_open_interest_chg": r["持买单-增减"],
"short_party_name": r["持卖单-会员简称"],
"short_open_interest": r["持卖单-合计"],
"short_open_interest_chg": r["持卖单-增减"],
"symbol": symbol,
})
return pd.DataFrame(rows)
def _fetch_gfex(date=None, variety=None):
vars_list = [variety.upper()] if variety else None
rank = ak.futures_gfex_position_rank(date=date, vars_list=vars_list)
rows = []
if isinstance(rank, dict):
for symbol, df in rank.items():
for _, r in df.iterrows():
rows.append({
"rank": int(r["名次"]),
"party_name": r["会员简称"],
"vol": r["成交量"],
"vol_chg": r["成交量增减"],
"long_party_name": r["持买单-会员简称"],
"long_open_interest": r["持买单-合计"],
"long_open_interest_chg": r["持买单-增减"],
"short_party_name": r["持卖单-会员简称"],
"short_open_interest": r["持卖单-合计"],
"short_open_interest_chg": r["持卖单-增减"],
"symbol": symbol,
})
return pd.DataFrame(rows)
def _normalize_czce(df, variety, exchange):
"""标准化 CZCE 品种汇总 DataFrame 字段名"""
df = df.copy()
df["symbol"] = df.get("symbol", variety)
df["exchange"] = exchange
return df
# ---------------------------------------------------------------------------
# 主力合约检测
# ---------------------------------------------------------------------------
def main_contract(variety: str, date: str = None) -> str:
"""检测该品种在指定交易日的主力合约代码。
逻辑:成交量/持仓量最大的合约即为主力。
"""
variety = variety.upper()
exchange = VARIETY_EXCHANGE.get(variety)
if exchange is None:
raise ValueError(f"未知品种 {variety}")
rank = ak.get_rank_table_czce(date=date) if exchange == "CZCE" else {}
if exchange == "CZCE":
# CZCE: 合约级别的 key 是 品种+年月 (FG609), 品种级是 FG
contract_keys = [k for k in rank if k.startswith(variety) and k != variety]
if not contract_keys:
raise ValueError(f"未找到品种 {variety} 的合约数据")
vols = {}
for ck in contract_keys:
df = rank[ck]
vol_sum = df["vol"].apply(_to_int).sum()
vols[ck] = vol_sum
return max(vols, key=vols.get)
else:
# 其他交易所:通过成交额最大的标的判断
raise NotImplementedError(f"{exchange} 主力合约检测待实现")
# ---------------------------------------------------------------------------
# 品种汇总对比(多交易所多品种)
# ---------------------------------------------------------------------------
def rank_summary(date: str = None, varieties: list = None) -> pd.DataFrame:
"""多品种前N会员成交量/持仓汇总对比(仅支持 CZCE 品种)。
使用 get_rank_table_czce 的品种级别汇总数据,
返回各品种前5/10/15/20成交量和多空持仓总和。
Parameters
----------
date : str
交易日。
varieties : list
品种代码列表。默认 FG, SA, MA, CF, TA, UR, PF, SM。
Returns
-------
pd.DataFrame
每行一个品种,含品种级前N成交量和多空持仓总和。
"""
if varieties is None:
varieties = ["FG", "SA", "MA", "CF", "TA", "UR", "PF", "SM"]
rank = ak.get_rank_table_czce(date=date)
rows = []
for var in varieties:
var = var.upper()
if var not in rank:
continue
df = rank[var]
df["vol_int"] = df["vol"].apply(_to_int)
df["long_int"] = df["long_open_interest"].apply(_to_int)
df["short_int"] = df["short_open_interest"].apply(_to_int)
total_vol = df["vol_int"].sum()
vol_5 = df.head(5)["vol_int"].sum()
vol_10 = df.head(10)["vol_int"].sum()
long_sum = df["long_int"].sum()
short_sum = df["short_int"].sum()
rows.append({
"var": var,
"vol_total": total_vol,
"vol_top5": vol_5,
"vol_top10": vol_10,
"vol_top20": df["vol_int"].sum(),
"long_sum": long_sum,
"short_sum": short_sum,
"net": long_sum - short_sum,
})
return pd.DataFrame(rows).sort_values("vol_total", ascending=False)
# ---------------------------------------------------------------------------
# 主力合约前20成交量
# ---------------------------------------------------------------------------
def main_top20(variety: str, date: str = None) -> pd.DataFrame:
"""获取品种主力合约的前20会员成交量排名。
Parameters
----------
variety : str
品种代码,如 "FG"。
date : str
交易日。None 表示最新。
Returns
-------
pd.DataFrame
主力合约前20会员成交量排名 + 多空持仓排名。
"""
exchange = VARIETY_EXCHANGE.get(variety.upper())
if exchange != "CZCE":
raise NotImplementedError(f"目前仅支持 CZCE 品种的主力合约前20查询")
rank = ak.get_rank_table_czce(date=date)
contract_keys = [k for k in rank if k.startswith(variety.upper()) and k != variety.upper()]
vols = {ck: rank[ck]["vol"].apply(_to_int).sum() for ck in contract_keys}
main_code = max(vols, key=vols.get)
df = rank[main_code].copy()
df["symbol"] = main_code
# 数值列处理(CZCE 返回字符串含逗号)
for col in ["vol", "vol_chg", "long_open_interest", "long_open_interest_chg",
"short_open_interest", "short_open_interest_chg"]:
df[col] = df[col].apply(_to_int)
return df.sort_values("rank")
# ---------------------------------------------------------------------------
# 输出格式化
# ---------------------------------------------------------------------------
def print_top20(df: pd.DataFrame, title: str = ""):
"""打印前20成交量排名 + 多空前10。"""
if title:
print(f"\n{'='*60}")
print(f" {title}")
print(f"{'='*60}")
print(f"\n【成交量前20】")
print(f"{'排名':>4} {'会员':<20} {'成交量':>10} {'变化':>10}")
print("-" * 50)
for _, r in df.iterrows():
print(f"{int(r['rank']):>4} {str(r['vol_party_name']):<20} "
f"{int(r['vol']):>10,} {int(r['vol_chg']):>10,}")
print(f"\n【持多单前10】")
print(f"{'排名':>4} {'会员':<20} {'多单':>10} {'变化':>10}")
print("-" * 50)
for _, r in df.head(10).iterrows():
print(f"{int(r['rank']):>4} {str(r['long_party_name']):<20} "
f"{int(r['long_open_interest']):>10,} {int(r['long_open_interest_chg']):>10,}")
print(f"\n【持空单前10】")
print(f"{'排名':>4} {'会员':<20} {'空单':>10} {'变化':>10}")
print("-" * 50)
for _, r in df.head(10).iterrows():
print(f"{int(r['rank']):>4} {str(r['short_party_name']):<20} "
f"{int(r['short_open_interest']):>10,} {int(r['short_open_interest_chg']):>10,}")