Files
auto-stock/backend/tests/test_fill_price_avg_split_20261008.py
shuim a264561576 fix: 하드닝 루프 8회차 — 체결 보정·고아 감지·청산 경보·실시간/차트·리스크 가드 일괄 수정
- 스윙 체결가 확인 전 executed_price NULL + fill_sweep, 진입가 resolve(ActiveTrade↔Portfolio), 0체결/부분체결 매수 장부 원복
- DT 고아 감지(근거 한정)·0체결 매도 복원·stale 점검 확대, 시간손절 -0.3% 원복
- 폴백 손절 ATR 캐시, 거래량부족-차단 신호 매수 차단, 현금 5% 유보 재적용, MDD peak/reset 수정
- KIS 서킷브레이커 청산 우회·경보, db_worker 재큐잉 순서, heartbeat·health-watch 사이드카
- 실시간 WS 재구독·41건 우선순위·다건 프레임 분할, 차트 샘플 제거·당일봉 합성, WS 서브프로토콜 인증
- 프론트: 미관리 그룹·확인 모달·차트 실시간 반영·예수금 표시 정정
- 전문가 에이전트 6종 추가(.claude/agents)
- 유닛 스위트 2667 passed (소켓 차단 하), tsc OK

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y83Za98XsNw9zuA4FTUytN
2026-10-10 12:17:49 +09:00

