From 9b876c45ed09323050ac98cd9ccadba178114a9d Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Wed, 20 May 2026 00:49:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86bsp=20state=EF=BC=8C?= =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChanLun.py | 2 - TF_DF.py | 47 +----- bsp_monitor/engine.py | 28 ++-- strategies/ChanLun_BTC_1m_old.py | 2 +- web/app.py | 103 +++++++++---- web/cn_stock_data.py | 247 ++++++++++++++++++++++++++++--- web/templates/index.html | 162 +++++++++++++------- 7 files changed, 430 insertions(+), 161 deletions(-) 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($('