101 lines
3.4 KiB
Python
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]
|