This commit is contained in:
2026-09-03 15:27:11 +08:00
parent 9ef063f4ce
commit cf5f33ab9e
4 changed files with 67 additions and 7 deletions

View File

@@ -43,7 +43,7 @@ def open_signal(run:Runtime, ticks, open_signals) -> None:
# 当前价高于昨收价可开仓 # 当前价高于昨收价可开仓
if signal_config.gt_last_price_is_open and item.last_close>0 and price>item.last_close: if signal_config.gt_last_price_is_open and item.last_close>0 and price>item.last_close:
try: try:
do_open(run,item.code,volume,item.signal_key) do_open(run, item.code, volume, item.signal_key, price)
log.info("[Open] %s 信号=%s,买入=%d股,原因=现价高于昨收", item.code, item.signal_key, volume) log.info("[Open] %s 信号=%s,买入=%d股,原因=现价高于昨收", item.code, item.signal_key, volume)
except RuntimeError as exc: except RuntimeError as exc:
log.info("[Open] %s 信号=%s,买入=%d股失败:%s", item.code, item.signal_key, volume, exc) log.info("[Open] %s 信号=%s,买入=%d股失败:%s", item.code, item.signal_key, volume, exc)
@@ -56,7 +56,7 @@ def open_signal(run:Runtime, ticks, open_signals) -> None:
continue continue
try: try:
do_open(run,item.code,volume,item.signal_key) do_open(run, item.code, volume, item.signal_key, price)
log.info("[Open] %s 信号=%s,买入=%d股,原因=反弹已确认", item.code, item.signal_key, volume) log.info("[Open] %s 信号=%s,买入=%d股,原因=反弹已确认", item.code, item.signal_key, volume)
except RuntimeError as exc: except RuntimeError as exc:
log.warning("[Open] %s 信号=%s,买入=%d股失败:%s", item.code, item.signal_key, volume, exc) log.warning("[Open] %s 信号=%s,买入=%d股失败:%s", item.code, item.signal_key, volume, exc)
@@ -64,7 +64,7 @@ def open_signal(run:Runtime, ticks, open_signals) -> None:
log.exception("[Open] %s 信号=%s,买入=%d股异常", item.code, item.signal_key, volume) log.exception("[Open] %s 信号=%s,买入=%d股异常", item.code, item.signal_key, volume)
def do_open(run:Runtime,code:str,volume:int,signal_key:str)->None: def do_open(run: Runtime, code: str, volume: int, signal_key: str, price: float) -> None:
"""生成本地订单号并按最新价提交开仓委托。""" """生成本地订单号并按最新价提交开仓委托。"""
order_id = run.orders.new_order_id("base") order_id = run.orders.new_order_id("base")
request = PlaceOrderRequest( request = PlaceOrderRequest(
@@ -78,6 +78,16 @@ def do_open(run:Runtime,code:str,volume:int,signal_key:str)->None:
if not run.orders.place(request): if not run.orders.place(request):
raise RuntimeError("订单提交失败") raise RuntimeError("订单提交失败")
run.state.set(StateItem(
code=code,
base_order_id=order_id,
base_qty=volume,
base_cost=round(price, 2),
base_status=STATUS_ING,
))
run.state.save()
run.open_watch.forget(code)
def check_timezone(timezone: str, now: datetime | None = None) -> bool: def check_timezone(timezone: str, now: datetime | None = None) -> bool:
"""验证当前时间是否处于配置区间。 """验证当前时间是否处于配置区间。

View File

@@ -41,9 +41,9 @@ class OrderBook:
self.mutex = Lock() self.mutex = Lock()
@staticmethod @staticmethod
def new_order_id(leg: str) -> str: def new_order_id(_leg: str) -> str:
"""生成短订单号,为 QMT 备注中的信号键预留空间""" """生成 ``trend-xxxxxxxx`` 格式的本地订单号"""
return f"zt-{leg[:1]}-{secrets.token_hex(4)}" return f"trend-{secrets.token_hex(4)}"
def busy(self, code: str, side: str) -> bool: def busy(self, code: str, side: str) -> bool:
"""判断证券是否存在仍在处理中的同方向委托。""" """判断证券是否存在仍在处理中的同方向委托。"""

View File

@@ -108,7 +108,7 @@ class State:
StateItem( StateItem(
code=position.stock_code, code=position.stock_code,
base_qty=position.volume, base_qty=position.volume,
base_cost=position.open_price, base_cost=round(position.open_price, 2),
base_status=STATUS_OK, base_status=STATUS_OK,
) )
) )
@@ -165,6 +165,12 @@ class State:
# reconciliation must therefore happen before stale state is removed. # reconciliation must therefore happen before stale state is removed.
for code in list(self.codes): for code in list(self.codes):
if code not in position_codes: if code not in position_codes:
item = self.get(code)
if any(
order_id and order_id in orders_by_local_id
for order_id in (item.base_order_id, item.added_order_id)
):
continue
if self.delete(code): if self.delete(code):
log.info("[状态] 已移除持仓状态,代码=%s", code) log.info("[状态] 已移除持仓状态,代码=%s", code)
self.save() self.save()

