修改了bsp state,继续测试
This commit is contained in:
@@ -128,8 +128,6 @@ class ChanLun():
|
||||
|
||||
def get_bsp_state(self, dataframe):
|
||||
return self.tf_df.get_bsp_state(dataframe)
|
||||
def get_bsp_signal_data(self, dataframe):
|
||||
return self.tf_df.get_bsp_signal_data(dataframe)
|
||||
|
||||
def get_structure_zones(self, current_price=None, config=None):
|
||||
if config is None:
|
||||
|
||||
@@ -157,41 +157,16 @@ class TF_DF():
|
||||
klu_state_list.append("00")
|
||||
print(klu_state_list[:20])
|
||||
return klu_state_list
|
||||
def get_bsp_signal_data(self, dataframe):
|
||||
|
||||
def get_bsp_state(self, dataframe):
|
||||
klu_list = self.get_klu_list(dataframe)
|
||||
klc_list = self.get_klc_list(klu_list)
|
||||
bi_list = self.cal_bi_list(klc_list)
|
||||
bi_zs_list = self.cal_bi_zs_list_pure(bi_list)
|
||||
seg_list = self.get_seg_list(bi_list)
|
||||
bi_zs_list = self.cal_bi_zs(seg_list)
|
||||
bsp_list = self.find_all_bsp(bi_list, bi_zs_list)
|
||||
bsp_by_bi_type = {}
|
||||
for bsp in bsp_list:
|
||||
if bsp and bsp.bi:
|
||||
bsp_by_bi_type[(bsp.bi.index, bsp.type)] = bsp
|
||||
bsp_state_list = [0] * len(dataframe)
|
||||
bsp_zg_list = [0.0] * len(dataframe)
|
||||
bsp_zd_list = [0.0] * len(dataframe)
|
||||
bsp_stop_price_list = [0.0] * len(dataframe)
|
||||
bsp_risk_ratio_list = [0.0] * len(dataframe)
|
||||
klc_index = 0
|
||||
def set_bsp_signal(index, state, bsp):
|
||||
bsp_state_list[index] = state
|
||||
if not bsp or not bsp.zs:
|
||||
return
|
||||
close = float(dataframe.iloc[index]['close'])
|
||||
atr = float(dataframe.iloc[index]['atr']) if 'atr' in dataframe.columns and not pd.isna(dataframe.iloc[index]['atr']) else 0.0
|
||||
atr_ratio = atr / close if close > 0 else 0.0
|
||||
buffer = atr * 0.1
|
||||
bsp_zg_list[index] = bsp.zs.zg
|
||||
bsp_zd_list[index] = bsp.zs.zd
|
||||
if state == -1:
|
||||
stop_price = bsp.zs.zg - buffer
|
||||
risk_ratio = (close - stop_price) / close if close > stop_price else atr_ratio
|
||||
else:
|
||||
stop_price = bsp.zs.zd + buffer
|
||||
risk_ratio = (stop_price - close) / close if close < stop_price else atr_ratio
|
||||
bsp_stop_price_list[index] = stop_price
|
||||
bsp_risk_ratio_list[index] = max(0.001, min(float(risk_ratio), 0.02))
|
||||
|
||||
for index in range(0, len(dataframe)):
|
||||
if klc_index == len(klc_list):
|
||||
klc_index = len(klc_list) - 1
|
||||
@@ -201,7 +176,7 @@ class TF_DF():
|
||||
bi = klc.bi.pre
|
||||
if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.B3:
|
||||
# 第三类买点
|
||||
set_bsp_signal(index, -1, bsp_by_bi_type.get((bi.index, Chan_BSP_TYPE.B3)))
|
||||
bsp_state_list[index] = -1
|
||||
#print(klc.end_time, "B3")
|
||||
else:
|
||||
bsp_state_list[index] = 0
|
||||
@@ -209,22 +184,14 @@ class TF_DF():
|
||||
bi = klc.bi.pre
|
||||
if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.S3:
|
||||
# 第三类卖点
|
||||
set_bsp_signal(index, 1, bsp_by_bi_type.get((bi.index, Chan_BSP_TYPE.S3)))
|
||||
bsp_state_list[index] = 1
|
||||
#print(klc.end_time, "S3")
|
||||
else:
|
||||
bsp_state_list[index] = 0
|
||||
klc_index += 1
|
||||
else:
|
||||
bsp_state_list[index] = 0
|
||||
return {
|
||||
'bsp_state': bsp_state_list,
|
||||
'bsp_zg': bsp_zg_list,
|
||||
'bsp_zd': bsp_zd_list,
|
||||
'bsp_stop_price': bsp_stop_price_list,
|
||||
'bsp_risk_ratio': bsp_risk_ratio_list,
|
||||
}
|
||||
def get_bsp_state(self, dataframe):
|
||||
return self.get_bsp_signal_data(dataframe)['bsp_state']
|
||||
return bsp_state_list
|
||||
def get_ema_state(self, dataframe):
|
||||
klu_list = self.get_klu_list(dataframe)
|
||||
klc_list = self.get_klc_list(klu_list)
|
||||
|
||||
+10
-18
@@ -1,8 +1,7 @@
|
||||
"""
|
||||
engine.py - 缠论管线封装:DataFrame → KLC → BI → ZS → BSP。
|
||||
engine.py - 缠论管线封装:DataFrame → KLU → KLC → BI → SEG → ZS → BSP。
|
||||
|
||||
复用 ~/Project/Chan/ 下的 TF_DF 模块。
|
||||
注意:TF_DF.__init__ 有 bug(get_zs_list 不存在),这里手动调用各步骤。
|
||||
复用 ~/Project/Chan/ 下的 TF_DF 模块,管线步骤对齐 TF_DF.get_bsp_state()。
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
@@ -13,21 +12,19 @@ if _PARENT not in sys.path:
|
||||
sys.path.insert(0, _PARENT)
|
||||
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
from ChanEnum import (
|
||||
Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_BI_DIR,
|
||||
Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR, Chan_ZS_DIR,
|
||||
Chan_ZS_DIR,
|
||||
)
|
||||
from ChanBSP import ChanBSP
|
||||
from ChanBI import ChanBI
|
||||
from ChanZS import ChanZS
|
||||
|
||||
# 仅导入类,不触发 TF_DF.__init__
|
||||
from TF_DF import TF_DF as _TF_DF_Class
|
||||
|
||||
|
||||
class ChanEngine:
|
||||
"""手动执行缠论管线,绕过 TF_DF.__init__ 的 bug。"""
|
||||
"""缠论管线,对齐 TF_DF.get_bsp_state() 的调用顺序。"""
|
||||
|
||||
def __init__(self, df: pd.DataFrame):
|
||||
if df.empty or len(df) < 50:
|
||||
@@ -39,18 +36,13 @@ class ChanEngine:
|
||||
self.df = df
|
||||
self._tf = _TF_DF_Class.__new__(_TF_DF_Class) # 不调用 __init__
|
||||
|
||||
# Step 0: 添加 TA 指标
|
||||
# Step 0: 添加 TA 指标 (MACD/EMA/BB/RSI)
|
||||
self._df_with_indicators = self._tf.add_indicators(df.copy())
|
||||
|
||||
# Step 1: KLU (K-line unit)
|
||||
self.klu_list = self._tf.cal_kl_data(self._df_with_indicators)
|
||||
# Step 1: KLU — get_klu_list → get_kl_data → cal_kl_data
|
||||
self.klu_list = self._tf.get_klu_list(self._df_with_indicators)
|
||||
|
||||
# Step 1.5: MACD state
|
||||
from ChanMACD import ChanMACD
|
||||
chanmacd = ChanMACD(self.klu_list)
|
||||
self.klu_list = chanmacd.cal_macd_state()
|
||||
|
||||
# Step 2: KLC (combined K-line)
|
||||
# Step 2: KLC — 内部已含 ChanMACD.cal_macd_state() + cal_trend()
|
||||
self.klc_list = self._tf.get_klc_list(self.klu_list)
|
||||
|
||||
# Step 3: BI (stroke)
|
||||
@@ -59,8 +51,8 @@ class ChanEngine:
|
||||
# Step 4: SEG (segment)
|
||||
self.seg_list = self._tf.get_seg_list(self.bi_list)
|
||||
|
||||
# Step 5: ZS (bi-level center) — 供 find_all_bsp 使用
|
||||
self.bi_zs_list: List = self._tf.cal_bi_zs_list(self.bi_list)
|
||||
# Step 5: ZS — cal_bi_zs(seg_list) 对齐 get_bsp_state(从线段计算笔中枢)
|
||||
self.bi_zs_list: List = self._tf.cal_bi_zs(self.seg_list)
|
||||
|
||||
# Step 6: BSP (buy/sell points)
|
||||
self.bsp_list: List[ChanBSP] = self._tf.find_all_bsp(
|
||||
|
||||
@@ -31,7 +31,7 @@ logger = logging.getLogger(__name__)
|
||||
# sudo docker compose run --rm chanlun_btc download-data -c ./user_data/Chan/config/ChanLun_BTC_1m.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101-
|
||||
# sudo docker compose run --rm chanlun_btc trade -c ./user_data/Chan/config/ChanLun_BTC_1m.json --strategy ChanLun_BTC_1m --strategy-path ./user_data/Chan/strategies
|
||||
|
||||
class ChanLun_BTC_1m(IStrategy):
|
||||
class ChanLun_BTC_1m_old(IStrategy):
|
||||
"""
|
||||
交易核心(缠论):
|
||||
- 仅在缠论一/二/三类买卖点出现时交易。
|
||||
|
||||
+74
-29
@@ -45,7 +45,7 @@ exchange = ccxt.binance({
|
||||
'enableRateLimit': True,
|
||||
})
|
||||
|
||||
# 初始化A股数据获取器
|
||||
# 初始化 A 股数据获取器(K 线优先请求 A-Share Data Platform,默认 http://103.179.242.166:8000 ,见 /api/v1/klines 文档;ASHARE_DP_URL 覆盖,置空则仅用 AKShare)
|
||||
china_stock = ChinaStockData()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -65,6 +65,7 @@ def _zone_cache_ttl(tf_name: str) -> int:
|
||||
else:
|
||||
return 1800 # 4h+: 30分钟
|
||||
|
||||
# 加密货币本地/自建行情服务(与 A 股 ASHARE_DP_URL 端口可不同)
|
||||
DATA_SERVICE_URL = os.environ.get("DATA_SERVICE_URL", os.environ.get("DATASVC_URL", "http://103.179.242.166"))
|
||||
|
||||
DEFAULT_TIMEFRAME_LABELS = OrderedDict([
|
||||
@@ -150,6 +151,40 @@ def build_timeframe_labels(timeframes):
|
||||
return labels
|
||||
|
||||
|
||||
def compute_timeframe_defaults(labels_ordered):
|
||||
"""
|
||||
根据已排序的「周期 → 中文标签」映射,计算主 / 次 / 次次周期默认值。
|
||||
labels_ordered: OrderedDict 或按插入顺序排列的 dict。
|
||||
"""
|
||||
if not labels_ordered:
|
||||
labels_ordered = DEFAULT_TIMEFRAME_LABELS.copy()
|
||||
timeframe_keys = list(labels_ordered.keys())
|
||||
preferred_main = next((tf for tf in ['5m', '15m', '1h'] if tf in labels_ordered), None)
|
||||
default_main = preferred_main or (timeframe_keys[0] if timeframe_keys else '1m')
|
||||
if default_main not in labels_ordered and timeframe_keys:
|
||||
default_main = timeframe_keys[0]
|
||||
|
||||
if timeframe_keys:
|
||||
try:
|
||||
idx = timeframe_keys.index(default_main)
|
||||
default_element = timeframe_keys[idx - 1] if idx > 0 else timeframe_keys[0]
|
||||
except ValueError:
|
||||
default_element = timeframe_keys[0]
|
||||
else:
|
||||
default_element = default_main
|
||||
|
||||
if timeframe_keys:
|
||||
try:
|
||||
idx_el = timeframe_keys.index(default_element)
|
||||
default_sub_sub = timeframe_keys[idx_el - 1] if idx_el > 0 else timeframe_keys[0]
|
||||
except ValueError:
|
||||
default_sub_sub = timeframe_keys[0]
|
||||
else:
|
||||
default_sub_sub = default_element
|
||||
|
||||
return default_main, default_element, default_sub_sub, timeframe_keys
|
||||
|
||||
|
||||
def _parse_time_input(value):
|
||||
if value in (None, '', 0):
|
||||
return None
|
||||
@@ -230,7 +265,8 @@ def _fetch_kl_from_datasvc(symbol, timeframe, start_ms=None, end_ms=None, limit=
|
||||
refresh_data_service_metadata(force=True)
|
||||
|
||||
# A股热门股票
|
||||
A_STOCK_SYMBOLS = china_stock.get_popular_stocks()
|
||||
# 模板中 A 股下拉仅放默认一项;用户切换到「A股」时由前端请求 /api/a_stocks 填充全市场(约 5500+)
|
||||
A_STOCK_SYMBOLS = [{'symbol': '000001', 'name': '平安银行'}]
|
||||
|
||||
def detect_symbol_type(symbol):
|
||||
"""检测交易对类型:crypto 或 a_stock"""
|
||||
@@ -1226,39 +1262,15 @@ def serve_charting_library(filename):
|
||||
def index():
|
||||
"""主页"""
|
||||
refresh_data_service_metadata()
|
||||
timeframe_items = list(TIMEFRAMES.items())
|
||||
timeframe_keys = [item[0] for item in timeframe_items]
|
||||
tf_map = TIMEFRAMES if TIMEFRAMES else DEFAULT_TIMEFRAME_LABELS.copy()
|
||||
default_main, default_element, default_sub_sub, timeframe_keys = compute_timeframe_defaults(OrderedDict(tf_map))
|
||||
symbols = SYMBOLS if SYMBOLS else DEFAULT_SYMBOLS
|
||||
|
||||
preferred_main = next((tf for tf in ['5m', '15m', '1h'] if tf in TIMEFRAMES), None)
|
||||
default_main = preferred_main or (timeframe_keys[0] if timeframe_keys else '1m')
|
||||
if default_main not in TIMEFRAMES and timeframe_keys:
|
||||
default_main = timeframe_keys[0]
|
||||
|
||||
if timeframe_keys:
|
||||
try:
|
||||
idx = timeframe_keys.index(default_main)
|
||||
default_element = timeframe_keys[idx - 1] if idx > 0 else timeframe_keys[0]
|
||||
except ValueError:
|
||||
default_element = timeframe_keys[0]
|
||||
else:
|
||||
default_element = default_main
|
||||
|
||||
# 次次周期默认比次周期小一档
|
||||
if timeframe_keys:
|
||||
try:
|
||||
idx_el = timeframe_keys.index(default_element)
|
||||
default_sub_sub = timeframe_keys[idx_el - 1] if idx_el > 0 else timeframe_keys[0]
|
||||
except ValueError:
|
||||
default_sub_sub = timeframe_keys[0]
|
||||
else:
|
||||
default_sub_sub = default_element
|
||||
|
||||
default_symbol = 'BTC/USDT:USDT' if 'BTC/USDT:USDT' in symbols else (symbols[0] if symbols else '')
|
||||
|
||||
return render_template(
|
||||
'index.html',
|
||||
timeframes=TIMEFRAMES,
|
||||
timeframes=tf_map,
|
||||
symbols=symbols,
|
||||
a_stock_symbols=A_STOCK_SYMBOLS,
|
||||
default_main_timeframe=default_main,
|
||||
@@ -1269,6 +1281,39 @@ def index():
|
||||
data_service_available=DATA_SERVICE_AVAILABLE,
|
||||
)
|
||||
|
||||
|
||||
@app.route('/api/chart_metadata')
|
||||
def api_chart_metadata():
|
||||
"""
|
||||
按数据源返回图表用 K 线周期(中文标签)及主/次/次次默认周期。
|
||||
crypto:强制刷新 DATA_SERVICE_URL /health 元信息;
|
||||
a_stock:读取 ASHARE_DP_URL 的 /api/v1/klines/available-freqs,不修改全局加密货币 TIMEFRAMES。
|
||||
"""
|
||||
source = (request.args.get('source') or 'crypto').strip().lower()
|
||||
if source not in ('crypto', 'a_stock'):
|
||||
source = 'crypto'
|
||||
try:
|
||||
if source == 'a_stock':
|
||||
raw = china_stock.get_available_kline_freqs()
|
||||
labels_od = build_timeframe_labels(raw)
|
||||
else:
|
||||
refresh_data_service_metadata(force=True)
|
||||
labels_od = OrderedDict(TIMEFRAMES if TIMEFRAMES else DEFAULT_TIMEFRAME_LABELS.copy())
|
||||
|
||||
default_main, default_element, default_sub_sub, keys = compute_timeframe_defaults(labels_od)
|
||||
return jsonify({
|
||||
'source': source,
|
||||
'timeframes': {k: v for k, v in labels_od.items()},
|
||||
'timeframe_keys': keys,
|
||||
'default_main': default_main,
|
||||
'default_element': default_element,
|
||||
'default_sub_sub': default_sub_sub,
|
||||
})
|
||||
except Exception as exc:
|
||||
logger.exception('chart_metadata 失败: %s', exc)
|
||||
return jsonify({'error': str(exc)}), 500
|
||||
|
||||
|
||||
@app.route('/api/analyze')
|
||||
def analyze():
|
||||
"""分析接口"""
|
||||
|
||||
+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 = []
|
||||
|
||||
+110
-52
@@ -2015,6 +2015,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用 /api/chart_metadata 返回的周期列表(切换 crypto / A股 时拉取) */
|
||||
function applyChartMetadata(meta) {
|
||||
if (!meta || meta.error || !Array.isArray(meta.timeframe_keys) || meta.timeframe_keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
window.AVAILABLE_TIMEFRAMES = meta.timeframe_keys;
|
||||
window.DEFAULT_MAIN_TIMEFRAME = meta.default_main;
|
||||
window.DEFAULT_ELEMENT_TIMEFRAME = meta.default_element;
|
||||
window.DEFAULT_SUB_SUB_TIMEFRAME = meta.default_sub_sub;
|
||||
const labels = meta.timeframes || {};
|
||||
function refill(selId, preferredVal) {
|
||||
const $el = $(selId);
|
||||
const cur = $el.val();
|
||||
$el.empty();
|
||||
meta.timeframe_keys.forEach(function(k) {
|
||||
$el.append($('<option>', { value: k, text: labels[k] || k }));
|
||||
});
|
||||
const pick = (cur && meta.timeframe_keys.indexOf(cur) >= 0) ? cur : preferredVal;
|
||||
if (pick && meta.timeframe_keys.indexOf(pick) >= 0) {
|
||||
$el.val(pick);
|
||||
} else {
|
||||
$el.val(meta.timeframe_keys[0]);
|
||||
}
|
||||
}
|
||||
refill('#timeframe', meta.default_main);
|
||||
refill('#elementTimeframe', meta.default_element);
|
||||
refill('#subSubTimeframe', meta.default_sub_sub);
|
||||
const mainTf = $('#timeframe').val();
|
||||
if (compareTimeframes($('#elementTimeframe').val(), mainTf) > 0) {
|
||||
setSmallestLargerTimeframe(mainTf);
|
||||
}
|
||||
ensureSubSubLteElement();
|
||||
}
|
||||
|
||||
// 比较两个时间周期的大小
|
||||
function compareTimeframes(tf1, tf2) {
|
||||
const v1 = window.timeframeToMs(tf1);
|
||||
@@ -8069,43 +8103,64 @@
|
||||
console.log('从本地存储恢复时区设置:', savedTimezone);
|
||||
}
|
||||
|
||||
// 初始化数据源切换
|
||||
// 初始化数据源切换:先按数据源重新拉取周期元信息,再切换 UI
|
||||
$('#dataSource').on('change', function() {
|
||||
const dataSource = $(this).val();
|
||||
if (dataSource === 'crypto') {
|
||||
$('#cryptoSymbolContainer').show();
|
||||
$('#astockSymbolContainer').hide();
|
||||
// 停止A股状态更新器
|
||||
if (window.astockStatusInterval) {
|
||||
clearInterval(window.astockStatusInterval);
|
||||
window.astockStatusInterval = null;
|
||||
}
|
||||
} else if (dataSource === 'a_stock') {
|
||||
$('#cryptoSymbolContainer').hide();
|
||||
$('#astockSymbolContainer').show();
|
||||
// 加载A股数据时,如果还没有加载股票列表,可以在这里触发加载
|
||||
loadAStockSymbols();
|
||||
// 启动A股交易时间状态更新器
|
||||
startAStockStatusUpdater();
|
||||
}
|
||||
const apiSrc = dataSource === 'a_stock' ? 'a_stock' : 'crypto';
|
||||
$.getJSON('/api/chart_metadata', { source: apiSrc })
|
||||
.done(function(meta) {
|
||||
applyChartMetadata(meta);
|
||||
})
|
||||
.always(function() {
|
||||
if (dataSource === 'crypto') {
|
||||
$('#cryptoSymbolContainer').show();
|
||||
$('#astockSymbolContainer').hide();
|
||||
if (window.astockStatusInterval) {
|
||||
clearInterval(window.astockStatusInterval);
|
||||
window.astockStatusInterval = null;
|
||||
}
|
||||
loadSymbols();
|
||||
} else if (dataSource === 'a_stock') {
|
||||
$('#cryptoSymbolContainer').hide();
|
||||
$('#astockSymbolContainer').show();
|
||||
loadAStockSymbols();
|
||||
startAStockStatusUpdater();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 检查初始数据源设置
|
||||
const initialDataSource = $('#dataSource').val();
|
||||
if (initialDataSource === 'a_stock') {
|
||||
startAStockStatusUpdater();
|
||||
$.getJSON('/api/chart_metadata', { source: 'a_stock' })
|
||||
.done(function(meta) {
|
||||
applyChartMetadata(meta);
|
||||
})
|
||||
.always(function() {
|
||||
loadAStockSymbols();
|
||||
startAStockStatusUpdater();
|
||||
setTimeout(function() {
|
||||
updateChart();
|
||||
}, 300);
|
||||
});
|
||||
} else {
|
||||
setTimeout(function() {
|
||||
updateChart();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 初始化交易对下拉菜单
|
||||
$('#symbol').val('BTC/USDT:USDT');
|
||||
$('#astockSymbol').val('000001');
|
||||
const mainDefault = window.DEFAULT_MAIN_TIMEFRAME || $('#timeframe option:first').val();
|
||||
const elementDefault = window.DEFAULT_ELEMENT_TIMEFRAME || $('#elementTimeframe option:first').val();
|
||||
if (mainDefault) {
|
||||
$('#timeframe').val(mainDefault);
|
||||
}
|
||||
if (elementDefault) {
|
||||
$('#elementTimeframe').val(elementDefault);
|
||||
if (initialDataSource !== 'a_stock') {
|
||||
const mainDefault = window.DEFAULT_MAIN_TIMEFRAME || $('#timeframe option:first').val();
|
||||
const elementDefault = window.DEFAULT_ELEMENT_TIMEFRAME || $('#elementTimeframe option:first').val();
|
||||
if (mainDefault) {
|
||||
$('#timeframe').val(mainDefault);
|
||||
}
|
||||
if (elementDefault) {
|
||||
$('#elementTimeframe').val(elementDefault);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试打印时区偏移量
|
||||
@@ -8127,10 +8182,12 @@
|
||||
// 尝试加载更多交易对
|
||||
loadSymbols();
|
||||
|
||||
// 初始化图表并加载默认数据
|
||||
setTimeout(function() {
|
||||
updateChart();
|
||||
}, 500);
|
||||
// 初始化图表:默认加密货币延迟拉取;若首屏为 A 股则在 chart_metadata 完成后再 updateChart
|
||||
if (initialDataSource !== 'a_stock') {
|
||||
setTimeout(function() {
|
||||
updateChart();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 初始化自动刷新功能
|
||||
initAutoRefresh();
|
||||
@@ -8634,31 +8691,32 @@
|
||||
})
|
||||
|
||||
|
||||
// 获取A股股票列表
|
||||
// 获取A股股票列表(全市场,来自 /api/a_stocks)
|
||||
function loadAStockSymbols() {
|
||||
$.get('/api/popular_a_stocks', function(data) {
|
||||
if (Array.isArray(data)) {
|
||||
const $select = $('#astockSymbol');
|
||||
const currentSymbol = $select.val(); // 保存当前选中的值
|
||||
$select.empty();
|
||||
|
||||
data.forEach(function(stock) {
|
||||
$select.append($('<option>', {
|
||||
value: stock.symbol,
|
||||
text: stock.symbol + ' - ' + stock.name
|
||||
}));
|
||||
});
|
||||
|
||||
// 如果有保存的选中值,恢复它
|
||||
if (currentSymbol && data.some(stock => stock.symbol === currentSymbol)) {
|
||||
$select.val(currentSymbol);
|
||||
} else {
|
||||
// 设置默认值为平安银行
|
||||
$select.val('000001');
|
||||
}
|
||||
const $select = $('#astockSymbol');
|
||||
const currentSymbol = $select.val();
|
||||
$select.prop('disabled', true);
|
||||
$.get('/api/a_stocks', function(data) {
|
||||
$select.prop('disabled', false);
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('加载A股股票列表失败: 返回非数组', data);
|
||||
return;
|
||||
}
|
||||
}).fail(function() {
|
||||
console.error('加载A股股票列表失败');
|
||||
$select.empty();
|
||||
data.forEach(function(stock) {
|
||||
$select.append($('<option>', {
|
||||
value: stock.symbol,
|
||||
text: stock.symbol + ' - ' + (stock.name || '')
|
||||
}));
|
||||
});
|
||||
if (currentSymbol && data.some(stock => stock.symbol === currentSymbol)) {
|
||||
$select.val(currentSymbol);
|
||||
} else {
|
||||
$select.val('000001');
|
||||
}
|
||||
}).fail(function(xhr) {
|
||||
$select.prop('disabled', false);
|
||||
console.error('加载A股股票列表失败', xhr && xhr.status);
|
||||
});
|
||||
}
|
||||
// 检测交易对类型并返回相应的配置
|
||||
|
||||
Reference in New Issue
Block a user