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:
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)
except RuntimeError as 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
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)
except RuntimeError as 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)
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")
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):
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:
"""验证当前时间是否处于配置区间。

View File

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

View File

@@ -108,7 +108,7 @@ class State:
StateItem(
code=position.stock_code,
base_qty=position.volume,
base_cost=position.open_price,
base_cost=round(position.open_price, 2),
base_status=STATUS_OK,
)
)
@@ -165,6 +165,12 @@ class State:
# reconciliation must therefore happen before stale state is removed.
for code in list(self.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):
log.info("[状态] 已移除持仓状态,代码=%s", code)
self.save()

View File

@@ -10,6 +10,7 @@ from unittest.mock import patch
from libs.grid_take_profit import GridState, GridTrailingTracker
from sdk import APIError, Assets, OrderItem, Portfolio, PositionItem, Tick
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.boot import RunOnce
from strategy.trend.state import STATUS_OK, STATUS_UNKNOWN, State, StateItem
@@ -42,6 +43,49 @@ class FailedOrderClient:
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):
tracker = GridTrailingTracker(1)
self.assertEqual(tracker.observe("A:code", 2.1).state, GridState.ARMED)