""" 股票基础信息同步 — 从 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]