π νκ΅μ΄λ‘ 보기
π Toss Auto-Trading Dev Journal β Full Series Index (currently Phase 7-7 / 25 posts total)
- #0 Project Kickoff & Foundation
- #0-1 Fixing a Currency Unit Bug
- #3 Config Separation & Retry Logic
- #4 Config/Retry Follow-up Fixes
- #5 Adding pytest Unit Tests
- #6 Log Rotation with RotatingFileHandler
- #7 Designing the Strategy Class
- #8 Multi-Symbol Monitoring
- #9 Data Persistence Design
- #10 Building the DB Module
- #11 DB Integration
- #12 Strategy Plugin Interface
- #13 Building a Backtest Engine
- #14 Candle API Pagination
- #15 Read-Only Dashboard
- #16 Telegram Control Logic (Part 1)
- #17 Telegram Control Logic (Part 2)
- #18 Scheduler & Watchdog
- #19 Order Limit Guard
- #20 Loss-Limit Auto-Pause
- #21 Dual-Channel Notifications
- #22 Security Diagnostics
- #23 Per-Symbol Take-Profit/Stop-Loss
- #24 FastAPI Dashboard & DB-Backed Strategy Config
- #25 Toss Securities Holdings Dashboard & One-Click Setup (this post)
β¬ Previous: Phase 7-6 β FastAPI Dashboard & DB-Backed Strategy Config
Currently reading: Phase 7-7 / 25 posts total
π‘ What Phase 7-7 covers
- Goal: Wire up a live Toss Securities holdings dashboard with a one-click take-profit/stop-loss setup form.
- Key decision: A single
get_display_name()helper normalizes every table to “Samsung Electronics (005930)” instead of a bare ticker. - Verification: 130 pytest cases passing, including the new python-multipart form routes, plus a full DRY_RUN buyβtake-profit/stop-loss sell cycle test.
Without a proper Toss Securities holdings dashboard, checking what I actually held meant opening the Toss app every time β a separate step from the bot itself. Mixing up ticker codes for Samsung Electronics vs. SK Hynix got old fast too. This post closes that gap with live holdings and a one-click take-profit/stop-loss form.
1. Toss Securities API: Live Holdings & One-Click Setup (dashboard.py)
toss_portfolio.fetch_holdings() pulls quantity, average cost, current price, unrealized P&L, and return rate for every position, rendered at the top of the dashboard. The next problem was workflow: setting a take-profit target meant switching to a separate strategy page every time. So I put an inline form directly on each holdings row.
<form action="/api/strategies" method="post">
<input type="hidden" name="symbol" value="{symbol}">
<input type="number" name="target_price" placeholder="Target buy price">
<input type="number" name="take_profit_pct" placeholder="Take-profit %" step="0.1">
<input type="number" name="stop_loss_pct" placeholder="Stop-loss %" step="0.1">
<button type="submit">Save</button>
</form>
FastAPI needs python-multipart to parse form submissions. I skipped that dependency at first and hit RuntimeError: Form data requires "python-multipart" to be installed. straight out of the box. A one-line pip install python-multipart fixed it, but I made sure to pin it in requirements.txt right away so it wouldn’t break again on a fresh deploy. If the form-handling flow is unclear, FastAPI’s official Form data guide covers it well.
Two routes handle it: POST /api/strategies for saving and POST /api/strategies/delete for removing a config. The save handler does an upsert into strategy_configs β update if a config already exists for that symbol, insert otherwise.
2. Stock Portfolio Display: Name Caching (db.py)
Every table used to show a bare 6-digit ticker. I added a stock_names cache table in SQLite, populated via save_stock_name(code, name) whenever the API returns a name.
def get_display_name(code: str) -> str:
"""005930 -> 'Samsung Electronics (005930)'. Falls back to the code if uncached."""
name = db.get_stock_name(code)
return f"{name} ({code})" if name else code
Applying that one helper across holdings, active strategy configs, recent trades, and recent notifications made the stock portfolio visualization noticeably easier to scan β no more mentally mapping ticker codes to companies.
3. Config Sync & Take-Profit/Stop-Loss Automation (auto_trader.py)
Changing a target price on the web UI is useless if the bot needs a restart to pick it up. So the monitoring loop now re-reads the latest config from the DB every cycle (60 seconds by default) β edits made through the form apply on the next tick.
With DRY_RUN = True, I set a virtual position β Samsung Electronics (005930) β with a target buy price of β©70,000. When the simulated price dropped to that level, the bot triggered a virtual buy, then tracked the return: +5% triggered a virtual take-profit sell, -3% triggered a virtual stop-loss sell. Either outcome logs a SELL row to the trades table and fires a notification.
Virtual account example: 10 shares of Samsung Electronics (005930) at β©70,000 avg cost β take-profit sell triggers at β©73,500 (simulated data only, no real account figures shown).
4. Dual-Channel Notifications & Security Diagnostics
WARNING-level events and above now go out over both Telegram and SMTP email, so one channel failing doesn’t mean silence. security_check.py non-destructively verifies that .env isn’t tracked in Git, that all five safety-brake conditions are active, and that OAuth2 tokens and account connectivity are healthy β without touching balances or live order state.
5. Verification: 130 pytest cases
The new form routes, caching helper, and virtual buy-sell cycle are all covered. Once the python-multipart dependency was fixed, the form-related tests went green too.
$ pytest -v
...
130 passed in 8.42s
Added a live Toss Securities holdings dashboard and one-click take-profit/stop-loss setup to a Python trading bot. A caching helper now displays “Name (Code)” instead of bare tickers, and a DRY_RUN cycle test verifies the full virtual buy-to-sell flow. All 130 pytest cases pass, including the new python-multipart form routes.
FAQ
Q1. Why is python-multipart required?
FastAPI needs it to parse HTML <form> submissions. Without it you’ll get RuntimeError: Form data requires "python-multipart" to be installed.
Q2. Does DRY_RUN mode place real orders?
No. With DRY_RUN=True, no live orders are sent β only logs and DB records for a simulated trade cycle.
Q3. How often does the stock-name cache refresh?
It updates on every API fetch via save_stock_name(). There’s no separate batch refresh job.
Q4. Is the source code for this series public?
Not currently β it’s a private learning project, and I share the core logic here as I go.