feat dev
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -74,7 +74,7 @@ def StartTrend() -> None:
|
|||||||
assets = client.assets()
|
assets = client.assets()
|
||||||
_, positions = client.positions()
|
_, positions = client.positions()
|
||||||
order_book = OrderBook()
|
order_book = OrderBook()
|
||||||
order_book.refresh()
|
order_book.refresh(client)
|
||||||
|
|
||||||
storeState = State.for_strategy(
|
storeState = State.for_strategy(
|
||||||
config.global_config.qmt_data_dir,
|
config.global_config.qmt_data_dir,
|
||||||
@@ -149,8 +149,7 @@ def RunOnce(run: Runtime, signals) -> None:
|
|||||||
previous_state_codes = set(run.state.codes)
|
previous_state_codes = set(run.state.codes)
|
||||||
try:
|
try:
|
||||||
broker_orders = run.client.trade_detail_data("order")
|
broker_orders = run.client.trade_detail_data("order")
|
||||||
broker_deals = run.client.deals()
|
run.state.reconcile(positions, broker_orders)
|
||||||
run.state.reconcile(positions, broker_orders, broker_deals)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("订单状态对账失败,本轮禁止自动交易")
|
logging.exception("订单状态对账失败,本轮禁止自动交易")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -115,6 +115,6 @@ class OrderBook:
|
|||||||
)
|
)
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
key = f"{side}-{request.code}"
|
key = f"{side}-{request.code}"
|
||||||
self.data[key] = pending
|
self.data.append(pending)
|
||||||
self.lock[key] = pending.created_at.timestamp()
|
self.lock[key] = pending.created_at.timestamp()
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -70,16 +70,6 @@ class State:
|
|||||||
with self.lock:
|
with self.lock:
|
||||||
return list(self.items)
|
return list(self.items)
|
||||||
|
|
||||||
@property
|
|
||||||
def unresolved_codes(self) -> list[str]:
|
|
||||||
"""返回存在处理中或未知订单状态的证券代码快照。"""
|
|
||||||
with self.lock:
|
|
||||||
return [
|
|
||||||
code
|
|
||||||
for code, item in self.items.items()
|
|
||||||
if _has_unresolved_order(item)
|
|
||||||
]
|
|
||||||
|
|
||||||
def get(self, code: str) -> StateItem:
|
def get(self, code: str) -> StateItem:
|
||||||
"""获取指定证券的状态;不存在时抛出 KeyError。"""
|
"""获取指定证券的状态;不存在时抛出 KeyError。"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@@ -93,16 +83,8 @@ class State:
|
|||||||
def delete(self, code: str) -> bool:
|
def delete(self, code: str) -> bool:
|
||||||
"""删除已终结的证券状态,并返回是否实际删除。"""
|
"""删除已终结的证券状态,并返回是否实际删除。"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
item = self.items.get(code)
|
|
||||||
if item is not None and _has_unresolved_order(item):
|
|
||||||
return False
|
|
||||||
return self.items.pop(code, None) is not None
|
return self.items.pop(code, None) is not None
|
||||||
|
|
||||||
def has_unresolved_order(self, code: str) -> bool:
|
|
||||||
"""判断证券是否存在必须阻止自动下单的未决订单。"""
|
|
||||||
with self.lock:
|
|
||||||
item = self.items.get(code)
|
|
||||||
return item is not None and _has_unresolved_order(item)
|
|
||||||
|
|
||||||
def sync_positions(self, positions: Iterable[PositionItem]) -> None:
|
def sync_positions(self, positions: Iterable[PositionItem]) -> None:
|
||||||
"""把尚未接管的真实持仓初始化为已完成底仓。
|
"""把尚未接管的真实持仓初始化为已完成底仓。
|
||||||
@@ -137,20 +119,39 @@ class State:
|
|||||||
positions: Iterable[PositionItem],
|
positions: Iterable[PositionItem],
|
||||||
orders: list[OrderItem],
|
orders: list[OrderItem],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""用真实持仓、委托和成交恢复本地状态,不增加持久化字段。"""
|
"""用真实持仓和委托恢复本地状态;拆分订单全部完成才算完成。"""
|
||||||
position_list = list(positions)
|
position_list = list(positions)
|
||||||
self.sync_positions(position_list)
|
self.sync_positions(position_list)
|
||||||
position_codes = {
|
position_codes = {
|
||||||
item.stock_code for item in position_list if item.volume > 0
|
item.stock_code for item in position_list if item.volume > 0
|
||||||
}
|
}
|
||||||
|
orders_by_local_id: dict[str, list[OrderItem]] = {}
|
||||||
|
for order in orders:
|
||||||
|
if order.local_order_id:
|
||||||
|
orders_by_local_id.setdefault(order.local_order_id, []).append(order)
|
||||||
|
|
||||||
for code in list(self.codes):
|
for code in list(self.codes):
|
||||||
item = self.get(code)
|
item = self.get(code)
|
||||||
item.base_status = _reconcile_leg(
|
for order_id_attr, status_attr in (
|
||||||
item.base_order_id, item.base_status, item.base_qty, orders, deals
|
("base_order_id", "base_status"),
|
||||||
)
|
("added_order_id", "added_status"),
|
||||||
item.added_status = _reconcile_leg(
|
):
|
||||||
item.added_order_id, item.added_status, item.added_qty, orders, deals
|
local_order_id = getattr(item, order_id_attr)
|
||||||
|
current_status = getattr(item, status_attr)
|
||||||
|
if (
|
||||||
|
current_status not in {STATUS_ING, STATUS_UNKNOWN}
|
||||||
|
or not local_order_id
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
matching_orders = orders_by_local_id.get(local_order_id)
|
||||||
|
if matching_orders:
|
||||||
|
status = (
|
||||||
|
STATUS_OK
|
||||||
|
if all(order.status == "56" for order in matching_orders)
|
||||||
|
else STATUS_ING
|
||||||
)
|
)
|
||||||
|
setattr(item, status_attr, status)
|
||||||
self.set(item)
|
self.set(item)
|
||||||
|
|
||||||
# Opening orders normally have no position until their first fill. Order
|
# Opening orders normally have no position until their first fill. Order
|
||||||
@@ -194,79 +195,3 @@ class State:
|
|||||||
}
|
}
|
||||||
except (TypeError, ValueError) as exc:
|
except (TypeError, ValueError) as exc:
|
||||||
raise ValueError(f"[状态] 状态字段无效: {exc}") from exc
|
raise ValueError(f"[状态] 状态字段无效: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
def _reconcile_leg(
|
|
||||||
local_order_id: str,
|
|
||||||
current_status: str,
|
|
||||||
expected_qty: int,
|
|
||||||
orders: list[OrderItem],
|
|
||||||
deals: list[dict[str, str]],
|
|
||||||
) -> str:
|
|
||||||
if current_status not in {STATUS_ING, STATUS_UNKNOWN} or not local_order_id:
|
|
||||||
return current_status
|
|
||||||
matching_orders = [
|
|
||||||
order for order in orders if order.local_order_id == local_order_id
|
|
||||||
]
|
|
||||||
order = matching_orders[-1] if matching_orders else None
|
|
||||||
if order is None:
|
|
||||||
matching_deals = [
|
|
||||||
row for row in deals if _matches_local_order(row, local_order_id)
|
|
||||||
]
|
|
||||||
dealt = sum(_deal_volume(row) for row in matching_deals)
|
|
||||||
if expected_qty > 0 and dealt >= expected_qty:
|
|
||||||
return STATUS_OK
|
|
||||||
if expected_qty <= 0 and matching_deals:
|
|
||||||
return STATUS_OK
|
|
||||||
return STATUS_UNKNOWN
|
|
||||||
|
|
||||||
system_order_id = order.id.strip()
|
|
||||||
matching_deals = [
|
|
||||||
row
|
|
||||||
for row in deals
|
|
||||||
if (
|
|
||||||
system_order_id
|
|
||||||
and str(row.get("m_strOrderSysID") or "").strip() == system_order_id
|
|
||||||
)
|
|
||||||
or (not system_order_id and _matches_local_order(row, local_order_id))
|
|
||||||
]
|
|
||||||
dealt = sum(_deal_volume(row) for row in matching_deals)
|
|
||||||
traded = max(order.traded_volume, dealt)
|
|
||||||
ordered = order.volume or expected_qty
|
|
||||||
status = order.status
|
|
||||||
if ordered > 0 and traded >= ordered:
|
|
||||||
return STATUS_OK
|
|
||||||
if status in {"48", "49", "50", "51", "52", "55"}:
|
|
||||||
return STATUS_ING
|
|
||||||
if status in {"54", "56"}:
|
|
||||||
return STATUS_UNKNOWN if traded > 0 else STATUS_CANCELED
|
|
||||||
if status in {"57", "58"}:
|
|
||||||
return STATUS_UNKNOWN if traded > 0 else STATUS_FAILED
|
|
||||||
return STATUS_UNKNOWN
|
|
||||||
|
|
||||||
|
|
||||||
def _has_unresolved_order(item: StateItem) -> bool:
|
|
||||||
return item.base_status in {STATUS_ING, STATUS_UNKNOWN} or item.added_status in {
|
|
||||||
STATUS_ING,
|
|
||||||
STATUS_UNKNOWN,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _matches_local_order(row: dict[str, str], local_order_id: str) -> bool:
|
|
||||||
remark = str(row.get("m_strRemark") or "")
|
|
||||||
return remark.split("|", 1)[0] == local_order_id
|
|
||||||
|
|
||||||
|
|
||||||
def _deal_volume(deal: dict[str, str]) -> int:
|
|
||||||
for key in ("m_nVolume", "m_nTradeVolume", "m_nVolumeTraded"):
|
|
||||||
volume = _as_int(deal.get(key))
|
|
||||||
if volume > 0:
|
|
||||||
return volume
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _as_int(value: object) -> int:
|
|
||||||
try:
|
|
||||||
return int(value or 0)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return 0
|
|
||||||
|
|||||||
Binary file not shown.
@@ -65,7 +65,7 @@ class TrendTests(unittest.TestCase):
|
|||||||
|
|
||||||
book.refresh(client)
|
book.refresh(client)
|
||||||
|
|
||||||
self.assertEqual(set(book.data), {"BUY-A", "SELL-B"})
|
self.assertEqual({item.id for item in book.data}, {"completed"})
|
||||||
self.assertEqual(client.canceled, ["active"])
|
self.assertEqual(client.canceled, ["active"])
|
||||||
|
|
||||||
def test_position_dataclasses_execute_without_type_error(self):
|
def test_position_dataclasses_execute_without_type_error(self):
|
||||||
@@ -126,15 +126,20 @@ class TrendTests(unittest.TestCase):
|
|||||||
second = handle_loss(runtime, position, Tick(last_price=5), -50, 5000)
|
second = handle_loss(runtime, position, Tick(last_price=5), -50, 5000)
|
||||||
self.assertIn("等待", second.message)
|
self.assertIn("等待", second.message)
|
||||||
|
|
||||||
def test_reconcile_ing_order_from_deal(self):
|
def test_reconcile_split_orders_complete_only_when_all_are_status_56(self):
|
||||||
with TemporaryDirectory() as directory:
|
with TemporaryDirectory() as directory:
|
||||||
state = State.for_strategy(directory, "trend", "A")
|
state = State.for_strategy(directory, "trend", "A")
|
||||||
position = PositionItem(stock_code="A", volume=100, open_price=10)
|
position = PositionItem(stock_code="A", volume=100, open_price=10)
|
||||||
state.set(StateItem("A", base_order_id="local-1", base_status="ING"))
|
state.set(StateItem("A", base_order_id="local-1", base_status="ING"))
|
||||||
|
completed = OrderItem("1", "A", "BUY", "", "56", None, 50, "local-1")
|
||||||
|
processing = OrderItem("2", "A", "BUY", "", "50", None, 50, "local-1")
|
||||||
|
|
||||||
|
state.reconcile([position], [completed, processing])
|
||||||
|
self.assertEqual(state.get("A").base_status, "ING")
|
||||||
|
|
||||||
state.reconcile(
|
state.reconcile(
|
||||||
[position],
|
[position],
|
||||||
[],
|
[completed, OrderItem("2", "A", "BUY", "", "56", None, 50, "local-1")],
|
||||||
[{"m_strRemark": "local-1|morning"}],
|
|
||||||
)
|
)
|
||||||
self.assertEqual(state.get("A").base_status, STATUS_OK)
|
self.assertEqual(state.get("A").base_status, STATUS_OK)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user