feat: add Quant OS A-share baseline
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
import unittest
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from quant60.broker import SimBroker
|
||||
from quant60.config import ExecutionConfig, FeeConfig
|
||||
from quant60.domain import (
|
||||
Bar,
|
||||
Fill,
|
||||
Order,
|
||||
OrderStatus,
|
||||
Portfolio,
|
||||
Position,
|
||||
Side,
|
||||
)
|
||||
from quant60.ledger import HashChainLedger
|
||||
|
||||
|
||||
def bar(
|
||||
day,
|
||||
*,
|
||||
symbol="600000.XSHG",
|
||||
price="10",
|
||||
volume=1000,
|
||||
suspended=False,
|
||||
limit_up=None,
|
||||
limit_down=None,
|
||||
):
|
||||
value = Decimal(price)
|
||||
return Bar(
|
||||
trading_date=day,
|
||||
symbol=symbol,
|
||||
open=value,
|
||||
high=value,
|
||||
low=value,
|
||||
close=value,
|
||||
volume=volume,
|
||||
suspended=suspended,
|
||||
limit_up=Decimal(limit_up) if limit_up else None,
|
||||
limit_down=Decimal(limit_down) if limit_down else None,
|
||||
)
|
||||
|
||||
|
||||
class DomainBrokerTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.portfolio = Portfolio(Decimal("100000"))
|
||||
self.ledger = HashChainLedger()
|
||||
self.broker = SimBroker(
|
||||
self.portfolio,
|
||||
ExecutionConfig(
|
||||
lot_size=100,
|
||||
participation_rate=0.1,
|
||||
slippage_bps=0,
|
||||
),
|
||||
FeeConfig(
|
||||
commission_rate=0,
|
||||
minimum_commission=0,
|
||||
stamp_duty_rate=0,
|
||||
transfer_fee_rate=0,
|
||||
),
|
||||
self.ledger,
|
||||
)
|
||||
|
||||
def test_duplicate_state_callback_is_idempotent(self):
|
||||
order = Order("O1", "600000.SH", Side.BUY, 100, date(2024, 1, 2))
|
||||
order.transition(OrderStatus.ACCEPTED)
|
||||
order.transition(OrderStatus.ACCEPTED)
|
||||
self.assertEqual(order.status, OrderStatus.ACCEPTED)
|
||||
order.transition(OrderStatus.UNKNOWN)
|
||||
order.transition(OrderStatus.UNKNOWN)
|
||||
order.transition(OrderStatus.ACCEPTED)
|
||||
self.assertEqual(order.status, OrderStatus.ACCEPTED)
|
||||
|
||||
def test_late_fill_during_cancel_pending_is_accepted(self):
|
||||
order = Order("O1", "600000.SH", Side.BUY, 200, date(2024, 1, 2))
|
||||
order.transition(OrderStatus.ACCEPTED)
|
||||
order.transition(OrderStatus.CANCEL_PENDING)
|
||||
order.apply_fill(100, Decimal("10"))
|
||||
self.assertEqual(order.status, OrderStatus.PARTIALLY_FILLED)
|
||||
self.assertEqual(order.filled_quantity, 100)
|
||||
|
||||
order.transition(OrderStatus.CANCEL_PENDING)
|
||||
order.apply_fill(100, Decimal("10.1"))
|
||||
self.assertEqual(order.status, OrderStatus.FILLED)
|
||||
self.assertEqual(order.filled_quantity, 200)
|
||||
|
||||
def test_partial_fill_and_t_plus_one(self):
|
||||
submit_day = date(2024, 1, 2)
|
||||
fill_day = date(2024, 1, 3)
|
||||
self.broker.execute_session(
|
||||
{"600000.XSHG": bar(submit_day, volume=2000)}
|
||||
)
|
||||
self.broker.submit("600000.SH", Side.BUY, 500, submit_day)
|
||||
fills = self.broker.execute_session(
|
||||
{"600000.XSHG": bar(fill_day, volume=100_000)}
|
||||
)
|
||||
self.assertEqual(sum(item.quantity for item in fills), 200)
|
||||
position = self.portfolio.position("600000.XSHG")
|
||||
self.assertEqual(position.quantity, 200)
|
||||
self.assertEqual(position.sellable_quantity, 0)
|
||||
with self.assertRaises(ValueError):
|
||||
position.sell(100)
|
||||
self.portfolio.start_session()
|
||||
self.assertEqual(position.sellable_quantity, 200)
|
||||
|
||||
def test_suspension_and_locked_limit_block(self):
|
||||
submit_day = date(2024, 1, 2)
|
||||
next_day = date(2024, 1, 3)
|
||||
self.broker.execute_session(
|
||||
{"600000.XSHG": bar(submit_day, volume=1000)}
|
||||
)
|
||||
self.broker.submit("600000.SH", Side.BUY, 100, submit_day)
|
||||
self.assertEqual(
|
||||
self.broker.execute_session(
|
||||
{"600000.XSHG": bar(next_day, suspended=True, volume=0)}
|
||||
),
|
||||
[],
|
||||
)
|
||||
reasons = [
|
||||
record.payload["reason"]
|
||||
for record in self.ledger.records
|
||||
if record.event_type == "ORDER_BLOCKED"
|
||||
]
|
||||
self.assertIn("SUSPENDED", reasons)
|
||||
|
||||
other = SimBroker(
|
||||
Portfolio(Decimal("100000")),
|
||||
self.broker.execution,
|
||||
self.broker.fees,
|
||||
HashChainLedger(),
|
||||
)
|
||||
other.execute_session(
|
||||
{"600000.XSHG": bar(submit_day, volume=1000)}
|
||||
)
|
||||
other.submit("600000.SH", Side.BUY, 100, submit_day)
|
||||
self.assertEqual(
|
||||
other.execute_session(
|
||||
{
|
||||
"600000.XSHG": bar(
|
||||
next_day,
|
||||
price="11",
|
||||
limit_up="11",
|
||||
limit_down="9",
|
||||
)
|
||||
}
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
def test_open_fill_capacity_uses_only_prior_session_volume(self):
|
||||
submit_day = date(2024, 1, 2)
|
||||
fill_day = date(2024, 1, 3)
|
||||
|
||||
def executed_quantity(current_day_volume):
|
||||
broker = SimBroker(
|
||||
Portfolio(Decimal("100000")),
|
||||
self.broker.execution,
|
||||
self.broker.fees,
|
||||
HashChainLedger(),
|
||||
)
|
||||
broker.execute_session(
|
||||
{"600000.XSHG": bar(submit_day, volume=2000)}
|
||||
)
|
||||
broker.submit("600000.SH", Side.BUY, 500, submit_day)
|
||||
fills = broker.execute_session(
|
||||
{
|
||||
"600000.XSHG": bar(
|
||||
fill_day,
|
||||
volume=current_day_volume,
|
||||
)
|
||||
}
|
||||
)
|
||||
return sum(item.quantity for item in fills)
|
||||
|
||||
self.assertEqual(executed_quantity(0), 200)
|
||||
self.assertEqual(executed_quantity(10_000_000), 200)
|
||||
|
||||
def test_fill_cash_and_position_conservation(self):
|
||||
fill = Fill(
|
||||
"F1",
|
||||
"O1",
|
||||
date(2024, 1, 3),
|
||||
"600000.SH",
|
||||
Side.BUY,
|
||||
100,
|
||||
Decimal("10"),
|
||||
Decimal("5"),
|
||||
Decimal("0"),
|
||||
Decimal("0.01"),
|
||||
)
|
||||
self.portfolio.apply_fill(fill)
|
||||
self.assertEqual(self.portfolio.position("600000.SH").quantity, 100)
|
||||
self.assertEqual(self.portfolio.cash, Decimal("98994.990000"))
|
||||
|
||||
def test_complete_odd_lot_sell_is_accepted_and_filled(self):
|
||||
submit_day = date(2024, 1, 2)
|
||||
fill_day = date(2024, 1, 3)
|
||||
symbol = "600000.XSHG"
|
||||
self.portfolio.positions[symbol] = Position(
|
||||
symbol,
|
||||
quantity=1050,
|
||||
sellable_quantity=1050,
|
||||
today_bought=0,
|
||||
average_cost=Decimal("9"),
|
||||
)
|
||||
self.broker.execute_session(
|
||||
{symbol: bar(submit_day, volume=200_000)}
|
||||
)
|
||||
order = self.broker.submit(
|
||||
symbol,
|
||||
Side.SELL,
|
||||
1050,
|
||||
submit_day,
|
||||
)
|
||||
self.assertEqual(order.status, OrderStatus.ACCEPTED)
|
||||
fills = self.broker.execute_session(
|
||||
{symbol: bar(fill_day, volume=200_000)}
|
||||
)
|
||||
self.assertEqual([item.quantity for item in fills], [1050])
|
||||
self.assertEqual(order.status, OrderStatus.FILLED)
|
||||
self.assertEqual(self.portfolio.position(symbol).quantity, 0)
|
||||
|
||||
def test_partial_odd_lot_and_star_subminimum_are_rejected(self):
|
||||
submit_day = date(2024, 1, 2)
|
||||
symbol = "600000.XSHG"
|
||||
self.portfolio.positions[symbol] = Position(
|
||||
symbol,
|
||||
quantity=1050,
|
||||
sellable_quantity=1050,
|
||||
today_bought=0,
|
||||
average_cost=Decimal("9"),
|
||||
)
|
||||
odd = self.broker.submit(symbol, Side.SELL, 50, submit_day)
|
||||
self.assertEqual(odd.status, OrderStatus.REJECTED)
|
||||
self.assertEqual(odd.status_reason, "NOT_BOARD_LOT")
|
||||
star = self.broker.submit(
|
||||
"688301.XSHG",
|
||||
Side.BUY,
|
||||
100,
|
||||
submit_day,
|
||||
)
|
||||
self.assertEqual(star.status, OrderStatus.REJECTED)
|
||||
self.assertEqual(star.status_reason, "BELOW_MARKET_MINIMUM")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user