A Terminal Stock Ticker That Disguises Itself as a Docker Build
I Built a Terminal Stock Ticker — Press 'b' to Pretend You're Compiling Code
Project updated to v0.2.1: added position and P&L display (press
tto toggle), Chinese/English UI toggle (pressl), shortcut hints (pressh), and defaults to an all-English UI to strengthen the 'at work' disguise.
Here's how it started.
One afternoon at three o'clock, I switched over to my brokerage app for the fifth time, glanced at it, and switched back to my code. The motion was smooth, under a second. But the colleague at the next desk still noticed.
"How much are you down on stocks?"
I said I wasn't down. He didn't believe me. With a screen full of red and green, who would?
But the real problem wasn't whether I was losing money. It was that the act of watching the market is inherently public. Every time you switch windows, every time you stare at those red and green numbers, you're broadcasting to everyone: I am not working right now. Even if you really did just glance and go back to fixing a bug, that feeling of "getting caught" is deeply unpleasant.
I work in the terminal. Eight hours a day, I'm either in Vim or in tmux. My thought at the time was: if I could just watch the market inside the terminal, anyone walking by would only see code, and I wouldn't have to keep switching windows.
I looked around, and the existing solutions were all wrong:
- tushare/baostock: requires registration, requires a token. The data interface is built for quantitative analysis, not for a human watching the tape.
- Web versions of brokerage platforms: a browser tab sitting there with a title like "Market Quotes" is just fooling yourself.
- curl a request and grep it: it works, but honestly, you won't last three minutes staring at that.
So I started writing.
Technology Choices
The requirements were clear:
- Run in the terminal, no browser.
- No API Key, no registration.
- When someone walks by, they shouldn't be able to tell at a glance that you're watching stocks.
- Installation should be as simple as possible.
The third point was the most important. Plain text isn't enough—if it says "Kweichow Moutai 1690.00 +0.90%", anyone in the know will see what you're doing immediately. There had to be a one-key disguise mode.
So the final tech stack is:
- Python 3.10+ standard library plus three third-party packages
- Rich for the terminal UI (Live + Table)
- Sina Finance API as the data source (free, no key required)
- TOML for the configuration file
The whole project is called BossKey-Stock. That BossKey is the core feature.
Architecture
The codebase is small, split into a few modules:
bosskey_stock/
├── __main__.py CLI entry point
├── app.py Main loop + terminal control
├── data.py Sina data source
├── boss.py Boss mode disguise
├── config.py Config read/write
└── i18n.py Chinese/English string table (added in v0.2.1)
The CLI layer is nothing special—argparse routes a few subcommands. Config read/write just uses tomlkit to read and write ~/.bosskey.toml.
The interesting parts are terminal control and boss mode.
Rich Live
Rich comes with a Live component that maintains a "dynamic area" in the terminal, with content that can update in real-time. Under the hood, it uses the alternate screen buffer—the same mode vim and top use, where the terminal restores its previous content after the program exits.
Typical usage looks like this:
with Live(auto_refresh=False, screen=True) as live:
while True:
live.update(build_table(), refresh=True)
time.sleep(interval)
Written this way, the program is frozen during the refresh interval and can't handle keyboard input. So it needs a bit of reworking.
Non-blocking Keyboard Input
This is where I hit the most snags.
Why not use input()? input() is line-buffered—it waits for the user to press Enter before returning. But I needed single-key response: press b to switch modes instantly, press q to quit instantly, no Enter required.
Why not use curses? curses can do it, but it takes over the entire terminal and conflicts with Rich's Live component. Rich relies on low-level terminal control for colors and rendering; curses stepping in messes everything up. Two libraries fighting over terminal control, neither willing to yield.
The solution: operate on termios directly. Python's standard library provides termios and select, which allow precise control over terminal behavior. The core idea: only change the input configuration, leave the output side alone.
def _setup_tty(fd):
attrs = termios.tcgetattr(fd)
# Input side: disable ICANON (character-at-a-time mode), ECHO (no echo), ISIG (no signals)
attrs[3] &= ~(termios.ECHO | termios.ICANON | termios.ISIG)
# Output side: don't touch it, keep OPOST to ensure \n -> \r\n conversion works
termios.tcsetattr(fd, termios.TCSAFLUSH, attrs)
After this change, os.read(fd, 1) can read a single keystroke without needing Enter. Then the main loop uses select for a non-blocking check:
def _read_key(fd):
r, _, _ = select.select([fd], [], [], 0.5) # wait at most 0.5 seconds
if r:
return os.read(fd, 1).decode("utf-8", errors="replace")
return None
There's a detail here: select's timeout is set to 0.5 seconds, rather than using non-blocking mode. The reason is that the main loop also needs to do timed refreshes (pull market data every 3 seconds), so it can't truly block on read. After select returns, if it was a timeout (no keypress), execution continues to check whether data needs refreshing. If the user pressed a key, it handles the key logic.
This way the entire main loop runs in a single thread: keyboard listening + timed refresh + UI rendering. No background threads, no locks.
One easy pitfall: ICRNL. During debugging, I found a strange phenomenon: the Enter key (\r) was being converted to a newline (\n), causing key detection errors. This only happened on certain terminal emulators. Looking into it, the termios ICRNL flag was the culprit. This flag automatically converts received \r into \n. My solution was to turn it off:
attrs[0] &= ~(termios.ICRNL | termios.IXON) # ICRNL is the key one
Boss Mode
The implementation of boss mode is actually very simple. Pressing the b key toggles a boolean, and then different content is rendered:
if boss_mode:
live.update(boss.render(), refresh=True)
continue
boss.render() generates a simulated Docker build log. The core logic randomly selects some steps and combines them into output that looks like a legitimate build.
To make the log more convincing, I added some details:
- Docker build step numbering
- Occasional WARNING messages
- A Running time: MM:SS timer
- The log scrolls two lines per second (offsetting an array position by elapsed time)
This logic is all inside the BossGenerator class, which has a pre-generated pool of log lines, repeated 5 times and then cycled. The effect is pretty good—I showed another colleague at the company, and he said, "Messing with Docker again?"
I said yep, always messing with it.
Data Layer
The data source is Sina Finance's quote interface. No API Key needed, just one HTTP request:
GET https://hq.sinajs.cn/list=sh600519,sz000001
Referer: https://finance.sina.com.cn
The response is a JavaScript variable assignment, GBK encoded:
var hq_str_sh600519="Kweichow Moutai,1680.00,1675.00,1690.00,1700.00,1670.00,...";
The parsing logic isn't complicated: split by comma, grab the needed fields. But there are a few points to note:
- Code prefix conversion. Codes starting with 6 or 9 are Shanghai Stock Exchange (sh prefix); others are Shenzhen Stock Exchange (sz prefix). When extracting the code from the response line with regex, the pattern used is hq_str_[a-z]+(\d+)=".
- Volume formatting. The volume returned by Sina is in shares (not lots). One lot of A-shares equals 100 shares. Display must convert to lots.
- Trading session detection. There's no need to fetch data on non-trading days or during off hours. The detection logic: Monday through Friday, 9:30-11:30 or 13:00-15:00. Outside trading hours, it displays [After Hours] and shows the data source's last trade time.
- Offline handling. On network request timeout or exception, requests doesn't throw an exception but returns None. The main loop detects None and displays a yellow [Offline] indicator, while preserving the last successfully fetched data instead of clearing the table.
v0.2.x: Positions & P&L, Chinese/English Toggle, Shortcut Hints
After the first version could display quotes and disguise itself, I iterated two more versions this month (v0.2.0 / v0.2.1), with a few key updates:
Position and P&L Display
Just watching quotes isn't enough; a slacker also needs to know if they're making money. v0.2.0 added full position management:
- CLI subcommands
pos add/pos rm/pos list.pos addis interactive—multi-select stocks from the watchlist, enter share count and cost basis one by one, and they're automatically added to the watchlist. - In the TUI, press
tto cycle through columns for position, cost, position P&L (%), and today's P&L (%). - A bottom summary row aggregates total market value / total cost, total P&L (%), and today's P&L (%).
The P&L calculation is straightforward: position P&L = (current price − cost) × shares; today's P&L = price change × shares. P&L columns use independent red-for-profit, green-for-loss coloring, the reverse of the quote table, so you can see at a glance.
Chinese/English UI Toggle
v0.2.1 extracted all UI strings into i18n.py (a key → (en, zh) lookup table) and defaulted to English.
This default is deliberate: an English interface paired with boss mode makes the disguise complete—Chinese characters like "持仓" (positions) and "收益" (P&L) are instantly readable, while English (for non-native speakers) is harder to parse at a glance and looks more like "glancing at a log while coding."
There are three ways to switch, depending on need:
- Press
linside the TUI for an instant in-session toggle; - CLI global parameter
--lang {en,zh}for a one-time override; - The config file ~/.bosskey.toml's [display] lang setting for the initial language.
The boss mode log body stays as English Docker commands; only the bottom status bar is localized—after all, Docker logs are in English to begin with, and changing them in either language would actually blow the cover.
Shortcut Hints
Press h to toggle a bottom shortcut hint bar (hidden by default, to keep things minimal). The first time you use it and don't know what keys to press, hit h and everything is listed; once you're familiar, hit it again to hide.
I also cleaned up a redundancy along the way: the original "Today's P&L" mode had a TodayP/L% column whose value was identical to the base Chg% (percent change) column. Deleted it; mode 3 is now 14 columns.
The full set of shortcuts now:
| Key | Function |
|---|---|
b |
Boss mode / switch back to quotes |
t |
Toggle position / cost / position P&L / today's P&L columns |
l |
Toggle Chinese/English UI |
h |
Toggle shortcut hint bar |
r |
Manual refresh |
q / Ctrl+C |
Quit |
Beyond Slacking Off
Back to the original problem.
What this tool really solves isn't "being able to watch stocks." It's "watching stocks without causing psychological pressure for anyone else."
The concept of a boss key is actually very old—it goes back to the DOS era, where pressing F1 would turn a game screen into a legitimate Lotus 1-2-3 spreadsheet. What I've built just turns the terminal into a disguise tool.
Technically, there's nothing particularly profound here. But the process made me realize one thing: many needs that look like "slacking off" are fundamentally a mismatch with the current environment. A programmer works in the terminal for 8 hours and occasionally wants to glance at the market—that's perfectly normal. What's abnormal is that this behavior carries a negative social signal in the office—"he's not working hard."
A tool can't solve that problem, but at least it spares people a little embarrassment.
Installation
pip install bosskey-stock
Run bosskey to start. Requires Python 3.10+. Current version v0.2.1.
Related Links
- Project source: github.com/Angryshark128/bosskey-stock
- PyPI: pypi.org/project/bosskey-stock
- Usage instructions: README.md (includes three screenshots for normal / boss / demo modes, reproducible with scripts/make_screenshots.py)
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
👍