修改了bsp state,继续测试
This commit is contained in:
+228
-19
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user