# -*- coding: utf-8 -*-
"""
简化和重构后的同花顺交易相关数据处理脚本
主要功能:
1. 定期从 Supermind API 获取指定账户的资产和持仓信息,并存入 Redis。
2. 监听 Redis 中的交易信号列表,并将新信号写入本地文件后上传。
依赖库:
- redis_api (假设是本地封装的 Redis 操作库)
- supermind (包含 api 和 mod.tradeapi.api)
- ts_api (假设是本地封装的 Tushare 或类似数据接口)
- pandas
- os
- time
- threading
假设存在一个名为 `disp` 的辅助函数,用于获取格式化的中文日期/时间字符串。
假设 `redis_api`, `ts_api` 已正确配置和实现。
"""
import os
import time
import threading
import pandas as pd
from redis_api import redis_api # 假设的 Redis 操作库
from supermind.api import get_api_usage, download_file, upload_file # Supermind 基础 API
from supermind.mod.tradeapi.api import TradeAPI, TradeCredit # Supermind 交易 API
from ts_api import ts_api # 假设的 Tushare 封装
# --- 全局初始化与配置 ---
# 检查 Supermind API 使用情况
try:
get_api_usage()
except Exception as e:
print(f"检查 Supermind API 使用情况失败: {e}")
# 初始化 Redis 连接 (假设 dbG=2 用于特定数据库)
try:
redisD = redis_api(dbG=2)
print("Redis 连接成功 (dbG=2)")
except Exception as e:
print(f"初始化 Redis 连接失败: {e}")
# 可能需要退出或进行错误处理
exit()
# 初始化 Tushare API (用于获取交易日历)
try:
ts = ts_api()
# 获取2023年至今的交易日期,假设 disp 函数存在且能格式化
# 注意:原始代码中 disp(strTime='日期_') 的具体格式未知,这里假设为 'YYYYMMDD'
# dateD = ts.get_date('20230101', disp(strTime='日期_'))
# 为了代码能独立运行,我们先用 Tushare 的标准方法获取,后续可能需要调整 disp
dateD = ts.get_trade_cal(start_date='20230101', end_date=time.strftime('%Y%m%d'), is_open=1)
dateD = dateD[['cal_date']].rename(columns={'cal_date': 'date'}) # 提取交易日期列并重命名
print("交易日历获取成功")
except Exception as e:
print(f"初始化 Tushare API 或获取交易日历失败: {e}")
# 如果交易日历是核心功能,可能需要退出
dateD = pd.DataFrame({'date': []}) # 提供空 DataFrame 以免后续代码出错
def get_date_d1d2(trade_dates_df):
"""
获取当前日期和上一个交易日日期。
Args:
trade_dates_df (pd.DataFrame): 包含 'date' 列的交易日历 DataFrame (格式 YYYYMMDD)。
Returns:
tuple: (d1, d2),d1 是当天或之前的最后一个交易日,d2 是 d1 之前的交易日。
如果找不到足够的日期,可能会返回相同日期。
"""
try:
# 假设 disp 函数返回 'YYYYMMDD' 格式
dateNow = time.strftime('%Y%m%d') # 使用当前日期代替 disp
# 筛选出不晚于今天的交易日
valid_dates = trade_dates_df[trade_dates_df['date'] <= dateNow]['date'].values
if len(valid_dates) >= 2:
d1 = valid_dates[-1]
d2 = valid_dates[-2]
elif len(valid_dates) == 1:
d1 = valid_dates[-1]
d2 = valid_dates[-1] # 如果只有一个,d2 等于 d1
else:
# 没有找到合适的交易日,返回今天作为备选
d1 = dateNow
d2 = dateNow
print("警告: 未能在交易日历中找到今天或之前的交易日。")
return d1, d2
except Exception as e:
print(f"获取 d1, d2 日期时出错: {e}")
# 返回当前日期作为备选
today = time.strftime('%Y%m%d')
return today, today
# 获取当前和上一交易日
d1, d2 = get_date_d1d2(dateD)
print(f"当前交易日 (d1): {d1}, 上一交易日 (d2): {d2}")
# --- 定义数据列名 ---
# 这些列表定义了从不同来源读取或写入数据时 DataFrame 的列名
# 账户资产信息列名
ASSET_COLS = ['时间', '净资产', '总资产', '可用资金']
# 持仓信息列名
POSITION_COLS = ['时间', '证券代码', '证券数量', '可卖数量', '成本价', '当前价']
# 委托/订单信息列名 (此脚本中未使用,但保留定义以备将来使用)
# ORDER_COLS = ['时间','证券代码','证券名称','委托编号','方向','委托时间','状态','委托数量','委托价格', '成交数量','成交价格']
# --- 账户配置 ---
# 需要监控的账户 ID 列表
# 注意:区分普通账户和信用账户,因为它们使用不同的 API 类
NORMAL_ACCOUNTS = ['56555', '69069316', '69085414', '101345718', '101702950', '69085393', '62638225']
CREDIT_ACCOUNTS = ['4144'] # 假设这是信用账户
# --- Supermind Trade API 初始化 ---
trade_api_instances = {}
try:
for account_id in NORMAL_ACCOUNTS:
trade_api_instances[account_id] = TradeAPI(account_id)
print(f"普通账户 TradeAPI 初始化成功: {account_id}")
for account_id in CREDIT_ACCOUNTS:
trade_api_instances[account_id] = TradeCredit(account_id) # 信用账户使用 TradeCredit
print(f"信用账户 TradeCredit 初始化成功: {account_id}")
except Exception as e:
print(f"初始化 Supermind Trade API 实例时出错: {e}")
# 根据需要决定是否继续
# --- 核心类定义 ---
class ThsTraderHelper:
"""
同花顺智能交易辅助类,用于管理账户数据获取和交易信号处理。
"""
def __init__(self, trade_apis, redis_conn, today_date):
"""
初始化辅助类。
Args:
trade_apis (dict): 包含账户ID到TradeAPI/TradeCredit实例的字典。
redis_conn: Redis 连接实例。
today_date (str): 当天日期 (YYYYMMDD),用于 Redis key。
"""
self.trade_apis = trade_apis
self.redisD = redis_conn
self.d1 = today_date # 保存当天日期,用于 Redis hset
self.dataG = {} # 用于存储运行时状态,如信号列表长度
self.posGA = {} # 用于缓存账户的持仓信息,减少不必要的 Redis写入
self.accounts_to_monitor = list(trade_apis.keys()) # 获取所有需要监控的账户ID
# 标记是否首次运行持仓检查,用于首次强制写入 Redis
self.first_pos_check = {acc_id: True for acc_id in self.accounts_to_monitor}
def _get_current_time_str(self):
"""获取当前格式化的日期时间字符串 (模拟 disp 函数)"""
# 假设 disp(strTime='日期时间') 返回 'YYYY-MM-DD HH:MM:SS'
return time.strftime('%Y-%m-%d %H:%M:%S')
def _get_current_date_str(self):
"""获取当前格式化的日期字符串 (模拟 disp 函数)"""
# 假设 disp(strTime='日期_') 返回 'YYYYMMDD'
return time.strftime('%Y%m%d')
def fetch_and_store_account_data(self):
"""
定期获取并存储所有监控账户的资产和持仓数据到 Redis。
"""
last_check_time = time.time()
print("启动账户数据获取线程...")
while True:
now = time.time()
# 每 5 分钟强制更新一次持仓,或者首次运行时更新
force_pos_update = (now - last_check_time) > 300 # 300秒 = 5分钟
if force_pos_update:
last_check_time = now
print(f"{self._get_current_time_str()} - 触发持仓强制更新检查")
for account_id in self.accounts_to_monitor:
try:
if account_id not in self.trade_apis:
print(f"警告: 账户 {account_id} 没有对应的 Trade API 实例,跳过。")
continue
api = self.trade_apis[account_id]
# 1. 获取资产信息
portfolio_data = api.portfolio
# 兼容不同 API 可能返回的字段名 ('net_assets' 或 'total_value')
net_assets = portfolio_data.get('net_assets', portfolio_data.get('total_value', 0))
available_cash = portfolio_data.get('available_cash', 0)
# 格式化数据
asset_info = [[
self._get_current_time_str(),
round(net_assets / 10000, 2), # 单位:万元
round(net_assets / 10000, 2), # 总资产用净资产代替(根据原代码逻辑)
round(available_cash / 10000, 2) # 单位:万元
]]
df_asset = pd.DataFrame(asset_info, columns=ASSET_COLS)
# 存储到 Redis
redis_key_asset = f'asset_{account_id}'
redis_key_asset_hist = f'asset_his_{account_id}'
asset_dict_str = str(df_asset.to_dict(orient="records"))
self.redisD.set(redis_key_asset, asset_dict_str)
# 使用 hset 存储当日历史记录,键是日期 d1
self.redisD.hset(redis_key_asset_hist, self.d1, asset_dict_str)
# print(f"账户 {account_id} 资产信息已更新到 Redis") # 调试信息,可注释掉
# 2. 获取持仓信息 (仅在需要时或强制更新时)
should_update_pos = force_pos_update or self.first_pos_check[account_id]
if should_update_pos:
positions = api.positions
pos_list = []
for symbol, pos_details in positions.items():
# 确保持仓数量大于0
if pos_details.get('amount', 0) > 0:
pos_list.append({
'时间': self._get_current_time_str(),
'证券代码': pos_details.get('symbol', symbol),
'证券数量': pos_details.get('amount', 0),
'可卖数量': pos_details.get('available_amount', 0),
'成本价': pos_details.get('cost_basis', 0.0),
'当前价': pos_details.get('last_price', 0.0)
})
df_pos = pd.DataFrame(pos_list, columns=POSITION_COLS) if pos_list else pd.DataFrame(columns=POSITION_COLS)
# 比较当前持仓与缓存,仅在变化时或首次检查时写入 Redis
pos_summary_str = str(df_pos[['证券代码', '证券数量']].values.tolist())
cached_pos = self.posGA.get(account_id)
if self.first_pos_check[account_id] or cached_pos != pos_summary_str:
redis_key_pos = f'pos_{account_id}'
self.redisD.set(redis_key_pos, str(df_pos.to_dict(orient="records")))
self.posGA[account_id] = pos_summary_str # 更新缓存
if not self.first_pos_check[account_id]:
print(f"{self._get_current_time_str()} - 账户 {account_id} 持仓发生变化,已更新到 Redis。")
else:
print(f"{self._get_current_time_str()} - 账户 {account_id} 首次持仓信息已写入 Redis。")
self.first_pos_check[account_id] = False # 不再是首次检查
# else:
# print(f"账户 {account_id} 持仓无变化,跳过 Redis 写入。") # 调试信息
except Exception as ex:
print(f"{self._get_current_time_str()} - 处理账户 {account_id} 数据时出错: {ex}")
# 短暂休眠,避免过于频繁的 API 请求
time.sleep(5) # 轮询间隔调整为 5 秒
def _write_signal_to_file_and_upload(self, order_data, mode='a', separator='\n'):
"""
将单个订单信号写入本地文件,然后上传。
Args:
order_data (list or str): 订单数据。
mode (str): 文件写入模式 ('a' for append, 'w' for write).
separator (str): 行分隔符。
"""
signal_file = './ths_data/sig.txt' # 本地信号文件路径
os.makedirs(os.path.dirname(signal_file), exist_ok=True) # 确保目录存在
try:
# 格式化信号数据为字符串
if isinstance(order_data, list):
# 移除列表的中括号,元素间用逗号分隔
content_to_write = ", ".join(map(str, order_data))
else:
content_to_write = str(order_data)
# 写入文件
with open(signal_file, mode=mode, encoding='utf-8') as f:
if mode == 'a' and os.path.getsize(signal_file) > 0: # 追加模式且文件非空时加分隔符
f.write(separator)
f.write(content_to_write)
# print(f"信号已写入本地文件: {signal_file}") # 调试信息
# 上传文件到 Supermind (假设 path='' 表示上传到默认路径)
upload_file(signal_file, path='')
# print(f"信号文件已上传: {signal_file}") # 调试信息
except Exception as e:
print(f"{self._get_current_time_str()} - 写入或上传信号文件 {signal_file} 时出错: {e}")
def monitor_and_process_redis_signals(self):
"""
监听 Redis 列表中的新交易信号,并将其处理(写入文件并上传)。
"""
redis_signal_key = 'sig' + self._get_current_date_str() # Redis key: sigYYYYMMDD
print(f"启动 Redis 信号监听线程 (Key: {redis_signal_key})...")
# 初始化上次处理的列表长度
try:
last_list_count = self.redisD.llen(redis_signal_key)
self.dataG['list_count'] = last_list_count
print(f"初始 Redis 列表 '{redis_signal_key}' 长度: {last_list_count}")
# 首次运行时,写入一个虚拟信号(如果需要初始化文件)
if last_list_count == 0:
# 根据原代码逻辑,可能需要在启动时清空并写入一个标记性内容
dummy_signal = ['初始化标记', '000000.SZ', 0, 0, 0, 0, self._get_current_time_str()]
print("Redis 列表为空,写入初始标记信号到本地文件...")
# 使用 'w' 模式覆盖旧文件(如果有的话)
self._write_signal_to_file_and_upload(dummy_signal, mode='w', separator='')
else:
# 如果列表非空,确保本地文件存在且内容与云端同步(这里简化为仅上传一次最新信号)
# 这里可以优化为下载云端sig.txt与本地比较,或假设本地总是需要被云端覆盖
# 为了简化,我们只在检测到新信号时追加
pass
except Exception as e:
print(f"初始化 Redis 信号监听时出错 (Key: {redis_signal_key}): {e}")
self.dataG['list_count'] = 0 # 出错时设为0
while True:
try:
current_list_count = self.redisD.llen(redis_signal_key)
last_processed_count = self.dataG.get('list_count', 0)
if current_list_count > last_processed_count:
print(f"{self._get_current_time_str()} - 检测到 Redis 列表 '{redis_signal_key}' 中有 {current_list_count - last_processed_count} 条新信号。")
# 获取新增的信号 (从上次数目之后到列表末尾)
new_signals_raw = self.redisD.lrange(redis_signal_key, last_processed_count, -1)
new_signals = [s.decode('utf-8') for s in new_signals_raw] # 解码
# 处理并上传每一条新信号
for signal_str in new_signals:
print(f" 处理新信号: {signal_str}")
# 假设信号是字符串形式的列表或元组,尝试 eval 解析
# !!! 注意:eval 非常危险,如果来源不可信,应使用更安全的方法如 json.loads !!!
try:
# 假设信号字符串可以直接被 eval 解析成列表
order_data = eval(signal_str)
# 将解析后的信号追加写入文件并上传
self._write_signal_to_file_and_upload(order_data, mode='a', separator='\n')
except Exception as parse_ex:
print(f" 解析信号 '{signal_str}' 时出错: {parse_ex}. 跳过此信号。")
# 更新已处理的列表长度
self.dataG['list_count'] = current_list_count
except Exception as ex:
print(f"{self._get_current_time_str()} - 监听 Redis 信号 (Key: {redis_signal_key}) 时发生异常: {ex}")
# 每隔几秒检查一次
time.sleep(5) # 信号检查间隔
def start_all_threads(self):
"""启动所有后台任务线程。"""
print("准备启动后台线程...")
# 创建并启动账户数据获取线程
thread1 = threading.Thread(target=self.fetch_and_store_account_data, args=(), daemon=True)
thread1.start()
print("账户数据获取线程已启动。")
# 创建并启动 Redis 信号监听线程
thread2 = threading.Thread(target=self.monitor_and_process_redis_signals, args=(), daemon=True)
thread2.start()
print("Redis 信号监听线程已启动。")
print("所有后台线程已启动。主线程将等待 (或执行其他任务)。")
# 让主线程保持运行,以便 daemon 线程可以继续工作
while True:
time.sleep(60) # 主线程可以休眠或执行其他监控任务
# --- 主程序入口 ---
if __name__ == '__main__':
print("程序开始执行...")
# 实例化核心类
helper = ThsTraderHelper(
trade_apis=trade_api_instances,
redis_conn=redisD,
today_date=d1 # 传入当天日期
)
# 启动所有后台服务
helper.start_all_threads()
# 主线程可以阻塞在这里,或者执行其他前台任务
# 由于线程是 daemon=True,主线程退出时它们也会退出
# 如果希望主线程等待子线程(虽然这里不需要),可以使用 thread.join()
print("主程序初始化完成,后台任务运行中...")
# 在实际应用中,这里可能是一个无限循环或者等待某个退出信号
# time.sleep(3600) # 例如,运行一小时后退出
代码解释与重构说明:
-
依赖清理:
- 移除了
spider_api
的导入和使用,因为它在原代码中被ts_api
覆盖且未使用。 - 移除了
numpy
的直接导入,因为pandas
内部会处理numpy
,除非特定版本兼容性需要,否则直接导入非必需。 - 保留了
os
,time
,threading
,pandas
以及自定义的redis_api
,supermind
,ts_api
。
- 移除了
-
全局变量与配置:
- API 初始化 (
get_api_usage
,redisD
,ts
) 放在脚本开头,并增加了简单的try-except
块来提示初始化成功或失败。 - 交易日历获取 (
get_date_d1d2
) 被保留,用于获取d1
(当天或最近交易日) 和d2
(上一交易日)。d1
主要用于 Redis 的hset
键。增加了错误处理。 - 账户列表 (
NORMAL_ACCOUNTS
,CREDIT_ACCOUNTS
) 被明确定义在配置区,而不是依赖于下载文件,使代码更清晰、独立。 TradeAPI
和TradeCredit
的实例化基于上述明确的账户列表,存储在trade_api_instances
字典中,供后续使用。移除了原代码中冗余和令人困惑的trade_api
初始化逻辑。- 列名常量 (
ASSET_COLS
,POSITION_COLS
) 被定义,提高了可读性。
- API 初始化 (
-
类结构 (
ThsTraderHelper
):- 将核心逻辑封装在
ThsTraderHelper
类中,提高了代码的组织性。 __init__
方法接收必要的依赖(API实例、Redis连接、日期)并初始化内部状态(dataG
,posGA
)。posGA
(持仓缓存) 和first_pos_check
(首次检查标记) 被移到__init__
中作为实例属性,更符合面向对象的设计。- 移除了原代码中未使用的
is_time
方法和被注释掉的下载客户端数据run
方法。核心功能由fetch_and_store_account_data
替代。
- 将核心逻辑封装在
-
账户数据获取 (
fetch_and_store_account_data
):- 此方法取代了原代码中注释掉的
下载客户端数据run
和部分获取客户端数据run
的逻辑。 - 它直接使用
trade_api_instances
中的 Supermind API 对象 (api.portfolio
,api.positions
) 来获取实时数据,而不是依赖下载/读取本地文件。这更直接高效。 - 对所有配置的账户进行迭代处理。
- 资产处理: 获取资产信息,进行单位转换(万元),格式化为 DataFrame,然后存入 Redis 的
set
(最新值) 和hset
(当日历史值)。 - 持仓处理:
- 增加了
force_pos_update
逻辑(每5分钟)和first_pos_check
标记,以确保持仓定期刷新或在首次运行时写入 Redis。 - 获取持仓信息,过滤掉数量为0的持仓。
- 将当前持仓的关键信息(代码、数量)与
posGA
缓存进行比较。 - 仅在持仓发生变化或需要强制更新时,才将完整的持仓 DataFrame 存入 Redis (
set
) 并更新posGA
缓存。这大大减少了不必要的 Redis 写入操作。
- 增加了
- 增加了更详细的日志输出(打印消息)和错误处理。
- 轮询间隔设为 5 秒,可以根据实际需求调整。
- 此方法取代了原代码中注释掉的
-
信号处理 (
_write_signal_to_file_and_upload
,monitor_and_process_redis_signals
):_write_signal_to_file_and_upload
: 封装了将信号写入本地sig.txt
文件并上传到 Supermind 的逻辑。增加了目录检查 (os.makedirs
) 和文件写入模式处理(追加时加换行符)。monitor_and_process_redis_signals
:- 此方法取代了原代码的
读取信号下单run
。 - 监听 Redis 中特定格式的列表 (
sigYYYYMMDD
)。 - 使用
llen
和lrange
高效地获取新增的信号,避免重复处理。 - 警告: 仍然使用
eval()
来解析从 Redis 获取的信号字符串。这存在安全风险,如果信号来源不可控,强烈建议改用更安全的数据交换格式(如 JSON)和解析方法 (json.loads
)。 - 对每个新信号,调用
_write_signal_to_file_and_upload
进行处理。 - 增加了初始化逻辑:检查列表是否为空,如果是,则写入一个初始标记到文件(覆盖模式)。
- 包括了错误处理和日志记录。
- 轮询间隔设为 5 秒。
- 此方法取代了原代码的
-
线程管理 (
start_all_threads
):- 提供一个方法来启动两个核心功能的后台线程:账户数据获取和信号监听。
- 线程被设置为
daemon=True
,这意味着当主线程退出时,这些后台线程会自动终止。 - 主线程中使用
while True: time.sleep(60)
来保持运行,允许后台线程工作。在实际部署中,这部分可能需要更复杂的生命周期管理。
-
主程序 (
if __name__ == '__main__':
):- 清晰地展示了如何实例化
ThsTraderHelper
类并启动其服务。
- 清晰地展示了如何实例化
-
辅助函数 (
disp
):- 原代码依赖一个未提供的
disp
函数。重构后的代码用标准的time.strftime
替代了disp
来获取日期和时间字符串,并假设了其输出格式。如果disp
有特殊功能(如获取交易时间),则需要将其实现或替换为相应逻辑。
- 原代码依赖一个未提供的
总结:
重构后的代码更加结构化、模块化,易于理解和维护。它消除了冗余代码,明确了配置,并直接使用 API 获取数据,提高了效率和实时性。同时,通过缓存比较减少了不必要的 Redis 写入。错误处理和日志记录也得到了加强。主要的潜在问题是 eval()
的使用,应考虑替换为更安全的替代方案。