Apply 200k scope when CTP offline; trailing breakeven order UX.
When SimNow or live CTP is disconnected, default to the four-product whitelist regardless of reference capital. Trailing breakeven defaults off; when enabled hide take-profit and risk-reward, monitor exits via trailing stop only. Document both behaviors in TRADING.md and FEATURES.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -791,11 +791,12 @@ def api_symbol_search():
|
||||
q = request.args.get("q", "")
|
||||
conn = get_db()
|
||||
try:
|
||||
from trading_context import get_account_capital
|
||||
from trading_context import get_account_capital, is_ctp_connected
|
||||
capital = get_account_capital(conn, get_setting)
|
||||
ctp_connected = is_ctp_connected(get_setting)
|
||||
finally:
|
||||
conn.close()
|
||||
return jsonify(search_symbols(q, capital=capital))
|
||||
return jsonify(search_symbols(q, capital=capital, ctp_connected=ctp_connected))
|
||||
|
||||
|
||||
@app.route("/api/symbols/mains")
|
||||
|
||||
+5
-2
@@ -45,9 +45,10 @@
|
||||
|
||||
### 期货下单
|
||||
|
||||
- 品种联想(仅列出可开仓品种或全部主力,取决于计仓模式)
|
||||
- 品种联想(可开仓品种表与下拉一致;小账户或 CTP 未连接时仅四品种,见 [TRADING.md](./TRADING.md))
|
||||
- 方向、手数(固定手数 / 固定金额计仓)
|
||||
- 限价 / 市价(FAK)、止盈、止损
|
||||
- **移动保本**(默认关闭):开启后隐藏止盈与盈亏比,仅填止损;由移动止损监控平仓,不设固定止盈
|
||||
- 非交易时段禁止报单
|
||||
|
||||
### 当前持仓
|
||||
@@ -60,6 +61,8 @@
|
||||
### 可开仓品种
|
||||
|
||||
- 按当前权益与保证金上限筛选可开品种,养成开仓纪律、限制仓位
|
||||
- **权益 ≤20 万** 或 **CTP 未连接** 时,仅展示并可交易:玉米、豆粕、甲醇、螺纹钢(SimNow/实盘一致)
|
||||
- **夜盘时段** 仅显示有夜盘品种,并标注「夜盘」
|
||||
- **行业分类**、走势(多头/空头/震荡/转多/转空)、跳空、昨日成交量(手)、成交额
|
||||
- 支持行业筛选与多字段排序
|
||||
- 每日后台刷新缓存
|
||||
@@ -172,7 +175,7 @@
|
||||
| 导航显示 | 开关可选菜单项 |
|
||||
| 交易模式 | SimNow / 实盘 CTP |
|
||||
| 计仓模式 | 固定手数、固定金额 |
|
||||
| 保证金上限、移动保本、挂单超时 | 见表单说明 |
|
||||
| 保证金上限、移动保本缓冲、挂单超时 | 保证金上限默认 30%;移动保本缓冲为达 1R 后止损相对开仓价的跳数(默认 2 跳) |
|
||||
| CTP 连接 | SimNow / 实盘前置与账号(可覆盖 `.env`) |
|
||||
| 参考资金 | CTP 未连接时用于可开仓筛选与估算 |
|
||||
| 企业微信 Webhook | 计划/关键位推送 |
|
||||
|
||||
+54
-2
@@ -7,7 +7,7 @@
|
||||
| 区域 | 说明 |
|
||||
|------|------|
|
||||
| 顶栏 | 交易模式、CTP 状态、权益/可用、连接 CTP |
|
||||
| 期货下单 | 限价/市价报单、止盈止损、以损定仓/固定手数 |
|
||||
| 期货下单 | 限价/市价报单、止盈/止损、移动保本、以损定仓/固定手数 |
|
||||
| 当前持仓 | CTP 持仓卡片、挂单中、撤单、平仓 |
|
||||
| 可开仓品种 | 按权益与保证金上限筛选、行业分类、走势/跳空/成交量排序 |
|
||||
|
||||
@@ -36,6 +36,56 @@
|
||||
- 最大手数 = floor(权益 × 保证金上限 ÷ 1 手保证金)
|
||||
- 展示近一周日线走势、跳空、昨日成交量(手)、成交额
|
||||
- 可按 **行业** 筛选,支持多字段排序
|
||||
- **夜盘时段**:品种下拉与可开仓表仅显示有夜盘交易的品种,并带「夜盘」标记
|
||||
|
||||
### 小账户品种范围(≤20 万)
|
||||
|
||||
权益 **不超过 20 万元** 时,系统限制可浏览、可搜索、可报单的品种为以下 **4 个**:
|
||||
|
||||
| 品种 | 代码 |
|
||||
|------|------|
|
||||
| 玉米 | `c` |
|
||||
| 豆粕 | `m` |
|
||||
| 甲醇 | `MA` |
|
||||
| 螺纹钢 | `rb` |
|
||||
|
||||
适用范围:
|
||||
|
||||
- 可开仓品种表
|
||||
- 期货下单品种联想 / 下拉
|
||||
- 开仓报单校验(含趋势策略首仓)
|
||||
|
||||
**SimNow 与实盘规则一致**:**CTP 未连接** 时,无论系统设置中的参考资金是否大于 20 万,均 **默认按 20 万以下四品种范围** 展示与校验;连接 CTP 后改用柜台权益判断是否启用上述白名单。
|
||||
|
||||
页面会提示:「未连接 CTP,默认按 20 万以下账户:仅显示并可交易 玉米、豆粕、甲醇、螺纹钢」。
|
||||
|
||||
## 期货下单 · 止盈止损与移动保本
|
||||
|
||||
本地止盈止损由程序监控持仓,触发后市价平仓(与 CTP 柜台委托/持仓数据独立)。
|
||||
|
||||
### 默认模式(移动保本关闭)
|
||||
|
||||
- 可同时填写 **止盈**、**止损**
|
||||
- 填写入场价与止损/止盈后,表单下方显示 **盈亏比**、止损金额、止盈金额(如有)
|
||||
- 本地监控:价格触及止盈或止损即平仓
|
||||
|
||||
### 移动保本(可选,默认关闭)
|
||||
|
||||
勾选 **移动保本** 后:
|
||||
|
||||
| 表单项 | 行为 |
|
||||
|--------|------|
|
||||
| 止盈 | **隐藏**,不提交止盈价 |
|
||||
| 盈亏比 / 止盈金额 | **不显示** |
|
||||
| 止损 | **必填**,仅保留止损输入 |
|
||||
|
||||
平仓逻辑:
|
||||
|
||||
- **不再** 按固定止盈价监控
|
||||
- 程序按 **移动止损** 管理出场:浮盈达 **1R** 后止损移至开仓价 ± N 跳(保本);达 **2R** 移至 1R,依次类推(N 见系统设置「移动保本缓冲」)
|
||||
- 开启移动保本 **必须填写止损价**,否则无法开仓
|
||||
|
||||
持仓卡片在开启移动保本时同样 **不展示盈亏比、盈利金额、止盈状态**,仅保留止损与移动保本进度(如已锁 N R)。
|
||||
|
||||
## 策略交易
|
||||
|
||||
@@ -48,7 +98,9 @@
|
||||
|
||||
## 参考资金
|
||||
|
||||
系统设置中的「参考资金」仅在 **CTP 未连接** 时用于可开仓品种筛选与以损定仓估算;连接后自动改用柜台权益。
|
||||
系统设置中的「参考资金」在 **CTP 未连接** 时用于以损定仓估算;连接后自动改用柜台权益。
|
||||
|
||||
可开仓品种与品种白名单:**未连接 CTP 时一律按 20 万以下四品种范围**(见上文);连接后若柜台权益 ≤20 万,同样仅上述四品种。
|
||||
|
||||
## 首次使用 SimNow
|
||||
|
||||
|
||||
+14
-6
@@ -30,7 +30,7 @@ from position_sizing import (
|
||||
)
|
||||
from product_recommend import (
|
||||
assert_product_allowed_for_capital,
|
||||
is_small_account,
|
||||
should_apply_small_account_scope,
|
||||
small_account_scope_hint,
|
||||
SMALL_ACCOUNT_SCOPE_LABEL,
|
||||
)
|
||||
@@ -96,6 +96,7 @@ from trading_context import (
|
||||
get_sizing_mode,
|
||||
get_trailing_be_tick_buffer,
|
||||
get_trading_mode,
|
||||
is_ctp_connected,
|
||||
trading_mode_label,
|
||||
)
|
||||
from ctp_symbol import ths_to_vnpy_symbol
|
||||
@@ -1629,6 +1630,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
rec_cache = _recommend_payload(conn)
|
||||
if rec_cache.get("needs_refresh"):
|
||||
_schedule_recommend_refresh()
|
||||
ctp_connected = is_ctp_connected(get_setting)
|
||||
return render_template(
|
||||
"trade.html",
|
||||
trading_mode=mode,
|
||||
@@ -1651,8 +1653,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
recommend_rows=rec_cache.get("rows") or [],
|
||||
recommend_updated_at=rec_cache.get("updated_at"),
|
||||
night_session=is_night_trading_session(),
|
||||
small_account_scope=is_small_account(capital),
|
||||
small_account_scope_hint=small_account_scope_hint(),
|
||||
small_account_scope=should_apply_small_account_scope(
|
||||
capital, ctp_connected=ctp_connected,
|
||||
),
|
||||
small_account_scope_hint=small_account_scope_hint(ctp_connected=ctp_connected),
|
||||
product_categories=PRODUCT_CATEGORIES,
|
||||
)
|
||||
finally:
|
||||
@@ -2156,7 +2160,9 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
if err:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": err}), 403
|
||||
scope_err = assert_product_allowed_for_capital(sym, _capital(conn))
|
||||
scope_err = assert_product_allowed_for_capital(
|
||||
sym, _capital(conn), ctp_connected=is_ctp_connected(get_setting),
|
||||
)
|
||||
if scope_err:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": scope_err}), 403
|
||||
@@ -2220,8 +2226,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
if offset.startswith("open"):
|
||||
from zoneinfo import ZoneInfo
|
||||
sl = d.get("stop_loss")
|
||||
tp = d.get("take_profit")
|
||||
trailing_be = 1 if d.get("trailing_be") else 0
|
||||
tp = None if trailing_be else d.get("take_profit")
|
||||
open_ts = datetime.now(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
vt_order_id = str(result.get("order_id") or "")
|
||||
mid = _upsert_open_monitor(
|
||||
@@ -2515,7 +2521,9 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": err}), 403
|
||||
capital = _capital(conn)
|
||||
scope_err = assert_product_allowed_for_capital(sym, capital)
|
||||
scope_err = assert_product_allowed_for_capital(
|
||||
sym, capital, ctp_connected=is_ctp_connected(get_setting),
|
||||
)
|
||||
if scope_err:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": scope_err}), 403
|
||||
|
||||
+48
-6
@@ -24,8 +24,13 @@ SMALL_ACCOUNT_PRODUCT_THS = frozenset({"c", "m", "MA", "rb"})
|
||||
SMALL_ACCOUNT_SCOPE_LABEL = "玉米、豆粕、甲醇、螺纹钢"
|
||||
|
||||
|
||||
def small_account_scope_hint() -> str:
|
||||
def small_account_scope_hint(*, ctp_connected: bool = True) -> str:
|
||||
wan = int(SMALL_ACCOUNT_CAPITAL_MAX // 10_000)
|
||||
if not ctp_connected:
|
||||
return (
|
||||
f"未连接 CTP,默认按 {wan} 万以下账户:"
|
||||
f"仅显示并可交易 {SMALL_ACCOUNT_SCOPE_LABEL}"
|
||||
)
|
||||
return f"权益 {wan} 万以下仅显示并可交易:{SMALL_ACCOUNT_SCOPE_LABEL}"
|
||||
|
||||
|
||||
@@ -34,6 +39,28 @@ def small_account_scope_status_label() -> str:
|
||||
return f"权益{wan}万以下限{SMALL_ACCOUNT_SCOPE_LABEL}"
|
||||
|
||||
|
||||
def should_apply_small_account_scope(
|
||||
capital: float,
|
||||
*,
|
||||
ctp_connected: bool,
|
||||
) -> bool:
|
||||
"""SimNow/实盘一致:未连接 CTP 时默认按 20 万以下四品种范围。"""
|
||||
if not ctp_connected:
|
||||
return True
|
||||
return is_small_account(capital)
|
||||
|
||||
|
||||
def filter_rows_for_account_scope(
|
||||
rows: list[dict],
|
||||
capital: float,
|
||||
*,
|
||||
ctp_connected: bool,
|
||||
) -> list[dict]:
|
||||
if not should_apply_small_account_scope(capital, ctp_connected=ctp_connected):
|
||||
return rows
|
||||
return [r for r in rows if product_in_small_account_whitelist(r.get("ths") or "")]
|
||||
|
||||
|
||||
def normalize_product_ths(ths: str) -> str:
|
||||
import re
|
||||
s = (ths or "").strip()
|
||||
@@ -60,18 +87,30 @@ def product_in_small_account_whitelist(ths_or_product) -> bool:
|
||||
return upper in {x.upper() for x in SMALL_ACCOUNT_PRODUCT_THS}
|
||||
|
||||
|
||||
def assert_product_allowed_for_capital(ths: str, capital: float) -> Optional[str]:
|
||||
def assert_product_allowed_for_capital(
|
||||
ths: str,
|
||||
capital: float,
|
||||
*,
|
||||
ctp_connected: bool = True,
|
||||
) -> Optional[str]:
|
||||
"""小账户品种白名单校验;通过返回 None。"""
|
||||
if not is_small_account(capital):
|
||||
if not should_apply_small_account_scope(capital, ctp_connected=ctp_connected):
|
||||
return None
|
||||
if product_in_small_account_whitelist(ths):
|
||||
return None
|
||||
wan = int(SMALL_ACCOUNT_CAPITAL_MAX // 10_000)
|
||||
if not ctp_connected:
|
||||
return f"未连接 CTP,仅可交易:{SMALL_ACCOUNT_SCOPE_LABEL}"
|
||||
return f"权益 {wan} 万以下仅可交易:{SMALL_ACCOUNT_SCOPE_LABEL}"
|
||||
|
||||
|
||||
def filter_products_for_capital(products: list[dict], capital: float) -> list[dict]:
|
||||
if not is_small_account(capital):
|
||||
def filter_products_for_capital(
|
||||
products: list[dict],
|
||||
capital: float,
|
||||
*,
|
||||
ctp_connected: bool = True,
|
||||
) -> list[dict]:
|
||||
if not should_apply_small_account_scope(capital, ctp_connected=ctp_connected):
|
||||
return list(products)
|
||||
return [p for p in products if product_in_small_account_whitelist(p)]
|
||||
|
||||
@@ -103,6 +142,7 @@ def assess_product_for_capital(
|
||||
default_stop_ticks: int = 20,
|
||||
reward_risk_ratio: float = 2.0,
|
||||
trading_mode: str = "simulation",
|
||||
ctp_connected: bool = True,
|
||||
) -> dict:
|
||||
"""评估单品种在当前资金下是否可交易。"""
|
||||
ths = product.get("ths") or ""
|
||||
@@ -117,7 +157,7 @@ def assess_product_for_capital(
|
||||
cap = float(capital or 0)
|
||||
margin_pct = max(1.0, min(100.0, float(max_margin_pct or 30.0)))
|
||||
|
||||
if is_small_account(cap) and not product_in_small_account_whitelist(product):
|
||||
if should_apply_small_account_scope(cap, ctp_connected=ctp_connected) and not product_in_small_account_whitelist(product):
|
||||
return {
|
||||
"ths": ths,
|
||||
"name": name,
|
||||
@@ -210,6 +250,7 @@ def list_product_recommendations(
|
||||
*,
|
||||
max_margin_pct: float = 30.0,
|
||||
trading_mode: str = "simulation",
|
||||
ctp_connected: bool = True,
|
||||
) -> list[dict]:
|
||||
"""扫描全部品种并排序:可开且纪律友好 > 可开 > 不足。quote_fn(品种代码) -> {price, ths_code, ...}"""
|
||||
|
||||
@@ -222,6 +263,7 @@ def list_product_recommendations(
|
||||
product, capital, price,
|
||||
max_margin_pct=max_margin_pct,
|
||||
trading_mode=trading_mode,
|
||||
ctp_connected=ctp_connected,
|
||||
)
|
||||
main_code = (quote.get("ths_code") or "").strip()
|
||||
row["main_code"] = main_code
|
||||
|
||||
+23
-2
@@ -14,7 +14,11 @@ from typing import Callable, Optional
|
||||
|
||||
from contract_specs import get_contract_spec
|
||||
from fee_specs import ensure_fee_rates_schema
|
||||
from product_recommend import _attach_turnover, list_product_recommendations
|
||||
from product_recommend import (
|
||||
_attach_turnover,
|
||||
filter_rows_for_account_scope,
|
||||
list_product_recommendations,
|
||||
)
|
||||
from recommend_trend import sort_recommend_by_trend
|
||||
from symbols import product_category
|
||||
|
||||
@@ -104,6 +108,15 @@ def recommend_cache_needs_refresh(
|
||||
return False
|
||||
|
||||
|
||||
def _ctp_connected_for_mode(trading_mode: str) -> bool:
|
||||
try:
|
||||
from vnpy_bridge import ctp_status
|
||||
|
||||
return bool(ctp_status(trading_mode).get("connected"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def enrich_recommend_rows(
|
||||
rows: list[dict],
|
||||
capital: float,
|
||||
@@ -213,8 +226,13 @@ def refresh_recommend_cache(
|
||||
"""后台拉行情、筛选并写入数据库。"""
|
||||
ensure_recommend_tables(conn)
|
||||
ensure_fee_rates_schema(conn)
|
||||
ctp_connected = _ctp_connected_for_mode(trading_mode)
|
||||
all_rows = list_product_recommendations(
|
||||
capital, quote_fn, max_margin_pct=max_margin_pct, trading_mode=trading_mode,
|
||||
capital,
|
||||
quote_fn,
|
||||
max_margin_pct=max_margin_pct,
|
||||
trading_mode=trading_mode,
|
||||
ctp_connected=ctp_connected,
|
||||
)
|
||||
rows = filter_affordable_recommendations(all_rows)
|
||||
if not rows and float(capital or 0) > 0:
|
||||
@@ -289,6 +307,9 @@ def recommend_payload(
|
||||
rows = enrich_recommend_rows(
|
||||
rows, cap, max_margin_pct=pct, trading_mode=trading_mode,
|
||||
)
|
||||
rows = filter_rows_for_account_scope(
|
||||
rows, cap, ctp_connected=_ctp_connected_for_mode(trading_mode),
|
||||
)
|
||||
rows = filter_recommend_by_sizing(rows, sizing_mode=sizing_mode, fixed_lots=fixed_lots)
|
||||
rows = sort_recommend_by_trend(rows)
|
||||
payload["rows"] = rows
|
||||
|
||||
+2
-2
@@ -732,7 +732,7 @@ def check_sl_tp_on_tick(
|
||||
pass
|
||||
|
||||
reason = None
|
||||
if tp_f is not None and _tp_triggered(direction, tp_f, mark, tick):
|
||||
if tp_f is not None and not mon.get("trailing_be") and _tp_triggered(direction, tp_f, mark, tick):
|
||||
reason = "take_profit"
|
||||
elif sl_f is not None and _sl_triggered(direction, sl_f, mark, tick):
|
||||
reason = "stop_loss"
|
||||
@@ -813,7 +813,7 @@ def check_monitors_locally(
|
||||
pass
|
||||
|
||||
reason = None
|
||||
if tp_f is not None and _tp_triggered(direction, tp_f, mark, tick):
|
||||
if tp_f is not None and not mon.get("trailing_be") and _tp_triggered(direction, tp_f, mark, tick):
|
||||
reason = "take_profit"
|
||||
elif sl_f is not None and _sl_triggered(direction, sl_f, mark, tick):
|
||||
reason = "stop_loss"
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
.trade-action-row .btn-open.btn-session-off{background:var(--text-muted);border-color:var(--text-muted)}
|
||||
.trailing-be-toggle{display:flex;align-items:center;gap:.4rem;font-size:.78rem;color:var(--text-label);margin-bottom:.45rem;cursor:pointer;user-select:none}
|
||||
.trailing-be-toggle input{width:auto;margin:0}
|
||||
.trailing-be-hint{font-size:.72rem;margin:0;color:var(--text-muted)}
|
||||
.trade-form-line.line-3 #field-tp.is-hidden{display:none}
|
||||
.trade-rr-hint{font-size:.78rem;color:var(--text-accent);margin:0}
|
||||
.session-hint{font-size:.72rem;margin:.35rem 0 0;text-align:center}
|
||||
.trade-order-msg{font-size:.82rem;text-align:center;margin:0;padding:.35rem}
|
||||
|
||||
+45
-18
@@ -313,6 +313,22 @@
|
||||
return parseFloat(priceInput && priceInput.value) || 0;
|
||||
}
|
||||
|
||||
function isTrailingBeOn() {
|
||||
var el = document.getElementById('trailing-be');
|
||||
return !!(el && el.checked);
|
||||
}
|
||||
|
||||
function updateTrailingBeUi() {
|
||||
var on = isTrailingBeOn();
|
||||
var tpField = document.getElementById('field-tp');
|
||||
var hint = document.getElementById('trailing-be-hint');
|
||||
if (tpField) tpField.classList.toggle('is-hidden', on);
|
||||
if (hint) hint.hidden = !on;
|
||||
if (on && tpInput) tpInput.value = '';
|
||||
updateRRDisplay();
|
||||
scheduleAutoCalc();
|
||||
}
|
||||
|
||||
function calcRR(direction, entry, sl, tp) {
|
||||
entry = parseFloat(entry);
|
||||
sl = parseFloat(sl);
|
||||
@@ -335,19 +351,22 @@
|
||||
function updateRRDisplay() {
|
||||
var el = document.getElementById('trade-rr-hint');
|
||||
if (!el) return;
|
||||
var trailingOn = isTrailingBeOn();
|
||||
var dir = dirSelect ? dirSelect.value : 'long';
|
||||
var entry = entryPrice();
|
||||
var sl = slInput && slInput.value ? parseFloat(slInput.value) : 0;
|
||||
var tp = tpInput && tpInput.value ? parseFloat(tpInput.value) : 0;
|
||||
var tp = trailingOn ? 0 : (tpInput && tpInput.value ? parseFloat(tpInput.value) : 0);
|
||||
var lots = effectiveLots();
|
||||
var parts = [];
|
||||
var rr = calcRR(dir, entry, sl, tp);
|
||||
if (rr) parts.push('盈亏比 ' + rr + ':1');
|
||||
if (!trailingOn) {
|
||||
var rr = calcRR(dir, entry, sl, tp);
|
||||
if (rr) parts.push('盈亏比 ' + rr + ':1');
|
||||
}
|
||||
if (sl > 0 && entry > 0 && lots > 0 && lastPreviewMetrics) {
|
||||
if (lastPreviewMetrics.risk_amount != null) {
|
||||
parts.push('止损金额 ' + fmtNum(lastPreviewMetrics.risk_amount) + ' 元');
|
||||
}
|
||||
if (lastPreviewMetrics.reward_amount != null && tp > 0) {
|
||||
if (!trailingOn && lastPreviewMetrics.reward_amount != null && tp > 0) {
|
||||
parts.push('止盈金额 ' + fmtNum(lastPreviewMetrics.reward_amount) + ' 元');
|
||||
}
|
||||
}
|
||||
@@ -572,7 +591,7 @@
|
||||
var sym = selectedSymbol();
|
||||
var entry = entryPrice() || parseFloat(priceInput && priceInput.value) || 0;
|
||||
var sl = parseFloat(slInput && slInput.value) || 0;
|
||||
var tp = parseFloat(tpInput && tpInput.value) || 0;
|
||||
var tp = isTrailingBeOn() ? 0 : (parseFloat(tpInput && tpInput.value) || 0);
|
||||
if (isFixedMode()) {
|
||||
var fixedLots = parseInt(window.TRADE_FIXED_LOTS, 10) || 1;
|
||||
lotsCalc.value = String(fixedLots);
|
||||
@@ -671,12 +690,12 @@
|
||||
}
|
||||
var lots = effectiveLots();
|
||||
var trailingBeEl = document.getElementById('trailing-be');
|
||||
var trailingOn = !!(trailingBeEl && trailingBeEl.checked);
|
||||
if (offset === 'open') {
|
||||
if (!isTradingSession) {
|
||||
showOrderMsg('不在交易时间段', false);
|
||||
return;
|
||||
}
|
||||
var trailingOn = !!(trailingBeEl && trailingBeEl.checked);
|
||||
if (trailingOn && !(slInput && slInput.value)) {
|
||||
showOrderMsg('开启移动保本须填写止损价', false);
|
||||
return;
|
||||
@@ -713,8 +732,8 @@
|
||||
price: price,
|
||||
order_type: priceType,
|
||||
stop_loss: slInput && slInput.value ? parseFloat(slInput.value) : null,
|
||||
take_profit: tpInput && tpInput.value ? parseFloat(tpInput.value) : null,
|
||||
trailing_be: !!(trailingBeEl && trailingBeEl.checked)
|
||||
take_profit: (trailingOn || !(tpInput && tpInput.value)) ? null : parseFloat(tpInput.value),
|
||||
trailing_be: trailingOn
|
||||
};
|
||||
fetch('/api/trade/order', {
|
||||
method: 'POST',
|
||||
@@ -861,10 +880,12 @@
|
||||
} else if (row.stop_loss != null) {
|
||||
parts.push('<span class="text-muted">止损已设</span>');
|
||||
}
|
||||
if (row.tp_order_active || row.tp_monitoring) {
|
||||
parts.push('<span class="text-profit">止盈监控中</span>');
|
||||
} else if (row.take_profit != null) {
|
||||
parts.push('<span class="text-muted">止盈已设</span>');
|
||||
if (!row.trailing_be) {
|
||||
if (row.tp_order_active || row.tp_monitoring) {
|
||||
parts.push('<span class="text-profit">止盈监控中</span>');
|
||||
} else if (row.take_profit != null) {
|
||||
parts.push('<span class="text-muted">止盈已设</span>');
|
||||
}
|
||||
}
|
||||
if (!parts.length) return '<span class="text-muted">未设置</span>';
|
||||
return parts.join(' · ');
|
||||
@@ -926,8 +947,8 @@
|
||||
var metaLine =
|
||||
'状态 <strong class="text-accent">' + pendingLabel + '</strong>' +
|
||||
' · 委托价 <strong>' + fmtNum(orderPx) + '</strong>' +
|
||||
(row.rr_ratio != null ? ' · 盈亏比 <strong>' + row.rr_ratio + ':1</strong>' : '') +
|
||||
(row.stop_loss != null || row.take_profit != null ? ' · ' + slTpStatusHtml(row) : '') +
|
||||
(!row.trailing_be && row.rr_ratio != null ? ' · 盈亏比 <strong>' + row.rr_ratio + ':1</strong>' : '') +
|
||||
(row.stop_loss != null || (!row.trailing_be && row.take_profit != null) ? ' · ' + slTpStatusHtml(row) : '') +
|
||||
(row.trailing_be ? ' · 移动保本 ' + trailingStatusHtml(row) : '') +
|
||||
(!isCloseOrder ? ' · <span class="text-muted">约 ' + remainMin + ' 分钟内未成交自动撤单</span>' : '');
|
||||
return (
|
||||
@@ -988,13 +1009,14 @@
|
||||
'<div class="pos-card-actions">' + entrustBtn + orderBtn + closeBtn + '</div>' : '';
|
||||
var metaLine =
|
||||
'来源 <strong>' + (row.source_label || 'CTP') + '</strong>' +
|
||||
(row.rr_ratio != null ? ' · 盈亏比 <strong>' + row.rr_ratio + ':1</strong>' : '') +
|
||||
(!row.trailing_be && row.rr_ratio != null ? ' · 盈亏比 <strong>' + row.rr_ratio + ':1</strong>' : '') +
|
||||
' · 止损金额 <strong class="text-loss">' +
|
||||
(row.risk_amount != null ? fmtNum(row.risk_amount) + ' 元' : '--') + '</strong>' +
|
||||
' · 盈利金额 <strong class="text-profit">' +
|
||||
(row.reward_amount != null ? fmtNum(row.reward_amount) + ' 元' : '--') + '</strong>' +
|
||||
(!row.trailing_be ?
|
||||
(' · 盈利金额 <strong class="text-profit">' +
|
||||
(row.reward_amount != null ? fmtNum(row.reward_amount) + ' 元' : '--') + '</strong>') : '') +
|
||||
' · ' + slTpStatusHtml(row) +
|
||||
' · 移动保本 ' + trailingStatusHtml(row) +
|
||||
(row.trailing_be ? ' · 移动保本 ' + trailingStatusHtml(row) : '') +
|
||||
(slTpBtn ? ' · ' + slTpBtn : '') +
|
||||
(function () {
|
||||
if (row.order_state === 'pending' || !row.monitor_id) return '';
|
||||
@@ -1565,6 +1587,11 @@
|
||||
scheduleAutoCalc();
|
||||
updateRRDisplay();
|
||||
});
|
||||
var trailingBeEl = document.getElementById('trailing-be');
|
||||
if (trailingBeEl) {
|
||||
trailingBeEl.addEventListener('change', updateTrailingBeUi);
|
||||
updateTrailingBeUi();
|
||||
}
|
||||
if (priceInput) {
|
||||
priceInput.addEventListener('input', function () {
|
||||
if (priceType === 'limit') priceInput.dataset.manual = '1';
|
||||
|
||||
+10
-6
@@ -461,19 +461,21 @@ def _match_score(product: dict, q_lower: str) -> int:
|
||||
return 10
|
||||
|
||||
|
||||
def search_symbols(query: str, *, capital: float | None = None) -> list:
|
||||
def search_symbols(query: str, *, capital: float | None = None, ctp_connected: bool = True) -> list:
|
||||
q = query.strip()
|
||||
if not q:
|
||||
return []
|
||||
|
||||
q_lower = q.lower()
|
||||
from market_sessions import is_night_trading_session
|
||||
from product_recommend import filter_products_for_capital, is_small_account
|
||||
from product_recommend import filter_products_for_capital, should_apply_small_account_scope
|
||||
|
||||
night_only = is_night_trading_session()
|
||||
product_pool = PRODUCTS
|
||||
if capital is not None and is_small_account(capital):
|
||||
product_pool = filter_products_for_capital(PRODUCTS, capital)
|
||||
if capital is not None and should_apply_small_account_scope(capital, ctp_connected=ctp_connected):
|
||||
product_pool = filter_products_for_capital(
|
||||
PRODUCTS, capital, ctp_connected=ctp_connected,
|
||||
)
|
||||
with _main_index_lock:
|
||||
index = dict(_main_index)
|
||||
index_ready = bool(index)
|
||||
@@ -498,8 +500,10 @@ def search_symbols(query: str, *, capital: float | None = None) -> list:
|
||||
codes = ths_to_codes(q)
|
||||
if codes:
|
||||
product = _product_for_contract_code(codes["ths_code"])
|
||||
if capital is not None and is_small_account(capital):
|
||||
from product_recommend import is_small_account, product_in_small_account_whitelist
|
||||
if capital is not None and should_apply_small_account_scope(
|
||||
capital, ctp_connected=ctp_connected,
|
||||
):
|
||||
from product_recommend import product_in_small_account_whitelist
|
||||
if not product or not product_in_small_account_whitelist(product):
|
||||
return results
|
||||
raw = fetch_raw_for_volume(codes["sina_code"])
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<input type="number" id="trade-price" step="any" placeholder="限价">
|
||||
<p class="hint market-hint" id="market-hint" hidden>市价以 FAK 即时成交报单(非限价挂单)</p>
|
||||
</div>
|
||||
<div class="trade-field">
|
||||
<div class="trade-field" id="field-tp">
|
||||
<label class="text-label">止盈</label>
|
||||
<input type="number" id="trade-tp" step="any">
|
||||
</div>
|
||||
@@ -90,9 +90,10 @@
|
||||
|
||||
<div class="trade-action-row">
|
||||
<label class="trailing-be-toggle">
|
||||
<input type="checkbox" id="trailing-be" checked>
|
||||
<input type="checkbox" id="trailing-be">
|
||||
<span>移动保本</span>
|
||||
</label>
|
||||
<p class="hint trailing-be-hint" id="trailing-be-hint" hidden>已开启:仅监控止损,达 1R 后移动保本平仓</p>
|
||||
<span class="hint trade-rr-hint" id="trade-rr-hint" hidden></span>
|
||||
<button type="button" class="btn-primary btn-open" id="btn-open">开仓</button>
|
||||
<p class="hint session-hint text-muted" id="session-hint" hidden>不在交易时间段</p>
|
||||
|
||||
@@ -91,5 +91,16 @@ def get_account_capital(conn, get_setting: Callable[[str, str], str]) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
def is_ctp_connected(get_setting: Callable[[str, str], str]) -> bool:
|
||||
"""当前交易模式(SimNow / 实盘)是否已连接 CTP。"""
|
||||
try:
|
||||
from vnpy_bridge import ctp_status
|
||||
|
||||
mode = get_trading_mode(get_setting)
|
||||
return bool(ctp_status(mode).get("connected"))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def trading_mode_label(get_setting: Callable[[str, str], str]) -> str:
|
||||
return "SimNow" if get_trading_mode(get_setting) == TRADING_MODE_SIM else "期货公司实盘"
|
||||
|
||||
Reference in New Issue
Block a user