Files
sanguo_vnpy_v2/tests/trader/test_matcher.py
T

235 lines
7.3 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.
"""matcher.py 撮合纯函数测试(match_session / 费率 / 100 股 / 封板)。
raw 价格:PREV=10.0 即 raw 前收。
拒单判断统一用 isinstance(r, PaperReject)(计划修正点:避免 walrus 表达式 bug)。
"""
import pandas as pd
import pytest
from sanguo_trader.matcher import cross_order
from sanguo_trader.models import (
AccountConfig,
MatchSession,
OrderSide,
PaperOrder,
PaperReject,
PaperTrade,
)
CFG = AccountConfig(initial_capital=1_000_000)
PREV = 10.0 # raw 前收
def mkbar(open, high, low, close):
return pd.Series({"open": open, "high": high, "low": low, "close": close})
def buy(price=0, volume=100, market=True, session=MatchSession.NEXT_OPEN, symbol="600000"):
return PaperOrder("s1", symbol, OrderSide.BUY, price, volume, market, session)
def sell(price=0, volume=100, market=True, session=MatchSession.NEXT_OPEN, symbol="600000"):
return PaperOrder("s1", symbol, OrderSide.SELL, price, volume, market, session)
# ---- 撮合时点 ----
def test_next_open_market_fill_uses_next_open():
t = cross_order(buy(market=True), mkbar(10.5, 11, 10.2, 10.8), PREV, CFG)
assert isinstance(t, PaperTrade)
assert t.price == 10.5
def test_current_close_fill_uses_current_close():
o = buy(market=True, session=MatchSession.CURRENT_CLOSE)
t = cross_order(o, mkbar(10.5, 11, 10.2, 10.8), PREV, CFG)
assert isinstance(t, PaperTrade)
assert t.price == 10.8
# ---- 涨跌停封板拒单 ----
def test_limit_up_one_word_rejects_buy():
up = 11.0 # 10*1.1
r = cross_order(buy(market=True), mkbar(up, up, up, up), PREV, CFG)
assert isinstance(r, PaperReject) and r.reason == "limit_up_locked"
def test_limit_up_t_lock_rejects_buy_conservatively():
up = 11.0
r = cross_order(buy(market=True), mkbar(up, up, 10.5, up), PREV, CFG)
assert isinstance(r, PaperReject) and r.reason == "limit_up_locked"
def test_limit_down_one_word_rejects_sell():
down = 9.0 # 10*0.9
r = cross_order(sell(market=True), mkbar(down, down, down, down), PREV, CFG)
assert isinstance(r, PaperReject) and r.reason == "limit_down_locked"
def test_gem_board_20pct_limit_up():
# 创业板 300750prev 10.00 → 涨停 12.0020%
r = cross_order(
PaperOrder("s1", "300750", OrderSide.BUY, 0, 100, True),
mkbar(12.0, 12.0, 12.0, 12.0),
10.0,
CFG,
)
assert isinstance(r, PaperReject) and r.reason == "limit_up_locked"
def test_star_board_20pct_limit_down():
# 科创板 688981prev 10.00 → 跌停 8.0020%
r = cross_order(
PaperOrder("s1", "688981", OrderSide.SELL, 0, 100, True),
mkbar(8.0, 8.0, 8.0, 8.0),
10.0,
CFG,
)
assert isinstance(r, PaperReject) and r.reason == "limit_down_locked"
def test_bse_board_30pct_limit_up():
# 北交所 830799prev 10.00 → 涨停 13.0030%
r = cross_order(
PaperOrder("s1", "830799", OrderSide.BUY, 0, 100, True),
mkbar(13.0, 13.0, 13.0, 13.0),
10.0,
CFG,
)
assert isinstance(r, PaperReject) and r.reason == "limit_up_locked"
def test_st_5pct_limit_up():
# ST 主板 600000prev 10.00 → 涨停 10.505%
r = cross_order(
buy(market=True, symbol="600000"),
mkbar(10.50, 10.50, 10.50, 10.50),
10.0,
CFG,
is_st=True,
)
assert isinstance(r, PaperReject) and r.reason == "limit_up_locked"
def test_call_auction_session_rejected():
"""CALL_AUCTION 首版预留,不支持。"""
o = buy(market=True, session=MatchSession.CALL_AUCTION)
r = cross_order(o, mkbar(10.5, 11, 10.2, 10.8), PREV, CFG)
assert isinstance(r, PaperReject) and r.reason == "unsupported_match_session"
# ---- 限价单触价 ----
def test_limit_buy_not_touched_rejected():
o = PaperOrder("s1", "600000", OrderSide.BUY, 10.0, 100, is_market=False)
# open 10.5 > 委托 10.0 → 触不到
r = cross_order(o, mkbar(10.5, 11, 10.2, 10.8), PREV, CFG)
assert isinstance(r, PaperReject) and r.reason == "limit_not_touched"
def test_limit_buy_touched_fills():
o = PaperOrder("s1", "600000", OrderSide.BUY, 10.5, 100, is_market=False)
# open 10.5 <= 委托 10.5 → 成交
t = cross_order(o, mkbar(10.5, 11, 10.2, 10.8), PREV, CFG)
assert isinstance(t, PaperTrade) and t.price == 10.5
def test_limit_sell_not_touched_rejected():
o = PaperOrder(
"s1", "600000", OrderSide.SELL, 11.0, 100,
is_market=False, match_session=MatchSession.CURRENT_CLOSE,
)
# close 10.8 < 委托 11.0 → 卖不出
r = cross_order(o, mkbar(10.5, 11, 10.2, 10.8), PREV, CFG)
assert isinstance(r, PaperReject) and r.reason == "limit_not_touched"
# ---- 100 股取整(买入)----
def test_buy_rounds_down_to_100():
t = cross_order(
PaperOrder("s1", "600000", OrderSide.BUY, 0, 250, True),
mkbar(10, 10, 10, 10),
PREV,
CFG,
)
assert isinstance(t, PaperTrade) and t.volume == 200
def test_buy_below_100_rejected():
r = cross_order(
PaperOrder("s1", "600000", OrderSide.BUY, 0, 50, True),
mkbar(10, 10, 10, 10),
PREV,
CFG,
)
assert isinstance(r, PaperReject) and r.reason == "volume_below_min_lot"
def test_sell_odd_lot_allowed():
"""卖出允许零股(退出持仓基本操作),不取整。"""
t = cross_order(
PaperOrder("s1", "600000", OrderSide.SELL, 0, 50, True),
mkbar(10, 10, 10, 10),
PREV,
CFG,
)
assert isinstance(t, PaperTrade) and t.volume == 50
# ---- 费用 ----
def test_commission_uses_min_5_yuan():
# 100 股 × 10 元 × 0.0003 = 0.3 → 不足 5 元,收 5
t = cross_order(buy(market=True), mkbar(10, 10, 10, 10), PREV, CFG)
assert isinstance(t, PaperTrade)
assert t.commission == 5.0
def test_commission_above_min():
# 1000 股 × 10 元 × 0.0003 = 3 → 仍 < 5;用 5000 股 × 10 = 50000 × 0.0003 = 15
t = cross_order(
PaperOrder("s1", "600000", OrderSide.BUY, 0, 5000, True),
mkbar(10, 10, 10, 10),
PREV,
CFG,
)
assert isinstance(t, PaperTrade)
assert t.commission == pytest.approx(15.0)
def test_stamp_duty_only_on_sell():
t_buy = cross_order(buy(market=True), mkbar(10, 10, 10, 10), PREV, CFG)
assert isinstance(t_buy, PaperTrade)
assert t_buy.stamp_duty == 0.0
t_sell = cross_order(sell(market=True), mkbar(10, 10, 10, 10), PREV, CFG)
assert isinstance(t_sell, PaperTrade)
# 100*10*0.0005 = 0.5
assert t_sell.stamp_duty == pytest.approx(0.5)
def test_transfer_fee_single_sided_in_trade():
t = cross_order(buy(market=True), mkbar(10, 10, 10, 10), PREV, CFG)
assert isinstance(t, PaperTrade)
# 单边 100*10*0.00001 = 0.01trade 里存单边,Account 算 ×2
assert t.transfer_fee == pytest.approx(0.01)
def test_trade_carries_match_session():
t = cross_order(
buy(market=True, session=MatchSession.CURRENT_CLOSE),
mkbar(10.5, 11, 10.2, 10.8),
PREV,
CFG,
)
assert isinstance(t, PaperTrade)
assert t.match_session == MatchSession.CURRENT_CLOSE
def test_trade_carries_strategy_id_and_symbol():
t = cross_order(
PaperOrder("my_strat", "300750", OrderSide.BUY, 0, 100, True),
mkbar(10, 10, 10, 10),
10.0,
CFG,
)
assert isinstance(t, PaperTrade)
assert t.strategy_id == "my_strat"
assert t.symbol == "300750"