Files
sfgrid/core/scoring/sync/stocks_sync.py
T

101 lines
3.4 KiB
Python

"""
股票基础信息同步 — 从 QMT get_instrument_detail 获取
"""
from datetime import date
from core.scoring.sync.base import BaseSync
from core.scoring.models import StockInfo
from core.database import db
from core.logger import LogLevel, PrintLog
BATCH_SIZE = 200
class StocksSync(BaseSync):
"""股票基础信息同步 — QMT get_instrument_detail_list"""
def _fetch(self, **kwargs):
"""从 QMT 拉取全部A股基础信息"""
from xtquant import xtdata
all_stocks = xtdata.get_stock_list_in_sector("沪深A股")
PrintLog(LogLevel.INFO, f'[sync] Stocks: 获取到 {len(all_stocks)} 只A股')
result = {}
total = len(all_stocks)
for i in range(0, total, BATCH_SIZE):
batch = all_stocks[i:i + BATCH_SIZE]
try:
details = xtdata.get_instrument_detail_list(batch)
result.update(details)
except Exception as e:
PrintLog(LogLevel.ERROR,
f'[sync] Stocks batch {i}-{min(i + BATCH_SIZE, total)} failed: {e}')
self.stats['errors'] += len(batch)
return result
def _upsert(self, data: dict):
"""写入 StockInfo 表"""
records = []
today = date.today()
for full_code, inst in data.items():
if not inst:
self.stats['skipped'] += 1
continue
code_num = full_code.split('.')[0]
# 判断交易所
exchange = inst.get('ExchangeID', '')
if not exchange:
if '.SH' in full_code:
exchange = 'SH'
elif '.SZ' in full_code:
exchange = 'SZ'
elif '.BJ' in full_code:
exchange = 'BJ'
# OpenDate 格式: 'YYYYMMDD' 或 int
open_date = inst.get('OpenDate', '')
if open_date and len(str(open_date)) == 8:
list_date_val = f'{str(open_date)[:4]}-{str(open_date)[4:6]}-{str(open_date)[6:8]}'
else:
list_date_val = None
# InstrumentStatus 含义:
# 0/1 = 正常股票(含已退市但仍在QMT列表的)
# -1 = ST / *ST
# 30/31 = *ST
# 已退市股(名字含XD/退)K线不足120日,会在候选股过滤时被排除
status = inst.get('InstrumentStatus', -1)
if status in (-1, 30, 31):
listing_status = 'ST'
else:
listing_status = 'normal'
total_share = inst.get('TotalVolume', None)
float_share = inst.get('FloatVolume', None)
records.append({
'code': full_code,
'name': str(inst.get('InstrumentName', '')),
'exchange': exchange,
'list_date': list_date_val,
'listing_status': listing_status,
'total_share': float(total_share) if total_share else None,
'float_share': float(float_share) if float_share else None,
'share_updated_at': today,
})
if records:
with db.atomic():
for batch in _chunked(records, 500):
StockInfo.insert_many(batch).on_conflict_replace().execute()
self.stats['inserted'] += len(records)
def _chunked(lst: list, n: int):
for i in range(0, len(lst), n):
yield lst[i:i + n]