I’m building an auto-trading bot on top of a Korean brokerage’s Open API (I’ll just call it “the broker” here — it’s a mainstream Korean stock trading platform with a REST API). Before touching real orders, I wanted to make sure authentication and market data were rock solid. That’s what Phase 0 was about.
Splitting into five files
I started with everything in one script. That fell apart fast, so I split it by responsibility.
| File | Role | Key contents |
|---|---|---|
| toss_common.py | Shared module | .env-based credential loading, OAuth2 token issuance, account number lookup, single/batch price quotes, a unified auth function, dual console+file logging |
| get_price.py | Price fetcher | Batch current-price lookup for a watchlist, printed to console |
| toss_portfolio.py | Portfolio dashboard | Holdings lookup, plus width-aware alignment for mixed Korean/English text using unicodedata.east_asian_width() |
| toss_test.py | Connection diagnostics | 3-stage smoke test: auth → auth again → raw holdings response |
| auto_trader.py | Trading engine | Price-watching loop, a triple safety net, a Telegram-notification skeleton, and retry/backoff for expired tokens and network errors |
The shared module came first
I didn’t want to copy-paste auth logic across every file, so toss_common.py handles credentials, token issuance, account lookup, and price quotes in one place. Every other file just calls authenticate(). Logging to both console and file turned out to be more useful than expected once I started chasing an intermittent 401.
Korean text alignment was more annoying than it should have been
toss_portfolio.py prints holdings in a table, and mixing Korean stock names with English/numeric fields wrecked the column alignment. Korean characters render at roughly double the width of Latin characters, and plain string-length math doesn’t know that. I ended up using unicodedata.east_asian_width() to compute actual display width, wrapped into three helpers — get_display_width, fit_to_width, and center_korean. Felt like overkill for console output, but the table is genuinely readable now.
The triple safety net — non-negotiable
A trading bot with a bug can lose real money fast, so auto_trader.py ships with three independent layers before it, so it never places a live order in this phase.
DRY_RUN = True # no live orders, period
TARGET_BUY_PRICE = 1 # placeholder target price, effectively unfillable
# the live order call is physically commented out
# response = requests.post(ORDER_URL, headers=headers, json=order_payload)
I didn’t trust a single DRY_RUN flag. The target price is set low enough that a buy condition can’t realistically be met, and the actual order-placing call is commented out in the source. All three would have to fail at once for a real order to go through. Might be overkill, but I’d rather over-engineer the safety net than find out the hard way.
Phase 0 covers authentication, price quotes, and portfolio lookup only — no live orders. A triple safety net (DRY_RUN flag, a 1-won placeholder target price, and a commented-out order call) keeps it that way.
Retry logic for token expiry and network hiccups
Like most OAuth2 APIs, access tokens expire after a while. Instead of crashing on a 401, the bot re-issues a token and retries the original request once. Network blips get a few retries with exponential backoff too. I haven’t run this for long stretches yet, so I’ll be watching how well it holds up in the next phase.
What’s next
toss_test.py passes all three diagnostic stages. Next phase, I want to run the watch loop for longer stretches to stress-test the retry logic, and turn the Telegram notification skeleton into something that actually fires.
Pingback: [Toss Auto-Trading Dev Journal #0-1] The Case of the Vanishing Overseas Holdings - Orbit - Space & ETF Investing
Pingback: [Toss Auto-Trading Dev Journal #3] Phase 1: Centralizing Config, Unifying Retry Logic, and a Regression Scare - Orbit - Space & ETF Investing
Pingback: [Toss Auto-Trading Dev Journal #4] Centralizing Config, Unifying Retry Logic, and a Regression Scare - Orbit - Space & ETF Investing
Pingback: [Toss Auto-Trading Dev Journal #5] Adding pytest: Turning a Regression Scare Into a Safety Net - Orbit - Space & ETF Investing
Pingback: [Toss Auto-Trading Dev Journal #6] Taming Infinite Log Growth — Wrapping Up Phase 1 - Orbit - Space & ETF Investing
Pingback: [Toss Auto-Trading Dev Journal #7] Extracting the Buy Condition into a Strategy Class (Phase 2 Kickoff) - Orbit - Space & ETF Investing
Pingback: [Toss Auto-Trading Dev Journal #8] From One Symbol to Many - Orbit - Space & ETF Investing
Pingback: [Toss Auto-Trading Dev Journal #9] Why I Locked Down the Data Persistence Design Before Writing a Single Line - Orbit - Space & ETF Investing
Pingback: [Toss Auto-Trading Dev Journal #10] Building the SQLite Persistence Module - Orbit - Space & ETF Investing
Pingback: Wiring the Trading Engine Into SQLite Logging | Toss Auto-Trading Dev Journal #11 (Phase 3-3) - Orbit - Space & ETF Investing
Pingback: Plugin Interface for Trading Strategies | Toss Auto-Trading Dev Journal #12 (Phase 2-3) - Orbit - Space & ETF Investing
Pingback: Backtesting on Just a Price List | Toss Auto-Trading Dev Journal #13 (Phase 4) - Orbit - Space & ETF Investing
Pingback: Candle API Pagination for Historical Backtesting | Toss Auto-Trading Dev Journal #14 - Orbit - Space & ETF Investing
Pingback: File-Based Signals for Bot Pause Control | Toss Auto-Trading Dev Journal #16 - Orbit - Space & ETF Investing
Pingback: Wiring Up python-telegram-bot Command Handlers | Toss Auto-Trading Dev Journal #17 - Orbit - Space & ETF Investing