添加交易逻辑
This commit is contained in:
parent
f6aac0ca17
commit
43ccc9bf0f
|
|
@ -50,6 +50,10 @@ app/services/strategy_advisor.py(新增);app/services/strategy_service.py
|
|||
**真机判收**
|
||||
部分判收(2026-08-25 盘中)。桥机 make deploy + make test 见 ALL SUITES PASS;dry-run 试算真机四票读数全部合理:688802.SH 想挂网格但「按投入比例折出 47,250 元买不起一手」被挡(科创板高价票,判定正确);002179.SZ 有在途指令主动让路;其余两票无信号静默跳过;unknown_states 为空(子串归类修对了)。**尚未判收的**:真挂一条(等在途清了重扫或次日 09:40)、边三/边四真机走一遍、strategy_runner 对自动网格的逐档发单。
|
||||
|
||||
**当日盘中第三批(赶进度拍板后加的两件)**
|
||||
一,科创板取整从「挂载侧兜」补到「执行侧真修」:strategy_runner 新增 _min_lot(688/689 按 200 股),网格手工挂的 100 股档在评估时抬成合法数量、卖出量不足 200 股原地等不推进档位;跟踪止盈的部分卖低于 200 股时量够就抬到 200(方向是保利润,偏保守)、可卖本身不足 200 时部分卖放弃,全清路径按交易所例外允许不足 200 股一次性全卖。主板行为一字未变,单测把两侧都钉住(批十七扩到 38 例,总数 557)。做T是命令授权的手工策略,科创板做T的开仓与平回数量怎么处理需要单独拍板,本次不动。
|
||||
二,管理页「我的策略」加了「自动挂载 · 看看今天想挂什么」按钮:调 09:40 调度位同一份实现的试算模式,把想挂、想换挂、想停买入、被挡、跳过的每一条连原因渲染成可读文字,不再需要敲命令;空结果明说「没有想动的是正常克制」。
|
||||
|
||||
**当日盘中补两处(第二次交付)**
|
||||
一,科创板一手口径:dry-run 里 688802.SH 暴露全库按 100 股一手,而 688/689 最小申报 200 股——自动网格若生成 per_lot=100 会被券商拒单。advisor 补 lot_of()(688/689→200),买得起一手与 per_lot 下限都按它算(200 仍是 100 的整数倍,runner 的取整不会磨掉)。runner 侧卖出数量对科创板的整百取整仍不完美,见欠账。二,判分读数脚本 scripts/report_strategy_score.py 交付:四段只读统计(样本盘点 / 网格差价按策略内均价配对 / 止盈「卖点之后又跌多少=保住的钱」/ 挂上 vs 名额挡下的对照组涨跌),样本不足三条只报数不下结论,连不上库或取不到现价都说人话。batch17 扩到 36 例,总数 555。
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,16 @@ def _round_lot(qty) -> int:
|
|||
return (n // LOT) * LOT
|
||||
|
||||
|
||||
def _min_lot(code) -> int:
|
||||
"""最小申报数量: 科创板 (688/689 开头) 买卖都是 200 股起, 其余 100 股。
|
||||
|
||||
2026-08-25 补: 持仓里出现科创板票后发现全库取整都按 100 股, 而科创板 100 股的申报
|
||||
会被券商直接拒掉。规则还有一条例外: 持仓不足 200 股时允许**一次性全部卖出**,
|
||||
网格与止盈的各卖出落点分别处理了这一条。做T是命令授权的手工策略, 科创板做T的
|
||||
开腿与平回数量怎么处理需要单独拍板, 本次不动 (见 DEVLOG)。"""
|
||||
return 200 if str(code or "").startswith(("688", "689")) else 100
|
||||
|
||||
|
||||
def _today() -> int:
|
||||
return td.ymd()
|
||||
|
||||
|
|
@ -354,7 +364,10 @@ def _eval_grid(st, pos, day, now, ctx):
|
|||
if not levels:
|
||||
return None
|
||||
lo = levels[0]
|
||||
mlot = _min_lot(st.get("ts_code"))
|
||||
per_lot = _round_lot(params.get("per_lot")) or LOT
|
||||
if per_lot < mlot:
|
||||
per_lot = mlot # 科创板 200 股起: 手工挂的 100 股档在这里抬成合法数量
|
||||
max_capital = _f(params.get("max_capital"))
|
||||
filled = {int(k): dict(v) for k, v in (state.get("filled_levels") or {}).items()}
|
||||
invested = _f(state.get("invested"))
|
||||
|
|
@ -403,7 +416,7 @@ def _eval_grid(st, pos, day, now, ctx):
|
|||
k = last
|
||||
if k in filled and avail >= LOT and actual_add >= LOT:
|
||||
q = min(per_lot, _round_lot(avail))
|
||||
if q >= LOT:
|
||||
if q >= mlot: # 科创板部分卖低于 200 股不合法, 量不足先不推进
|
||||
state["last_band"] = last + 1
|
||||
info = filled[k]
|
||||
return {"side": "sell", "action": A_SELL, "qty": q, "leg": f"grid_sell:{k}",
|
||||
|
|
@ -467,16 +480,28 @@ def _eval_trail(st, pos, day, now, ctx):
|
|||
|
||||
if avail < LOT:
|
||||
return None # 无 T+1 可卖, 只更新高水位
|
||||
mlot = _min_lot(st.get("ts_code"))
|
||||
|
||||
def _all_out():
|
||||
# 全清数量: 科创板持仓不足 200 股时按交易所例外一次性全卖, 否则整百
|
||||
return avail if avail < mlot else (_round_lot(avail) or avail)
|
||||
|
||||
# 硬止盈目标: 直接全清
|
||||
if hard_target > 0 and profit >= hard_target:
|
||||
return {"side": "sell", "action": A_EXIT, "qty": _round_lot(avail) or avail, "leg": "trail_hard",
|
||||
return {"side": "sell", "action": A_EXIT, "qty": _all_out(), "leg": "trail_hard",
|
||||
"reason": f"跟踪止盈-硬目标: 浮盈 {profit:.1%} ≥ {hard_target:.1%}, 全清 avail {avail} 股"}
|
||||
|
||||
# 已武装且从高点回落到设定比例 → 卖
|
||||
if state.get("armed") and hw > 0 and price <= hw * (1 - giveback) and giveback > 0:
|
||||
q = _round_lot(avail * sell_ratio) if sell_ratio < 1 else (_round_lot(avail) or avail)
|
||||
if q >= LOT or (sell_ratio >= 1 and q > 0):
|
||||
if sell_ratio < 1:
|
||||
q = _round_lot(avail * sell_ratio)
|
||||
if mlot > LOT:
|
||||
# 科创板: 部分卖低于 200 股不合法 —— 量够就抬到 200 (方向是保利润, 多卖
|
||||
# 一点偏保守), 可卖的本来就不足 200 则这条部分卖路径放弃, 等硬目标或人工
|
||||
q = 0 if avail < mlot else min(max(q, mlot), _round_lot(avail))
|
||||
else:
|
||||
q = _all_out()
|
||||
if q >= mlot or (sell_ratio >= 1 and q > 0):
|
||||
act = A_EXIT if sell_ratio >= 1 else A_SELL
|
||||
return {"side": "sell", "action": act, "qty": q, "leg": "trail_sell",
|
||||
"reason": f"跟踪止盈: 现价 {price} 自高点 {hw} 回落 {1 - price / hw:.1%} ≥ {giveback:.1%}, 卖 {q} 股"}
|
||||
|
|
|
|||
|
|
@ -654,9 +654,45 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
|
|||
<el-tag size="small" :type="stratEnabled?'primary':'info'" effect="dark" style="margin-left:8px">
|
||||
{{ stratEnabled ? '策略层已启用' : '策略层总开关未开(PMS_STRATEGY_ENABLED)' }}</el-tag>
|
||||
<el-checkbox v-model="showAllStrategies" @change="loadStrategies" size="small" style="margin-left:12px;font-weight:normal">显示已撤下/已完成</el-checkbox>
|
||||
<el-button size="small" style="margin-left:12px" :loading="autoScanBusy" @click="autoScanPreview">
|
||||
自动挂载 · 看看今天想挂什么</el-button>
|
||||
</h3>
|
||||
<el-alert v-if="!stratEnabled" type="info" effect="dark" :closable="false" show-icon style="margin-bottom:8px"
|
||||
title="策略挂了也不会出手:请在「设置 / 运维」把 PMS_STRATEGY_ENABLED 打开;影子还是实盘由 PMS_DISPATCH_MODE 决定。"></el-alert>
|
||||
<!-- 自动挂载试算结果 (2026-08-25): 只判断不落任何东西, 真跑在每个交易日 09:40 的调度位 -->
|
||||
<div v-if="autoScanRes" style="border:1px solid var(--bd,#dcdfe6);border-radius:6px;padding:8px 12px;margin-bottom:8px">
|
||||
<div class="muted" style="margin-bottom:4px">
|
||||
试算只判断、不挂载不留痕;真跑在每个交易日 09:40 由调度自动执行。本轮查了
|
||||
<b>{{ autoScanRes.checked }}</b> 只持仓票。
|
||||
</div>
|
||||
<div v-for="(x,i) in (autoScanRes.attached||[])" :key="'aa'+i">
|
||||
<el-tag size="small" type="success" effect="dark">想挂</el-tag>
|
||||
<b> {{ nm(x.ts_code) }}</b> {{ tx('stype', x.type) }} —— {{ x.why }}</div>
|
||||
<div v-for="(x,i) in (autoScanRes.handoffs||[])" :key="'ah'+i">
|
||||
<el-tag size="small" type="success" effect="dark">想换挂</el-tag>
|
||||
<b> {{ nm(x.ts_code) }}</b> 网格换跟踪止盈 —— {{ x.why }}</div>
|
||||
<div v-for="(x,i) in (autoScanRes.paused||[])" :key="'ap'+i">
|
||||
<el-tag size="small" type="warning" effect="dark">想停买入</el-tag>
|
||||
<b> {{ nm(x.ts_code) }}</b> —— {{ x.why }}</div>
|
||||
<div v-for="(x,i) in (autoScanRes.resumed||[])" :key="'ar'+i">
|
||||
<el-tag size="small" type="success" effect="dark">想恢复买入</el-tag>
|
||||
<b> {{ nm(x.ts_code) }}</b></div>
|
||||
<div v-for="(x,i) in (autoScanRes.blocked||[])" :key="'ab'+i" style="color:var(--warn)">
|
||||
<el-tag size="small" type="warning" effect="dark">被挡</el-tag>
|
||||
<b> {{ nm(x.ts_code) }}</b> —— {{ x.why }}</div>
|
||||
<div v-for="(x,i) in (autoScanRes.skipped||[])" :key="'as'+i" class="muted">
|
||||
· <template v-if="x.ts_code">{{ nm(x.ts_code) }}:</template>{{ x.why }}</div>
|
||||
<div v-for="(e,i) in (autoScanRes.errors||[])" :key="'ae'+i" style="color:#d8574a">✗ {{ e }}</div>
|
||||
<div v-if="(autoScanRes.unknown_states||[]).length" style="color:var(--warn)">
|
||||
有 {{ autoScanRes.unknown_states.length }} 只票的吸筹说法不在约定词表里(按无标志处理),
|
||||
样本:{{ autoScanRes.unknown_states.slice(0,3).map(u=>u.state).join('、') }}——需与决策系统核对。
|
||||
</div>
|
||||
<div class="muted" v-if="!(autoScanRes.attached||[]).length && !(autoScanRes.handoffs||[]).length
|
||||
&& !(autoScanRes.paused||[]).length && !(autoScanRes.resumed||[]).length
|
||||
&& !(autoScanRes.blocked||[]).length">
|
||||
今天没有想动的:持仓里暂时没有够格的信号,这是正常的克制。
|
||||
</div>
|
||||
</div>
|
||||
<div class="muted" v-if="!strategies.length">还没有挂任何交易方案。到「我的持仓」某行「更多 ▾ → 挂交易方案」给它挂一个。</div>
|
||||
<el-table v-else :data="strategiesShown" size="small" border max-height="300" empty-text="没有进行中的策略(勾选『显示已撤下/已完成』看全部)">
|
||||
<el-table-column label="股票" width="150"><template #default="s"><b>{{ nm(s.row.ts_code) }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column>
|
||||
|
|
@ -1576,6 +1612,7 @@ createApp({
|
|||
const positions = ref([]), lots = ref([]), lotsOf = ref('');
|
||||
const instructions = ref([]), ledger = ref([]), proposals = ref([]);
|
||||
const strategies = ref([]), stratEnabled = ref(false), opLog = ref([]);
|
||||
const autoScanRes = ref(null), autoScanBusy = ref(false); // 自动挂载试算 (2026-08-25)
|
||||
// 「显示已完成/已移除」开关 (默认关: 只看进行中)。命令与策略勾选时会拉全量含已归档 (loadX 传
|
||||
// include_archived); 在途指令纯前端过滤, 不重拉。
|
||||
const showAllCommands = ref(false), showAllStrategies = ref(false), showAllInstr = ref(false);
|
||||
|
|
@ -2345,6 +2382,16 @@ createApp({
|
|||
strategies.value = d.strategies || (d.data && d.data.strategies) || [];
|
||||
stratEnabled.value = !!(d.enabled != null ? d.enabled : (d.data && d.data.enabled));
|
||||
}
|
||||
// 自动挂载试算: 只判断不落任何东西 (与 09:40 调度位同一份实现, 带 dry_run)
|
||||
async function autoScanPreview() {
|
||||
autoScanBusy.value = true;
|
||||
try {
|
||||
const d = await call('post', '/api/ops/strategy-attach-scan?dry_run=true', {}) || {};
|
||||
if (d.checked == null) d.checked = 0; // 后端不可达时也别渲染出 undefined
|
||||
if (d.error && !(d.errors || []).length) d.errors = [d.error];
|
||||
autoScanRes.value = d;
|
||||
} finally { autoScanBusy.value = false; }
|
||||
}
|
||||
// 软归档: 把终态记录从在办视图移除 (on=true) 或恢复显示 (on=false)。kind = commands/strategies/instructions/proposals
|
||||
async function archiveRow(kind, id, on) {
|
||||
const d = await call('post', '/api/' + kind + '/' + id + '/archive', { archived: on });
|
||||
|
|
@ -2497,6 +2544,7 @@ createApp({
|
|||
pctOf, tgtPos, posMoveValid, posMovePreview, doPosMove, heldSectors, secSel, doSectorExit,
|
||||
pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy,
|
||||
strategies, stratEnabled, opLog, stratDlg, stratQty, loadStrategies, loadOpLog,
|
||||
autoScanRes, autoScanBusy, autoScanPreview,
|
||||
openStrategy, validateStrategy, attachStrategy, setStrategyStatus, stratStateText, resumeBuy,
|
||||
showLedger, onPosExpand,
|
||||
openScan, insTab, todayYmd, insLive, insDone, insEnd, dispOf, loadOpenScan, denyToday,
|
||||
|
|
|
|||
|
|
@ -27,11 +27,12 @@
|
|||
在途与配额文案拆分; 减仓不掐策略腿 (12 例)
|
||||
test_batch16_units.py 软归档 archived_at: 单表守卫/只归终态/列表默认排除 (5 例)
|
||||
test_batch17_units.py 策略自动挂载: 定性归类(子串+保守优先)/网格参数生成/
|
||||
科创板一手200/连边矩阵/note 约定与冷却推导/接力判定/
|
||||
clear_buypause/编排冒烟(dry_run 滴水不写/名额/边三/
|
||||
边四全链)/判分脚本聚合与对照分组 (36 例)
|
||||
科创板一手200(advisor 与 runner 两侧)/连边矩阵/
|
||||
note 约定与冷却推导/接力判定/clear_buypause/
|
||||
编排冒烟(dry_run 滴水不写/名额/暂停买入/接力全链)/
|
||||
判分脚本聚合与对照分组 (38 例)
|
||||
test_wiring.py 装配自检: 服务层→核心→落表 全链路 (内存桩) (58 例)
|
||||
共 555 例
|
||||
共 557 例
|
||||
任一子集失败即整体失败 (退出码 1)。
|
||||
"""
|
||||
import os
|
||||
|
|
|
|||
|
|
@ -679,6 +679,55 @@ def _():
|
|||
param_store.get_bool, adv._params = orig, orig_p
|
||||
|
||||
|
||||
@case("[科创板] runner 止盈卖出: 200 股起, 不足 200 只许一次性全清, 主板行为一字不变")
|
||||
def _():
|
||||
from app.services import strategy_runner as srun
|
||||
assert srun._min_lot("688802.SH") == 200 and srun._min_lot("600000.SH") == 100
|
||||
|
||||
def trail(code, avail, ratio, avg=8.0, price=10.0):
|
||||
st = {"ts_code": code, "params": {"giveback": 0.05, "sell_ratio": ratio}}
|
||||
pos = {"avg_cost": avg, "avail_qty": avail, "cushion_pct": price / avg - 1}
|
||||
ctx = {"state": {"armed": True, "high_water": 12.0}, "notes": []}
|
||||
return srun._eval_trail(st, pos, {"price": price}, None, ctx)
|
||||
|
||||
d = trail("688802.SH", 150, 1.0) # 全清: 不足 200 按例外一次性全卖
|
||||
assert d and d["qty"] == 150 and d["action"] == srun.A_EXIT, d
|
||||
d = trail("688802.SH", 700, 0.25) # 部分卖 175→整百 100 不合法 → 抬到 200
|
||||
assert d and d["qty"] == 200, d
|
||||
assert trail("688802.SH", 150, 0.5) is None # 可卖不足 200, 部分卖放弃
|
||||
d = trail("600000.SH", 700, 0.25) # 主板照旧 100
|
||||
assert d and d["qty"] == 100, d
|
||||
d = trail("600000.SH", 150, 1.0) # 主板全清照旧整百
|
||||
assert d and d["qty"] == 100, d
|
||||
|
||||
|
||||
@case("[科创板] runner 网格: 手工 100 股档抬到 200 / 卖出量不足 200 不推进档位")
|
||||
def _():
|
||||
from app.services import strategy_runner as srun
|
||||
prm = {"lower": 9.0, "upper": 11.0, "center": 10.0, "step_pct": 0.02,
|
||||
"per_lot": 100, "max_capital": 50000}
|
||||
|
||||
def grid(code, price, avail, state, add_qty=None):
|
||||
st = {"ts_code": code, "params": prm}
|
||||
pos = {"avail_qty": avail, "add_qty": avail if add_qty is None else add_qty}
|
||||
ctx = {"state": state, "notes": [], "buy_paused": False}
|
||||
return srun._eval_grid(st, pos, {"price": price}, None, ctx)
|
||||
|
||||
lv = srun._grid_levels(prm)
|
||||
# 下行跌破一档 → 买: 科创板把 100 股/档抬成 200
|
||||
b0 = srun._band(lv, 9.5)
|
||||
d = grid("688802.SH", 9.5, 0, {"last_band": b0 + 1, "filled_levels": {}})
|
||||
assert d and d["side"] == "buy" and d["qty"] == 200, d
|
||||
d = grid("600000.SH", 9.5, 0, {"last_band": b0 + 1, "filled_levels": {}})
|
||||
assert d and d["qty"] == 100, d # 主板照旧
|
||||
# 上行涨破 → 卖: 网格股有 200 但今天可卖只有 100, 科创板卖 100 不合法, 原地等
|
||||
b1 = srun._band(lv, 10.5)
|
||||
st1 = {"last_band": b1 - 1,
|
||||
"filled_levels": {str(b1 - 1): {"qty": 200, "price": 9.8}}}
|
||||
assert grid("688802.SH", 10.5, 100, st1, add_qty=200) is None
|
||||
assert st1.get("last_band") == b1 - 1, st1 # 档位没被推进, 下跳凑够了还能卖
|
||||
|
||||
|
||||
@case("[判分] report_strategy_score: 成交聚合按边分侧 / 对照只收名额挡下的 / 涨跌口径空值安全")
|
||||
def _():
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
|
|
|||
Loading…
Reference in New Issue