Backtesting on Just a Price List | Toss Auto-Trading Dev Journal #13 (Phase 4)

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

πŸ’‘ Phase 4 at a glance

  • Goal: bolt a backtest engine onto the bot without touching strategy code, driven purely by a price list, computing win rate, profit factor, and max drawdown.
  • Key decision: separate data source from backtest logic, and keep exit rules (take-profit/stop-loss) inside backtest.py rather than strategy.py.
  • Verification: 13 new tests in test_backtest.py, full suite at 71/71 passing.

Phases 0 through 3 covered the foundation, stability, strategy modularity, and data persistence. Next on the roadmap: backtesting. First question that stopped me cold β€” where do I even get historical prices?

Separating “where the data comes from” from “how the backtest works”

I wasn’t sure whether the Toss Securities Open API even exposes a historical-price endpoint β€” that check got pushed to next session. My own price_snapshots table also hasn’t run long enough to be useful yet.

So I made a call: backtest.py doesn’t care where prices come from. Feed it a plain list[float] β€” self-collected data, public external data, or masked synthetic data using placeholder tickers like 005930 and 000660 β€” and it behaves identically.

def run_backtest(strategy, prices: list[float], take_profit=0.05, stop_loss=0.03):
    trades = []
    i = 0
    while i < len(prices):
        if strategy.should_buy(prices[:i+1]):
            entry_price = prices[i]
            exit_i, reason = _find_exit(prices, i, entry_price, take_profit, stop_loss)
            trades.append({
                "entry": entry_price,
                "exit": prices[exit_i],
                "reason": reason,  # "take_profit" | "stop_loss" | "period_end"
            })
            i = exit_i + 1
        else:
            i += 1
    return trades

strategy.py stays untouched β€” exit rules live elsewhere

The existing should_buy() only ever answered “when to buy.” In live trading that was fine, since the bot stops watching a symbol right after a buy. But win rate and profit factor need an exit point.

Instead of touching the strategy class, I added take-profit (+5%) and stop-loss (-3%) thresholds to config.py, and let backtest.py own the exit decision entirely. Strategy owns entries; the backtest engine owns exits.

compute_metrics() β€” win rate, profit factor, drawdown

compute_metrics(trades) takes the trade list and computes win rate, profit factor (average win / average loss), and max drawdown off the cumulative equity curve. When there are zero losing trades, profit factor comes back as None rather than an arbitrary infinity or zero β€” so “no losses yet” never gets confused with “profit factor is zero” in the report.

One test failed out of 13 β€” and it wasn’t a code bug

First pytest run against the new test_backtest.py gave a FAILED on test_stop_loss_trigger. I assumed a logic bug and spent a while inside run_backtest, but the real problem was the test data itself β€” prices stayed below the entry threshold even after the stop-loss, so the engine correctly re-entered a new trade. Fixing the price sequence in the test fixed everything.

Full suite: 58 existing + 13 new = 71/71 passing, with no changes to any safety thresholds. Running the engine against 30 days of synthetic price data produced 4 trades (3 take-profit, 1 stop-loss), a 75% win rate, a 1.90 profit factor, and 3.12% max drawdown.

A known limitation I left in place

MovingAverageStrategy, which keeps internal price history, has a quirk: because the backtest loop skips ahead after a sell, the skipped segment never feeds into its moving-average calculation. The stateless ThresholdBuyStrategy has no such issue. I documented this directly in the code comments so I don’t re-discover it later.

What’s next

Two of Phase 4’s three pieces are done: simulation and reporting. What’s left is checking whether Toss Securities’ Open API exposes daily/minute historical price endpoints β€” if it does, that becomes the primary data source, and the next step is running backtest.py across strategies to actually compare performance.

πŸ‘‰ See the full series in the navigation box above.

1 thought on “Backtesting on Just a Price List | Toss Auto-Trading Dev Journal #13 (Phase 4)”

  1. Pingback: 가격 리슀트 ν•˜λ‚˜λ‘œ 승λ₯ κΉŒμ§€: ν•˜μ΄λΈŒλ¦¬λ“œ λ°±ν…ŒμŠ€νŠΈ μ—”μ§„ λ§Œλ“€κΈ° | ν† μŠ€ μžλ™λ§€λ§€ 개발기 #13 (Phase 4) - TS ν€€νŠΈ: 주식 μžλ™λ§€λ§€ μ—°κ΅¬μ†Œ

Leave a Comment

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

Scroll to Top