修改了bsp state,继续测试

This commit is contained in:
jackyu66git
2026-05-20 00:49:53 +08:00
parent 91148a648a
commit 9b876c45ed
7 changed files with 430 additions and 161 deletions
+228 -19
View File
@@ -1,3 +1,4 @@
import os
import akshare as ak
import pandas as pd
from datetime import datetime, timedelta, time
@@ -7,6 +8,14 @@ from pytz import timezone
import warnings
warnings.filterwarnings('ignore')
import logging
logger = logging.getLogger(__name__)
# 与 A-Share Data Platform REST 文档一致的周期(分钟线依赖服务端积累,无数据时会回退 AKShare)
ASHARE_REST_TIMEFRAMES = frozenset({'1m', '5m', '15m', '30m', '1h', '2h', '1d', '1w', '1M'})
class ChinaStockData:
"""A股数据获取类"""
@@ -17,43 +26,42 @@ class ChinaStockData:
'morning': {'start': '09:30', 'end': '11:30'},
'afternoon': {'start': '13:00', 'end': '15:00'}
}
# 例: http://103.179.242.166:8000 — 设 ASHARE_DP_URL= 空字符串可禁用,仅用 AKShare
_base = os.environ.get('ASHARE_DP_URL', 'http://103.179.242.166:8000')
self.ashare_dp_base = _base.rstrip('/') if (_base or '').strip() else ''
# 全量股票列表内存缓存(秒),默认 1 小时
try:
self.stock_list_cache_ttl = int(os.environ.get('ASHARE_STOCK_LIST_CACHE_SEC', '3600'))
except ValueError:
self.stock_list_cache_ttl = 3600
self._stock_list_cache = None
self._stock_list_cache_expires = 0.0
def get_stock_list(self):
"""获取A股股票列表"""
def _get_stock_list_akshare(self):
"""通过 AKShare 获取 A 股列表(约 2000 条非 ST,作备用)。"""
try:
import requests
# 设置较短的超时时间,避免长时间等待
import akshare as ak
pass
# 尝试获取沪深A股实时行情,设置超时时间
try:
# 临时设置requests的默认超时
original_timeout = getattr(requests, 'timeout', None)
requests.timeout = 10 # 10秒超时
requests.timeout = 10
stock_info = ak.stock_zh_a_spot_em()
# 恢复原始超时设置
if original_timeout:
requests.timeout = original_timeout
else:
delattr(requests, 'timeout')
except Exception as network_error:
pass
# 网络失败时返回空列表,让调用方使用备用方案
except Exception:
return []
if stock_info is None or len(stock_info) == 0:
return []
# 增加到前2000只股票,提供更多选择
stock_list = []
for index, row in stock_info.head(2000).iterrows():
try:
# 过滤掉ST股票和停牌股票
stock_name = str(row['名称'])
if 'ST' not in stock_name and '*' not in stock_name:
stock_list.append({
@@ -64,16 +72,108 @@ class ChinaStockData:
'volume': float(row['成交量']) if pd.notna(row['成交量']) else 0.0,
'amount': float(row['成交额']) if pd.notna(row['成交额']) else 0.0
})
except Exception as row_error:
except Exception:
continue
# 按成交金额排序,优先显示活跃股票
stock_list.sort(key=lambda x: x['amount'], reverse=True)
return stock_list
except Exception as e:
except Exception:
return []
def _fetch_all_stocks_ashare_dp(self):
"""分页拉取 A-Share Data Platform /api/v1/stocks 全市场标的。"""
import requests
page_size = 1000
offset = 0
all_rows = []
reported_total = None
url = f'{self.ashare_dp_base}/api/v1/stocks'
while True:
resp = requests.get(
url,
params={'limit': page_size, 'offset': offset},
timeout=45,
)
resp.raise_for_status()
payload = resp.json()
items = payload.get('items') or []
if reported_total is None:
reported_total = int(payload.get('total') or 0)
all_rows.extend(items)
if len(items) == 0:
break
if len(items) < page_size:
break
offset += page_size
if reported_total and offset >= reported_total:
break
if not all_rows:
return []
out = []
for row in all_rows:
sym = row.get('symbol')
if not sym and row.get('ts_code'):
sym = str(row['ts_code']).split('.')[0]
if not sym:
continue
name = row.get('name') or ''
out.append({
'symbol': str(sym).strip(),
'name': str(name).strip(),
'ts_code': row.get('ts_code'),
'price': 0.0,
'change_pct': 0.0,
'volume': 0.0,
'amount': 0.0,
})
out.sort(key=lambda x: x['symbol'])
return out
def get_stock_list(self, use_cache=True):
"""获取 A 股股票列表:优先全量 REST(约 5500+),失败则 AKShare。"""
now = time_module.time()
if use_cache and self._stock_list_cache is not None and now < self._stock_list_cache_expires:
return list(self._stock_list_cache)
if self.ashare_dp_base:
try:
dp_list = self._fetch_all_stocks_ashare_dp()
if dp_list:
self._stock_list_cache = dp_list
self._stock_list_cache_expires = now + self.stock_list_cache_ttl
return list(dp_list)
except Exception as exc:
logger.warning('A股列表从数据服务拉取失败,回退 AKShare: %s', exc)
ak_list = self._get_stock_list_akshare()
if ak_list:
self._stock_list_cache = ak_list
self._stock_list_cache_expires = now + min(self.stock_list_cache_ttl, 300)
return ak_list or []
def get_available_kline_freqs(self):
"""
A-Share Data Platform 支持的 K 线周期列表(原始顺序不保证,由上层按粒度排序)。
文档: GET /api/v1/klines/available-freqs
"""
import requests
fallback = ['1m', '5m', '15m', '30m', '1h', '2h', '1d', '1w', '1M']
if not self.ashare_dp_base:
return list(fallback)
try:
url = f'{self.ashare_dp_base}/api/v1/klines/available-freqs'
resp = requests.get(url, timeout=10)
resp.raise_for_status()
data = resp.json()
freqs = data.get('frequencies') or []
return list(freqs) if freqs else list(fallback)
except Exception as exc:
logger.warning('获取 A 股可用 K 线周期失败: %s', exc)
return list(fallback)
def get_popular_stocks(self):
"""获取热门A股股票代码列表 - 扩展版本,按行业分类"""
return [
@@ -194,6 +294,110 @@ class ChinaStockData:
}
return mapping.get(timeframe, 'daily')
@staticmethod
def symbol_to_ts_code(symbol):
"""六位代码或已是 ts_code(000001.SZ)→ 交易所后缀。"""
if symbol is None:
return ''
s = str(symbol).strip().upper()
if '.' in s and s.count('.') == 1:
return s
if len(s) != 6 or not s.isdigit():
return s
if s.startswith('6'):
return f'{s}.SH'
if s.startswith(('0', '3')):
return f'{s}.SZ'
if s.startswith('920'):
return f'{s}.BJ'
if s.startswith(('8', '4')):
return f'{s}.BJ'
return f'{s}.SZ'
@staticmethod
def _ymd_compact_to_api_date(ymd_compact):
"""YYYYMMDD → YYYY-MM-DD"""
if not ymd_compact or len(ymd_compact) != 8:
return None
return f'{ymd_compact[:4]}-{ymd_compact[4:6]}-{ymd_compact[6:8]}'
def get_kl_data_from_ashare_dp(self, symbol, timeframe, start_date, end_date, limit):
"""
从 A-Share Data Platform/api/v1/klines/{freq})拉取 K 线。
start_date / end_date 为 YYYYMMDD 字符串。
"""
if not self.ashare_dp_base or timeframe not in ASHARE_REST_TIMEFRAMES:
return None
import requests
ts_code = self.symbol_to_ts_code(symbol)
if not ts_code or '.' not in ts_code:
return None
start_api = self._ymd_compact_to_api_date(start_date)
end_api = self._ymd_compact_to_api_date(end_date)
if not start_api or not end_api:
return None
api_limit = 10000
if limit is not None:
try:
api_limit = min(int(limit), 10000)
except (TypeError, ValueError):
api_limit = 10000
url = f'{self.ashare_dp_base}/api/v1/klines/{timeframe}'
params = {
'ts_code': ts_code,
'start_date': start_api,
'end_date': end_api,
'limit': api_limit,
}
try:
resp = requests.get(url, params=params, timeout=20)
resp.raise_for_status()
payload = resp.json()
except Exception as exc:
logger.debug('A股数据服务 K 线请求失败: %s', exc)
return None
items = payload.get('items') or payload.get('data') or []
if not items:
return None
rows = []
for row in items:
t = row.get('trade_time') or row.get('trade_date')
if not t:
continue
rows.append({
'date': t,
'open': row.get('open'),
'high': row.get('high'),
'low': row.get('low'),
'close': row.get('close'),
'volume': row.get('volume'),
})
if not rows:
return None
df = pd.DataFrame(rows)
df['date'] = pd.to_datetime(df['date'])
for col in ('open', 'high', 'low', 'close', 'volume'):
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce')
df = df.dropna(subset=['open', 'high', 'low', 'close'])
df = df.sort_values('date').reset_index(drop=True)
df = self.adjust_timestamp_for_trading_hours(df, timeframe)
df = self.clean_a_stock_data(df, timeframe)
if df is None or len(df) == 0:
return None
if limit is not None:
try:
lim = int(limit)
if len(df) > lim:
df = df.tail(lim).reset_index(drop=True)
except (TypeError, ValueError):
pass
elif len(df) > 10000:
df = df.tail(10000).reset_index(drop=True)
df = self.add_indicators(df)
return df
def get_kl_data(self, symbol, timeframe='1d', start_date=None, end_date=None, limit=10000):
"""
获取A股K线数据 - 支持分批次获取突破单次限制
@@ -222,7 +426,12 @@ class ChinaStockData:
if '-' in end_date:
end_date = end_date.replace('-', '')
pass
if self.ashare_dp_base:
df_dp = self.get_kl_data_from_ashare_dp(
symbol, timeframe, start_date, end_date, limit
)
if df_dp is not None and len(df_dp) > 0:
return df_dp
# 分批次获取数据以突破单次限制
all_data = []