Auto-Pausing on Cumulative Loss Limit | Toss Auto-Trading Dev Journal #20

Yesterday I finished the per-order quantity guard. Today’s task, the second Phase 7 prep item, was a cumulative loss limit that auto-pauses the bot β€” and most of the time went into design decisions, not code.

🌐 ν•œκ΅­μ–΄λ‘œ 보기

πŸ’‘ Phase 7 (Part 2) Summary
  • Goal: Auto-pause new-buy monitoring once account-wide unrealized P&L crosses a configured threshold (default -10%).
  • Key decision: With no realized-P&L tracking yet, I used the unrealized P&L field the API already returns (profitLoss.rate), evaluated cumulatively with no midnight reset.
  • Verification: 7 new tests in test_risk_control.py, plus updated existing tests β€” full suite at 131 passing.

Why Unrealized P&L Instead of Realized P&L

The first thing I had to settle was what “loss” even means here. A proper risk system usually tracks realized P&L from actual sell trades. But this bot’s current architecture is “buy, then monitor” β€” there’s no table recording sells at all. Building realized-P&L tracking from scratch would’ve turned today’s task into a data-model project instead of a risk-control feature.

So I used what the Toss Securities API already returns: profitLoss.rate, the unrealized P&L percentage per holding. Summing that across the account and checking it against a threshold (default -10%) needed no new data model at all.

No Midnight Reset β€” Cumulative by Design

Second decision: when does the loss window reset? A daily reset (“down 10% today”) sounded reasonable, but it drags in timezone handling and boundary-condition tests I didn’t want to deal with yet. I went with a simple cumulative check across the whole account instead β€” no reset logic, fewer edge cases to get wrong.

risk_control.py β€” check_and_enforce_loss_limit()

The new risk_control.py module’s core function, check_and_enforce_loss_limit(), checks account-wide unrealized P&L and, if it breaches the threshold, calls the existing Phase 5 engine_control.pause() to stop the buy-monitoring loop only.

One design principle I nailed down explicitly: this module never sells anything. Deciding whether to sell existing holdings stays a human decision. Automation’s job here is “stop buying more” β€” automating the sell decision would be a completely different scope of responsibility.

I also added a state check so it doesn’t send duplicate alerts if the account is already paused and still over the threshold on the next scan.

Integrating into scheduler.py β€” Avoiding Duplicate API Calls

Where to run the check was its own question. It could’ve gone into the real-time buy-monitoring loop in auto_trader.py, but scheduler.py β€” which already scans the whole account every 30 minutes β€” was the better fit for separation of concerns.

That required one refactor. The existing toss_portfolio.collect_price_snapshots() handled the API fetch and the snapshot write in one function. For the scheduler to reuse the same fetch result for both snapshot storage and the loss check, I split out the storage half into a pure function, store_snapshots_from_holdings(), and left collect_price_snapshots() as a thin wrapper calling it. That way a single scan feeds two consumers without a second API call.

I didn’t build new alerting either. When a pause triggers, the pause-detection logic already in auto_trader.py from Phase 5 sends the Telegram notification on its next cycle β€” reusing existing infrastructure instead of duplicating it.

Verification β€” 131 Tests Passing

test_risk_control.py got 7 new tests: within/over threshold, boundary values, alerts-table logging, no duplicate alerts while already paused, and edge cases like missing data or a missing rate field.

test_scheduler.py was updated to confirm fetch_holdings() is called exactly once and the same data is reused for both snapshot storage and the loss check. test_config.py got a regression test confirming the loss-limit value itself stays within a safe range (1%–50%).

Despite the refactor, the existing collect_price_snapshots tests passed unchanged β€” same signature, same return value. A full pytest run confirmed 131 passing (122 existing + 9 new).

Now a 5-Layer Safety Net

The existing 4-layer safety net (per-order limit, DRY_RUN simulation, 1-won target price, physically commented-out order API) now has a fifth layer: cumulative loss-limit auto-pause.

LayerWhat it does
0Reject immediately if per-order quantity exceeds the limit
NewAuto-pause monitoring if cumulative unrealized P&L breaches the limit
1DRY_RUN=True simulation mode
2Target price set to 1 won (unfillable)
3Order API call physically commented out

What’s Left

Remaining Phase 7 prep items: reviewing API key permission scope, redundant alerting (Telegram + email), and backtesting with real target prices and strategy logic. Once those are done, the plan is to roll back the safety layers step by step, only with explicit sign-off. (This post covers the safety-net development process only β€” no real account or trade data is included.)

πŸ’» Full code is in the project repository.

FAQ

Q1. Why unrealized P&L instead of realized P&L for the loss check?

The bot’s current “buy then monitor” architecture has no table tracking sells, so there’s no realized P&L to compute yet. I used the unrealized P&L field (profitLoss.rate) the API already returns.

Q2. Does breaching the loss limit auto-sell holdings?

No. It only pauses new-buy monitoring. Selling existing holdings stays a manual decision β€” that’s intentional, not a gap.

Q3. How does the loss-tracking window reset?

It doesn’t reset at midnight β€” the check is cumulative across the whole account, to avoid timezone and boundary-condition complexity.

Q4. Doesn’t this double the API calls?

That’s what the refactor solved. Storage logic was split into store_snapshots_from_holdings() so a single fetch feeds both snapshot storage and the loss check.

2 thoughts on “Auto-Pausing on Cumulative Loss Limit | Toss Auto-Trading Dev Journal #20”

  1. Pingback: Dual-Channel Alerts: Telegram + SMTP Failover | Toss Auto-Trading Dev Journal #21 - Orbit - Space & ETF Investing

  2. Pingback: Toss Securities Holdings Dashboard | Dev Journal #25 - Orbit - Space & ETF Investing

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top