rebuild(stage-9): deliver deterministic intelligent screening
This commit is contained in:
@@ -157,8 +157,7 @@ class TushareProvider:
|
||||
code = str(row.get("ts_code") or "")
|
||||
current = deduplicated.get(code)
|
||||
if code and (
|
||||
current is None
|
||||
or str(row.get("in_date") or "") > str(current.get("in_date") or "")
|
||||
current is None or str(row.get("in_date") or "") > str(current.get("in_date") or "")
|
||||
):
|
||||
deduplicated[code] = row
|
||||
if not deduplicated:
|
||||
@@ -222,8 +221,7 @@ class TushareProvider:
|
||||
"daily": self._optional_query(
|
||||
"ths_daily",
|
||||
{"trade_date": current},
|
||||
"ts_code,trade_date,open,high,low,close,pre_close,pct_change,"
|
||||
"vol,turnover_rate",
|
||||
"ts_code,trade_date,open,high,low,close,pre_close,pct_change,vol,turnover_rate",
|
||||
),
|
||||
"hot": self._optional_query("ths_hot", {"trade_date": current}, ""),
|
||||
}
|
||||
@@ -244,12 +242,8 @@ class TushareProvider:
|
||||
return {
|
||||
"ths": self._optional_query("ths_hot", {"trade_date": current}, ""),
|
||||
"dc": self._optional_query("dc_hot", {"trade_date": current}, ""),
|
||||
"previous_ths": self._optional_query(
|
||||
"ths_hot", {"trade_date": previous}, ""
|
||||
),
|
||||
"previous_dc": self._optional_query(
|
||||
"dc_hot", {"trade_date": previous}, ""
|
||||
),
|
||||
"previous_ths": self._optional_query("ths_hot", {"trade_date": previous}, ""),
|
||||
"previous_dc": self._optional_query("dc_hot", {"trade_date": previous}, ""),
|
||||
}
|
||||
if kind == "dragon-list":
|
||||
return {
|
||||
@@ -278,6 +272,147 @@ class TushareProvider:
|
||||
) -> ProviderResult:
|
||||
raise ProviderError("Tushare不提供动态竞价快照")
|
||||
|
||||
def screener_inputs(self, trade_dates: tuple[str, ...]) -> dict[str, ProviderResult | None]:
|
||||
if len(trade_dates) < 21:
|
||||
raise ProviderError("选股因子至少需要21个交易日")
|
||||
compact_dates = tuple(_compact(value) for value in trade_dates)
|
||||
current = compact_dates[-1]
|
||||
quarters = _quarter_periods(current, 5)
|
||||
years = tuple(f"{int(current[:4]) - offset}1231" for offset in range(1, 6))
|
||||
return {
|
||||
"directory": self._optional_query(
|
||||
"stock_basic",
|
||||
{"exchange": "", "list_status": "L"},
|
||||
"ts_code,symbol,name,industry,market,list_date,list_status",
|
||||
),
|
||||
"industry": self._optional_query(
|
||||
"index_member_all",
|
||||
{"is_new": "Y"},
|
||||
"l1_code,l1_name,l2_code,l2_name,ts_code,name,in_date,out_date,is_new",
|
||||
),
|
||||
"daily": self._series_query(
|
||||
"daily",
|
||||
compact_dates,
|
||||
"ts_code,trade_date,open,high,low,close,pre_close,pct_chg,vol,amount",
|
||||
),
|
||||
"daily_basic": self._series_query(
|
||||
"daily_basic",
|
||||
compact_dates[-5:],
|
||||
"ts_code,trade_date,turnover_rate,volume_ratio,pe_ttm,pb,ps_ttm,dv_ttm,"
|
||||
"total_mv,circ_mv",
|
||||
),
|
||||
"moneyflow": self._series_query(
|
||||
"moneyflow",
|
||||
compact_dates[-5:],
|
||||
"ts_code,trade_date,buy_lg_amount,sell_lg_amount,buy_elg_amount,"
|
||||
"sell_elg_amount,net_mf_amount",
|
||||
),
|
||||
"benchmark": self._optional_query(
|
||||
"index_daily",
|
||||
{
|
||||
"ts_code": "000300.SH",
|
||||
"start_date": compact_dates[0],
|
||||
"end_date": current,
|
||||
},
|
||||
"ts_code,trade_date,close,pct_chg",
|
||||
),
|
||||
"fundamentals": self._period_query(
|
||||
"fina_indicator",
|
||||
quarters,
|
||||
"ts_code,ann_date,end_date,roe,roa,roic,grossprofit_margin,"
|
||||
"netprofit_yoy,or_yoy,ocf_to_or",
|
||||
),
|
||||
"dividends": self._period_query(
|
||||
"dividend",
|
||||
years,
|
||||
"ts_code,end_date,ann_date,div_proc,cash_div_tax,ex_date",
|
||||
parameter="end_date",
|
||||
),
|
||||
"auction": self._optional_query(
|
||||
"stk_auction",
|
||||
{"trade_date": current},
|
||||
"ts_code,trade_date,price,pre_close,amount,turnover_rate,volume_ratio",
|
||||
),
|
||||
"limit_events": self._series_query(
|
||||
"limit_list_d",
|
||||
compact_dates[-80:],
|
||||
"trade_date,ts_code,name,limit_type,limit_times",
|
||||
empty_is_complete=True,
|
||||
),
|
||||
"forecast": self._period_query(
|
||||
"forecast",
|
||||
quarters,
|
||||
"ts_code,ann_date,end_date,type,p_change_min,p_change_max,"
|
||||
"net_profit_min,net_profit_max,last_parent_net",
|
||||
),
|
||||
"express": self._period_query(
|
||||
"express",
|
||||
quarters,
|
||||
"ts_code,ann_date,end_date,revenue,operate_profit,total_profit,n_income,"
|
||||
"total_assets,diluted_roe,yoy_net_profit",
|
||||
),
|
||||
}
|
||||
|
||||
def _series_query(
|
||||
self,
|
||||
api_name: str,
|
||||
dates: tuple[str, ...],
|
||||
fields: str,
|
||||
*,
|
||||
empty_is_complete: bool = False,
|
||||
) -> ProviderResult | None:
|
||||
rows: list[dict[str, Any]] = []
|
||||
completed = 0
|
||||
for trade_date in dates:
|
||||
try:
|
||||
result = self._query(
|
||||
api_name,
|
||||
{"trade_date": trade_date},
|
||||
fields,
|
||||
unit="mixed",
|
||||
empty_is_complete=empty_is_complete,
|
||||
)
|
||||
except ProviderError:
|
||||
continue
|
||||
rows.extend(result.rows)
|
||||
completed += 1
|
||||
if completed == 0:
|
||||
return None
|
||||
return ProviderResult(
|
||||
tuple(rows),
|
||||
_metadata(self.source, "mixed", completed / len(dates)),
|
||||
)
|
||||
|
||||
def _period_query(
|
||||
self,
|
||||
api_name: str,
|
||||
periods: tuple[str, ...],
|
||||
fields: str,
|
||||
*,
|
||||
parameter: str = "period",
|
||||
) -> ProviderResult | None:
|
||||
rows: list[dict[str, Any]] = []
|
||||
completed = 0
|
||||
for period in periods:
|
||||
try:
|
||||
result = self._query(
|
||||
api_name,
|
||||
{parameter: period},
|
||||
fields,
|
||||
unit="mixed",
|
||||
empty_is_complete=True,
|
||||
)
|
||||
except ProviderError:
|
||||
continue
|
||||
rows.extend(result.rows)
|
||||
completed += 1
|
||||
if completed == 0:
|
||||
return None
|
||||
return ProviderResult(
|
||||
tuple(rows),
|
||||
_metadata(self.source, "mixed", completed / len(periods)),
|
||||
)
|
||||
|
||||
def _optional_query(
|
||||
self, api_name: str, params: dict[str, Any], fields: str
|
||||
) -> ProviderResult | None:
|
||||
@@ -295,8 +430,7 @@ class TushareProvider:
|
||||
def _membership_rows(self, params: dict[str, str]) -> list[dict[str, Any]]:
|
||||
rows: list[dict[str, Any]] = []
|
||||
fields = (
|
||||
"l1_code,l1_name,l2_code,l2_name,l3_code,l3_name,"
|
||||
"ts_code,name,in_date,out_date,is_new"
|
||||
"l1_code,l1_name,l2_code,l2_name,l3_code,l3_name,ts_code,name,in_date,out_date,is_new"
|
||||
)
|
||||
for is_new in ("Y", "N"):
|
||||
result = self._query(
|
||||
@@ -378,6 +512,21 @@ def _display(value: str) -> str:
|
||||
return f"{compact[:4]}-{compact[4:6]}-{compact[6:]}"
|
||||
|
||||
|
||||
def _quarter_periods(through: str, count: int) -> tuple[str, ...]:
|
||||
year = int(through[:4])
|
||||
quarter = (int(through[4:6]) - 1) // 3
|
||||
periods = []
|
||||
for offset in range(count + 4):
|
||||
index = year * 4 + quarter - offset
|
||||
period_year, period_quarter = divmod(index, 4)
|
||||
period = f"{period_year}{('0331', '0630', '0930', '1231')[period_quarter]}"
|
||||
if period <= through:
|
||||
periods.append(period)
|
||||
if len(periods) == count:
|
||||
break
|
||||
return tuple(reversed(periods))
|
||||
|
||||
|
||||
def _active_on(row: dict[str, Any], trade_date: str) -> bool:
|
||||
start = str(row.get("in_date") or "")
|
||||
end = str(row.get("out_date") or "")
|
||||
|
||||
Reference in New Issue
Block a user