diff --git a/ChanLun.py b/ChanLun.py
index dd23fee..35154b7 100644
--- a/ChanLun.py
+++ b/ChanLun.py
@@ -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:
diff --git a/TF_DF.py b/TF_DF.py
index 95d8c47..a2bbb70 100644
--- a/TF_DF.py
+++ b/TF_DF.py
@@ -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)
diff --git a/bsp_monitor/engine.py b/bsp_monitor/engine.py
index aae1f74..be01e6a 100644
--- a/bsp_monitor/engine.py
+++ b/bsp_monitor/engine.py
@@ -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(
diff --git a/strategies/ChanLun_BTC_1m_old.py b/strategies/ChanLun_BTC_1m_old.py
index 40f0d69..7da8c53 100644
--- a/strategies/ChanLun_BTC_1m_old.py
+++ b/strategies/ChanLun_BTC_1m_old.py
@@ -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):
"""
交易核心(缠论):
- 仅在缠论一/二/三类买卖点出现时交易。
diff --git a/web/app.py b/web/app.py
index 9915639..252e380 100644
--- a/web/app.py
+++ b/web/app.py
@@ -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():
"""分析接口"""
diff --git a/web/cn_stock_data.py b/web/cn_stock_data.py
index 276be9b..449306e 100644
--- a/web/cn_stock_data.py
+++ b/web/cn_stock_data.py
@@ -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 = []
diff --git a/web/templates/index.html b/web/templates/index.html
index 4f3cc84..fe8cca4 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -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($('