Adding pytest to a Python Trading Bot | Toss Dev #1-3

While refactoring my retry-logic decorator, I stumbled onto something unsettling: the KRW/USD currency-splitting logic I’d fixed earlier had quietly disappeared from the code. I caught it before deploying, but the fact that a fixed bug can silently come back with zero warning was enough to change my priorities for the day.

Testing had been sitting on my roadmap for a while, unstarted. That scare finally pushed me to add pytest. Here’s what I tested, what I found along the way, and a small Windows headache that cost me ten minutes I’ll never get back.

Toss Auto-Trading Dev Journal series (Phase 1 of 5 posts)

← Previous: Phase 1-2 | Next: added after publishing

Why now

pytest had been on the roadmap from the start, but it kept losing to the classic “make it work first, test it later” excuse. That changed the moment I found the currency-splitting logic missing mid-refactor. Manual code review clearly wasn’t going to catch every regression — I needed a machine to do it for me.

What I decided to test

The original plan only covered display-formatting helpers like get_display_width and fit_to_width. I widened the scope considerably:

  • safe_int / safe_float / center_korean — small pure utility functions
  • extract_amounts_by_currency — the exact logic behind the USD-dropping bug I’d fixed recently. This one absolutely needed a regression test.
  • The retry_on_network_error decorator — success, failure, partial failure, backoff growth, and exception propagation
  • Safety-critical constants like DRY_RUN and TARGET_BUY_PRICE — pinned so that any accidental change fails the test suite immediately

Pinning safety constants as tests

My favorite part of this whole exercise was treating safety values like DRY_RUN and TARGET_BUY_PRICE as things worth testing, not just configuring. Simplified example:

def test_dry_run_is_true_by_default():
    # if DRY_RUN accidentally flips to False, this fails immediately
    assert config.DRY_RUN is True

def test_target_buy_price_masked_example():
    # illustrative ticker/price, not a real holding
    assert config.TARGET_BUY_PRICE["005930"] == 70000

Testing a config value instead of logic felt odd at first, but it’s the cheapest insurance against ever accidentally shipping with safety guards disabled.

The regression test that mattered most

Since this function was the actual site of the recent incident, I put the most care into this one:

def test_extract_amounts_separates_krw_and_usd():
    raw_text = "Balance: 1,000,000 KRW / $500.00"
    result = extract_amounts_by_currency(raw_text)
    assert result["KRW"] == 1000000
    assert result["USD"] == 500.00

Now if future-me (or anyone) touches this function again and breaks the currency split, this test will scream before it ever reaches a live run.

Two “not bugs” I found along the way

Writing tests always surfaces a few “wait, why does it do that?” moments. This time, two came up — both turned out to be intentional, existing behavior, not bugs.

  • safe_int("1234.0") fails to convert and returns the default 0. It looked wrong until I remembered the real API always returns KRW amounts as plain integer strings, so this never actually triggers in production.
  • fit_to_width sometimes pads Korean strings with a trailing space to hit an exact width, so truncated text doesn’t always end in an ellipsis (…). Also confirmed as intentional design.

In both cases, instead of “fixing” the code, I adjusted the tests to accurately describe the actual behavior. When the test’s expectation and the code’s behavior disagree, checking intent first — before blindly rewriting either one — turned out to matter more than I expected.

The small thing that ate ten minutes: Windows PATH

pip install pytest went fine. Running pytest -v afterward threw a “‘pytest’ is not recognized” error — a pretty common symptom of pip’s installed scripts not being on the Windows PATH.

The fix was simple: python -m pytest -v worked immediately, and that’s now the standard way I run tests in this project going forward.

Results and what’s next

Final tally: 33 tests total across test_config.py (3), test_toss_common.py (5), and test_toss_portfolio.py (25) — all passing. Verified in both the local working folder and the project repo.

The last item left in Phase 1 is fixing unbounded log growth with a RotatingFileHandler. Once that’s done, Phase 1’s stability work wraps up, and Phase 2 (strategy modularization) begins.

1 thought on “Adding pytest to a Python Trading Bot | Toss Dev #1-3”

  1. Pingback: [Toss Auto-Trading Dev Journal #7] Extracting the Buy Condition into a Strategy Class (Phase 2 Kickoff) - Orbit - Space & ETF Investing

Leave a Comment

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

Scroll to Top