|
67 | 67 | from tqsdk.objs import Quote, TradingStatus, Kline, Tick, Account, Position, Order, Trade, RiskManagementRule, RiskManagementData |
68 | 68 | from tqsdk.objs import SecurityAccount, SecurityOrder, SecurityTrade, SecurityPosition |
69 | 69 | from tqsdk.objs_not_entity import QuoteList, TqDataFrame, TqSymbolDataFrame, SymbolList, SymbolLevelList, \ |
70 | | - TqSymbolRankingDataFrame, TqOptionGreeksDataFrame, TqMdSettlementDataFrame |
| 70 | + TqSymbolRankingDataFrame, TqOptionGreeksDataFrame, TqMdSettlementDataFrame, TqEdbIndexDataFrame |
71 | 71 | from tqsdk.risk_manager import TqRiskManager |
72 | 72 | from tqsdk.risk_rule import TqRiskRule |
73 | 73 | from tqsdk.ins_schema import ins_schema, basic, derivative, future, option |
@@ -251,6 +251,7 @@ def __init__(self, account: Optional[Union[TqMultiAccount, UnionTradeable]] = No |
251 | 251 | "quotes": set(), |
252 | 252 | "klines": {}, |
253 | 253 | "ticks": {}, |
| 254 | + "edb": {}, # 记录已发出的 EDB 请求(key: 参数元组, value: DataFrame 对象),用于请求去重/复用 |
254 | 255 | } # 记录已发出的请求 |
255 | 256 | self._serials = {} # 记录所有数据序列 |
256 | 257 | # 记录所有(若有多个serial 则仅data_length不同, right_id相同)合约、周期相同的多合约K线中最大的更新数据范围 |
@@ -2375,6 +2376,167 @@ def query_symbol_settlement(self, symbol: Union[str, List[str]], days: int = 1, |
2375 | 2376 | raise TqTimeoutError("获取每日结算价信息超时,请检查客户端及网络是否正常") |
2376 | 2377 | return df |
2377 | 2378 |
|
| 2379 | + def query_edb_data(self, ids: List[int], n: int = 1, align: Optional[str] = None, fill: Optional[str] = None) -> TqEdbIndexDataFrame: |
| 2380 | + """ |
| 2381 | + 查询非量价指标数据 |
| 2382 | +
|
| 2383 | + .. warning:: |
| 2384 | + 这是一个实验性接口(**unstable**):EDB 后端的 id/表结构/数据口径可能变更, |
| 2385 | + 本接口的参数与返回结构未来可能调整,届时可能不兼容旧代码。 |
| 2386 | +
|
| 2387 | + EDB 指标数据说明: |
| 2388 | + - EDB 指标数据网站入口:`https://edb.shinnytech.com` |
| 2389 | + - 指标 id 列表、指标含义、以及数据可视化展示,可在上述网站中在线查询与查看 |
| 2390 | +
|
| 2391 | + Args: |
| 2392 | + ids (list[int]): [必填] 指标 id 列表,长度 1~100(会去重并保持请求顺序) |
| 2393 | + n (int): [可选] 时间窗口长度(单位:自然日,闭区间),默认 1。 |
| 2394 | + - end 使用当前 api 时间(回测模式下为回测推进的当前时间对应日期) |
| 2395 | + - start = end - (n - 1) 天 |
| 2396 | + align (str/None): [可选] 对齐方式: |
| 2397 | + - None [默认]:仅返回实际有值的日期(稀疏),不补齐缺失日期 |
| 2398 | + - "day":在 [start, end] 内按自然日补齐日期 |
| 2399 | + fill (str): [可选] 填充方式(仅在 align="day" 时生效): |
| 2400 | + - None [默认]:不填充,缺失为 NaN |
| 2401 | + - "ffill":用前一个已知值向后填充 |
| 2402 | + - "bfill":用后一个已知值向前填充 |
| 2403 | +
|
| 2404 | + Returns: |
| 2405 | + pandas.DataFrame: 本函数返回 pandas.DataFrame 实例。返回值不会再更新。包含以下结构说明: |
| 2406 | +
|
| 2407 | + * index: 日期字符串,格式为 YYYY-MM-DD |
| 2408 | + * align=None 时:仅包含实际有值的日期(稀疏),不补齐缺失日期 |
| 2409 | + * align="day" 时:包含 [start, end] 内的所有自然日(补齐缺失日期) |
| 2410 | + * columns: 指标 id(会对 ids 去重并保持请求顺序;最终以服务端返回为准) |
| 2411 | + * values: 指标取值 |
| 2412 | + * 缺失值为 NaN |
| 2413 | + * align="day" 且 fill="ffill"/"bfill" 时,会对缺失值进行前向/后向填充 |
| 2414 | +
|
| 2415 | + Example (实盘):: |
| 2416 | +
|
| 2417 | + from tqsdk import TqApi, TqAuth |
| 2418 | +
|
| 2419 | + api = TqApi(auth=TqAuth("快期账号", "快期密码")) |
| 2420 | + try: |
| 2421 | + # Case 1: 最小调用(只传 ids,其余用默认) |
| 2422 | + # - n 默认为 1:窗口为 [end, end] |
| 2423 | + # - align=None:稀疏返回,不补齐日期 |
| 2424 | + df1 = api.query_edb_data(ids=[472]) |
| 2425 | +
|
| 2426 | + # Case 2: ids 去重并保持顺序(重复的 472 会被去掉) |
| 2427 | + df2 = api.query_edb_data(ids=[472, 497, 472]) |
| 2428 | +
|
| 2429 | + # Case 3: 指定 n(自然日,闭区间) |
| 2430 | + # - end 为 api 当前日期(回测模式下为回测推进到的当前日期) |
| 2431 | + # - start = end - (n-1) |
| 2432 | + df3 = api.query_edb_data(ids=[472, 497], n=10) |
| 2433 | +
|
| 2434 | + # Case 4: align=None(稀疏,不补齐),fill 会被忽略 |
| 2435 | + df4 = api.query_edb_data(ids=[472, 497, 10350], n=10, align=None, fill=None) |
| 2436 | +
|
| 2437 | + # Case 5: align="day"(自然日补齐),不填充缺失(NaN) |
| 2438 | + df5 = api.query_edb_data(ids=[472, 497, 10350], n=10, align="day", fill=None) |
| 2439 | +
|
| 2440 | + # Case 6: align="day" + fill="ffill"(前值填充) |
| 2441 | + df6 = api.query_edb_data(ids=[472, 497, 10350], n=10, align="day", fill="ffill") |
| 2442 | +
|
| 2443 | + # Case 7: align="day" + fill="bfill"(后值填充) |
| 2444 | + df7 = api.query_edb_data(ids=[472, 497, 10350], n=10, align="day", fill="bfill") |
| 2445 | +
|
| 2446 | + print(df1.to_string()) |
| 2447 | + print(df2.to_string()) |
| 2448 | + print(df3.to_string()) |
| 2449 | + print(df4.to_string()) |
| 2450 | + print(df5.to_string()) |
| 2451 | + print(df6.to_string()) |
| 2452 | + print(df7.to_string()) |
| 2453 | + finally: |
| 2454 | + api.close() |
| 2455 | +
|
| 2456 | + Example (回测):: |
| 2457 | +
|
| 2458 | + from datetime import datetime |
| 2459 | + from tqsdk import TqApi, TqAuth, TqBacktest |
| 2460 | +
|
| 2461 | + api = TqApi( |
| 2462 | + auth=TqAuth("快期账号", "快期密码"), |
| 2463 | + backtest=TqBacktest(start_dt=datetime(2025, 10, 1), end_dt=datetime(2026, 1, 12)), |
| 2464 | + ) |
| 2465 | + klines = api.get_kline_serial(symbol="SHFE.ni2512", duration_seconds=86400, data_length=10) |
| 2466 | + try: |
| 2467 | + # 初次查询:以当前回测推进到的日期作为 end |
| 2468 | + edb = api.query_edb_data(ids=[472, 497, 10350], n=10, align="day", fill="ffill") |
| 2469 | + print(edb.to_string()) |
| 2470 | + # 之后随着回测时间推进,end 会变化 |
| 2471 | + while True: |
| 2472 | + api.wait_update() |
| 2473 | + if api.is_changing(klines.iloc[-1], "datetime"): |
| 2474 | + edb = api.query_edb_data(ids=[472, 497, 10350], n=10, align="day", fill="ffill") |
| 2475 | + print(edb.to_string()) |
| 2476 | + finally: |
| 2477 | + api.close() |
| 2478 | + """ |
| 2479 | + if not isinstance(ids, list) or len(ids) == 0: |
| 2480 | + raise Exception("ids 不能为空。") |
| 2481 | + if not isinstance(n, int) or n < 1: |
| 2482 | + raise Exception(f"n 参数 {n} 错误,应为 >= 1 的整数。") |
| 2483 | + if align not in (None, "day"): |
| 2484 | + raise Exception(f"align 参数 {align} 错误,仅支持 None 或 'day'") |
| 2485 | + if fill not in (None, "ffill", "bfill"): |
| 2486 | + raise Exception(f"fill 参数 {fill} 错误,仅支持 None/'ffill'/'bfill'") |
| 2487 | + |
| 2488 | + # 以 api 当前时间计算窗口(回测模式下会随回测推进) |
| 2489 | + now_dt = self._get_current_datetime() |
| 2490 | + # backtest 初始化早期 current_dt 可能为 0,兜底到 backtest start_dt |
| 2491 | + if isinstance(self._backtest, TqBacktest) and getattr(now_dt, "year", 1970) <= 1970: |
| 2492 | + now_dt = _timestamp_nano_to_datetime(self._backtest._start_dt) |
| 2493 | + end_date = now_dt.date() |
| 2494 | + start_date = end_date - timedelta(days=n - 1) |
| 2495 | + start_s = start_date.strftime("%Y-%m-%d") |
| 2496 | + end_s = end_date.strftime("%Y-%m-%d") |
| 2497 | + |
| 2498 | + # ids 归一化:去重保持顺序 |
| 2499 | + seen = set() |
| 2500 | + norm_ids = [] |
| 2501 | + for x in ids: |
| 2502 | + if not isinstance(x, int): |
| 2503 | + raise Exception(f"ids 中包含非法 id: {x} (仅支持 int)") |
| 2504 | + v = x |
| 2505 | + if v not in seen: |
| 2506 | + seen.add(v) |
| 2507 | + norm_ids.append(v) |
| 2508 | + if len(norm_ids) > 100: |
| 2509 | + raise Exception("ids 数量超过限制(<=100)") |
| 2510 | + |
| 2511 | + edb_cache = self._requests["edb"] |
| 2512 | + |
| 2513 | + # 统一策略(实盘/回测): |
| 2514 | + # - api._requests 只缓存“原始数据”(HTTP 拉取得到的稀疏日期数据),缓存 key 不包含 align/fill,避免重复下载 |
| 2515 | + # - 返回给用户时再按窗口切片,并做 align/fill(回测下先切片再 fill,避免引入未来数据) |
| 2516 | + if isinstance(self._backtest, TqBacktest): |
| 2517 | + bt_start_d = _timestamp_nano_to_datetime(self._backtest._start_dt).date() |
| 2518 | + bt_end_d = _timestamp_nano_to_datetime(self._backtest._end_dt).date() |
| 2519 | + raw_start_s = (bt_start_d - timedelta(days=n - 1)).strftime("%Y-%m-%d") |
| 2520 | + raw_end_s = bt_end_d.strftime("%Y-%m-%d") |
| 2521 | + else: |
| 2522 | + raw_start_s = start_s |
| 2523 | + raw_end_s = end_s |
| 2524 | + |
| 2525 | + raw_request = (tuple(norm_ids), raw_start_s, raw_end_s) |
| 2526 | + raw_df = edb_cache.get(raw_request, None) |
| 2527 | + if raw_df is None: |
| 2528 | + raw_df = TqEdbIndexDataFrame(self, ids=norm_ids, start=raw_start_s, end=raw_end_s, |
| 2529 | + align=None, fill=None) |
| 2530 | + edb_cache[raw_request] = raw_df |
| 2531 | + |
| 2532 | + # 每次调用返回窗口 df(不缓存窗口对象:同一个 raw 可派生出多种 align/fill) |
| 2533 | + df = TqEdbIndexDataFrame(self, ids=norm_ids, start=start_s, end=end_s, align=align, fill=fill, raw_df=raw_df) |
| 2534 | + deadline = time.time() + 30 |
| 2535 | + while not self._loop.is_running() and not df.__dict__["_task"].done(): |
| 2536 | + if not self.wait_update(deadline=deadline, _task=df.__dict__["_task"]): |
| 2537 | + raise TqTimeoutError("获取 EDB 指标取值超时,请检查客户端及网络是否正常") |
| 2538 | + return df |
| 2539 | + |
2378 | 2540 | def query_symbol_ranking(self, symbol: str, ranking_type: str, days: int = 1, start_dt: date = None, broker: str = None)\ |
2379 | 2541 | -> TqSymbolRankingDataFrame: |
2380 | 2542 | """ |
|
0 commit comments