feat(trader): PositionLedger 单标的持仓(T+1冻结/均价)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"""单标的持仓对象(raw 计均价、T+1 冻结)。
|
||||
|
||||
mutable,被 Account / StrategyRunner 持有(spec §6.3 双层记账)。
|
||||
- 均价:移动加权平均,买入刷新;卖出不影响剩余持仓成本(A 股惯例)
|
||||
- T+1:买入当日 frozen,次日开盘前由 Account.unfreeze_all() 解冻
|
||||
- 零股:卖出允许零股(退出持仓基本操作),买入由 matcher 保证 100 股整取
|
||||
"""
|
||||
class PositionLedger:
|
||||
def __init__(self, symbol: str):
|
||||
self.symbol: str = symbol
|
||||
self.volume: int = 0
|
||||
self.frozen: int = 0 # T+1 当日买入冻结
|
||||
self.avg_price: float = 0.0
|
||||
|
||||
@property
|
||||
def available(self) -> int:
|
||||
"""可卖出量 = 总持仓 - T+1 冻结。"""
|
||||
return self.volume - self.frozen
|
||||
|
||||
def apply_buy(self, price: float, volume: int) -> None:
|
||||
"""买入:刷新移动加权均价,新买入量计入 frozen(T+1)。"""
|
||||
total_cost = self.avg_price * self.volume + price * volume
|
||||
self.volume += volume
|
||||
self.avg_price = total_cost / self.volume if self.volume else 0.0
|
||||
self.frozen += volume # T+1
|
||||
|
||||
def apply_sell(self, price: float, volume: int) -> None:
|
||||
"""卖出:扣减持仓量。price 保留接口对称(不影响剩余持仓均价)。
|
||||
清仓时 avg_price 归零(避免下一次买入残留历史成本)。"""
|
||||
if volume > self.available:
|
||||
raise ValueError(
|
||||
f"卖出超过可卖量: want {volume}, available {self.available}"
|
||||
)
|
||||
self.volume -= volume
|
||||
if self.volume == 0:
|
||||
self.avg_price = 0.0
|
||||
|
||||
def unfreeze(self) -> None:
|
||||
"""次日开盘前调用:frozen → available。"""
|
||||
self.frozen = 0
|
||||
@@ -0,0 +1,91 @@
|
||||
"""PositionLedger 单标的持仓对象测试(raw 计均价、T+1 冻结)。"""
|
||||
import pytest
|
||||
|
||||
from sanguo_trader.position_ledger import PositionLedger
|
||||
|
||||
|
||||
def test_new_position_empty():
|
||||
p = PositionLedger(symbol="600000")
|
||||
assert p.volume == 0
|
||||
assert p.frozen == 0
|
||||
assert p.avg_price == 0.0
|
||||
assert p.available == 0
|
||||
|
||||
|
||||
def test_buy_sets_avg_price_and_freezes():
|
||||
p = PositionLedger(symbol="600000")
|
||||
p.apply_buy(price=10.0, volume=100)
|
||||
assert p.volume == 100
|
||||
assert p.frozen == 100 # T+1:买入当日冻结
|
||||
assert p.available == 0 # 当日不可卖
|
||||
assert p.avg_price == 10.0
|
||||
|
||||
|
||||
def test_avg_price_weighted_on_add():
|
||||
p = PositionLedger(symbol="600000")
|
||||
p.apply_buy(10.0, 100)
|
||||
p.unfreeze() # 次日解冻
|
||||
p.apply_buy(12.0, 100)
|
||||
assert p.avg_price == 11.0 # (10*100 + 12*100)/200
|
||||
assert p.volume == 200
|
||||
# 新买的 100 又被冻结(T+1)
|
||||
assert p.frozen == 100
|
||||
assert p.available == 100
|
||||
|
||||
|
||||
def test_cannot_sell_frozen():
|
||||
p = PositionLedger(symbol="600000")
|
||||
p.apply_buy(10.0, 100)
|
||||
assert p.frozen == 100
|
||||
assert p.available == 0 # 当日不可卖
|
||||
|
||||
|
||||
def test_unfreeze_makes_available():
|
||||
p = PositionLedger(symbol="600000")
|
||||
p.apply_buy(10.0, 100)
|
||||
p.unfreeze()
|
||||
assert p.frozen == 0
|
||||
assert p.available == 100
|
||||
|
||||
|
||||
def test_sell_reduces_volume():
|
||||
p = PositionLedger(symbol="600000")
|
||||
p.apply_buy(10.0, 200)
|
||||
p.unfreeze()
|
||||
p.apply_sell(11.0, 100)
|
||||
assert p.volume == 100
|
||||
assert p.available == 100
|
||||
|
||||
|
||||
def test_sell_odd_lot_allowed():
|
||||
"""A 股卖出允许零股(退出持仓的基本操作)。"""
|
||||
p = PositionLedger(symbol="600000")
|
||||
p.apply_buy(10.0, 200)
|
||||
p.unfreeze()
|
||||
p.apply_sell(11.0, 50) # 50 股零股
|
||||
assert p.volume == 150
|
||||
|
||||
|
||||
def test_sell_over_available_raises():
|
||||
p = PositionLedger(symbol="600000")
|
||||
p.apply_buy(10.0, 100)
|
||||
p.unfreeze()
|
||||
with pytest.raises(ValueError, match="available"):
|
||||
p.apply_sell(11.0, 200)
|
||||
|
||||
|
||||
def test_sell_frozen_raises():
|
||||
"""T+1:当日买入冻结,卖出冻结量必报错。"""
|
||||
p = PositionLedger(symbol="600000")
|
||||
p.apply_buy(10.0, 100)
|
||||
with pytest.raises(ValueError):
|
||||
p.apply_sell(11.0, 50)
|
||||
|
||||
|
||||
def test_clear_position_avg_price_resets():
|
||||
p = PositionLedger(symbol="600000")
|
||||
p.apply_buy(10.0, 100)
|
||||
p.unfreeze()
|
||||
p.apply_sell(11.0, 100)
|
||||
assert p.volume == 0
|
||||
assert p.avg_price == 0.0
|
||||
Reference in New Issue
Block a user