事情是这样的。
某天下午三点,我第五次切到同花顺看了一眼,又切回来继续写代码。动作很流畅,不到一秒。但隔壁工位的同事还是注意到了。
"你股票亏了多少?"
我说没亏。他不信。因为屏幕上一片红绿,你说没亏谁信啊。
但其实问题不是亏不亏,而是盯盘这件事本身是公开的。你每一次切屏、每一次盯着红绿数字看,都等于在告诉别人------我现在没在工作。哪怕你确实只是扫了一眼就回来改 bug 了,那种"被抓到"的感觉也很不爽。
我在终端里工作。一天八个小时,不是在 Vim 里就是在 tmux 里。我当时想的是:如果能在终端里看行情就好了,别人路过看到的全是代码,我也用不着来回切窗口了。
翻了翻,已有的方案都不太对劲:
- tushare/baostock:要注册、要 token、数据接口是给量化分析用的,不是给人盯盘用的。
- 同花顺/东方财富的 Web 版:一个浏览器标签页在那摆着,标题就写着"行情",掩耳盗铃。
- curl 请求然后 grep:能用,但这个体验,讲真,你盯不了三分钟。
于是我开始写了。
选型
需求很明确:
- 终端里跑,不要开浏览器
- 不要 API Key,不要注册
- 别人路过看一眼时,不能一眼看出是在看股票
- 安装尽量简单
第三点是最重要的。纯文本还不够------要是上面写着"贵州茅台 1690.00 +0.90%",懂行的看一眼就知道你在干嘛。必须有一个一键切换伪装模式的功能。
所以最后的选型是:
- Python 3.10+ 标准库 + 三个第三方包
- Rich 做终端 UI(Live + Table)
- Sina Finance API 做数据源(免费的,不需要 Key)
- TOML 做配置文件
整个项目叫 BossKey-Stock。这个 BossKey(老板键)才是核心功能。
架构
代码不多,500 行,拆了五个模块:
arduino
bosskey_stock/
├── __main__.py CLI 入口
├── app.py 主循环 + 终端控制
├── data.py Sina 数据源
├── boss.py 老板模式伪装
└── config.py 配置读写
CLI 层没什么好说的,argparse 路由几个子命令。配置读写就是 tomlkit 读写 ~/.bosskey.toml。
有意思的部分是终端控制和老板模式。
Rich Live
Rich 自带一个 Live 组件,可以在终端里维护一个"动态区域",内容可以实时更新。实现上它用了 alternate screen buffer------就是 vim、top 那种模式,程序退出后终端会恢复到之前的内容。
典型用法就是:
python
with Live(auto_refresh=False, screen=True) as live:
while True:
live.update(build_table(), refresh=True)
time.sleep(interval)
直接这样写的话,刷新间隔内程序是卡死的,没法处理键盘输入。所以需要一点改造。
非阻塞键盘输入
这也是踩坑最多的地方。
为什么不用 input()?
input() 是行缓冲的------它要等到用户按了回车才会返回。但我要的是单键响应:按 b 立刻切模式,按 q 立刻退出,不能等回车。
为什么不用 curses?
curses 可以做到,但它接管了整个终端,和 Rich 的 Live 组件有冲突。Rich 依赖底层的终端控制来做色彩和渲染,curses 一插手就乱套了。两个库抢终端控制权,谁都不让谁。
方案:直接操作 termios
Python 标准库提供了 termios 和 select,可以精确控制终端的行为。
核心思路是:只改输入的配置,不改输出侧。
python
def _setup_tty(fd):
attrs = termios.tcgetattr(fd)
# 输入侧:关掉 ICANON(逐键模式)、ECHO(不回显)、ISIG(不发信号)
attrs[3] &= ~(termios.ECHO | termios.ICANON | termios.ISIG)
# 输出侧:不动,保留 OPOST,确保 \n → \r\n 正常转换
termios.tcsetattr(fd, termios.TCSAFLUSH, attrs)
改完之后,os.read(fd, 1) 就能读到单个按键了,不需要回车。
然后主循环里用 select 做非阻塞检查:
python
def _read_key(fd):
r, _, _ = select.select([fd], [], [], 0.5) # 最多等 0.5 秒
if r:
return os.read(fd, 1).decode("utf-8", errors="replace")
return None
这里有个细节:select 的 timeout 设成 0.5 秒 ,而不是用非阻塞模式。原因是主循环里还要做定时刷新(每 3 秒拉一次行情),不能真的阻塞在 read 上。select 返回后如果是超时(没按键),就继续往下走,检查要不要刷新数据。如果用户按了键,就处理按键逻辑。
这样整个主循环就在一个线程里完成了:键盘监听 + 定时刷新 + UI 渲染。不需要后台线程,不需要锁。
一个容易踩的坑:ICRNL
在调试过程中发现一个奇怪的现象:回车键(\r)会被转换成换行(\n),导致按键检测出错。这个问题只在某些终端模拟器上出现。
查了一下,是 termios 的 ICRNL 标志在搞鬼。这个标志会把收到的 \r 自动转成 \n。我的解决方案是关掉它:
python
attrs[0] &= ~(termios.ICRNL | termios.IXON) # ICRNL 是关键
老板模式
老板模式的实现其实很简单。就是按 b 键时,切换一个布尔值,然后渲染不同的内容:
python
if boss_mode:
live.update(boss.render(), refresh=True)
continue
boss.render() 生成一段模拟的 Docker build 日志。核心逻辑就是随机选一些步骤,组合成看起来像正经构建的输出。
为了让日志更逼真,我加了一些细节:
- Docker 的构建步骤编号
- 偶尔的 WARNING 信息
Running time: MM:SS的计时器- 日志每秒滚动两行(通过时间来偏移数组位置)
这些逻辑都在 BossGenerator 类里,70 行代码,有个预生成的日志池子,5 倍重复后循环滚动。效果还不错------我让公司另一个同事看了看,他说"你又在折腾 Docker?"
我说对,一直在折腾。
数据层
数据源是 Sina Finance 的行情接口。不需要 API Key,只要一个 HTTP 请求:
arduino
GET https://hq.sinajs.cn/list=sh600519,sz000001
Referer: https://finance.sina.com.cn
返回的是 JavaScript 变量赋值,GBK 编码:
ini
var hq_str_sh600519="贵州茅台,1680.00,1675.00,1690.00,1700.00,1670.00,...";
解析逻辑不复杂,按逗号分割,取需要的字段。但有一些需要注意的点:
1. 代码前缀转换。 以 6 和 9 开头的代码是上交所(sh前缀),其他是深交所(sz前缀)。正则从响应行里提取代码时,用的是 hq_str_[a-z]+(\d+)=" 这个模式。
2. 成交量格式化。 Sina 返回的成交量单位是股(不是手)。A 股一手等于 100 股。显示时要转成手为单位:
python
if lots >= 100_000_000: # 一亿手以上
return f"{lots / 100_000_000:.2f}亿"
if lots >= 10_000: # 一万手以上
return f"{lots / 10_000:.2f}万"
return f"{lots:.0f}"
3. 交易时段检测。 没必要在非交易日和休息时间刷数据。判断逻辑是:周一到周五,9:30-11:30 或 13:00-15:00。非交易时段显示 [After Hours] 并展示数据源的最后成交时间。
4. 离线处理。 网络请求超时或异常时, requests 不会抛异常,而是返回 None。主循环里检测到 None 会显示 [Offline] 黄色提示,并且保留上一次拉到的数据,而不是清空表格。
python
result = fetch(codes)
if result is None:
offline = True
else:
offline = False
stocks_cache = result
摸鱼之外
回到最开始的问题。
这个工具解决的其实不是"能看股票",而是"看股票这件事不给别人带来心理压力"。
老板键这个概念其实很老了------从 DOS 时代就有,按一下 F1 把一个游戏界面变成一张正版的 Lotus 1-2-3 表格。我做的这个,只不过是把终端变成了伪装工具。
技术上没什么特别高深的。但这个过程让我想通了一件事:很多看似"偷懒"的需求,本质是对当前环境的一种不适应。程序员在终端里工作 8 小时,偶尔想看一眼行情,这是很正常的。不正常的是这个行为在办公室里带有负面的社会信号------"他不好好工作"。
工具解决不了这个问题,但至少让人少了一点尴尬。
项目地址 : github.com/Angryshark1...
安装方式 : pip install bosskey-stock,运行 bosskey 即可。需要 Python 3.10+。