480 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""2026-10-08 체결가/평단/환율/총자산 정합 회귀.
1) 체결 확인 전 executed_price 에 주문가를 넣지 않고, 확인 시 체결가×체결수량·순손익으로 갱신
2) 스윙 손익·손절 진입가 = 스윙 ActiveTrade.entry_price (없으면 Portfolio 평단 폴백, 괴리 시 보수적 max)
3) 환율 0 → 마지막 성공값/설정값 폴백, 모두 없으면 환산 생략 플래그
4) 총자산 = 주식평가 + D+2 예수금 분해 필드
"""
import asyncio
from decimal import Decimal
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from app.core.config import settings
import app.services.db_worker as dbw
import app.services.trade_executor as te
from app.services.swing import fill_sweep as fs
from app.services.swing.position_manager import (
resolve_swing_entry_price_v2,
SWING_ENTRY_PRICE_MAX_DEVIATION,
)
from app.api.endpoints import portfolio as pf
def _run(coro):
return asyncio.run(coro)
# ── 1. 체결 기준 TradeHistory 필드 ────────────────────────────────────────────
def test_fill_fields_buy_amount_is_fill_price_times_qty():
f = te._fill_update_fields_v2("BUY", 9, 41200.0, -0.74, None)
assert f["executed_price"] == Decimal("41200.0")
assert f["executed_quantity"] == 9
assert f["total_amount"] == Decimal(str(round(41200.0 * 9, 2)))
assert "net_profit_loss" not in f
def test_fill_fields_no_price_never_substitutes_order_price():
f = te._fill_update_fields_v2("BUY", 9, 0.0, 0.0, None)
assert "executed_price" not in f
assert "total_amount" not in f
assert f["executed_quantity"] == 9
def test_fill_fields_sell_recomputes_net_from_fill():
basis = {"entry_price": 10000.0, "market": "KOSPI", "is_domestic": True, "net_rate": False}
f = te._fill_update_fields_v2("SELL", 10, 11000.0, 0.0, basis)
from app.services.trade_helpers import calculate_trade_costs
c = calculate_trade_costs(entry_price=10000.0, exit_price=11000.0, quantity=10,
is_domestic=True, market="KOSPI")
assert float(f["net_profit_loss"]) == pytest.approx(10000.0 - float(c["total_cost"]), abs=0.01)
assert float(f["profit_loss_rate"]) == pytest.approx(10.0, abs=1e-6) # gross 규약
assert float(f["commission"]) == pytest.approx(float(c["total_cost"]))
def test_fill_fields_sell_net_rate():
basis = {"entry_price": 10000.0, "net_rate": True}
f = te._fill_update_fields_v2("SELL", 10, 11000.0, 0.0, basis)
assert float(f["profit_loss_rate"]) == pytest.approx(
float(f["net_profit_loss"]) / 100000.0 * 100, abs=1e-3)
@pytest.fixture
def verify_env(monkeypatch):
calls = {"th": [], "at": []}
async def _th(order_id, fields):
calls["th"].append((order_id, dict(fields)))
async def _at(data):
calls["at"].append(dict(data))
monkeypatch.setattr(dbw, "enqueue_update_trade_history_by_order_id", _th)
monkeypatch.setattr(dbw, "enqueue_active_trade_upsert", _at)
monkeypatch.setattr(te, "_delayed_sync_portfolio", AsyncMock())
monkeypatch.setattr(te.asyncio, "sleep", AsyncMock())
te._entry_fill_adjusted_orders.clear()
return calls
def _patch_exec(monkeypatch, result):
"""체결조회 목 — 테스트 시점에 두 참조(trade_executor 캡처본, 소스 모듈 속성)를 모두 덮는다.
(다른 테스트 파일이 sys.modules 의 KIS 모듈을 MagicMock 으로 바꿔도 순서 무관하게 동작)
"""
import sys
m = AsyncMock(return_value=result)
stub = SimpleNamespace(get_order_execution=m)
monkeypatch.setattr(te, "korea_investment_api", stub)
mod = sys.modules.get("app.services.korea_investment_api")
if mod is not None:
monkeypatch.setattr(mod, "korea_investment_api", stub, raising=False)
return m
def test_verify_v3_buy_updates_amount_and_entry(monkeypatch, verify_env):
_patch_exec(monkeypatch, {"ok": True, "found": True, "filled_qty": 9,
"filled_price": 41200.0, "remaining_qty": 0})
_run(te._verify_and_sync_execution_v3(
"0000123", "082740", "한화엔진", 9, 41506.0, "BUY", True, "KRX", 0,
fill_entry_strategy_name="swing", sell_pnl_basis={"strategy_type": "swing"},
))
(oid, fields), = verify_env["th"]
assert fields["executed_price"] == Decimal("41200.0")
assert fields["total_amount"] == Decimal(str(41200.0 * 9))
assert fields["order_type"] == "BUY" and fields["strategy_type"] == "swing"
(at,) = verify_env["at"]
assert at["strategy_name"] == "swing"
assert at["entry_fill_adjust"] == {"order_price": 41506.0, "filled_price": 41200.0, "filled_qty": 9}
assert te.entry_fill_already_adjusted_v2("123")
def test_verify_v3_missing_fill_price_no_price_written(monkeypatch, verify_env):
_patch_exec(monkeypatch, {"ok": True, "found": True, "filled_qty": 9,
"filled_price": 0, "remaining_qty": 0})
_run(te._verify_and_sync_execution_v3(
"0000124", "005380", "현대차", 9, 337177.0, "BUY", True, "KRX", 0,
fill_entry_strategy_name="swing",
))
(oid, fields), = verify_env["th"]
assert "executed_price" not in fields and "total_amount" not in fields
assert verify_env["at"] == [] # 체결가 모르면 진입가 보정 없음
def test_verify_v3_sell_net_from_fill(monkeypatch, verify_env):
_patch_exec(monkeypatch, {"ok": True, "found": True, "filled_qty": 5,
"filled_price": 9500.0, "remaining_qty": 0})
_run(te._verify_and_sync_execution_v3(
"0000125", "005930", "삼성", 5, 9600.0, "SELL", True, "KRX", 0,
sell_pnl_basis={"entry_price": 10000.0, "market": "KOSPI", "strategy_type": "swing"},
))
(oid, fields), = verify_env["th"]
assert float(fields["net_profit_loss"]) < -2500.0 # gross -2500 에서 비용 차감
assert float(fields["profit_loss_rate"]) == pytest.approx(-5.0)
# ── 1-b. ActiveTrade 진입가 누적/체결 보정 (db_worker 순수 로직) ──────────────
def _rec(**kw):
base = dict(id=7, quantity=10, entry_price=10000.0, highest_since_entry=10500.0, remaining_pct=1.0)
base.update(kw)
return SimpleNamespace(**base)
def test_accumulate_weighted_average_and_pinned_retry():
r = _rec()
data = {"accumulate_entry": True, "quantity": 10, "entry_price": 8000.0, "highest_since_entry": 8000.0}
skip = dbw._apply_entry_price_adjustments_v2(r, data)
assert {"quantity", "entry_price", "highest_since_entry"} <= skip
assert r.quantity == 20 and r.entry_price == 9000.0
assert r.highest_since_entry == 10500.0 # 고점은 낮추지 않는다
# 같은 payload 재실행(종료 drain) — 두 번 누적하지 않는다
r2 = _rec()
dbw._apply_entry_price_adjustments_v2(r2, data)
assert r2.quantity == 20 and r2.entry_price == 9000.0
def test_accumulate_after_partial_exit_weights_held_only():
r = _rec(remaining_pct=0.5) # 10주 중 5주 보유
dbw._apply_entry_price_adjustments_v2(r, {"accumulate_entry": True, "quantity": 5, "entry_price": 8000.0})
assert r.entry_price == 9000.0 # (5×10000 + 5×8000)/10
def test_accumulate_into_empty_row_takes_new_price():
r = _rec(quantity=0, entry_price=0)
dbw._apply_entry_price_adjustments_v2(r, {"accumulate_entry": True, "quantity": 3, "entry_price": 5000.0})
assert r.quantity == 3 and r.entry_price == 5000.0
def test_fill_adjust_new_entry_becomes_fill_price():
r = _rec(quantity=9, entry_price=41506.0)
d = {"entry_fill_adjust": {"order_price": 41506.0, "filled_price": 41200.0, "filled_qty": 9}}
dbw._apply_entry_price_adjustments_v2(r, d)
assert r.entry_price == 41200.0
# 재실행 시 고정값 재사용(두 번 빼지 않음)
r.entry_price = 41506.0
dbw._apply_entry_price_adjustments_v2(r, d)
assert r.entry_price == 41200.0
def test_fill_adjust_partial_of_accumulated_row():
r = _rec(quantity=20, entry_price=9000.0) # 10@10000 + 10@8000(주문가)
dbw._apply_entry_price_adjustments_v2(
r, {"entry_fill_adjust": {"order_price": 8000.0, "filled_price": 7900.0, "filled_qty": 10}})
assert r.entry_price == 8950.0
def test_fill_adjust_bad_input_keeps_price():
r = _rec()
dbw._apply_entry_price_adjustments_v2(
r, {"entry_fill_adjust": {"order_price": 0, "filled_price": 7900.0, "filled_qty": 10}})
assert r.entry_price == 10000.0
def test_upsert_handler_skips_fill_adjust_when_no_row():
class _S:
async def execute(self, q):
return SimpleNamespace(scalars=lambda: SimpleNamespace(first=lambda: None))
def add(self, x): # pragma: no cover
raise AssertionError("must not create")
async def commit(self): # pragma: no cover
raise AssertionError("must not commit")
_run(dbw._handle_upsert_active_trade(_S(), {
"stock_code": "005930", "strategy_name": "swing",
"entry_fill_adjust": {"order_price": 1, "filled_price": 1, "filled_qty": 1},
}))
# ── 2. 스윙 진입가 해석 (손절 판정 직결 — 경계값) ────────────────────────────
def test_entry_price_prefers_active_trade_within_band():
assert resolve_swing_entry_price_v2(68000, 67323.08) == (68000.0, "active_trade")
def test_entry_price_boundary_exact_deviation_uses_active_trade():
pa = 10000.0
at = pa * (1 + SWING_ENTRY_PRICE_MAX_DEVIATION)
price, basis = resolve_swing_entry_price_v2(at, pa)
assert basis == "active_trade" and price == pytest.approx(at)
def test_entry_price_over_deviation_uses_kis_portfolio_avg():
# [2026-10-09 하드닝 R1] 괴리 > 한도면 유령 행 의심 → KIS 실제 평단(Portfolio).
# 이전 정책(max)은 유령 고가 entry 로 허위 전량 손절을 냈다.
pa = 10000.0
lo = pa * (1 - SWING_ENTRY_PRICE_MAX_DEVIATION - 0.01)
assert resolve_swing_entry_price_v2(lo, pa) == (pa, "portfolio_avg_deviation")
hi = pa * (1 + SWING_ENTRY_PRICE_MAX_DEVIATION + 0.01)
assert resolve_swing_entry_price_v2(hi, pa) == (pa, "portfolio_avg_deviation")
@pytest.mark.parametrize("at,pa,exp", [
(None, 5000, (5000.0, "portfolio_avg")),
(0, 5000, (5000.0, "portfolio_avg")),
("bad", 5000, (5000.0, "portfolio_avg")),
(5000, 0, (5000.0, "active_trade")),
(None, None, (0.0, "none")),
])
def test_entry_price_fallbacks(at, pa, exp):
assert resolve_swing_entry_price_v2(at, pa) == exp
def test_executor_entry_price_dt_tag_uses_portfolio():
h = SimpleNamespace(average_price=Decimal("67323.08"))
assert te._swing_entry_price_for_stock_v2(
{"strategy_type": "daytrading", "_swing_entry_price": 68000}, h, "103140") == 67323.08
assert te._swing_entry_price_for_stock_v2(
{"_swing_entry_price": 68000}, h, "103140") == 68000.0
assert te._swing_entry_price_for_stock_v2({}, h, "103140") == 67323.08
def test_mixed_avg_stop_loss_trigger_differs():
"""혼합 평단(67,323)으로는 -7% 미발동인데 스윙 진입가(68,000)로는 발동 — 스윙 기준이 맞다."""
stop = -abs(float(settings.STOP_LOSS_PERCENT))
price = 68000 * (1 + stop / 100) - 1
swing_pl = (price / 68000 - 1) * 100
mixed_pl = (price / 67323.08 - 1) * 100
assert swing_pl <= stop < mixed_pl
# ── 2-b. API 전략별 분해 필드 ────────────────────────────────────────────────
def _h(**kw):
base = dict(stock_code="103140", quantity=13, average_price=Decimal("67323.08"),
current_price=Decimal("70000"))
base.update(kw)
return SimpleNamespace(**base)
def test_breakdown_swing_with_dt():
b = pf._strategy_breakdown_v2(_h(), 68000.0, {"quantity": 8, "entry_price": 66900.0})
assert b["swing_quantity"] == 5 and b["dt_quantity"] == 8
assert b["swing_avg_price"] == 68000.0 and b["swing_avg_price_basis"] == "active_trade"
assert b["dt_avg_price"] == 66900.0
assert b["swing_profit_loss"] == pytest.approx(10000.0)
assert b["avg_price_mixed"] is True
def test_breakdown_derived_when_no_active_trade():
b = pf._strategy_breakdown_v2(_h(), None, {"quantity": 8, "entry_price": 66900.0})
assert b["swing_avg_price_basis"] == "derived_from_mixed"
assert b["swing_avg_price"] == pytest.approx(round((67323.08 * 13 - 66900 * 8) / 5, 2))
def test_breakdown_no_dt_portfolio_fallback_rounded():
b = pf._strategy_breakdown_v2(_h(quantity=5, average_price=Decimal("67323.0769")), None, None)
assert b["swing_avg_price"] == 67323.08 and b["swing_avg_price_basis"] == "portfolio_avg"
assert b["dt_quantity"] == 0 and b["avg_price_mixed"] is False
def test_breakdown_dt_only():
b = pf._strategy_breakdown_v2(_h(quantity=8), None, {"quantity": 8, "entry_price": 66900.0})
assert b["swing_quantity"] == 0 and b["swing_avg_price"] is None
def test_quantize_price_policy():
assert pf._quantize_price_v2("67323.0769") == Decimal("67323.08")
assert pf._quantize_price_v2("1.005") == Decimal("1.01")
assert pf._quantize_price_v2(None) == Decimal("0.00")
def test_response_model_has_swing_avg_price():
assert "swing_avg_price" in pf.PortfolioResponse.model_fields
# ── 3. 환율 ──────────────────────────────────────────────────────────────────
@pytest.fixture
def fx_reset(monkeypatch):
monkeypatch.setattr(pf, "_last_good_fx_rate", 0.0)
yield
def test_fx_from_kis_and_last_good(fx_reset, monkeypatch):
assert pf._resolve_fx_rate_v2({"output2": [{"bass_exrt": "1395.5"}]}) == (1395.5, "kis")
assert pf._resolve_fx_rate_v2({"output2": []}) == (1395.5, "last_good")
def test_fx_out_of_range_ignored_then_config(fx_reset):
rate, src = pf._resolve_fx_rate_v2({"output2": {"bass_exrt": "3.2"}})
assert src == "config_default" and rate == float(settings.US_DEFAULT_FX_RATE)
def test_fx_unavailable_when_all_zero(fx_reset, monkeypatch):
monkeypatch.setattr(settings, "US_DEFAULT_FX_RATE", 0.0)
assert pf._resolve_fx_rate_v2(None) == (0.0, "unavailable")
def test_fx_derived_ratio_no_double_count(fx_reset):
data = {"output2": {"tot_evlu_amt": "1400000", "frcr_evlu_tota": "1000", "usd_amt": "1000"}}
assert pf._resolve_fx_rate_v2(data) == (1400.0, "kis")
def test_cache_summary_v2_fx_and_cash_fields(fx_reset, monkeypatch):
cache = pf.PortfolioCache()
cache.holdings = [
SimpleNamespace(stock_code="005930", market="KRX", quantity=10, average_price=Decimal("70000"),
current_price=Decimal("71000"), strategy_type="swing", profit_loss_rate=None),
SimpleNamespace(stock_code="AAPL", market="NASD", quantity=2, average_price=Decimal("150"),
current_price=Decimal("200"), strategy_type="swing", profit_loss_rate=None),
]
bal = {"output2": [{"tot_evlu_amt": "1000000", "scts_evlu_amt": "710000",
"prvs_rcdl_excc_amt": "290000", "_ord_psbl_cash": "264854",
"dnca_tot_amt": "290000"}]}
_run(cache.cache_summary_v2(bal, {"output2": []}))
s = cache.summary
# [R1 2026-10-09] 설정 기본값 환율은 추정치 — 총자산·원화 환산에 합산하지 않고 별도 노출
assert s["exchange_rate"] == 0.0
assert s["exchange_rate_estimate"] == float(settings.US_DEFAULT_FX_RATE)
assert s["exchange_rate_source"] == "config_default" and s["exchange_rate_unavailable"] is True
assert s["overseas_value_usd"] == 400.0
assert s["overseas_value_krw"] == 0.0
assert s["available_cash"] == 264854.0 # 주문가능현금(매매 사이징 입력) — 의미 불변
assert s["settlement_cash"] == 290000.0
assert s["cash_basis_gap"] == pytest.approx(25146.0)
assert s["cash_for_total_assets"] == 290000.0
assert s["total_assets_residual"] == 0.0
def test_cache_summary_v2_fx_unavailable_flag(fx_reset, monkeypatch):
monkeypatch.setattr(settings, "US_DEFAULT_FX_RATE", 0.0)
cache = pf.PortfolioCache()
cache.holdings = [SimpleNamespace(stock_code="AAPL", market="NASD", quantity=2,
average_price=Decimal("150"), current_price=Decimal("200"),
strategy_type="swing", profit_loss_rate=None)]
_run(cache.cache_summary_v2(None, None))
s = cache.summary
assert s["exchange_rate"] == 0.0 and s["exchange_rate_unavailable"] is True
assert s["fx_conversion_skipped"] is True and s["overseas_value_krw"] == 0.0
# ── 1-c. 후속 체결 보정(fill_sweep) ──────────────────────────────────────────
def test_derive_sell_entry_price():
row = SimpleNamespace(order_quantity=10, total_amount=Decimal("110000"),
net_profit_loss=Decimal("9790"), commission=Decimal("210"))
assert fs.derive_sell_entry_price_v2(row) == pytest.approx(10000.0)
row2 = SimpleNamespace(order_quantity=10, total_amount=110000, net_profit_loss=None, commission=None)
assert fs.derive_sell_entry_price_v2(row2) is None
class _FakeDB:
"""1번째 execute = TradeHistory 후보, 이후 = ActiveTrade 조회(진입가 보정 대상 확정)."""
def __init__(self, rows, active_rows=None):
self.rows = rows
self.active_rows = list(active_rows or [])
self.calls = 0
async def execute(self, q):
self.calls += 1
rows = self.rows if self.calls == 1 else self.active_rows
return SimpleNamespace(scalars=lambda: SimpleNamespace(all=lambda: rows))
def _th_row(**kw):
base = dict(id=448, stock_code="082740", order_type="BUY", order_id="0000777",
order_quantity=9, order_price=Decimal("41506"), total_amount=Decimal("373554"),
net_profit_loss=None, commission=None, strategy_type="swing",
entry_reason="RSI 과매도 반등")
base.update(kw)
return SimpleNamespace(**base)
@pytest.fixture
def sweep_env(monkeypatch):
calls = {"th": [], "at": []}
async def _th(tid, fields):
calls["th"].append((tid, dict(fields)))
async def _at(data):
calls["at"].append(dict(data))
monkeypatch.setattr(dbw, "enqueue_update_trade_history", _th)
monkeypatch.setattr(dbw, "enqueue_active_trade_upsert", _at)
fs._attempts.clear()
fs._queued_ids.clear()
te._entry_fill_adjusted_orders.clear()
return calls
def test_sweep_buy_updates_price_amount_and_entry(monkeypatch, sweep_env):
_patch_exec(monkeypatch, {"ok": True, "found": True, "filled_qty": 9,
"filled_price": 41200.0, "remaining_qty": 0})
active = [SimpleNamespace(id=225, strategy_name="swing", entry_reason="RSI 과매도 반등")]
n = _run(fs.sweep_unconfirmed_fills_v2(_FakeDB([_th_row()], active)))
assert n == 1
(tid, f), = sweep_env["th"]
assert tid == 448 and f["executed_price"] == Decimal("41200.0")
assert f["total_amount"] == Decimal(str(41200.0 * 9))
assert "executed_at" not in f
(at,) = sweep_env["at"]
# [하드닝 R1 2026-10-09] '최신 비-DT 행'이 아니라 이 매수의 전략명으로 정확히 지정
assert at["strategy_name"] == "swing" and "strategy_name_not" not in at
assert at["entry_fill_adjust"]["filled_price"] == 41200.0
def test_sweep_buy_skips_entry_adjust_if_already_done(monkeypatch, sweep_env):
te.note_entry_fill_adjusted_v2("777")
_patch_exec(monkeypatch, {"ok": True, "found": True, "filled_qty": 9,
"filled_price": 41200.0, "remaining_qty": 0})
_run(fs.sweep_unconfirmed_fills_v2(_FakeDB([_th_row()])))
assert len(sweep_env["th"]) == 1 and sweep_env["at"] == []
def test_sweep_no_fill_price_no_update_and_attempts_capped(monkeypatch, sweep_env):
_patch_exec(monkeypatch, {"ok": True, "found": True, "filled_qty": 9,
"filled_price": 0, "remaining_qty": 0})
monkeypatch.setattr(fs, "FILL_SWEEP_RETRY_INTERVAL_SEC", 0)
for _ in range(fs.FILL_SWEEP_MAX_ATTEMPTS + 2):
_run(fs.sweep_unconfirmed_fills_v2(_FakeDB([_th_row()])))
assert sweep_env["th"] == []
assert fs._attempts[448][0] == fs.FILL_SWEEP_MAX_ATTEMPTS
assert 448 in fs._exhausted_ids()
def test_sweep_sell_recomputes_net(monkeypatch, sweep_env):
row = _th_row(id=500, order_type="SELL", order_quantity=10, order_price=Decimal("11000"),
total_amount=Decimal("110000"), net_profit_loss=Decimal("10000"), commission=None)
_patch_exec(monkeypatch, {"ok": True, "found": True, "filled_qty": 10,
"filled_price": 10900.0, "remaining_qty": 0})
_run(fs.sweep_unconfirmed_fills_v2(_FakeDB([row])))
(tid, f), = sweep_env["th"]
assert f["total_amount"] == Decimal("109000.0")
assert float(f["net_profit_loss"]) < 9000.0 # (10900-10000)×10 − 비용
assert float(f["profit_loss_rate"]) == pytest.approx(9.0)
assert sweep_env["at"] == []
def test_sweep_partial_unsettled_skipped(monkeypatch, sweep_env):
_patch_exec(monkeypatch, {"ok": True, "found": True, "filled_qty": 4,
"filled_price": 41200.0, "remaining_qty": 5})
_run(fs.sweep_unconfirmed_fills_v2(_FakeDB([_th_row()])))
assert sweep_env["th"] == []
def test_sweep_skips_overseas(monkeypatch, sweep_env):
m = _patch_exec(monkeypatch, {"ok": True})
_run(fs.sweep_unconfirmed_fills_v2(_FakeDB([_th_row(stock_code="AAPL")])))
m.assert_not_called()