π νκ΅μ΄λ‘ 보기
Toss Auto-Trading Dev Journal (Phase 5 | Post 16 of 16)
Previous: #15 Read-Only Dashboard | Next: coming soon
π‘ Phase 5 (Part 2) Summary
- Goal: Build the control logic that lets a Telegram bot pause/resume the live trading loop (actual bot wiring comes next)
- Key decision: Use the mere existence of a signal file β no database, no process merging β to track pause state
- Verification: 14 new tests added, full suite of 102 passing
A quick note for readers unfamiliar with the setup: Toss Securities is a Korean brokerage, and this bot trades through its Open API against KRW-denominated stocks. After wiring up a read-only dashboard last time, the obvious next itch was: what if I could actually pause the bot from my phone?
A genuinely different kind of problem
The dashboard only had to read from a database. The Telegram bot’s /pause command is different β it needs to reach into an already-running loop in auto_trader.py and stop it. The bot process and the trading loop process had no way to talk to each other. I needed a bridge.
Why a file signal beat the alternatives
I weighed three options: merging the bot and the loop into one process, using a file’s mere existence as a signal, or building a command queue in the database. Merging processes meant too much rework; a DB queue felt like overkill at this scale. A file-based signal won β if the file exists, pause; if not, run normally.
One thing I nailed down early: /pause stops the entire watch loop (price checks and buy decisions), but never touches the triple safeguard values like DRY_RUN or the target price.
engine_control.py β keeping state as dumb as possible
class EngineControl:
def __init__(self, signal_path="pause.signal"):
self.signal_path = signal_path
def pause(self):
with open(self.signal_path, "w") as f:
f.write("paused")
def resume(self):
if os.path.exists(self.signal_path):
os.remove(self.signal_path)
def is_paused(self):
return os.path.exists(self.signal_path)
auto_trader.py β skip everything while paused
The loop just checks is_paused() each cycle. To avoid alert spam, notifications only fire on the transition, not every cycle.
if engine_control.is_paused():
if not was_paused:
notify("βΈ Watch loop paused (000660)")
was_paused = True
time.sleep(WATCH_INTERVAL)
continue
if was_paused:
notify("βΆ Watch loop resumed")
was_paused = False
price = get_current_price("000660")
# buy logic continues as before
bot_commands.py β pure functions, no API calls
Since the actual Telegram library isn’t wired in yet, I split out the text-assembly logic for /status and /holdings as pure functions, fully testable without any bot running.
def get_status_text(is_paused, trade_count, alert_count):
mode = "Paused" if is_paused else "Running"
return (
f"Status: {mode}\n"
f"Total trades: {trade_count}\n"
f"Total alerts: {alert_count}"
)
def get_holdings_text(holdings):
# example holdings: [{"ticker": "005930", "qty": 3, "avg_price": 1}]
lines = [f"{h['ticker']} x{h['qty']} (avg {h['avg_price']})" for h in holdings]
return "\n".join(lines) if lines else "No holdings"
Verifying it actually stops
The test I cared most about was in test_auto_trader.py. It wasn’t enough for the code to run β I needed to confirm get_current_price() was never called while paused.
def test_price_check_skipped_while_paused(mocker):
mock_price = mocker.patch("auto_trader.get_current_price")
mock_is_paused = mocker.patch(
"engine_control.EngineControl.is_paused",
side_effect=[True, True, False]
)
run_watch_cycle(times=3)
mock_price.assert_not_called()
5 tests covered pause/resume via signal file creation and deletion, 6 covered the /status and /holdings text assembly, and 3 covered loop integration β 14 new tests on top of the existing 88, all 102 passing. I also confirmed none of the safeguard values had shifted.
To let a Telegram bot control a running trading process, the simplest and safest approach is a signal file whose mere existence marks the paused state β no database or process merging required.
What’s left β wiring up the real bot
This session only finished the “what to stop, what to show” logic. What remains is connecting an actual library like python-telegram-bot so that /status calls bot_commands.get_status_text() and /pause calls engine_control.pause() directly. Once that’s done, Phase 5 (alerting/monitoring) wraps up entirely, and Phase 6 (operational automation) begins.
FAQ
Q1. Why a file signal instead of a database command queue?
A1. A DB queue felt like overkill at this scale; checking a file’s existence was simple and reliable enough for pause/resume state.
Q2. Does /pause affect the safeguard settings?
A2. No. DRY_RUN and the target price are never touched, regardless of the loop’s pause state.
Q3. Is the real Telegram command already working?
A3. Not yet β this session only built the control logic. Actual library wiring comes in the next session.
Q4. Won’t alerts spam every cycle while paused?
A4. No, alerts only fire once on the pause/resume transition, not on every cycle.
Pingback: νμΌ μκ·Έλλ‘ ν λ κ·Έλ¨ λ΄ μΌμμ μ§ κ΅¬ννκΈ° | ν μ€ μλλ§€λ§€ κ°λ°κΈ° #16 - TS ννΈ: μ£Όμ μλλ§€λ§€ μ°κ΅¬μ