View File

@@ -10,6 +10,7 @@ from unittest.mock import patch
from libs.grid_take_profit import GridState, GridTrailingTracker from libs.grid_take_profit import GridState, GridTrailingTracker
from sdk import APIError, Assets, OrderItem, Portfolio, PositionItem, Tick from sdk import APIError, Assets, OrderItem, Portfolio, PositionItem, Tick
from strategy.trend.order import OrderBook, PlaceOrderRequest from strategy.trend.order import OrderBook, PlaceOrderRequest
from strategy.trend.open import do_open
from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions
from strategy.trend.boot import RunOnce from strategy.trend.boot import RunOnce
from strategy.trend.state import STATUS_OK, STATUS_UNKNOWN, State, StateItem from strategy.trend.state import STATUS_OK, STATUS_UNKNOWN, State, StateItem
@@ -42,6 +43,49 @@ class FailedOrderClient:
class TrendTests(unittest.TestCase): class TrendTests(unittest.TestCase):
def test_trend_order_id_format(self):
self.assertRegex(OrderBook.new_order_id("base"), r"^trend-[0-9a-f]{8}$")
def test_open_records_base_order_and_rounded_cost(self):
with TemporaryDirectory() as directory:
state = State.for_strategy(directory, "trend", "A")
forgotten = []
runtime = SimpleNamespace(
client=FakeClient(),
orders=OrderBook(),
state=state,
open_watch=SimpleNamespace(forget=forgotten.append),
)
do_open(runtime, "000001.SZ", 100, "morning", 12.345)
item = state.get("000001.SZ")
self.assertRegex(item.base_order_id, r"^trend-[0-9a-f]{8}$")
self.assertEqual(item.base_qty, 100)
self.assertEqual(item.base_cost, 12.35)
self.assertEqual(item.base_status, "ING")
self.assertEqual(forgotten, ["000001.SZ"])
def test_pending_base_order_survives_position_delay(self):
with TemporaryDirectory() as directory:
state = State.for_strategy(directory, "trend", "A")
state.set(StateItem(
"000001.SZ",
base_order_id="trend-12345678",
base_status="ING",
))
pending = OrderItem(
"1", "000001.SZ", "BUY", "", "50", None, 100,
local_order_id="trend-12345678",
)
state.reconcile([], [pending])
self.assertEqual(
state.get("000001.SZ").base_order_id,
"trend-12345678",
)
def test_grid_states_and_account_isolation(self): def test_grid_states_and_account_isolation(self):
tracker = GridTrailingTracker(1) tracker = GridTrailingTracker(1)
self.assertEqual(tracker.observe("A:code", 2.1).state, GridState.ARMED) self.assertEqual(tracker.observe("A:code", 2.1).state, GridState.ARMED)