交易执行:人工清仓保留待可卖任务,归零仍停止旧买入(台账 076)
一、人工采纳清仓原来按当日可卖量裁剪。T+1为零时返回已采纳却丢弃执行安排,旧买入仍可能续单。现在只按确认数量与当前总持仓取较小值,执行器继续限制当日可卖量。持仓已归零仍停止旧买入,不额外生成卖单。 二、补齐两项真实入口回归,24专项与45批本地隔离禁网验证通过。普通减仓、成交核销与业务阈值保持。首次部署已由自然调度停止科士达剩余买入,补修尚待部署判收。
This commit is contained in:
parent
fad1bb1fb5
commit
28053f3bfc
15
DEVLOG.md
15
DEVLOG.md
|
|
@ -2029,3 +2029,18 @@ bafecb7 经 45 策略用例、44 批本地及候选镜像禁网回归通过,11
|
||||||
**真机判收。** 修复前14:10仅科士达存在买卖重叠。已保存指令、成交、账务与参数原值,详见规格。未改成交,不恢复七条历史暂停指令,不操作188。
|
**真机判收。** 修复前14:10仅科士达存在买卖重叠。已保存指令、成交、账务与参数原值,详见规格。未改成交,不恢复七条历史暂停指令,不操作188。
|
||||||
|
|
||||||
**还欠什么。** 候选镜像测试、部署、生效与页面核验,按真实证据追加。
|
**还欠什么。** 候选镜像测试、部署、生效与页面核验,按真实证据追加。
|
||||||
|
|
||||||
|
|
||||||
|
### 2026-10-19 14:22 首次部署与补查
|
||||||
|
|
||||||
|
fad1bb1于14:19:39在155四容器生效。镜像2e5213b9d37f9c7337f4857ab0401e7426ea196f80d1e6b78325aecc1a993d6b。
|
||||||
|
45批候选镜像禁网测试通过,22专项通过。14:20:05正常调度终止科士达剩余买入。
|
||||||
|
买入600/400保持,清仓400/200保持。两条新审计6912、6913均为system。
|
||||||
|
部署前后15项参数一致,全部持仓账务字段及批次账一致,零新增委托。
|
||||||
|
14:21再次调度正常,对账0差异,QMT在线。93行指令展示查询0.344秒。
|
||||||
|
本地Chrome使用接口替身渲染,显示等待可卖与等待下一批,无页面错误。
|
||||||
|
生产读取显示科士达清仓等待可卖、旧买入已撤销。未冒充登录后的生产浏览器验收。
|
||||||
|
|
||||||
|
补查人工清仓:T+1不可卖时原路径会采纳但不留指令。
|
||||||
|
改为保留不超过确认量与当前总持仓的清仓任务;持仓已归零仍停止旧买入。
|
||||||
|
新增两项专项,共24例。本项待候选镜像和第二次部署;没有代替用户采纳。
|
||||||
|
|
|
||||||
|
|
@ -573,10 +573,23 @@ def api_decide(request: Request, proposal_id: str, payload: dict = Body(default=
|
||||||
# 等人点采纳时持仓可能已被别的路卖少, 照原数落单会被卖出前的检查整条驳回,
|
# 等人点采纳时持仓可能已被别的路卖少, 照原数落单会被卖出前的检查整条驳回,
|
||||||
# 人点了「清掉」结果一股没卖, 且那条指令会永久在途、挡住后续所有清仓。
|
# 人点了「清掉」结果一股没卖, 且那条指令会永久在途、挡住后续所有清仓。
|
||||||
cur = pms_repo.get_position(p["ts_code"]) or {}
|
cur = pms_repo.get_position(p["ts_code"]) or {}
|
||||||
|
if p['action'] == 'EXIT':
|
||||||
|
# 与命令清仓、信号清仓一致:T+1限制当次出手,不丢弃已确认的清仓安排。
|
||||||
|
# 不扩大人确认的股数;已卖少的持仓按当前总量缩减。
|
||||||
|
qty = min(qty, max(0, int(cur.get('total_qty') or 0)))
|
||||||
|
else:
|
||||||
qty = ae.clamp_sell_qty(qty, cur)
|
qty = ae.clamp_sell_qty(qty, cur)
|
||||||
if qty <= 0:
|
if qty <= 0:
|
||||||
|
if p['action'] == 'EXIT':
|
||||||
|
from app.services.execution_conflicts import enforce_exit
|
||||||
|
stopped = enforce_exit({'instruction_id': proposal_id, 'ts_code': p['ts_code'],
|
||||||
|
'action': 'EXIT', 'side': 'sell'})
|
||||||
|
if not stopped['ok']:
|
||||||
|
return {'ok': False, 'decision': decision,
|
||||||
|
'error': '已确认清仓,但旧买入收尾未完成:' + ';'.join(stopped['errors'])}
|
||||||
return {"ok": True, "decision": decision, "instruction_id": None,
|
return {"ok": True, "decision": decision, "instruction_id": None,
|
||||||
"note": "这只票现在没有可卖的股数 (已被清掉或今天刚买入), 未落单"}
|
"note": "当前无需要卖出的股数,清仓对应的旧买入已停止" if p['action'] == 'EXIT'
|
||||||
|
else "当前没有可卖的股数,本次未落单"}
|
||||||
pms_repo.insert_instruction(
|
pms_repo.insert_instruction(
|
||||||
instruction_id=instruction_id, origin_type="proposal", origin_id=proposal_id,
|
instruction_id=instruction_id, origin_type="proposal", origin_id=proposal_id,
|
||||||
ts_code=p["ts_code"], action=p["action"], side=side, qty=qty,
|
ts_code=p["ts_code"], action=p["action"], side=side, qty=qty,
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,10 @@
|
||||||
22项专项回归通过,覆盖五类清仓入口共用执行器、撤单失败与旧进度覆盖、T0不误挡、人工确认被拦时保持待确认。
|
22项专项回归通过,覆盖五类清仓入口共用执行器、撤单失败与旧进度覆盖、T0不误挡、人工确认被拦时保持待确认。
|
||||||
45批配置隔离、禁止联网的回归通过,包含三页面守卫。生产状态另记部署判收。
|
45批配置隔离、禁止联网的回归通过,包含三页面守卫。生产状态另记部署判收。
|
||||||
原临时虚拟环境被系统清理,本轮另建 /private/tmp/pms-conflict-20261019-venv。
|
原临时虚拟环境被系统清理,本轮另建 /private/tmp/pms-conflict-20261019-venv。
|
||||||
|
|
||||||
|
## 人工清仓补查
|
||||||
|
|
||||||
|
人工采纳EXIT原来按当日可卖量裁剪,T+1可卖量为零时会返回已采纳却不留清仓指令。
|
||||||
|
改为按已确认数量与当前总持仓取较小值,执行器继续按当日可卖量分批,不扩大用户授权数量。
|
||||||
|
持仓已经归零时不再生成卖单,但仍停止未完成的旧买入。普通减仓保持按可卖量处理。
|
||||||
|
增加两项回归,专项共24例。此项为代码发现的边界,尚无自然人工确认事件判收。
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
运行: docker compose run --rm pms-web python scripts/run_tests.py
|
运行: docker compose run --rm pms-web python scripts/run_tests.py
|
||||||
|
|
||||||
包含 (例数按 2026-10-12 隔离回归实跑校正):
|
包含 (例数按 2026-10-12 隔离回归实跑校正):
|
||||||
test_batch41_units.py 跨流程买卖互斥、清仓收尾、出口撤单与真实进展 (22 例)
|
test_batch41_units.py 跨流程买卖互斥、清仓收尾、出口撤单与真实进展及人工清仓T+1 (24 例)
|
||||||
test_batch40_units.py 逐笔历史修复、进度暂停、并发与事务回滚与均价失效 (22 例)
|
test_batch40_units.py 逐笔历史修复、进度暂停、并发与事务回滚与均价失效 (22 例)
|
||||||
test_batch39_units.py 成交双来源归属、迟到、部分成交与失败守成 (12 例)
|
test_batch39_units.py 成交双来源归属、迟到、部分成交与失败守成 (12 例)
|
||||||
test_batch38_units.py 每日计划顺序、版本核验、重新确认、并发采纳、历史保留及采纳失败说明、明确否决撤下 (30 例)
|
test_batch38_units.py 每日计划顺序、版本核验、重新确认、并发采纳、历史保留及采纳失败说明、明确否决撤下 (30 例)
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,33 @@ class ConflictsTest(unittest.TestCase):
|
||||||
self.assertEqual(self.fake.proposals['p']['status'],'WAIT_USER')
|
self.assertEqual(self.fake.proposals['p']['status'],'WAIT_USER')
|
||||||
self.assertEqual(len(self.fake.instructions),1)
|
self.assertEqual(len(self.fake.instructions),1)
|
||||||
|
|
||||||
|
def test_human_exit_retains_t1_locked_quantity(self):
|
||||||
|
from app.web import main
|
||||||
|
self.fake.positions[CODE]={'ts_code':CODE,'total_qty':400,'avail_qty':0}
|
||||||
|
self.fake.insert_proposal(proposal_id='exit_prop',ts_code=CODE,action='EXIT',qty=200,
|
||||||
|
hard_numbers={},expire_at=datetime.now()+timedelta(hours=1))
|
||||||
|
with patch.object(main,'ok_logged',side_effect=lambda name,fn,**kw:fn()), \
|
||||||
|
patch.object(main,'_actor',return_value='test'), \
|
||||||
|
patch.object(main.td,'window_deadline',return_value='2026-10-21'):
|
||||||
|
result=main.api_decide(None,'exit_prop',{'decision':'ACCEPTED','reason':'确认清仓'})
|
||||||
|
self.assertTrue(result['ok'],result)
|
||||||
|
row=self.fake.instructions[result['instruction_id']]
|
||||||
|
self.assertEqual(row['qty'],200) # 不扩成当前400股
|
||||||
|
self.assertEqual(row['action'],'EXIT')
|
||||||
|
self.assertEqual(row['exec_qty'],0)
|
||||||
|
|
||||||
|
def test_human_exit_after_position_closed_still_stops_old_buy(self):
|
||||||
|
from app.web import main
|
||||||
|
buy=ins(self.fake,'old','OPEN','buy',600,400)
|
||||||
|
self.fake.positions[CODE]={'ts_code':CODE,'total_qty':0,'avail_qty':0}
|
||||||
|
self.fake.insert_proposal(proposal_id='exit_prop',ts_code=CODE,action='EXIT',qty=200,
|
||||||
|
hard_numbers={},expire_at=datetime.now()+timedelta(hours=1))
|
||||||
|
with patch.object(main,'ok_logged',side_effect=lambda name,fn,**kw:fn()), \
|
||||||
|
patch.object(main,'_actor',return_value='test'):
|
||||||
|
result=main.api_decide(None,'exit_prop',{'decision':'ACCEPTED','reason':'确认清仓'})
|
||||||
|
self.assertTrue(result['ok'],result)
|
||||||
|
self.assertIsNone(result['instruction_id'])
|
||||||
|
self.assertEqual(buy['status'],'CANCELLED')
|
||||||
|
self.assertEqual(buy['exec_qty'],400)
|
||||||
|
|
||||||
if __name__=='__main__':unittest.main()
|
if __name__=='__main__':unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue