Series: Toss Auto-Trading Dev Journal (Phase 1-2 / 4 posts so far)
- Phase 0 — Building the Foundation
- Phase 0-1 — Fixing a Currency Display Bug
- Phase 1 — Config Cleanup & Retry Logic (Part 1)
- Phase 1-2 — Config Cleanup & Retry Logic, Part 2 (this post)
Previous: Phase 1 – Config & Retry · Next: coming soon
What This Phase Was About
This is a personal dev log about a Python bot that trades on Toss Securities (a Korean brokerage that offers a trading API — think of it as Korea’s version of a retail brokerage API like Alpaca). I closed out two remaining items from the Phase 1 roadmap: centralizing scattered config values into one file, and merging duplicated retry logic into a single decorator.
1. Centralizing config into config.py
Stock tickers, polling intervals, thresholds — all of it used to be hardcoded across three separate files. I pulled everything into a single config.py: things like TARGET_STOCKS, MONITOR_INTERVAL, ERROR_BACKOFF_INTERVAL, MAX_CONSECUTIVE_ERRORS, DEFAULT_TIMEOUT, along with safety-critical values like DRY_RUN and the target buy price.
Those safety values needed to move without changing at all, so I double-checked them right after the move. Then I ran the price-fetching script against a placeholder stock (Samsung Electronics, ticker 005930, used here purely as a stand-in) to confirm everything still worked.
2. Merging retry logic into one decorator
Token refresh, account lookup, price fetch, holdings fetch — four different functions, all with nearly identical retry-on-timeout code pasted in. I finally pulled it into a reusable decorator:
def retry_on_network_error(max_retries=None, backoff=None):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
retries = max_retries or config.MAX_CONSECUTIVE_ERRORS
wait = backoff or config.ERROR_BACKOFF_INTERVAL
for attempt in range(retries):
try:
return func(*args, **kwargs)
except (requests.Timeout, requests.ConnectionError) as e:
logging.warning(f"[retry {attempt+1}/{retries}] {e}")
time.sleep(wait)
logging.error("Retries exhausted, returning default")
return None
return wrapper
return decorator
The harder decision was where not to apply it. The price-check function inside the monitoring loop deliberately doesn’t get the decorator, because the loop itself already retries on a fixed interval. Stacking immediate retries on top of that would risk piling up requests exactly when the API is already struggling. So I split things into two layers: fast, local retries for calls that must succeed, and a slower backoff for the loop that’s already polling anyway.
The Scare: Almost Losing a Bug Fix
Midway through the retry work, I noticed something was off — a currency-split display fix I’d made just days earlier, separating KRW and USD balances in the portfolio view, had quietly disappeared.
Turned out I’d started this session’s edits from an older local copy of the file instead of the latest one, and stitched new code onto the stale version without realizing it. I only caught it because the actual output showed only KRW values where I expected both currencies — a live check against real output is what saved me here.
After restoring the fix, I re-ran the function against synthetic test data to confirm the currency split worked again. I also added a small process change going forward: before swapping in a new version of any file, grep (or PowerShell’s Select-String) for the key function names first, just to confirm they’re actually still there.
In this phase, I centralized config values into a single file and merged duplicated retry logic into one decorator. A file-version mixup briefly wiped out an earlier currency-split fix, but catching it early let me restore it and add a pre-edit function-existence check going forward.
What’s Next
The remaining Phase 1 items are adding a RotatingFileHandler so logs don’t grow forever, and introducing pytest-based unit tests. This regression scare is honestly a pretty good case study for why the latter matters, so that’ll be the natural next post.
FAQ
Q1. Why not apply the retry decorator everywhere?
Places that already have their own periodic retry loop, like the price-monitoring loop, don’t need an additional layer of immediate retries stacked on top — that can actually make things worse under load. Splitting fast retries from long backoffs kept the responsibilities clean.
Q2. How are you preventing regressions like this going forward?
A quick grep/Select-String check for key function names before swapping in file versions, plus a longer-term plan to add pytest unit tests that would catch this kind of thing automatically.
Q3. Is it safe to move safety-critical values like DRY_RUN into a config file?
Yes — the values themselves were never touched during the move, and I verified them again right after. The three-layer safety-brake principle for live trading stays intact through this refactor.
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