请继续优化交易备忘录代码:# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import os
import struct
import traceback
from collections import defaultdict
class TdxDataParser:
"""通达信数据解析器(支持自定义路径)"""
def __init__(self, tdx_path=None):
self.tdx_path = tdx_path or self._find_default_path()
self.stock_dict = {}
self.validate_tdx_path()
self.load_stock_info()
def _find_default_path(self):
"""尝试自动查找通达信安装路径"""
possible_paths = [
r"D:\new_tdx", r"C:\new_tdx",
r"D:\TdxW", r"C:\TdxW"
]
for path in possible_paths:
if os.path.exists(path):
return path
return None
def validate_tdx_path(self):
"""验证通达信路径及必要文件"""
if not self.tdx_path:
raise FileNotFoundError("未找到通达信安装目录,请手动选择")
required_files = [
"T0002/hq_cache/shm.tnf",
"T0002/hq_cache/szm.tnf",
"vipdoc/sh/lday",
"vipdoc/sz/lday"
]
for rel_path in required_files:
full_path = os.path.join(self.tdx_path, rel_path.replace("/", os.sep))
if not os.path.exists(full_path):
raise FileNotFoundError(f"缺失必要文件或目录:{full_path}")
if not os.access(full_path, os.R_OK):
raise PermissionError(f"无权限访问路径:{full_path}")
def load_stock_info(self):
"""加载股票代码表(带缓存)"""
self.stock_dict = {}
try:
# 使用缓存加速加载
cache_path = os.path.join(self.tdx_path, ".stock_cache")
if os.path.exists(cache_path):
with open(cache_path, "r", encoding="utf-8") as f:
for line in f:
code, name = line.strip().split("|")
self.stock_dict[code] = name
return
# 解析沪市代码表
self._parse_tnf_file(
os.path.join(self.tdx_path, "T0002", "hq_cache", "shm.tnf"),
market="SH"
)
# 解析深市代码表
self._parse_tnf_file(
os.path.join(self.tdx_path, "T0002", "hq_cache", "szm.tnf"),
market="SZ"
)
# 创建缓存
with open(cache_path, "w", encoding="utf-8") as f:
for code, name in self.stock_dict.items():
f.write(f"{code}|{name}\n")
except Exception as e:
raise RuntimeError(f"股票代码加载失败: {str(e)}")
def _parse_tnf_file(self, file_path, market):
"""解析shm/szm文件(带进度条)"""
try:
with open(file_path, "rb") as f:
while True:
block = f.read(288) # 每个记录块288字节
if not block or len(block) < 288:
break
# 解析股票代码(6字节ASCII)
code = block[0:6].decode("ascii").strip()
# 解析股票名称(16-32字节GBK编码)
name = block[16:32].decode("gbk").strip()
if code and name:
self.stock_dict[f"{market}{code}"] = name
except FileNotFoundError:
raise RuntimeError(f"未找到代码表文件: {file_path}")
def get_day_data(self, code):
"""读取日线数据(支持分页加载)"""
market = "sh" if code.startswith("SH") else "sz"
file_path = os.path.join(
self.tdx_path, "vipdoc", market, "lday",
f"{code.lower()}.day"
)
try:
data_list = []
with open(file_path, "rb") as f:
while True:
record = f.read(32) # 每次读取32字节
if not record:
break
# 使用提供的解析方法
unpacked = struct.unpack("<iffffiix", record)
data = {
"date": str(unpacked[0]),
"open": round(unpacked[1], 2),
"high": round(unpacked[2], 2),
"low": round(unpacked[3], 2),
"close": round(unpacked[4], 2),
"volume": unpacked[5],
"amount": unpacked[6]
}
data_list.append(data)
return data_list
except FileNotFoundError:
raise RuntimeError(f"未找到日线数据文件: {file_path}")
except Exception as e:
raise RuntimeError(f"日线数据读取失败: {str(e)}")
class TradeMemoApp:
"""交易备忘录主程序(优化版)"""
def __init__(self):
self.root = tk.Tk()
self.root.title("交易备忘录 - 通达信数据查看器 v2.0")
self.root.geometry("1200x800")
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
# 初始化解析器
self.tdx_parser = None
self.current_code = ""
self.data_cache = defaultdict(list) # 数据缓存
# 创建界面
self.create_widgets()
self.load_settings()
# 自动加载数据
if self.tdx_parser:
self.update_status("就绪 - 已加载通达信数据")
else:
self.select_tdx_path()
def create_widgets(self):
"""创建界面组件"""
# 样式设置
style = ttk.Style()
style.configure("Header.TLabel", font=("微软雅黑", 10, "bold"))
# 菜单栏
menubar = tk.Menu(self.root)
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="选择通达信路径", command=self.select_tdx_path)
file_menu.add_command(label="导出数据", command=self.export_data)
file_menu.add_separator()
file_menu.add_command(label="退出", command=self.on_close)
menubar.add_cascade(label="文件", menu=file_menu)
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label="关于", command=self.show_about)
menubar.add_cascade(label="帮助", menu=help_menu)
self.root.config(menu=menubar)
# 顶部区域
top_frame = ttk.Frame(self.root)
top_frame.pack(padx=10, pady=5, fill=tk.X)
# 路径显示
path_frame = ttk.LabelFrame(top_frame, text="通达信路径")
path_frame.pack(side=tk.LEFT, expand=True, fill=tk.X)
self.path_var = tk.StringVar()
self.path_entry = ttk.Entry(
path_frame,
textvariable=self.path_var,
state="readonly"
)
self.path_entry.pack(side=tk.LEFT, expand=True, fill=tk.X)
# 路径选择按钮
ttk.Button(
top_frame,
text="...",
width=3,
command=self.select_tdx_path
).pack(side=tk.LEFT, padx=5)
# 股票代码输入
code_frame = ttk.Frame(self.root)
code_frame.pack(padx=10, pady=5, fill=tk.X)
ttk.Label(code_frame, text="股票代码:", width=10).pack(side=tk.LEFT)
self.code_var = tk.StringVar()
self.code_entry = ttk.Entry(code_frame, textvariable=self.code_var, width=15)
self.code_entry.pack(side=tk.LEFT, padx=5)
self.code_entry.bind("<KeyRelease>", self.on_code_change)
self.name_label = ttk.Label(code_frame, text="股票名称", width=30)
self.name_label.pack(side=tk.LEFT)
# 数据表格
tree_frame = ttk.Frame(self.root)
tree_frame.pack(padx=10, pady=5, expand=True, fill=tk.BOTH)
# 创建带滚动条的表格
self.tree = ttk.Treeview(tree_frame, show="headings")
self.tree["columns"] = (
"日期", "开盘价", "最高价", "最低价", "收盘价", "成交量", "成交额"
)
# 配置各列
col_widths = {
"日期": 100, "开盘价": 80, "最高价": 80, "最低价": 80,
"收盘价": 80, "成交量": 100, "成交额": 120
}
for col in self.tree["columns"]:
self.tree.column(col, width=col_widths[col], anchor="center")
self.tree.heading(col, text=col)
# 滚动条
vsb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
hsb = ttk.Scrollbar(tree_frame, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
# 布局
self.tree.pack(side="left", fill="both", expand=True)
vsb.pack(side="right", fill="y")
hsb.pack(side="bottom", fill="x")
# 状态栏
self.status_bar = ttk.Label(self.root, text="就绪", relief=tk.SUNKEN)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def load_settings(self):
"""加载保存的设置"""
config_path = os.path.join(os.path.expanduser("~"), ".trade_memo_config")
if os.path.exists(config_path):
with open(config_path, "r") as f:
for line in f:
key, value = line.strip().split("=")
if key == "tdx_path" and os.path.exists(value):
self.tdx_path = value
self.path_var.set(value)
try:
self.tdx_parser = TdxDataParser(value)
except Exception as e:
messagebox.showerror("错误", f"加载配置路径失败: {str(e)}")
def save_settings(self):
"""保存设置"""
config_path = os.path.join(os.path.expanduser("~"), ".trade_memo_config")
with open(config_path, "w") as f:
f.write(f"tdx_path={self.tdx_path}\n")
def select_tdx_path(self):
"""选择通达信安装路径"""
path = filedialog.askdirectory(title="选择通达信安装目录")
if path:
try:
self.tdx_parser = TdxDataParser(path)
self.tdx_path = path
self.path_var.set(path)
self.save_settings()
self.update_status(f"已切换路径: {path}")
except Exception as e:
messagebox.showerror("错误", f"选择路径失败: {str(e)}")
def on_code_change(self, event=None):
"""股票代码变化处理"""
if not self.tdx_parser:
return
code = self.code_var.get().upper()
# 自动补全市场代码
if len(code) == 6:
if code.startswith("6"):
code = "SH" + code
elif code.startswith(("0", "3")):
code = "SZ" + code
self.code_var.set(code)
# 验证代码有效性
if len(code) < 8 or not code.startswith(("SH", "SZ")):
return
# 显示股票信息
name = self.tdx_parser.stock_dict.get(code, "未知代码")
self.name_label.config(text=name)
# 加载日线数据(使用缓存)
if code in self.tdx_parser.stock_dict:
if code in self.data_cache:
self.current_code = code
self.display_data(self.data_cache[code])
self.update_status(f"显示{code}缓存数据")
else:
self.current_code = code
self.load_day_data(code)
def load_day_data(self, code):
"""加载并缓存日线数据"""
try:
data = self.tdx_parser.get_day_data(code)
if data:
self.data_cache[code] = data
self.display_data(data[-30:]) # 显示最近30条
self.update_status(f"显示{code}最近{len(data[-30:])}条日线数据")
except Exception as e:
messagebox.showerror("错误", str(e))
def display_data(self, data):
"""显示数据到表格"""
# 清空现有数据
self.tree.delete(*self.tree.get_children())
# 显示数据
for item in data:
self.tree.insert("", 0, values=(
item["date"],
item["open"],
item["high"],
item["low"],
item["close"],
f"{item['volume']:,}",
f"{item['amount']:,}"
))
def update_status(self, message):
"""更新状态栏"""
self.status_bar.config(text=message)
def export_data(self):
"""导出数据为CSV"""
if not self.current_code or not self.data_cache.get(self.current_code):
messagebox.showwarning("警告", "请先加载数据")
return
file_path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")]
)
if file_path:
try:
with open(file_path, "w", encoding="utf-8-sig", newline="") as f:
f.write("日期,开盘价,最高价,最低价,收盘价,成交量,成交额\n")
for item in self.data_cache[self.current_code]:
f.write(f"{item['date']},{item['open']},{item['high']},{item['low']},{item['close']},{item['volume']},{item['amount']}\n")
messagebox.showinfo("成功", "数据导出成功")
except Exception as e:
messagebox.showerror("错误", f"导出失败: {str(e)}")
def show_about(self):
"""显示关于信息"""
messagebox.showinfo("关于", "交易备忘录 v2.0\n作者: AI助手\n基于通达信数据解析")
def on_close(self):
"""关闭事件处理"""
if messagebox.askokcancel("退出", "确定要退出程序吗?"):
self.root.destroy()
if __name__ == "__main__":
try:
app = TradeMemoApp()
app.root.mainloop()
except Exception as e:
messagebox.showerror("致命错误", f"程序启动失败: {str(e)}\n详情请查看日志。")
print("致命错误:", str(e))
print(traceback.format_exc())
最新发布