跪拜 Guibai
← Back to the summary

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 t to toggle), Chinese/English UI toggle (press l), shortcut hints (press h), and defaults to an all-English UI to strengthen the 'at work' disguise.

Insert image description here

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:

So I started writing.

Technology Choices

The requirements were clear:

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:

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:

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:

  1. 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+)=".
  2. 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.
  3. 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.
  4. 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:

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:

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

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

廾匸22

👍