diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml
index 6adeb5342..26aa810be 100644
--- a/.github/workflows/pythonpackage.yml
+++ b/.github/workflows/pythonpackage.yml
@@ -5,7 +5,7 @@ name: Python package
on:
push:
- branches: [ master, V0.9.53 ]
+ branches: [ master, V0.9.54 ]
pull_request:
branches: [ master ]
diff --git a/czsc/__init__.py b/czsc/__init__.py
index 9e0bd53f0..066b7917f 100644
--- a/czsc/__init__.py
+++ b/czsc/__init__.py
@@ -146,6 +146,7 @@
show_holds_backtest,
show_symbols_corr,
show_feature_returns,
+ show_czsc_trader,
)
from czsc.utils.bi_info import (
@@ -186,10 +187,10 @@
)
-__version__ = "0.9.53"
+__version__ = "0.9.54"
__author__ = "zengbin93"
__email__ = "zeng_bin8888@163.com"
-__date__ = "20240607"
+__date__ = "20240616"
def welcome():
diff --git a/czsc/connectors/qmt_connector.py b/czsc/connectors/qmt_connector.py
index 582398cc6..482cba81e 100644
--- a/czsc/connectors/qmt_connector.py
+++ b/czsc/connectors/qmt_connector.py
@@ -4,11 +4,17 @@
email: zeng_bin8888@163.com
create_dt: 2022/12/31 16:03
describe: QMT 量化交易平台接口
+
+需要额外安装:pyautogui,xtquant
+
+xtquant 的下载地址:http://dict.thinktrader.net/nativeApi/download_xtquant.html?id=7zqjlm
"""
import os
import time
import random
import czsc
+import pyautogui
+import subprocess
import pandas as pd
from typing import List
from tqdm import tqdm
@@ -26,6 +32,90 @@
dt_fmt = "%Y-%m-%d %H:%M:%S"
+def find_exe_window(title):
+ """windows系统:根据 title 查找 window"""
+ windows = pyautogui.getWindowsWithTitle(title)
+ if len(windows) > 1:
+ logger.warning(f"找到多个 {title} 窗口,数量:{len(windows)};请检查是否有多个程序实例")
+ return None
+
+ if len(windows) == 0:
+ return None
+
+ return windows[0]
+
+
+def close_exe_window(title):
+ """windows系统:关闭 exe 应用程序
+
+ :param title: 程序标题,支持模糊匹配,不需要完全匹配
+
+ 查看应用的标题(Title)通常指的是获取正在运行的应用程序窗口的标题栏上的文字。
+ 在Windows操作系统中,你可以通过几种不同的方法来查看应用的标题:
+
+ 1. 使用任务管理器
+ 1. 按下 Ctrl + Shift + Esc 打开任务管理器。
+ 2. 在“进程”标签页中,找到你想要查看标题的应用程序。
+ 3. 在“应用程序”列表中,找到对应的应用程序,其标题通常显示在“名称”列中。
+
+ 2. 使用Alt+Tab快捷键
+ 1. 按下 Alt + Tab 快捷键,可以快速切换到正在运行的应用程序。
+ 2. 在弹出的窗口中,你可以看到每个应用程序的缩略图和标题。
+
+ 3. 使用任务栏
+ 1. 将鼠标悬停在任务栏上的应用程序图标上。
+ 2. 通常,任务栏会显示该应用程序的标题。
+ """
+ window = find_exe_window(title)
+ if window:
+ window.activate()
+ window.close()
+ pyautogui.press("enter")
+ else:
+ logger.error(f"没有找到 {title} 程序")
+
+
+def start_qmt_exe(acc, pwd, qmt_exe, title, max_retry=6, **kwargs):
+ """windows系统:启动 QMT,并登录
+
+ :param acc: QMT 账号
+ :param pwd: QMT 密码
+ :param qmt_exe: QMT 应用路径,如 D:\\国金证券QMT交易端\\bin.x64\\XtItClient.exe
+ :param title: QMT 窗口标题,如 国金证券QMT交易端
+ :param max_retry: 最大重试次数
+ """
+ wait_seconds = kwargs.get("wait_seconds", 6)
+ i = 0
+ while not find_exe_window(acc):
+ if i > max_retry:
+ logger.warning(f"QMT连续{i}次尝试依旧无法启动,请人工检查!")
+ break
+
+ close_exe_window(title)
+
+ i += 1
+ try:
+ subprocess.Popen(qmt_exe)
+ time.sleep(wait_seconds)
+
+ qmt_windows = find_exe_window(title)
+ if not qmt_windows:
+ continue
+
+ qmt_windows.activate()
+ pyautogui.typewrite(acc)
+ pyautogui.press("tab")
+ pyautogui.typewrite(pwd)
+ pyautogui.press("enter")
+ qmt_windows.activate()
+ time.sleep(wait_seconds)
+
+ except Exception as e:
+ logger.exception(f"启动QMT失败:{e}")
+
+ logger.info(f"启动 QMT 成功,账号:{acc}")
+
+
def format_stock_kline(kline: pd.DataFrame, freq: Freq) -> List[RawBar]:
"""QMT A股市场K线数据转换
@@ -46,22 +136,29 @@ def format_stock_kline(kline: pd.DataFrame, freq: Freq) -> List[RawBar]:
:return: 转换好的K线数据
"""
bars = []
- dt_key = 'time'
+ dt_key = "time"
kline = kline.sort_values(dt_key, ascending=True, ignore_index=True)
- records = kline.to_dict('records')
+ records = kline.to_dict("records")
for i, record in enumerate(records):
# 将每一根K线转换成 RawBar 对象
- bar = RawBar(symbol=record['symbol'], dt=pd.to_datetime(record[dt_key]), id=i, freq=freq,
- open=record['open'], close=record['close'], high=record['high'], low=record['low'],
- vol=record['volume'] * 100 if record['volume'] else 0, # 成交量,单位:股
- amount=record['amount'] if record['amount'] > 0 else 0, # 成交额,单位:元
- )
+ bar = RawBar(
+ symbol=record["symbol"],
+ dt=pd.to_datetime(record[dt_key]),
+ id=i,
+ freq=freq,
+ open=record["open"],
+ close=record["close"],
+ high=record["high"],
+ low=record["low"],
+ vol=record["volume"] * 100 if record["volume"] else 0, # 成交量,单位:股
+ amount=record["amount"] if record["amount"] > 0 else 0, # 成交额,单位:元
+ )
bars.append(bar)
return bars
-def get_kline(symbol, period, start_time, end_time, count=-1, dividend_type='front_ratio', **kwargs):
+def get_kline(symbol, period, start_time, end_time, count=-1, dividend_type="front_ratio", **kwargs):
"""获取 QMT K线数据,实盘、回测通用
:param symbol: 股票代码 例如:300001.SZ
@@ -85,24 +182,31 @@ def get_kline(symbol, period, start_time, end_time, count=-1, dividend_type='fro
3 000001.SZ
4 000001.SZ
"""
- start_time = pd.to_datetime(start_time).strftime('%Y%m%d%H%M%S')
- if '1d' == period:
- end_time = pd.to_datetime(end_time).replace(hour=15, minute=0).strftime('%Y%m%d%H%M%S')
+ start_time = pd.to_datetime(start_time).strftime("%Y%m%d%H%M%S")
+ if "1d" == period:
+ end_time = pd.to_datetime(end_time).replace(hour=15, minute=0).strftime("%Y%m%d%H%M%S")
else:
- end_time = pd.to_datetime(end_time).strftime('%Y%m%d%H%M%S')
+ end_time = pd.to_datetime(end_time).strftime("%Y%m%d%H%M%S")
if kwargs.get("download_hist", True):
xtdata.download_history_data(symbol, period=period, start_time=start_time, end_time=end_time)
- field_list = ['time', 'open', 'high', 'low', 'close', 'volume', 'amount']
- data = xtdata.get_market_data(field_list, stock_list=[symbol], period=period, count=count,
- dividend_type=dividend_type, start_time=start_time,
- end_time=end_time, fill_data=kwargs.get("fill_data", False))
+ field_list = ["time", "open", "high", "low", "close", "volume", "amount"]
+ data = xtdata.get_market_data(
+ field_list,
+ stock_list=[symbol],
+ period=period,
+ count=count,
+ dividend_type=dividend_type,
+ start_time=start_time,
+ end_time=end_time,
+ fill_data=kwargs.get("fill_data", False),
+ )
df = pd.DataFrame({key: value.values[0] for key, value in data.items()})
- df['time'] = pd.to_datetime(df['time'], unit='ms') + pd.to_timedelta('8H')
+ df["time"] = pd.to_datetime(df["time"], unit="ms") + pd.to_timedelta("8H")
df.reset_index(inplace=True, drop=True)
- df['symbol'] = symbol
+ df["symbol"] = symbol
df = df.dropna()
if kwargs.get("df", True):
@@ -112,7 +216,7 @@ def get_kline(symbol, period, start_time, end_time, count=-1, dividend_type='fro
return format_stock_kline(df, freq=freq_map[period])
-def get_raw_bars(symbol, freq, sdt, edt, fq='前复权', **kwargs) -> List[RawBar]:
+def get_raw_bars(symbol, freq, sdt, edt, fq="前复权", **kwargs) -> List[RawBar]:
"""获取 CZSC 库定义的标准 RawBar 对象列表
:param symbol: 标的代码
@@ -125,27 +229,28 @@ def get_raw_bars(symbol, freq, sdt, edt, fq='前复权', **kwargs) -> List[RawBa
"""
freq = Freq(freq)
if freq == Freq.F1:
- period = '1m'
+ period = "1m"
elif freq in [Freq.F5, Freq.F15, Freq.F30, Freq.F60]:
- period = '5m'
+ period = "5m"
else:
- period = '1d'
+ period = "1d"
- if fq == '前复权':
- dividend_type = 'front_ratio'
- elif fq == '后复权':
- dividend_type = 'back_ratio'
+ if fq == "前复权":
+ dividend_type = "front_ratio"
+ elif fq == "后复权":
+ dividend_type = "back_ratio"
else:
- assert fq == '不复权'
- dividend_type = 'none'
+ assert fq == "不复权"
+ dividend_type = "none"
- kline = get_kline(symbol, period, sdt, edt, dividend_type=dividend_type,
- download_hist=kwargs.get("download_hist", True), df=True)
+ kline = get_kline(
+ symbol, period, sdt, edt, dividend_type=dividend_type, download_hist=kwargs.get("download_hist", True), df=True
+ )
if kline.empty:
return []
- kline['dt'] = pd.to_datetime(kline['time'])
- kline['vol'] = kline['volume']
+ kline["dt"] = pd.to_datetime(kline["time"])
+ kline["vol"] = kline["volume"]
bars = resample_bars(kline, freq, raw_bars=True)
return bars
@@ -156,21 +261,48 @@ def get_symbols(step):
:param step: 投研阶段
:return: 标的列表
"""
- stocks = xtdata.get_stock_list_in_sector('沪深A股')
+ stocks = xtdata.get_stock_list_in_sector("沪深A股")
stocks_map = {
- "index": ['000905.SH', '000016.SH', '000300.SH', '000001.SH', '000852.SH',
- '399001.SZ', '399006.SZ', '399376.SZ', '399377.SZ', '399317.SZ', '399303.SZ'],
+ "index": [
+ "000905.SH",
+ "000016.SH",
+ "000300.SH",
+ "000001.SH",
+ "000852.SH",
+ "399001.SZ",
+ "399006.SZ",
+ "399376.SZ",
+ "399377.SZ",
+ "399317.SZ",
+ "399303.SZ",
+ ],
"stock": stocks,
- "check": ['000001.SZ'],
+ "check": ["000001.SZ"],
"train": stocks[:200],
"valid": stocks[200:600],
- "etfs": ['512880.SH', '518880.SH', '515880.SH', '513050.SH', '512690.SH',
- '512660.SH', '512400.SH', '512010.SH', '512000.SH', '510900.SH',
- '510300.SH', '510500.SH', '510050.SH', '159992.SZ', '159985.SZ',
- '159981.SZ', '159949.SZ', '159915.SZ'],
+ "etfs": [
+ "512880.SH",
+ "518880.SH",
+ "515880.SH",
+ "513050.SH",
+ "512690.SH",
+ "512660.SH",
+ "512400.SH",
+ "512010.SH",
+ "512000.SH",
+ "510900.SH",
+ "510300.SH",
+ "510500.SH",
+ "510050.SH",
+ "159992.SZ",
+ "159985.SZ",
+ "159981.SZ",
+ "159949.SZ",
+ "159915.SZ",
+ ],
}
- if step.upper() == 'ALL':
- return stocks_map['index'] + stocks_map['stock'] + stocks_map['etfs']
+ if step.upper() == "ALL":
+ return stocks_map["index"] + stocks_map["stock"] + stocks_map["etfs"]
return stocks_map[step]
@@ -186,8 +318,8 @@ def is_trade_time(dt: datetime = datetime.now()):
def is_trade_day(dt: datetime = datetime.now()):
"""判断指定日期是否是交易日"""
- date = dt.strftime('%Y%m%d')
- return True if xtdata.get_trading_dates('SH', date, date) else False
+ date = dt.strftime("%Y%m%d")
+ return True if xtdata.get_trading_dates("SH", date, date) else False
class TraderCallback(XtQuantTraderCallback):
@@ -197,32 +329,32 @@ def __init__(self, **kwargs):
super(TraderCallback, self).__init__()
self.kwargs = kwargs
- if kwargs.get('feishu_app_id', None) and kwargs.get('feishu_app_secret', None):
- self.im = IM(app_id=kwargs['feishu_app_id'], app_secret=kwargs['feishu_app_secret'])
- self.members = kwargs['feishu_members']
+ if kwargs.get("feishu_app_id", None) and kwargs.get("feishu_app_secret", None):
+ self.im = IM(app_id=kwargs["feishu_app_id"], app_secret=kwargs["feishu_app_secret"])
+ self.members = kwargs["feishu_members"]
else:
self.im = None
self.members = None
# 推送模式:detail-详细模式,summary-汇总模式
- self.feishu_push_mode = kwargs.get('feishu_push_mode', 'detail')
+ self.feishu_push_mode = kwargs.get("feishu_push_mode", "detail")
- file_log = kwargs.get('file_log', None)
+ file_log = kwargs.get("file_log", None)
if file_log:
- logger.add(file_log, rotation='1 day', encoding='utf-8', enqueue=True)
+ logger.add(file_log, rotation="1 day", encoding="utf-8", enqueue=True)
self.file_log = file_log
logger.info(f"TraderCallback init: {kwargs}")
- def push_message(self, msg: str, msg_type='text'):
+ def push_message(self, msg: str, msg_type="text"):
"""批量推送消息"""
if self.im and self.members:
for member in self.members:
try:
- if msg_type == 'text':
+ if msg_type == "text":
self.im.send_text(msg, member)
- elif msg_type == 'image':
+ elif msg_type == "image":
self.im.send_image(msg, member)
- elif msg_type == 'file':
+ elif msg_type == "file":
self.im.send_file(msg, member)
else:
logger.error(f"不支持的消息类型:{msg_type}")
@@ -242,112 +374,132 @@ def on_stock_order(self, order):
http://docs.thinktrader.net/pages/198696/#%E5%A7%94%E6%89%98xtorder
http://docs.thinktrader.net/pages/198696/#%E5%A7%94%E6%89%98%E7%8A%B6%E6%80%81-order-status
"""
- order_status_map = {xtconstant.ORDER_UNREPORTED: '未报', xtconstant.ORDER_WAIT_REPORTING: '待报',
- xtconstant.ORDER_REPORTED: '已报', xtconstant.ORDER_REPORTED_CANCEL: '已报待撤',
- xtconstant.ORDER_PARTSUCC_CANCEL: '部成待撤', xtconstant.ORDER_PART_CANCEL: '部撤',
- xtconstant.ORDER_CANCELED: '已撤', xtconstant.ORDER_PART_SUCC: '部成',
- xtconstant.ORDER_SUCCEEDED: '已成', xtconstant.ORDER_JUNK: '废单',
- xtconstant.ORDER_UNKNOWN: '未知',
- }
-
- msg = f"委托回报通知:\n{'*' * 31}\n" \
- f"时间:{datetime.now().strftime(dt_fmt)}\n" \
- f"账户:{order.account_id}\n" \
- f"标的:{order.stock_code}\n" \
- f"方向:{'做多' if order.order_type == 23 else '平多'}\n" \
- f"数量:{int(order.order_volume)}\n" \
- f"价格:{order.price}\n" \
- f"状态:{order_status_map[order.order_status]}"
+ order_status_map = {
+ xtconstant.ORDER_UNREPORTED: "未报",
+ xtconstant.ORDER_WAIT_REPORTING: "待报",
+ xtconstant.ORDER_REPORTED: "已报",
+ xtconstant.ORDER_REPORTED_CANCEL: "已报待撤",
+ xtconstant.ORDER_PARTSUCC_CANCEL: "部成待撤",
+ xtconstant.ORDER_PART_CANCEL: "部撤",
+ xtconstant.ORDER_CANCELED: "已撤",
+ xtconstant.ORDER_PART_SUCC: "部成",
+ xtconstant.ORDER_SUCCEEDED: "已成",
+ xtconstant.ORDER_JUNK: "废单",
+ xtconstant.ORDER_UNKNOWN: "未知",
+ }
+
+ msg = (
+ f"委托回报通知:\n{'*' * 31}\n"
+ f"时间:{datetime.now().strftime(dt_fmt)}\n"
+ f"账户:{order.account_id}\n"
+ f"标的:{order.stock_code}\n"
+ f"方向:{'做多' if order.order_type == 23 else '平多'}\n"
+ f"数量:{int(order.order_volume)}\n"
+ f"价格:{order.price}\n"
+ f"状态:{order_status_map[order.order_status]}"
+ )
logger.info(f"on order callback: {msg}")
- if self.feishu_push_mode == 'detail' and is_trade_time():
- self.push_message(msg, msg_type='text')
+ if self.feishu_push_mode == "detail" and is_trade_time():
+ self.push_message(msg, msg_type="text")
def on_stock_asset(self, asset):
"""资金变动推送
:param asset: XtAsset对象
"""
- msg = f"资金变动通知: \n{'*' * 31}\n" \
- f"时间:{datetime.now().strftime(dt_fmt)}\n" \
- f"账户: {asset.account_id} \n" \
- f"可用资金:{asset.cash} \n" \
- f"总资产:{asset.total_asset}"
+ msg = (
+ f"资金变动通知: \n{'*' * 31}\n"
+ f"时间:{datetime.now().strftime(dt_fmt)}\n"
+ f"账户: {asset.account_id} \n"
+ f"可用资金:{asset.cash} \n"
+ f"总资产:{asset.total_asset}"
+ )
logger.info(f"on asset callback: {msg}")
- if self.feishu_push_mode == 'detail' and is_trade_time():
- self.push_message(msg, msg_type='text')
+ if self.feishu_push_mode == "detail" and is_trade_time():
+ self.push_message(msg, msg_type="text")
def on_stock_trade(self, trade):
"""成交变动推送
:param trade: XtTrade对象
"""
- msg = f"成交变动通知:\n{'*' * 31}\n" \
- f"时间:{datetime.now().strftime(dt_fmt)}\n" \
- f"账户:{trade.account_id}\n" \
- f"标的:{trade.stock_code}\n" \
- f"方向:{'开多' if trade.order_type == 23 else '平多'}\n" \
- f"成交量:{int(trade.traded_volume)}\n" \
- f"成交价:{round(trade.traded_price, 2)}"
+ msg = (
+ f"成交变动通知:\n{'*' * 31}\n"
+ f"时间:{datetime.now().strftime(dt_fmt)}\n"
+ f"账户:{trade.account_id}\n"
+ f"标的:{trade.stock_code}\n"
+ f"方向:{'开多' if trade.order_type == 23 else '平多'}\n"
+ f"成交量:{int(trade.traded_volume)}\n"
+ f"成交价:{round(trade.traded_price, 2)}"
+ )
logger.info(f"on trade callback: {msg}")
- if self.feishu_push_mode == 'detail' and is_trade_time():
- self.push_message(msg, msg_type='text')
+ if self.feishu_push_mode == "detail" and is_trade_time():
+ self.push_message(msg, msg_type="text")
def on_stock_position(self, position):
"""持仓变动推送
:param position: XtPosition对象
"""
- msg = f"持仓变动通知: \n{'*' * 31}\n" \
- f"时间:{datetime.now().strftime(dt_fmt)}\n" \
- f"账户:{position.account_id}\n" \
- f"标的:{position.stock_code}\n" \
- f"成交量:{position.volume}"
+ msg = (
+ f"持仓变动通知: \n{'*' * 31}\n"
+ f"时间:{datetime.now().strftime(dt_fmt)}\n"
+ f"账户:{position.account_id}\n"
+ f"标的:{position.stock_code}\n"
+ f"成交量:{position.volume}"
+ )
logger.info(f"on position callback: {msg}")
- if self.feishu_push_mode == 'detail' and is_trade_time():
- self.push_message(msg, msg_type='text')
+ if self.feishu_push_mode == "detail" and is_trade_time():
+ self.push_message(msg, msg_type="text")
def on_order_error(self, order_error):
"""委托失败推送
:param order_error:XtOrderError 对象
"""
- msg = f"委托失败通知: \n{'*' * 31}\n" \
- f"时间:{datetime.now().strftime(dt_fmt)}\n" \
- f"账户:{order_error.account_id}\n" \
- f"订单编号:{order_error.order_id}\n" \
- f"错误编码:{order_error.error_id}\n" \
- f"失败原因:{order_error.error_msg}"
+ msg = (
+ f"委托失败通知: \n{'*' * 31}\n"
+ f"时间:{datetime.now().strftime(dt_fmt)}\n"
+ f"账户:{order_error.account_id}\n"
+ f"订单编号:{order_error.order_id}\n"
+ f"错误编码:{order_error.error_id}\n"
+ f"失败原因:{order_error.error_msg}"
+ )
logger.info(f"on order_error callback: {msg}")
if is_trade_time():
- self.push_message(msg, msg_type='text')
+ self.push_message(msg, msg_type="text")
def on_cancel_error(self, cancel_error):
"""撤单失败推送
:param cancel_error: XtCancelError 对象
"""
- msg = f"撤单失败通知: \n{'*' * 31}\n" \
- f"时间:{datetime.now().strftime(dt_fmt)}\n" \
- f"账户:{cancel_error.account_id}\n" \
- f"订单编号:{cancel_error.order_id}\n" \
- f"错误编码:{cancel_error.error_id}\n" \
- f"失败原因:{cancel_error.error_msg}"
+ msg = (
+ f"撤单失败通知: \n{'*' * 31}\n"
+ f"时间:{datetime.now().strftime(dt_fmt)}\n"
+ f"账户:{cancel_error.account_id}\n"
+ f"订单编号:{cancel_error.order_id}\n"
+ f"错误编码:{cancel_error.error_id}\n"
+ f"失败原因:{cancel_error.error_msg}"
+ )
logger.info(f"on_cancel_error: {msg}")
if is_trade_time():
- self.push_message(msg, msg_type='text')
+ self.push_message(msg, msg_type="text")
def on_order_stock_async_response(self, response):
"""异步下单回报推送
:param response: XtOrderResponse 对象
"""
- msg = f"异步下单回报推送: \n{'*' * 31}\n" \
- f"时间:{datetime.now().strftime(dt_fmt)}\n" \
- f"账户:{response.account_id}\n" \
- f"订单编号:{response.order_id}\n" \
- f"策略名称:{response.strategy_name}"
+ msg = (
+ f"异步下单回报推送: \n{'*' * 31}\n"
+ f"时间:{datetime.now().strftime(dt_fmt)}\n"
+ f"账户:{response.account_id}\n"
+ f"订单编号:{response.order_id}\n"
+ f"策略名称:{response.strategy_name}"
+ )
if is_trade_time():
- self.push_message(msg, msg_type='text')
+ self.push_message(msg, msg_type="text")
logger.info(f"on_order_stock_async_response: {msg}")
def on_account_status(self, status):
@@ -355,22 +507,28 @@ def on_account_status(self, status):
:param status: XtAccountStatus 对象
"""
- status_map = {xtconstant.ACCOUNT_STATUS_OK: '正常',
- xtconstant.ACCOUNT_STATUS_WAITING_LOGIN: '连接中',
- xtconstant.ACCOUNT_STATUSING: '登陆中',
- xtconstant.ACCOUNT_STATUS_FAIL: '失败'}
- msg = f"账户状态变化推送:\n{'*' * 31}\n" \
- f"时间:{datetime.now().strftime(dt_fmt)}\n" \
- f"账户ID:{status.account_id}\n" \
- f"账号类型:{'证券账户' if status.account_type == 2 else '其他'}\n" \
- f"账户状态:{status_map[status.status]}\n"
-
- logger.info(f"账户ID: {status.account_id} "
- f"账号类型:{'证券账户' if status.account_type == 2 else '其他'} "
- f"账户状态:{status_map[status.status]}")
+ status_map = {
+ xtconstant.ACCOUNT_STATUS_OK: "正常",
+ xtconstant.ACCOUNT_STATUS_WAITING_LOGIN: "连接中",
+ xtconstant.ACCOUNT_STATUSING: "登陆中",
+ xtconstant.ACCOUNT_STATUS_FAIL: "失败",
+ }
+ msg = (
+ f"账户状态变化推送:\n{'*' * 31}\n"
+ f"时间:{datetime.now().strftime(dt_fmt)}\n"
+ f"账户ID:{status.account_id}\n"
+ f"账号类型:{'证券账户' if status.account_type == 2 else '其他'}\n"
+ f"账户状态:{status_map[status.status]}\n"
+ )
+
+ logger.info(
+ f"账户ID: {status.account_id} "
+ f"账号类型:{'证券账户' if status.account_type == 2 else '其他'} "
+ f"账户状态:{status_map[status.status]}"
+ )
if is_trade_time():
- self.push_message(msg, msg_type='text')
+ self.push_message(msg, msg_type="text")
def query_stock_positions(xtt: XtQuantTrader, acc: StockAccount):
@@ -390,9 +548,17 @@ def query_today_trades(xtt: XtQuantTrader, acc: StockAccount):
http://docs.thinktrader.net/pages/198696/#%E6%88%90%E4%BA%A4xttrade
"""
trades = xtt.query_stock_trades(acc)
- res = [{'品种': x.stock_code, '均价': x.traded_price, "方向": "买入" if x.order_type == 23 else "卖出",
- '数量': x.traded_volume, '金额': x.traded_amount,
- '时间': time.strftime("%H:%M:%S", time.localtime(x.traded_time))} for x in trades]
+ res = [
+ {
+ "品种": x.stock_code,
+ "均价": x.traded_price,
+ "方向": "买入" if x.order_type == 23 else "卖出",
+ "数量": x.traded_volume,
+ "金额": x.traded_amount,
+ "时间": time.strftime("%H:%M:%S", time.localtime(x.traded_time)),
+ }
+ for x in trades
+ ]
return res
@@ -437,14 +603,14 @@ def is_allow_open(xtt: XtQuantTrader, acc: StockAccount, symbol, price, **kwargs
:param price: 股票现价
:return: True 允许开仓,False 不允许开仓
"""
- symbol_max_pos = kwargs.get('max_pos', 0) # 最大持仓数量
+ symbol_max_pos = kwargs.get("max_pos", 0) # 最大持仓数量
# 如果 symbol_max_pos 为 0,不允许开仓
if symbol_max_pos <= 0:
return False
# 如果 symbol 在禁止交易的列表中,不允许开仓
- if symbol in kwargs.get('forbidden_symbols', []):
+ if symbol in kwargs.get("forbidden_symbols", []):
return False
# 如果 未成交的开仓委托单 存在,不允许开仓
@@ -472,7 +638,7 @@ def is_allow_exit(xtt: XtQuantTrader, acc: StockAccount, symbol, **kwargs):
:return: True 允许开仓,False 不允许开仓
"""
# symbol 在禁止交易的列表中,不允许平仓
- if symbol in kwargs.get('forbidden_symbols', []):
+ if symbol in kwargs.get("forbidden_symbols", []):
return False
# 没有持仓 或 可用数量为 0,不允许平仓
@@ -507,21 +673,23 @@ def send_stock_order(xtt: XtQuantTrader, acc: StockAccount, **kwargs):
:return: 返回下单请求序号, 成功委托后的下单请求序号为大于0的正整数, 如果为-1表示委托失败
"""
- stock_code = kwargs.get('stock_code')
- order_type = kwargs.get('order_type')
- order_volume = kwargs.get('order_volume') # 委托数量, 股票以'股'为单位, 债券以'张'为单位
- price_type = kwargs.get('price_type', 5)
- price = kwargs.get('price', 0)
- strategy_name = kwargs.get('strategy_name', "程序下单")
- order_remark = kwargs.get('order_remark', "程序下单")
+ stock_code = kwargs.get("stock_code")
+ order_type = kwargs.get("order_type")
+ order_volume = kwargs.get("order_volume") # 委托数量, 股票以'股'为单位, 债券以'张'为单位
+ price_type = kwargs.get("price_type", 5)
+ price = kwargs.get("price", 0)
+ strategy_name = kwargs.get("strategy_name", "程序下单")
+ order_remark = kwargs.get("order_remark", "程序下单")
if not xtt.connected:
xtt.start()
xtt.connect()
- order_volume = max(order_volume // 100 * 100, 0) # 股票市场只允许做多 100 的整数倍
+ order_volume = max(order_volume // 100 * 100, 0) # 股票市场只允许做多 100 的整数倍
assert xtt.connected, "交易服务器连接断开"
- _id = xtt.order_stock(acc, stock_code, order_type, int(order_volume), price_type, price, strategy_name, order_remark)
+ _id = xtt.order_stock(
+ acc, stock_code, order_type, int(order_volume), price_type, price, strategy_name, order_remark
+ )
return _id
@@ -549,16 +717,17 @@ def order_stock_target(xtt: XtQuantTrader, acc: StockAccount, symbol, target, **
if current == target:
return
- price_type = kwargs.get('price_type', 5)
- price = kwargs.get('price', 0)
+ price_type = kwargs.get("price_type", 5)
+ price = kwargs.get("price", 0)
# 如果目标小于当前,平仓
if target < current:
delta = min(current - target, pos.can_use_volume if pos else current)
logger.info(f"{symbol}平仓,目标仓位:{target},当前仓位:{current},平仓数量:{delta}")
if delta != 0:
- send_stock_order(xtt, acc, stock_code=symbol, order_type=24,
- order_volume=delta, price_type=price_type, price=price)
+ send_stock_order(
+ xtt, acc, stock_code=symbol, order_type=24, order_volume=delta, price_type=price_type, price=price
+ )
return
# 如果目标大于当前,开仓
@@ -566,8 +735,9 @@ def order_stock_target(xtt: XtQuantTrader, acc: StockAccount, symbol, target, **
delta = target - current
logger.info(f"{symbol}开仓,目标仓位:{target},当前仓位:{current},开仓数量:{delta}")
if delta != 0:
- send_stock_order(xtt, acc, stock_code=symbol, order_type=23,
- order_volume=delta, price_type=price_type, price=price)
+ send_stock_order(
+ xtt, acc, stock_code=symbol, order_type=23, order_volume=delta, price_type=price_type, price=price
+ )
return
@@ -591,24 +761,24 @@ def __init__(self, mini_qmt_dir, account_id, **kwargs):
:param kwargs:
"""
- self.cache_path = kwargs['cache_path'] # 交易缓存路径
+ self.cache_path = kwargs["cache_path"] # 交易缓存路径
os.makedirs(self.cache_path, exist_ok=True)
- self.symbols = kwargs.get('symbols', []) # 交易标的列表
- self.strategy = kwargs.get('strategy', []) # 交易策略
+ self.symbols = kwargs.get("symbols", []) # 交易标的列表
+ self.strategy = kwargs.get("strategy", []) # 交易策略
assert issubclass(self.strategy, czsc.CzscStrategyBase), "交易策略必须是CzscStrategyBase的子类"
- self.symbol_max_pos = kwargs.get('symbol_max_pos', 0.5) # 每个标的最大持仓比例
- self.trade_sdt = kwargs.get('trade_sdt', '20220601') # 交易跟踪开始日期
+ self.symbol_max_pos = kwargs.get("symbol_max_pos", 0.5) # 每个标的最大持仓比例
+ self.trade_sdt = kwargs.get("trade_sdt", "20220601") # 交易跟踪开始日期
self.mini_qmt_dir = mini_qmt_dir
self.account_id = account_id
- self.base_freq = self.strategy(symbol='symbol').sorted_freqs[0]
- self.delta_days = int(kwargs.get('delta_days', 1)) # 定时执行获取的K线天数
- self.forbidden_symbols = kwargs.get('forbidden_symbols', []) # 禁止交易的品种列表
+ self.base_freq = self.strategy(symbol="symbol").sorted_freqs[0]
+ self.delta_days = int(kwargs.get("delta_days", 1)) # 定时执行获取的K线天数
+ self.forbidden_symbols = kwargs.get("forbidden_symbols", []) # 禁止交易的品种列表
self.session = random.randint(10000, 20000)
- self.callback = TraderCallback(**kwargs.get('callback_params', {}))
+ self.callback = TraderCallback(**kwargs.get("callback_params", {}))
self.xtt = XtQuantTrader(mini_qmt_dir, session=self.session, callback=self.callback)
- self.acc = StockAccount(account_id, 'STOCK')
+ self.acc = StockAccount(account_id, "STOCK")
self.xtt.start()
self.xtt.connect()
assert self.xtt.connected, "交易服务器连接失败"
@@ -629,8 +799,9 @@ def __create_traders(self, **kwargs):
# 从缓存文件中恢复交易对象,并更新K线数据
trader: CzscTrader = czsc.dill_load(file_trader)
kline_sdt = pd.to_datetime(trader.end_dt) - timedelta(days=self.delta_days)
- bars = get_raw_bars(symbol, self.base_freq, kline_sdt, datetime.now(), fq="前复权",
- download_hist=True)
+ bars = get_raw_bars(
+ symbol, self.base_freq, kline_sdt, datetime.now(), fq="前复权", download_hist=True
+ )
news = [x for x in bars if x.dt > trader.end_dt]
if news:
logger.info(f"{symbol} 需要更新的K线数量:{len(news)} | 最新的K线时间是 {news[-1].dt}")
@@ -639,11 +810,11 @@ def __create_traders(self, **kwargs):
else:
# 从头创建交易对象
- bars = get_raw_bars(symbol, self.base_freq, '20180101', datetime.now(), fq="前复权")
+ bars = get_raw_bars(symbol, self.base_freq, "20180101", datetime.now(), fq="前复权")
trader: CzscTrader = self.strategy(symbol=symbol).init_trader(bars, sdt=self.trade_sdt)
czsc.dill_dump(trader, file_trader)
- mean_pos = trader.get_ensemble_pos('mean')
+ mean_pos = trader.get_ensemble_pos("mean")
if mean_pos == 0:
continue
@@ -651,7 +822,7 @@ def __create_traders(self, **kwargs):
pos_info = {x.name: x.pos for x in trader.positions if x.pos != 0}
logger.info(f"最新时间:{trader.end_dt};{symbol} trader pos:{pos_info} | mean_pos: {mean_pos}")
except Exception as e:
- logger.exception(f'创建交易对象失败,symbol={symbol}, e={e}')
+ logger.exception(f"创建交易对象失败,symbol={symbol}, e={e}")
return traders
@@ -673,10 +844,17 @@ def query_today_trades(self):
"""查询当日成交"""
# http://docs.thinktrader.net/pages/198696/#%E6%88%90%E4%BA%A4xttrade
trades = self.xtt.query_stock_trades(self.acc)
- res = [{'品种': x.stock_code, '均价': x.traded_price, "方向": "买入" if x.order_type == 23 else "卖出",
- '数量': x.traded_volume, '金额': x.traded_amount,
- '时间': time.strftime("%H:%M:%S", time.localtime(x.traded_time))}
- for x in trades]
+ res = [
+ {
+ "品种": x.stock_code,
+ "均价": x.traded_price,
+ "方向": "买入" if x.order_type == 23 else "卖出",
+ "数量": x.traded_volume,
+ "金额": x.traded_amount,
+ "时间": time.strftime("%H:%M:%S", time.localtime(x.traded_time)),
+ }
+ for x in trades
+ ]
return res
def cancel_timeout_orders(self, minutes=30):
@@ -787,13 +965,13 @@ def send_stock_order(self, **kwargs):
:return: 返回下单请求序号, 成功委托后的下单请求序号为大于0的正整数, 如果为-1表示委托失败
"""
- stock_code = kwargs.get('stock_code')
- order_type = kwargs.get('order_type')
- order_volume = kwargs.get('order_volume') # 委托数量, 股票以'股'为单位, 债券以'张'为单位
- price_type = kwargs.get('price_type', xtconstant.LATEST_PRICE)
- price = kwargs.get('price', 0)
- strategy_name = kwargs.get('strategy_name', "程序下单")
- order_remark = kwargs.get('order_remark', "程序下单")
+ stock_code = kwargs.get("stock_code")
+ order_type = kwargs.get("order_type")
+ order_volume = kwargs.get("order_volume") # 委托数量, 股票以'股'为单位, 债券以'张'为单位
+ price_type = kwargs.get("price_type", xtconstant.LATEST_PRICE)
+ price = kwargs.get("price", 0)
+ strategy_name = kwargs.get("strategy_name", "程序下单")
+ order_remark = kwargs.get("order_remark", "程序下单")
if not self.xtt.connected:
self.xtt.connect()
@@ -803,8 +981,9 @@ def send_stock_order(self, **kwargs):
order_volume = order_volume // 100 * 100
assert self.xtt.connected, "交易服务器连接断开"
- _id = self.xtt.order_stock(self.acc, stock_code, order_type, int(order_volume),
- price_type, price, strategy_name, order_remark)
+ _id = self.xtt.order_stock(
+ self.acc, stock_code, order_type, int(order_volume), price_type, price, strategy_name, order_remark
+ )
return _id
def update_traders(self):
@@ -824,24 +1003,28 @@ def update_traders(self):
trader.on_bar(bar)
# 根据策略的交易信号,下单【股票只有多头】,只有当信号变化时才下单
- if trader.get_ensemble_pos(method='vote') == 1 and trader.pos_changed \
- and self.is_allow_open(symbol, price=news[-1].close):
+ if (
+ trader.get_ensemble_pos(method="vote") == 1
+ and trader.pos_changed
+ and self.is_allow_open(symbol, price=news[-1].close)
+ ):
assets = self.get_assets()
order_volume = min(self.symbol_max_pos * assets.total_asset, assets.cash) // news[-1].close
self.send_stock_order(stock_code=symbol, order_type=23, order_volume=order_volume)
# 平多头
- if trader.get_ensemble_pos(method='vote') == 0 and self.is_allow_exit(symbol):
+ if trader.get_ensemble_pos(method="vote") == 0 and self.is_allow_exit(symbol):
order_volume = holds[symbol].can_use_volume
self.send_stock_order(stock_code=symbol, order_type=24, order_volume=order_volume)
else:
logger.info(f"{symbol} 没有需要更新的K线,最新的K线时间是 {trader.end_dt}")
- if trader.get_ensemble_pos('mean') > 0:
+ if trader.get_ensemble_pos("mean") > 0:
pos_info = {x.name: x.pos for x in trader.positions if x.pos != 0}
logger.info(
- f"{trader.end_dt} {symbol} trader pos:{pos_info} | ensemble_pos: {trader.get_ensemble_pos('mean')}")
+ f"{trader.end_dt} {symbol} trader pos:{pos_info} | ensemble_pos: {trader.get_ensemble_pos('mean')}"
+ )
# 更新交易对象
self.traders[symbol] = trader
@@ -874,15 +1057,18 @@ def update_offline_traders(self):
trader.on_bar(bar)
# 根据策略的交易信号,下单【股票只有多头】,只有当信号变化时才下单
- if trader.get_ensemble_pos(method='vote') == 1 and trader.pos_changed \
- and self.is_allow_open(symbol, price=news[-1].close):
+ if (
+ trader.get_ensemble_pos(method="vote") == 1
+ and trader.pos_changed
+ and self.is_allow_open(symbol, price=news[-1].close)
+ ):
assets = self.get_assets()
order_volume = min(self.symbol_max_pos * assets.total_asset, assets.cash) // news[-1].close
self.send_stock_order(stock_code=symbol, order_type=23, order_volume=order_volume)
czsc.dill_dump(trader, file_trader)
- mean_pos = trader.get_ensemble_pos('mean')
+ mean_pos = trader.get_ensemble_pos("mean")
if mean_pos == 0:
continue
@@ -890,7 +1076,7 @@ def update_offline_traders(self):
pos_info = {x.name: x.pos for x in trader.positions if x.pos != 0}
logger.info(f"最新时间:{trader.end_dt};{symbol} trader pos:{pos_info} | mean_pos: {mean_pos}")
except Exception as e:
- logger.exception(f'创建交易对象失败,symbol={symbol}, e={e}')
+ logger.exception(f"创建交易对象失败,symbol={symbol}, e={e}")
self.traders = traders
@@ -902,22 +1088,33 @@ def report(self):
writer.add_title("QMT 交易报告")
assets = self.get_assets()
- writer.add_heading('一、账户状态', level=1)
- writer.add_paragraph(f"交易品种数量:{len(self.traders)}\n"
- f"传入品种数量:{len(self.symbols)}\n"
- f"交易账户:{self.account_id}\n"
- f"账户资产:{assets.total_asset}\n"
- f"可用资金:{assets.cash}\n"
- f"持仓市值:{assets.market_value}\n"
- f"持仓情况:", first_line_indent=0)
+ writer.add_heading("一、账户状态", level=1)
+ writer.add_paragraph(
+ f"交易品种数量:{len(self.traders)}\n"
+ f"传入品种数量:{len(self.symbols)}\n"
+ f"交易账户:{self.account_id}\n"
+ f"账户资产:{assets.total_asset}\n"
+ f"可用资金:{assets.cash}\n"
+ f"持仓市值:{assets.market_value}\n"
+ f"持仓情况:",
+ first_line_indent=0,
+ )
sp = self.query_stock_positions()
if sp:
_res_sp = []
for k, v in sp.items():
is_auto = "程序" if k in self.traders.keys() else "人工"
- _res_sp.append({'品种': k, '持仓股数': v.volume, '可用股数': v.can_use_volume,
- '成本': v.open_price, '市值': int(v.market_value), '操盘手': is_auto})
+ _res_sp.append(
+ {
+ "品种": k,
+ "持仓股数": v.volume,
+ "可用股数": v.can_use_volume,
+ "成本": v.open_price,
+ "市值": int(v.market_value),
+ "操盘手": is_auto,
+ }
+ )
writer.add_df_table(pd.DataFrame(_res_sp))
else:
writer.add_paragraph("当前没有持仓", first_line_indent=0)
@@ -930,36 +1127,58 @@ def report(self):
else:
writer.add_paragraph("当日没有成交", first_line_indent=0)
- writer.add_heading('二、策略状态', level=1)
+ writer.add_heading("二、策略状态", level=1)
_res = []
for symbol, trader in self.traders.items():
- if trader.get_ensemble_pos('mean') > 0:
+ if trader.get_ensemble_pos("mean") > 0:
_pos_str = "\n\n".join([f"{x.name}:{x.pos}" for x in trader.positions if x.pos != 0])
_ops = [x.operates[-1] for x in trader.positions if x.pos != 0]
_ops_str = "\n\n".join([f"时间:{x['dt']}_价格:{x['price']}_描述:{x['op_desc']}" for x in _ops])
- _res.append({'symbol': symbol, 'pos': round(trader.get_ensemble_pos('mean'), 3),
- 'positions': _pos_str, 'operates': _ops_str})
+ _res.append(
+ {
+ "symbol": symbol,
+ "pos": round(trader.get_ensemble_pos("mean"), 3),
+ "positions": _pos_str,
+ "operates": _ops_str,
+ }
+ )
if _res:
- writer.add_df_table(pd.DataFrame(_res).sort_values(by='pos', ascending=False))
+ writer.add_df_table(pd.DataFrame(_res).sort_values(by="pos", ascending=False))
else:
writer.add_paragraph("当前所有品种都是空仓")
file_docx = f"QMT{self.account_id}_交易报告_{datetime.now().strftime('%Y%m%d_%H%M')}.docx"
writer.save(file_docx)
- self.callback.push_message(file_docx, msg_type='file')
+ self.callback.push_message(file_docx, msg_type="file")
os.remove(file_docx)
- def run(self, mode='30m', order_timeout=120):
+ def run(self, mode="30m", order_timeout=120):
"""运行策略"""
self.report()
- if mode.lower() == '15m':
- _times = ["09:45", "10:00", "10:15", "10:30", "10:45", "11:00", "11:15", "11:30",
- "13:15", "13:30", "13:45", "14:00", "14:15", "14:30", "14:45", "15:00"]
- elif mode.lower() == '30m':
+ if mode.lower() == "15m":
+ _times = [
+ "09:45",
+ "10:00",
+ "10:15",
+ "10:30",
+ "10:45",
+ "11:00",
+ "11:15",
+ "11:30",
+ "13:15",
+ "13:30",
+ "13:45",
+ "14:00",
+ "14:15",
+ "14:30",
+ "14:45",
+ "15:00",
+ ]
+ elif mode.lower() == "30m":
_times = ["09:45", "10:00", "10:30", "11:00", "11:30", "13:30", "14:00", "14:30", "15:00"]
- elif mode.lower() == '60m':
+ elif mode.lower() == "60m":
_times = ["10:30", "11:30", "13:45", "14:30"]
else:
raise ValueError("mode 只能是 15m, 30m, 60m")
@@ -981,7 +1200,7 @@ def run(self, mode='30m', order_timeout=120):
self.xtt.start()
if now_dt in ["11:35", "14:05", "15:05"]:
- self.callback.push_message(f"{self.account_id} 开始更新离线交易员对象", msg_type='text')
+ self.callback.push_message(f"{self.account_id} 开始更新离线交易员对象", msg_type="text")
self.update_offline_traders()
self.report()
@@ -990,32 +1209,36 @@ def run(self, mode='30m', order_timeout=120):
# 以下是测试代码
# ======================================================================================================================
+
def test_get_kline():
# 获取所有板块
slt = xtdata.get_sector_list()
- stocks = xtdata.get_stock_list_in_sector('沪深A股')
+ stocks = xtdata.get_stock_list_in_sector("沪深A股")
- df = get_kline(symbol='000001.SZ', period='1m', count=1000, dividend_type='front',
- start_time='20200427', end_time='20221231')
+ df = get_kline(
+ symbol="000001.SZ", period="1m", count=1000, dividend_type="front", start_time="20200427", end_time="20221231"
+ )
assert not df.empty
- df = get_kline(symbol='000001.SZ', period='5m', count=1000, dividend_type='front',
- start_time='20200427', end_time='20221231')
+ df = get_kline(
+ symbol="000001.SZ", period="5m", count=1000, dividend_type="front", start_time="20200427", end_time="20221231"
+ )
assert not df.empty
- df = get_kline(symbol='000001.SZ', period='1d', count=1000, dividend_type='front',
- start_time='20200427', end_time='20221231')
+ df = get_kline(
+ symbol="000001.SZ", period="1d", count=1000, dividend_type="front", start_time="20200427", end_time="20221231"
+ )
assert not df.empty
def test_get_symbols():
- symbols = get_symbols('index')
+ symbols = get_symbols("index")
assert len(symbols) > 0
- symbols = get_symbols('stock')
+ symbols = get_symbols("stock")
assert len(symbols) > 0
- symbols = get_symbols('check')
+ symbols = get_symbols("check")
assert len(symbols) > 0
- symbols = get_symbols('train')
+ symbols = get_symbols("train")
assert len(symbols) > 0
- symbols = get_symbols('valid')
+ symbols = get_symbols("valid")
assert len(symbols) > 0
- symbols = get_symbols('etfs')
+ symbols = get_symbols("etfs")
assert len(symbols) > 0
diff --git a/czsc/connectors/tq_connector.py b/czsc/connectors/tq_connector.py
index b6b754631..fe2f744e1 100644
--- a/czsc/connectors/tq_connector.py
+++ b/czsc/connectors/tq_connector.py
@@ -189,6 +189,8 @@ def create_symbol_trader(api: TqApi, symbol, **kwargs):
future_name_map = {
+ "EC": "欧线集运",
+ "LC": "碳酸锂",
"PG": "LPG",
"EB": "苯乙烯",
"CS": "玉米淀粉",
diff --git a/czsc/fsa/bi_table.py b/czsc/fsa/bi_table.py
index ff792f7dc..80d5d89b1 100644
--- a/czsc/fsa/bi_table.py
+++ b/czsc/fsa/bi_table.py
@@ -22,7 +22,7 @@ def list_tables(self, app_token):
https://open.feishu.cn/document/server-docs/docs/bitable-v1/app-table/list
- :param app_token: 应用token
+ :param app_token: 一个多维表格的唯一标识。示例值:"bascnKMKGS5oD3lmCHq9euO8cGh"
:return: 返回数据
"""
url = f"{self.host}/open-apis/bitable/v1/apps/{app_token}/tables"
@@ -33,7 +33,7 @@ def list_records(self, app_token, table_id, **kwargs):
https://open.feishu.cn/document/server-docs/docs/bitable-v1/app-table-record/list
- :param app_token: 应用token
+ :param app_token: 一个多维表格的唯一标识。示例值:"bascnKMKGS5oD3lmCHq9euO8cGh"
:param table_id: 数据表id
:return: 返回数据
"""
@@ -51,17 +51,17 @@ def list_records(self, app_token, table_id, **kwargs):
def read_table(self, app_token, table_id, **kwargs):
"""读取多维表格中指定表格的数据
- :param app_token: 多维表格应用token
+ :param app_token: 一个多维表格的唯一标识。示例值:"bascnKMKGS5oD3lmCHq9euO8cGh"
:param table_id: 表格id
:return:
"""
rows = []
- res = self.list_records(app_token, table_id, **kwargs)['data']
- total = res['total']
- rows.extend(res['items'])
- while res['has_more']:
- res = self.list_records(app_token, table_id, page_token=res['page_token'], **kwargs)['data']
- rows.extend(res['items'])
+ res = self.list_records(app_token, table_id, **kwargs)["data"]
+ total = res["total"]
+ rows.extend(res["items"])
+ while res["has_more"]:
+ res = self.list_records(app_token, table_id, page_token=res["page_token"], **kwargs)["data"]
+ rows.extend(res["items"])
assert len(rows) == total, "数据读取异常"
- return pd.DataFrame([x['fields'] for x in rows])
+ return pd.DataFrame([x["fields"] for x in rows])
diff --git a/czsc/signals/__init__.py b/czsc/signals/__init__.py
index 737d4733a..c01814e67 100644
--- a/czsc/signals/__init__.py
+++ b/czsc/signals/__init__.py
@@ -119,6 +119,8 @@
bar_classify_V240606,
bar_classify_V240607,
bar_decision_V240608,
+ bar_decision_V240616,
+ bar_td9_V240616,
)
from czsc.signals.jcc import (
@@ -213,6 +215,7 @@
tas_dma_bs_V240608,
tas_dif_zero_V240612,
tas_dif_zero_V240614,
+ cci_decision_V240620,
)
from czsc.signals.pos import (
@@ -287,4 +290,5 @@
xl_bar_trend_V240331,
xl_bar_basis_V240411,
xl_bar_basis_V240412,
+ xl_bar_trend_V240623,
)
diff --git a/czsc/signals/bar.py b/czsc/signals/bar.py
index a3c407fc5..692a8d699 100644
--- a/czsc/signals/bar.py
+++ b/czsc/signals/bar.py
@@ -2116,3 +2116,153 @@ def bar_decision_V240608(c: CZSC, **kwargs) -> OrderedDict:
if vol_match and n_diff < 0:
v1 = "看多"
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
+
+
+def bar_decision_V240616(c: CZSC, **kwargs) -> OrderedDict:
+ """新高/低后的弱/强势信号
+
+ 参数模板:"{freq}_W{w}N{n}强弱_决策区域V240616"
+
+ **信号逻辑:**
+
+ 1. 看多:最近 N 根K线出现新高,且后续出现低位收盘的弱势信号
+ 2. 看空:最近 N 根K线出现新低,且后续出现高位收盘的强势信号
+
+ https://s0cqcxuy3p.feishu.cn/wiki/MT47wiaalilwnnkAGo5c04Sxnfd
+
+ **信号列表:**
+
+ - Signal('60分钟_W100N5强弱_决策区域V240616_看多_任意_任意_0')
+ - Signal('60分钟_W100N5强弱_决策区域V240616_看空_任意_任意_0')
+
+ :param c: CZSC对象
+ :param kwargs:
+
+ - w: int, default 300, 窗口大小
+ - n: int, default 20, 最近N根K线
+
+ :return: 信号识别结果
+ """
+ w = int(kwargs.get("w", 100))
+ n = int(kwargs.get("n", 5))
+ assert w > n > 2, "参数 w 必须大于 n,且 n 必须大于 0"
+
+ freq = c.freq.value
+ k1, k2, k3 = f"{freq}_W{w}N{n}强弱_决策区域V240616".split("_")
+ v1 = "其他"
+
+ # 更新K线收盘位置信息
+ cache_key = "bar_decision_V240616_close_position"
+ for bar in c.bars_raw:
+ if not hasattr(bar.cache, cache_key):
+ hl = bar.high - bar.low
+ t1 = bar.low + hl * (2 / 3)
+ t2 = bar.low + hl * (1 / 3)
+ if bar.close > t1:
+ bar.cache[cache_key] = "高位收盘"
+ elif t2 < bar.close < t1:
+ bar.cache[cache_key] = "中位收盘"
+ else:
+ bar.cache[cache_key] = "低位收盘"
+
+ if len(c.bars_raw) < w + n + 10:
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
+
+ w_bars = get_sub_elements(c.bars_raw, di=n, n=w)
+ w_bars_high = max([x.high for x in w_bars])
+ w_bars_low = min([x.low for x in w_bars])
+
+ # K线平均长度
+ hl_mean = np.mean([x.high - x.low for x in w_bars])
+ n_bars = [x for x in get_sub_elements(c.bars_raw, di=1, n=n) if x.high - x.low > hl_mean]
+
+ # 寻找 n_bars 中的新高K线,及其后面的K线序列
+ for i, bar in enumerate(n_bars):
+ right_bars = n_bars[i + 1 :]
+ if not right_bars:
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
+
+ # 当前K线创新高,但是收盘价在HL中点以下
+ if bar.high >= w_bars_high and bar.cache[cache_key] != "高位收盘":
+ for rb in right_bars:
+ if rb.cache[cache_key] == "低位收盘" or rb.close < rb.low:
+ v1 = "看空"
+ break
+
+ # 当前K线创新低,但是收盘价在HL中点以上
+ if bar.low <= w_bars_low and bar.cache[cache_key] != "低位收盘":
+ for rb in right_bars:
+ if rb.cache[cache_key] == "高位收盘" or rb.close > rb.high:
+ v1 = "看多"
+ break
+
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
+
+
+def bar_td9_V240616(c: CZSC, **kwargs) -> OrderedDict:
+ """神奇九转计数
+
+ 参数模板:"{freq}_神奇九转N{n}_BS辅助V240616"
+
+ **信号逻辑:**
+
+ 1. 当前收盘价大于前4根K线的收盘价,+1,否则-1
+ 2. 如果最后一根K线为1,且连续值计数大于等于N,卖点;如果最后一根K线为-1,且连续值计数小于等于-N,买点
+
+ **信号列表:**
+
+ - Signal('60分钟_神奇九转N9_BS辅助V240616_买点_9转_任意_0')
+ - Signal('60分钟_神奇九转N9_BS辅助V240616_卖点_9转_任意_0')
+
+ :param c: CZSC对象
+ :param kwargs:
+
+ - n: int, default 9, 连续转折次数
+
+ :return: 信号识别结果
+ """
+ n = int(kwargs.get("n", 9))
+
+ freq = c.freq.value
+ k1, k2, k3 = f"{freq}_神奇九转N{n}_BS辅助V240616".split("_")
+ v1 = "其他"
+
+ # 更新缓存
+ cache_key = "bar_td9_V240616"
+ for i, bar in enumerate(c.bars_raw):
+ if i < 4 or hasattr(bar.cache, cache_key):
+ continue
+
+ if bar.close > c.bars_raw[i - 4].close:
+ bar.cache[cache_key] = 1
+ elif bar.close < c.bars_raw[i - 4].close:
+ bar.cache[cache_key] = -1
+ else:
+ bar.cache[cache_key] = 0
+
+ if len(c.bars_raw) < 30 + n:
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
+
+ v2 = "任意"
+ bars = get_sub_elements(c.bars_raw, di=1, n=n * 2)
+ if bars[-1].cache[cache_key] == 1:
+ count = 0
+ for bar in bars[::-1]:
+ if bar.cache[cache_key] != 1:
+ break
+ count += 1
+ if count >= n:
+ v1 = "卖点"
+ v2 = f"{count}转"
+
+ elif bars[-1].cache[cache_key] == -1:
+ count = 0
+ for bar in bars[::-1]:
+ if bar.cache[cache_key] != -1:
+ break
+ count += 1
+ if count >= n:
+ v1 = "买点"
+ v2 = f"{count}转"
+
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
diff --git a/czsc/signals/tas.py b/czsc/signals/tas.py
index 5b5f46d82..9d9d15b23 100644
--- a/czsc/signals/tas.py
+++ b/czsc/signals/tas.py
@@ -1991,6 +1991,49 @@ def tas_cci_base_V230402(c: CZSC, **kwargs) -> OrderedDict:
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
+def cci_decision_V240620(c: CZSC, **kwargs) -> OrderedDict:
+ """根据CCI指标逆势用法,判断买卖决策区域
+
+ 参数模板:"{freq}_N{n}CCI_决策区域V240620"
+
+ **信号逻辑:**
+
+ 取最近N根K线,如果最小的CCI值小于 -100,开多;如果最大的CCI值大于 100,开空。
+
+ **信号列表:**
+
+ - Signal('15分钟_N4CCI_决策区域V240620_开多_2次_任意_0')
+ - Signal('15分钟_N4CCI_决策区域V240620_开多_1次_任意_0')
+ - Signal('15分钟_N4CCI_决策区域V240620_开空_1次_任意_0')
+ - Signal('15分钟_N4CCI_决策区域V240620_开空_2次_任意_0')
+
+ :param c: CZSC对象
+ :param kwargs: 无
+ :return: 信号识别结果
+ """
+ n = int(kwargs.get("n", 2))
+
+ freq = c.freq.value
+ k1, k2, k3 = f"{freq}_N{n}CCI_决策区域V240620".split("_")
+ v1 = "其他"
+ cache_key = update_cci_cache(c, timeperiod=14)
+ if len(c.bars_raw) < 100:
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
+
+ cci_seq = [x.cache[cache_key] for x in c.bars_raw[-n:]]
+ short_cci = [x for x in cci_seq if x > 100]
+ long_cci = [x for x in cci_seq if x < -100]
+
+ v2 = "任意"
+ if min(cci_seq) < -100:
+ v1 = "开多"
+ v2 = f"{len(long_cci)}次"
+ if max(cci_seq) > 100:
+ v1 = "开空"
+ v2 = f"{len(short_cci)}次"
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
+
+
def tas_kdj_evc_V230401(c: CZSC, **kwargs) -> OrderedDict:
"""KDJ极值计数信号, evc 是 extreme value counts 的首字母缩写
@@ -3074,7 +3117,7 @@ def cat_macd_V230518(cat: CzscSignals, **kwargs) -> OrderedDict:
# 找出 c1 的最近一次金叉
macd_gold_bars = []
for bar1, bar2 in zip(c1_bars, c1_bars[1:]):
- if bar1.cache[cache_key]["macd"] < 0 and bar2.cache[cache_key]["macd"] > 0:
+ if bar1.cache[cache_key]["macd"] < 0 < bar2.cache[cache_key]["macd"]:
macd_gold_bars.append(bar2)
assert macd_gold_bars, "没有找到金叉"
macd_gold_bar = macd_gold_bars[-1]
@@ -3084,7 +3127,7 @@ def cat_macd_V230518(cat: CzscSignals, **kwargs) -> OrderedDict:
if len(c2_bars) > 3:
c2_gold_bars = []
for bar1, bar2 in zip(c2_bars, c2_bars[1:]):
- if bar1.cache[cache_key]["macd"] < 0 and bar2.cache[cache_key]["macd"] > 0:
+ if bar1.cache[cache_key]["macd"] < 0 < bar2.cache[cache_key]["macd"]:
c2_gold_bars.append(bar2)
if len(c2_gold_bars) == 1:
@@ -3094,7 +3137,7 @@ def cat_macd_V230518(cat: CzscSignals, **kwargs) -> OrderedDict:
# 找出 c1 的最近一次死叉
macd_dead_bars = []
for bar1, bar2 in zip(c1_bars, c1_bars[1:]):
- if bar1.cache[cache_key]["macd"] > 0 and bar2.cache[cache_key]["macd"] < 0:
+ if bar1.cache[cache_key]["macd"] > 0 > bar2.cache[cache_key]["macd"]:
macd_dead_bars.append(bar2)
assert macd_dead_bars, "没有找到死叉"
macd_dead_bar = macd_dead_bars[-1]
@@ -3104,7 +3147,7 @@ def cat_macd_V230518(cat: CzscSignals, **kwargs) -> OrderedDict:
if len(c2_bars) > 3:
c2_dead_bars = []
for bar1, bar2 in zip(c2_bars, c2_bars[1:]):
- if bar1.cache[cache_key]["macd"] > 0 and bar2.cache[cache_key]["macd"] < 0:
+ if bar1.cache[cache_key]["macd"] > 0 > bar2.cache[cache_key]["macd"]:
c2_dead_bars.append(bar2)
if len(c2_dead_bars) == 1:
diff --git a/czsc/signals/xls.py b/czsc/signals/xls.py
index cb58af2e2..2113ea173 100644
--- a/czsc/signals/xls.py
+++ b/czsc/signals/xls.py
@@ -89,18 +89,10 @@ def xl_bar_trend_V240329(c: CZSC, **kwargs) -> OrderedDict:
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
bar1, bar2 = get_sub_elements(c.bars_raw, di=1, n=2)
- if (
- check_szx(bar2, n)
- and bar1.close < bar1.open
- and (bar1.open - bar1.close) / (bar1.high - bar1.low) * 10 >= m
- ):
+ if check_szx(bar2, n) and bar1.close < bar1.open and (bar1.open - bar1.close) / (bar1.high - bar1.low) * 10 >= m:
v1 = "底部十字孕线"
- if (
- check_szx(bar2, n)
- and bar1.close > bar1.open
- and (bar1.close - bar1.open) / (bar1.high - bar1.low) * 10 >= m
- ):
+ if check_szx(bar2, n) and bar1.close > bar1.open and (bar1.close - bar1.open) / (bar1.high - bar1.low) * 10 >= m:
v1 = "顶部十字孕线"
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
@@ -311,17 +303,56 @@ def xl_bar_basis_V240411(c: CZSC, **kwargs) -> OrderedDict:
bar1, bar2 = get_sub_elements(c.bars_raw, di=1, n=2)
- if (
- (bar1.open > bar1.close)
- and (bar2.close > bar1.high)
- and (bar2.open <= bar1.low)
- ):
+ if (bar1.open > bar1.close) and (bar2.close > bar1.high) and (bar2.open <= bar1.low):
v1 = "看涨吞没"
- elif (
- (bar1.open < bar1.close)
- and (bar2.open >= bar1.high)
- and (bar2.close < bar1.low)
- ):
+ elif (bar1.open < bar1.close) and (bar2.open >= bar1.high) and (bar2.close < bar1.low):
v1 = "看跌吞没"
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
+
+
+def xl_bar_trend_V240623(c: CZSC, **kwargs) -> OrderedDict:
+ """突破信号; 贡献者:谢磊
+
+ 参数模板:"{freq}_N{n}通道_突破信号V240623"
+
+ **信号逻辑:**
+
+ 1, 突破前N日最高价,入场,做多
+ 2. 跌破前N日最低价,入场,做空
+
+ **信号列表:**
+
+ - Signal('30分钟_N20通道_突破信号V240623_做多_连续2次上涨_任意_0')
+ - Signal('30分钟_N20通道_突破信号V240623_做空_连续2次下跌_任意_0')
+
+ :param c: CZSC对象
+ :param kwargs:
+
+ - n: int, 默认20,突破前N日的最高价或最低价
+
+ :return: 信号识别结果
+ """
+ n = int(kwargs.get("n", 20))
+ freq = c.freq.value
+ k1, k2, k3 = f"{freq}_N{n}通道_突破信号V240623".split("_")
+ v1 = "其他"
+ v2 = "任意"
+
+ bars2 = get_sub_elements(c.bars_raw, di=1, n=n + 1)
+ hh = max([x.high for x in bars2[0:-2]])
+ ll = min([x.low for x in bars2[0:-2]])
+ _high = bars2[-2].high
+ _low = bars2[-2].low
+
+ if _high >= hh:
+ v1 = "做多"
+ if bars2[-1].high > _high:
+ v2 = "连续2次上涨"
+
+ elif _low <= ll:
+ v1 = "做空"
+ if bars2[-1].low < _low:
+ v2 = "连续2次下跌"
+
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
diff --git a/czsc/traders/weight_backtest.py b/czsc/traders/weight_backtest.py
index 826dccca1..b14e3b00f 100644
--- a/czsc/traders/weight_backtest.py
+++ b/czsc/traders/weight_backtest.py
@@ -15,6 +15,8 @@
from typing import Union, AnyStr, Callable
from multiprocessing import cpu_count
from concurrent.futures import ProcessPoolExecutor
+
+import czsc
from czsc.traders.base import CzscTrader
from czsc.utils.io import save_json
from czsc.utils.stats import daily_performance, evaluate_pairs
@@ -226,9 +228,13 @@ class WeightBacktest:
"""持仓权重回测
飞书文档:https://s0cqcxuy3p.feishu.cn/wiki/Pf1fw1woQi4iJikbKJmcYToznxb
+
+ 更新日志:
+
+ - V240627: 增加dailys属性,品种每日的交易信息
"""
- version = "V231126"
+ version = "V240627"
def __init__(self, dfw, digits=2, **kwargs) -> None:
"""持仓权重回测
@@ -278,6 +284,7 @@ def __init__(self, dfw, digits=2, **kwargs) -> None:
self.dfw["weight"] = self.dfw["weight"].astype("float").round(digits)
self.symbols = list(self.dfw["symbol"].unique().tolist())
default_n_jobs = min(cpu_count() // 2, len(self.symbols))
+ self._dailys = None
self.results = self.backtest(n_jobs=kwargs.get("n_jobs", default_n_jobs))
@property
@@ -290,6 +297,55 @@ def daily_return(self) -> pd.DataFrame:
"""品种等权费后日收益率"""
return self.results.get("品种等权日收益", pd.DataFrame())
+ @property
+ def dailys(self) -> pd.DataFrame:
+ """品种每日的交易信息
+
+ columns = ['date', 'symbol', 'edge', 'return', 'cost', 'n1b', 'turnover']
+
+ 其中:
+ date 交易日,
+ symbol 合约代码,
+ n1b 品种每日收益率,
+ edge 策略每日收益率,
+ return 策略每日收益率减去交易成本后的真实收益,
+ cost 交易成本
+ turnover 当日的单边换手率
+ """
+ return self._dailys.copy() if self._dailys is not None else pd.DataFrame()
+
+ @property
+ def alpha(self) -> pd.DataFrame:
+ """策略超额收益
+
+ columns = ['date', '策略', '基准', '超额']
+ """
+ if self._dailys is None:
+ return pd.DataFrame()
+ df1 = self._dailys.groupby("date").agg({"return": "mean", "n1b": "mean"})
+ df1["alpha"] = df1["return"] - df1["n1b"]
+ df1.rename(columns={"return": "策略", "n1b": "基准", "alpha": "超额"}, inplace=True)
+ df1 = df1.reset_index()
+ return df1
+
+ @property
+ def alpha_stats(self):
+ """策略超额收益统计"""
+ df = self.alpha.copy()
+ stats = czsc.daily_performance(df["超额"].to_list())
+ stats["开始日期"] = df["date"].min().strftime("%Y-%m-%d")
+ stats["结束日期"] = df["date"].max().strftime("%Y-%m-%d")
+ return stats
+
+ @property
+ def bench_stats(self):
+ """基准收益统计"""
+ df = self.alpha.copy()
+ stats = czsc.daily_performance(df["基准"].to_list())
+ stats["开始日期"] = df["date"].min().strftime("%Y-%m-%d")
+ stats["结束日期"] = df["date"].max().strftime("%Y-%m-%d")
+ return stats
+
def get_symbol_daily(self, symbol):
"""获取某个合约的每日收益率
@@ -301,19 +357,21 @@ def get_symbol_daily(self, symbol):
4. 计算每条数据扣除手续费后的收益(edge_post_fee):收益减去手续费。
5. 根据日期进行分组,并对每组进行求和操作,得到每日的总收益、总扣除手续费后的收益和总手续费。
6. 重置索引,并将交易标的符号添加到DataFrame中。
- 7. 重命名列名,将'edge_post_fee'列改为'return',将'dt'列改为'date'。
+ 7. 重命名列名,将'edge_post_fee'列改为 return,将'dt'列改为 date。
8. 选择需要的列,并返回包含日期、交易标的、收益、扣除手续费后的收益和手续费的DataFrame。
:param symbol: str,合约代码
:return: pd.DataFrame,品种每日收益率,
- columns = ['date', 'symbol', 'edge', 'return', 'cost']
+ columns = ['date', 'symbol', 'edge', 'return', 'cost', 'n1b']
其中
- date 为交易日,
- symbol 为合约代码,
- edge 为每日收益率,
- return 为每日收益率减去交易成本后的真实收益,
- cost 为交易成本
+ date 交易日,
+ symbol 合约代码,
+ n1b 品种每日收益率,
+ edge 策略每日收益率,
+ return 策略每日收益率减去交易成本后的真实收益,
+ cost 交易成本
+ turnover 当日的单边换手率
数据样例如下:
@@ -328,13 +386,19 @@ def get_symbol_daily(self, symbol):
========== ======== ============ ============ =======
"""
dfs = self.dfw[self.dfw["symbol"] == symbol].copy()
- dfs["edge"] = dfs["weight"] * (dfs["price"].shift(-1) / dfs["price"] - 1)
- dfs["cost"] = abs(dfs["weight"].shift(1) - dfs["weight"]) * self.fee_rate
+ dfs["n1b"] = dfs["price"].shift(-1) / dfs["price"] - 1
+ dfs["edge"] = dfs["weight"] * dfs["n1b"]
+ dfs["turnover"] = abs(dfs["weight"].shift(1) - dfs["weight"])
+ dfs["cost"] = dfs["turnover"] * self.fee_rate
dfs["edge_post_fee"] = dfs["edge"] - dfs["cost"]
- daily = dfs.groupby(dfs["dt"].dt.date).agg({"edge": "sum", "edge_post_fee": "sum", "cost": "sum"}).reset_index()
+ daily = (
+ dfs.groupby(dfs["dt"].dt.date)
+ .agg({"edge": "sum", "edge_post_fee": "sum", "cost": "sum", "n1b": "sum", "turnover": "sum"})
+ .reset_index()
+ )
daily["symbol"] = symbol
daily.rename(columns={"edge_post_fee": "return", "dt": "date"}, inplace=True)
- daily = daily[["date", "symbol", "edge", "return", "cost"]]
+ daily = daily[["date", "symbol", "n1b", "edge", "return", "cost", "turnover"]].copy()
return daily
def get_symbol_pairs(self, symbol):
@@ -485,6 +549,8 @@ def backtest(self, n_jobs=1):
):
res[symbol] = res_symbol
+ self._dailys = pd.concat([v["daily"] for k, v in res.items() if k in symbols], ignore_index=True)
+
dret = pd.concat([v["daily"] for k, v in res.items() if k in symbols], ignore_index=True)
dret = pd.pivot_table(dret, index="date", columns="symbol", values="return").fillna(0)
dret["total"] = dret[list(res.keys())].mean(axis=1)
@@ -528,6 +594,12 @@ def report(self, res_path):
fig.write_html(res_path.joinpath("daily_return.html"))
logger.info(f"费后日收益率资金曲线已保存到 {res_path.joinpath('daily_return.html')}")
+ # 绘制alpha曲线
+ alpha = self.alpha.copy()
+ alpha[["策略", "基准", "超额"]] = alpha[["策略", "基准", "超额"]].cumsum()
+ fig = px.line(alpha, x="date", y=["策略", "基准", "超额"], title="策略超额收益")
+ fig.write_html(res_path.joinpath("alpha.html"))
+
# 所有开平交易记录的表现
stats = res["绩效评价"].copy()
logger.info(f"绩效评价:{stats}")
diff --git a/czsc/utils/data_client.py b/czsc/utils/data_client.py
index 778e42351..3d1138a34 100644
--- a/czsc/utils/data_client.py
+++ b/czsc/utils/data_client.py
@@ -15,20 +15,20 @@ def set_url_token(token, url):
:param token: 凭证码
:param url: 数据接口地址
"""
- hash_key = hashlib.md5(str(url).encode('utf-8')).hexdigest()
+ hash_key = hashlib.md5(str(url).encode("utf-8")).hexdigest()
file_token = Path("~").expanduser() / f"{hash_key}.txt"
- with open(file_token, 'w', encoding='utf-8') as f:
+ with open(file_token, "w", encoding="utf-8") as f:
f.write(token)
logger.info(f"{url} 数据访问凭证码已保存到 {file_token}")
def get_url_token(url):
"""获取指定 URL 数据接口的凭证码"""
- hash_key = hashlib.md5(str(url).encode('utf-8')).hexdigest()
+ hash_key = hashlib.md5(str(url).encode("utf-8")).hexdigest()
file_token = Path("~").expanduser() / f"{hash_key}.txt"
if file_token.exists():
logger.info(f"从 {file_token} 读取 {url} 的访问凭证码")
- return open(file_token, 'r', encoding='utf-8').read()
+ return open(file_token, "r", encoding="utf-8").read()
logger.warning(f"请设置 {url} 的访问凭证码,如果没有请联系管理员申请")
token = input(f"请输入 {url} 的访问凭证码(token):")
@@ -41,7 +41,7 @@ def get_url_token(url):
class DataClient:
__version__ = "V231109"
- def __init__(self, token=None, url='http://api.tushare.pro', timeout=300, **kwargs):
+ def __init__(self, token=None, url="http://api.tushare.pro", timeout=300, **kwargs):
"""数据接口客户端,支持缓存,默认缓存路径为 ~/.quant_data_cache;兼容Tushare数据接口
:param token: str API接口TOKEN,用于用户认证
@@ -58,12 +58,14 @@ def __init__(self, token=None, url='http://api.tushare.pro', timeout=300, **kwar
self.__token = token or get_url_token(url)
self.__http_url = url
self.__timeout = timeout
- self.__url_hash = hashlib.md5(str(url).encode('utf-8')).hexdigest()[:8]
+ self.__url_hash = hashlib.md5(str(url).encode("utf-8")).hexdigest()[:8]
assert self.__token, "请设置czsc_token凭证码,如果没有请联系管理员申请"
self.cache_path = Path(kwargs.get("cache_path", os.path.expanduser("~/.quant_data_cache")))
self.cache_path.mkdir(exist_ok=True, parents=True)
- logger.info(f"数据URL: {url} 数据缓存路径:{self.cache_path} 占用磁盘空间:{get_dir_size(self.cache_path) / 1024 / 1024:.2f} MB")
+ logger.info(
+ f"数据URL: {url} 数据缓存路径:{self.cache_path} 占用磁盘空间:{get_dir_size(self.cache_path) / 1024 / 1024:.2f} MB"
+ )
if kwargs.get("clear_cache", False):
self.clear_cache()
@@ -71,8 +73,9 @@ def clear_cache(self):
"""清空缓存"""
shutil.rmtree(self.cache_path)
logger.info(f"{self.cache_path} 路径下的数据缓存已清空")
+ self.cache_path.mkdir(exist_ok=True, parents=True)
- def post_request(self, api_name, fields='', **kwargs):
+ def post_request(self, api_name, fields="", **kwargs):
"""执行API数据查询
:param api_name: str, 查询接口名称
@@ -84,11 +87,11 @@ def post_request(self, api_name, fields='', **kwargs):
:return: pd.DataFrame
"""
stime = time()
- if api_name in ['__getstate__', '__setstate__']:
+ if api_name in ["__getstate__", "__setstate__"]:
return pd.DataFrame()
ttl = int(kwargs.pop("ttl", -1))
- req_params = {'api_name': api_name, 'token': self.__token, 'params': kwargs, 'fields': fields}
+ req_params = {"api_name": api_name, "token": self.__token, "params": kwargs, "fields": fields}
path = self.cache_path / f"{self.__url_hash}_{api_name}"
path.mkdir(exist_ok=True, parents=True)
file_cache = path / f"{hashlib.md5(str(req_params).encode('utf-8')).hexdigest()}.pkl"
@@ -100,10 +103,10 @@ def post_request(self, api_name, fields='', **kwargs):
res = requests.post(self.__http_url, json=req_params, timeout=self.__timeout)
if res:
result = res.json()
- if result['code'] != 0:
+ if result["code"] != 0:
raise Exception(f"API: {api_name} - {kwargs} 数据获取失败: {result}")
- df = pd.DataFrame(result['data']['items'], columns=result['data']['fields'])
+ df = pd.DataFrame(result["data"]["items"], columns=result["data"]["fields"])
df.to_pickle(file_cache)
else:
df = pd.DataFrame()
diff --git a/czsc/utils/st_components.py b/czsc/utils/st_components.py
index 22906bb26..ea876124a 100644
--- a/czsc/utils/st_components.py
+++ b/czsc/utils/st_components.py
@@ -1042,15 +1042,15 @@ def show_event_return(df, factor, **kwargs):
- sub_title: str, 子标题
- max_overlap: int, 事件最大重叠次数
+ - max_unique: int, 因子独立值最大数量
"""
sub_title = kwargs.get("sub_title", "事件收益率特征")
- default_max_overlap = kwargs.get("max_overlap", 2)
-
+ max_unique = kwargs.get("max_unique", 20)
if sub_title:
st.subheader(sub_title, divider="rainbow")
- if df[factor].nunique() > 20:
+ if df[factor].nunique() > max_unique:
st.warning(f"因子分布过于离散,无法进行分析,请检查!!!因子独立值数量:{df[factor].nunique()}")
return
@@ -1065,7 +1065,7 @@ def show_event_return(df, factor, **kwargs):
)
sdt = pd.to_datetime(c2.date_input("开始时间", value=df["dt"].min()))
edt = pd.to_datetime(c3.date_input("结束时间", value=df["dt"].max()))
- max_overlap = c4.number_input("最大重叠次数", value=default_max_overlap, min_value=1, max_value=20)
+ max_overlap = c4.number_input("最大重叠次数", value=5, min_value=1, max_value=20)
df[factor] = df[factor].astype(str)
df = czsc.overlap(df, factor, new_col="overlap", max_overlap=max_overlap)
@@ -1361,3 +1361,116 @@ def show_symbols_corr(df, factor, target="n1b", method="pearson", **kwargs):
fig_title = kwargs.get("fig_title", f"{factor} 在品种上的相关性分布")
fig = px.bar(dfr, x="symbol", y="corr", title=fig_title, orientation="v")
st.plotly_chart(fig, use_container_width=True)
+
+
+def show_czsc_trader(trader: czsc.CzscTrader, max_k_num=300, **kwargs):
+ """显示缠中说禅交易员详情
+
+ :param trader: CzscTrader 对象
+ :param max_k_num: 最大显示 K 线数量
+ :param kwargs: 其他参数
+ """
+ from czsc.utils.ta import MACD
+
+ sub_title = kwargs.get("sub_title", "缠中说禅交易员详情")
+ if sub_title:
+ st.subheader(sub_title, divider="rainbow")
+
+ if not trader.freqs or not trader.kas or not trader.positions:
+ st.error("当前 trader 没有回测数据")
+ return
+
+ freqs = czsc.freqs_sorted(trader.freqs)
+ st.write(f"交易品种: {trader.symbol}")
+ tabs = st.tabs(freqs + ["策略详情"])
+
+ for freq, tab in zip(freqs, tabs[:-1]):
+
+ c = trader.kas[freq]
+ sdt = c.bars_raw[-max_k_num].dt if len(c.bars_raw) > max_k_num else c.bars_raw[0].dt
+ df = pd.DataFrame(c.bars_raw)
+ df["DIFF"], df["DEA"], df["MACD"] = MACD(df["close"], fastperiod=12, slowperiod=26, signalperiod=9)
+
+ df = df[df["dt"] >= sdt].copy()
+ kline = czsc.KlineChart(n_rows=3, row_heights=(0.5, 0.3, 0.2), title="", width="100%", height=800)
+ kline.add_kline(df, name="")
+
+ if len(c.bi_list) > 0:
+ bi = pd.DataFrame(
+ [{"dt": x.fx_a.dt, "bi": x.fx_a.fx} for x in c.bi_list]
+ + [{"dt": c.bi_list[-1].fx_b.dt, "bi": c.bi_list[-1].fx_b.fx}]
+ )
+ fx = pd.DataFrame([{"dt": x.dt, "fx": x.fx} for x in c.fx_list])
+ fx = fx[fx["dt"] >= sdt]
+ bi = bi[bi["dt"] >= sdt]
+ kline.add_scatter_indicator(
+ fx["dt"],
+ fx["fx"],
+ name="分型",
+ row=1,
+ line_width=1.2,
+ visible=True,
+ mode="lines",
+ line_dash="dot",
+ marker_color="white",
+ )
+ kline.add_scatter_indicator(bi["dt"], bi["bi"], name="笔", row=1, line_width=1.5)
+
+ kline.add_sma(df, ma_seq=(5, 20, 60), row=1, visible=False, line_width=1)
+ kline.add_vol(df, row=2, line_width=1)
+ kline.add_macd(df, row=3, line_width=1)
+
+ # 在基础周期上绘制交易信号
+ if freq == trader.base_freq:
+ for pos in trader.positions:
+ bs_df = pd.DataFrame([x for x in pos.operates if x["dt"] >= sdt])
+ if bs_df.empty:
+ continue
+
+ open_ops = [czsc.Operate.LO, czsc.Operate.SO]
+ bs_df["tag"] = bs_df["op"].apply(lambda x: "triangle-up" if x in open_ops else "triangle-down")
+ bs_df["color"] = bs_df["op"].apply(lambda x: "red" if x in open_ops else "white")
+
+ kline.add_scatter_indicator(
+ bs_df["dt"],
+ bs_df["price"],
+ name=pos.name,
+ text=bs_df["op_desc"],
+ row=1,
+ mode="markers",
+ marker_size=15,
+ marker_symbol=bs_df["tag"],
+ marker_color=bs_df["color"],
+ visible=False,
+ hover_template="价格: %{y:.2f}
时间: %{x}
操作: %{text}",
+ )
+
+ with tab:
+ config = {
+ "scrollZoom": True,
+ "displayModeBar": True,
+ "displaylogo": False,
+ "modeBarButtonsToRemove": [
+ "toggleSpikelines",
+ "select2d",
+ "zoomIn2d",
+ "zoomOut2d",
+ "lasso2d",
+ "autoScale2d",
+ "hoverClosestCartesian",
+ "hoverCompareCartesian",
+ ],
+ }
+ st.plotly_chart(kline.fig, use_container_width=True, config=config)
+
+ with tabs[-1]:
+ with st.expander("查看最新信号", expanded=False):
+ if len(trader.s):
+ s = {k: v for k, v in trader.s.items() if len(k.split("_")) == 3}
+ st.write(s)
+ else:
+ st.warning("当前没有信号配置信息")
+ for pos in trader.positions:
+ st.divider()
+ st.write(pos.name)
+ st.json(pos.dump(with_data=False))
diff --git a/examples/signals_dev/fenlei.py b/examples/signals_dev/fenlei.py
index d6787cceb..c8f54fe50 100644
--- a/examples/signals_dev/fenlei.py
+++ b/examples/signals_dev/fenlei.py
@@ -6,5 +6,5 @@
bars = research.get_raw_bars("000001.SH", "15分钟", "20101101", "20210101", fq="前复权")
-signals_config = [{"name": "czsc.signals.cxt_second_bs_V240524", "freq": "60分钟", "w": 9}]
+signals_config = [{"name": "czsc.signals.bar_decision_V240616", "freq": "60分钟"}]
czsc.check_signals_acc(bars, signals_config=signals_config, height="780px", delta_days=5) # type: ignore
diff --git a/examples/signals_dev/bar_classify_V240606.py b/examples/signals_dev/merged/bar_classify_V240606.py
similarity index 100%
rename from examples/signals_dev/bar_classify_V240606.py
rename to examples/signals_dev/merged/bar_classify_V240606.py
diff --git a/examples/signals_dev/bar_classify_V240607.py b/examples/signals_dev/merged/bar_classify_V240607.py
similarity index 100%
rename from examples/signals_dev/bar_classify_V240607.py
rename to examples/signals_dev/merged/bar_classify_V240607.py
diff --git a/examples/signals_dev/bar_decision_V240608.py b/examples/signals_dev/merged/bar_decision_V240608.py
similarity index 100%
rename from examples/signals_dev/bar_decision_V240608.py
rename to examples/signals_dev/merged/bar_decision_V240608.py
diff --git a/examples/signals_dev/merged/bar_td9_V240616.py b/examples/signals_dev/merged/bar_td9_V240616.py
new file mode 100644
index 000000000..480770965
--- /dev/null
+++ b/examples/signals_dev/merged/bar_td9_V240616.py
@@ -0,0 +1,93 @@
+from collections import OrderedDict
+
+import numpy as np
+
+from copy import deepcopy
+from czsc.analyze import CZSC
+from czsc.utils import create_single_signal, get_sub_elements
+
+
+def bar_td9_V240616(c: CZSC, **kwargs) -> OrderedDict:
+ """神奇九转计数
+
+ 参数模板:"{freq}_神奇九转N{n}_BS辅助V240616"
+
+ **信号逻辑:**
+
+ 1. 当前收盘价大于前4根K线的收盘价,+1,否则-1
+ 2. 如果最后一根K线为1,且连续值计数大于等于N,卖点;如果最后一根K线为-1,且连续值计数小于等于-N,买点
+
+ **信号列表:**
+
+ - Signal('60分钟_神奇九转N9_BS辅助V240616_买点_9转_任意_0')
+ - Signal('60分钟_神奇九转N9_BS辅助V240616_卖点_9转_任意_0')
+
+ :param c: CZSC对象
+ :param kwargs:
+
+ - n: int, default 9, 连续转折次数
+
+ :return: 信号识别结果
+ """
+ n = int(kwargs.get("n", 9))
+
+ freq = c.freq.value
+ k1, k2, k3 = f"{freq}_神奇九转N{n}_BS辅助V240616".split("_")
+ v1 = "其他"
+
+ # 更新缓存
+ cache_key = "bar_td9_V240616"
+ for i, bar in enumerate(c.bars_raw):
+ if i < 4 or hasattr(bar.cache, cache_key):
+ continue
+
+ if bar.close > c.bars_raw[i - 4].close:
+ bar.cache[cache_key] = 1
+ elif bar.close < c.bars_raw[i - 4].close:
+ bar.cache[cache_key] = -1
+ else:
+ bar.cache[cache_key] = 0
+
+ if len(c.bars_raw) < 30 + n:
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
+
+ v2 = "任意"
+ bars = get_sub_elements(c.bars_raw, di=1, n=n * 2)
+ if bars[-1].cache[cache_key] == 1:
+ count = 0
+ for bar in bars[::-1]:
+ if bar.cache[cache_key] != 1:
+ break
+ count += 1
+ if count >= n:
+ v1 = "卖点"
+ v2 = f"{count}转"
+
+ elif bars[-1].cache[cache_key] == -1:
+ count = 0
+ for bar in bars[::-1]:
+ if bar.cache[cache_key] != -1:
+ break
+ count += 1
+ if count >= n:
+ v1 = "买点"
+ v2 = f"{count}转"
+
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
+
+
+def check():
+ from czsc.connectors import research
+ from czsc.traders.base import check_signals_acc
+
+ symbols = research.get_symbols("A股主要指数")
+ bars = research.get_raw_bars(symbols[0], "15分钟", "20181101", "20210101", fq="前复权")
+
+ signals_config = [
+ {"name": bar_td9_V240616, "freq": "60分钟"},
+ ]
+ check_signals_acc(bars, signals_config=signals_config, height="780px", delta_days=5) # type: ignore
+
+
+if __name__ == "__main__":
+ check()
diff --git a/examples/signals_dev/merged/cci_decision_V240620.py b/examples/signals_dev/merged/cci_decision_V240620.py
new file mode 100644
index 000000000..f3f772f2d
--- /dev/null
+++ b/examples/signals_dev/merged/cci_decision_V240620.py
@@ -0,0 +1,64 @@
+import numpy as np
+from collections import OrderedDict
+from czsc.analyze import CZSC, BI, Direction
+from czsc.signals.tas import update_cci_cache
+from czsc.utils import create_single_signal, get_sub_elements
+from loguru import logger
+
+
+def cci_decision_V240620(c: CZSC, **kwargs) -> OrderedDict:
+ """根据CCI指标逆势用法,判断买卖决策区域
+
+ 参数模板:"{freq}_N{n}CCI_决策区域V240620"
+
+ **信号逻辑:**
+
+ 取最近N根K线,如果最小的CCI值小于 -100,开多;如果最大的CCI值大于 100,开空。
+
+ **信号列表:**
+
+ - Signal('15分钟_N4CCI_决策区域V240620_开多_2次_任意_0')
+ - Signal('15分钟_N4CCI_决策区域V240620_开多_1次_任意_0')
+ - Signal('15分钟_N4CCI_决策区域V240620_开空_1次_任意_0')
+ - Signal('15分钟_N4CCI_决策区域V240620_开空_2次_任意_0')
+
+ :param c: CZSC对象
+ :param kwargs: 无
+ :return: 信号识别结果
+ """
+ n = int(kwargs.get("n", 2))
+
+ freq = c.freq.value
+ k1, k2, k3 = f"{freq}_N{n}CCI_决策区域V240620".split("_")
+ v1 = "其他"
+ cache_key = update_cci_cache(c, timeperiod=14)
+ if len(c.bars_raw) < 100:
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
+
+ cci_seq = [x.cache[cache_key] for x in c.bars_raw[-n:]]
+ short_cci = [x for x in cci_seq if x > 100]
+ long_cci = [x for x in cci_seq if x < -100]
+
+ v2 = "任意"
+ if min(cci_seq) < -100:
+ v1 = "开多"
+ v2 = f"{len(long_cci)}次"
+ if max(cci_seq) > 100:
+ v1 = "开空"
+ v2 = f"{len(short_cci)}次"
+ return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
+
+
+def check():
+ from czsc.connectors import research
+ from czsc.traders.base import check_signals_acc
+
+ symbols = research.get_symbols("A股主要指数")
+ bars = research.get_raw_bars(symbols[0], "15分钟", "20181101", "20210101", fq="前复权")
+
+ signals_config = [{"name": cci_decision_V240620, "freq": "15分钟", "n": 4}]
+ check_signals_acc(bars, signals_config=signals_config, height="780px", delta_days=5) # type: ignore
+
+
+if __name__ == "__main__":
+ check()
diff --git a/examples/signals_dev/cxt_bs_V240526.py b/examples/signals_dev/merged/cxt_bs_V240526.py
similarity index 100%
rename from examples/signals_dev/cxt_bs_V240526.py
rename to examples/signals_dev/merged/cxt_bs_V240526.py
diff --git a/examples/signals_dev/cxt_bs_V240527.py b/examples/signals_dev/merged/cxt_bs_V240527.py
similarity index 100%
rename from examples/signals_dev/cxt_bs_V240527.py
rename to examples/signals_dev/merged/cxt_bs_V240527.py
diff --git a/examples/signals_dev/cxt_decision_V240526.py b/examples/signals_dev/merged/cxt_decision_V240526.py
similarity index 100%
rename from examples/signals_dev/cxt_decision_V240526.py
rename to examples/signals_dev/merged/cxt_decision_V240526.py
diff --git a/examples/signals_dev/cxt_decision_V240612.py b/examples/signals_dev/merged/cxt_decision_V240612.py
similarity index 100%
rename from examples/signals_dev/cxt_decision_V240612.py
rename to examples/signals_dev/merged/cxt_decision_V240612.py
diff --git a/examples/signals_dev/cxt_decision_V240613.py b/examples/signals_dev/merged/cxt_decision_V240613.py
similarity index 100%
rename from examples/signals_dev/cxt_decision_V240613.py
rename to examples/signals_dev/merged/cxt_decision_V240613.py
diff --git a/examples/signals_dev/cxt_decision_V240614.py b/examples/signals_dev/merged/cxt_decision_V240614.py
similarity index 100%
rename from examples/signals_dev/cxt_decision_V240614.py
rename to examples/signals_dev/merged/cxt_decision_V240614.py
diff --git a/examples/signals_dev/cxt_overlap_V240526.py b/examples/signals_dev/merged/cxt_overlap_V240526.py
similarity index 100%
rename from examples/signals_dev/cxt_overlap_V240526.py
rename to examples/signals_dev/merged/cxt_overlap_V240526.py
diff --git a/examples/signals_dev/cxt_overlap_V240612.py b/examples/signals_dev/merged/cxt_overlap_V240612.py
similarity index 100%
rename from examples/signals_dev/cxt_overlap_V240612.py
rename to examples/signals_dev/merged/cxt_overlap_V240612.py
diff --git a/examples/signals_dev/signal_match.py b/examples/signals_dev/signal_match.py
index cbf6a5f45..ddb48eb03 100644
--- a/examples/signals_dev/signal_match.py
+++ b/examples/signals_dev/signal_match.py
@@ -45,7 +45,7 @@
conf = sp.parse(signals_seq)
parsed_name = {x["name"] for x in conf}
print(f"total signal functions: {len(sp.sig_name_map)}; parsed: {len(parsed_name)}")
- # total signal functions: 218; parsed: 218
+ # total signal functions: 241; parsed: 241
# 测试信号配置生成信号
from czsc import generate_czsc_signals, get_signals_freqs, get_signals_config
diff --git a/examples/test_offline/test_weight_backtest.py b/examples/test_offline/test_weight_backtest.py
index 1657ebbd3..a7be4e278 100644
--- a/examples/test_offline/test_weight_backtest.py
+++ b/examples/test_offline/test_weight_backtest.py
@@ -1,10 +1,11 @@
import sys
+
sys.path.insert(0, ".")
sys.path.insert(0, "..")
import czsc
import pandas as pd
-assert czsc.WeightBacktest.version == "V231126"
+assert czsc.WeightBacktest.version == "V240627"
def run_by_weights():
@@ -12,11 +13,19 @@ def run_by_weights():
dfw = pd.read_feather(r"C:\Users\zengb\Downloads\weight_example.feather")
wb = czsc.WeightBacktest(dfw, digits=1, fee_rate=0.0002, n_jobs=1)
# wb = czsc.WeightBacktest(dfw, digits=1, fee_rate=0.0002)
+ dailys = wb.dailys
+ print(wb.stats)
+ print(wb.alpha_stats)
+ print(wb.bench_stats)
+
+ # 计算等权组合的超额
+ df1 = dailys.groupby("date").agg({"return": "mean", "n1b": "mean"})
+ df1["alpha"] = df1["return"] - df1["n1b"]
# ------------------------------------------------------------------------------------
# 查看绩效评价
# ------------------------------------------------------------------------------------
- print(wb.results['绩效评价'])
+ print(wb.results["绩效评价"])
# {'开始日期': '20170103',
# '结束日期': '20230731',
# '年化': 0.093, # 品种等权之后的年化收益率
@@ -41,5 +50,5 @@ def run_by_weights():
wb.report(res_path=r"C:\Users\zengb\Desktop\231005\weight_example")
-if __name__ == '__main__':
+if __name__ == "__main__":
run_by_weights()
diff --git a/examples/tushare_data_client.py b/examples/tushare_data_client.py
new file mode 100644
index 000000000..c37a3645a
--- /dev/null
+++ b/examples/tushare_data_client.py
@@ -0,0 +1,24 @@
+# https://s0cqcxuy3p.feishu.cn/wiki/OpxqwUjdaifQq9kigCUcIWeonsg
+import czsc
+
+# 首次使用需要设置 Tushare token,用于获取数据
+# czsc.set_url_token(token="your tushare token", url="https://api.tushare.pro")
+
+# 也可以在初始化 DataClient 时设置 token;不推荐直接在代码中写入 token
+# dc = czsc.DataClient(url="https://api.tushare.pro", cache_path="~/czsc", token="your tushare token", timeout=300)
+
+# 设置过 token 后,可以直接初始化 DataClient,不需要再次设置 token
+# cache_path 用于设置缓存路径,后面的缓存文件会保存在该路径下
+pro = czsc.DataClient(url="https://api.tushare.pro", cache_path=r"D:\.tushare_cache", timeout=300)
+
+# 创建 pro 对象后,可以直接使用 Tushare 数据接口,与 Tushare 官方接口一致
+# 首次调用会自动下载数据并缓存,后续调用,如果参数没有变化,会直接从缓存读取
+df1 = pro.stock_basic(exchange="", list_status="L", fields="ts_code,symbol,name,area,industry,list_date")
+
+# 再次执行同样参数的查询
+df2 = pro.stock_basic(exchange="", list_status="L", fields="ts_code,symbol,name,area,industry,list_date")
+
+# 如果需要刷新数据,可以设置 ttl 参数,单位秒;ttl=-1 表示不过期;ttl=0 表示每次都重新下载
+df3 = pro.stock_basic(exchange="", list_status="L", fields="ts_code,symbol,name,area,industry,list_date", ttl=0)
+
+# df = pro.daily(trade_date="20240614")