开单计划增加决策理由;品种联想加速;复盘支持品种匹配

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 13:15:06 +08:00
parent db2443273f
commit eaf72a13fc
5 changed files with 196 additions and 43 deletions
+22 -6
View File
@@ -176,6 +176,7 @@ def init_db():
"ALTER TABLE key_monitors ADD COLUMN market_code TEXT",
"ALTER TABLE trade_records ADD COLUMN market_code TEXT",
"ALTER TABLE order_plans ADD COLUMN plan_date TEXT",
"ALTER TABLE order_plans ADD COLUMN decision_reason TEXT",
"ALTER TABLE key_monitors ADD COLUMN status TEXT DEFAULT 'active'",
"ALTER TABLE key_monitors ADD COLUMN archived_at TEXT",
"ALTER TABLE review_records ADD COLUMN direction TEXT",
@@ -188,6 +189,9 @@ def init_db():
"ALTER TABLE review_records ADD COLUMN initial_pnl REAL",
"ALTER TABLE review_records ADD COLUMN actual_pnl REAL",
"ALTER TABLE review_records ADD COLUMN is_emotion INTEGER DEFAULT 0",
"ALTER TABLE review_records ADD COLUMN symbol_name TEXT",
"ALTER TABLE review_records ADD COLUMN market_code TEXT",
"ALTER TABLE review_records ADD COLUMN sina_code TEXT",
]
for sql in migrations:
try:
@@ -321,6 +325,7 @@ def check_order_plans():
status = r["status"]
pid = r["id"]
name = r["symbol_name"] or sym
reason = r["decision_reason"] if "decision_reason" in r.keys() and r["decision_reason"] else ""
# 计划状态:价格进入决策区间则激活并通知
if status == "planned":
@@ -330,6 +335,7 @@ def check_order_plans():
f"【开单计划触发】{name} ({sym})\n"
f"方向:{'做多' if direction == 'long' else '做空'}\n"
f"决策区间:{zone_lower} ~ {zone_upper}\n"
f"决策理由:{reason}\n"
f"当前价:{p}\n"
f"止损:{stop_loss} 止盈:{take_profit}"
)
@@ -535,13 +541,14 @@ def add_plan():
conn.execute(
"""INSERT INTO order_plans
(symbol, symbol_name, market_code, sina_code, direction,
zone_upper, zone_lower, stop_loss, take_profit, plan_date)
VALUES (?,?,?,?,?,?,?,?,?,?)""",
zone_upper, zone_lower, stop_loss, take_profit, plan_date, decision_reason)
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
(
symbol, symbol_name, market_code, sina_code, direction,
float(d["zone_upper"]), float(d["zone_lower"]),
float(d["stop_loss"]), float(d["take_profit"]),
today_str(),
d.get("decision_reason", "").strip(),
),
)
conn.commit()
@@ -671,6 +678,14 @@ def add_review():
flash("请选择离场触发")
return redirect(url_for("records"))
symbol = d.get("symbol", "").strip()
symbol_name = d.get("symbol_name", "").strip()
market_code = d.get("market_code", "").strip()
sina_code = d.get("sina_code", "").strip()
if not symbol or not market_code:
flash("请从下拉列表选择品种(同花顺合约代码)")
return redirect(url_for("records"))
screenshot = ""
f = request.files.get("screenshot")
if f and f.filename:
@@ -705,7 +720,7 @@ def add_review():
if auto_kline and not screenshot:
try:
generated = generate_review_kline_chart(
symbol=d.get("symbol", "").strip(),
symbol=symbol,
periods=[d.get("kline_period1", "15m"), d.get("kline_period2", "1h")],
count=int(d.get("kline_count") or 300),
cutoff_label=d.get("kline_cutoff", "平仓时间"),
@@ -725,17 +740,18 @@ def add_review():
conn = get_db()
conn.execute(
"""INSERT INTO review_records
(open_time, close_time, symbol, timeframe, direction,
(open_time, close_time, symbol, symbol_name, market_code, sina_code,
timeframe, direction,
entry_price, stop_loss, take_profit, close_price, lots,
holding_duration, initial_pnl, actual_pnl, pnl,
open_type, expected_rr, actual_rr, exit_trigger, exit_supplement,
watch_after_breakeven, new_position_while_occupied, screenshot,
auto_kline, kline_period1, kline_period2, kline_count, kline_cutoff,
behavior_tags, is_emotion, notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
open_time, close_time,
d.get("symbol", "").strip(),
symbol, symbol_name, market_code, sina_code,
d.get("timeframe", "").strip(),
direction,
entry_price, stop_loss, take_profit, close_price, lots,
+42 -14
View File
@@ -1,4 +1,14 @@
(function () {
function formatSub(item) {
return '同花顺 ' + item.ths_code +
(item.market_code ? ' · ' + item.market_code : '') +
' · ' + (item.exchange || '');
}
function formatInputLabel(item) {
return item.input_label || (item.name + ' ' + item.ths_code);
}
function initSymbolInput(wrapper) {
const input = wrapper.querySelector('.symbol-input');
const hiddenThs = wrapper.querySelector('input[name="symbol"]');
@@ -8,19 +18,21 @@
const dropdown = wrapper.querySelector('.symbol-dropdown');
const selectedEl = wrapper.querySelector('.symbol-selected');
let timer = null;
let abortCtrl = null;
const cache = new Map();
function hideDropdown() {
dropdown.classList.remove('show');
}
function selectItem(item) {
input.value = item.name;
const label = formatInputLabel(item);
input.value = label;
hiddenThs.value = item.ths_code;
hiddenName.value = item.name;
if (hiddenMarket) hiddenMarket.value = item.market_code || '';
if (hiddenSina) hiddenSina.value = item.sina_code || '';
selectedEl.textContent = '同花顺: ' + item.ths_code +
(item.market_code ? ' (' + item.market_code + ')' : '');
selectedEl.textContent = formatSub(item);
hideDropdown();
}
@@ -33,9 +45,7 @@
const div = document.createElement('div');
div.className = 'symbol-option';
div.innerHTML = item.display +
'<div class="sub">同花顺 ' + item.ths_code +
(item.market_code ? ' · ' + item.market_code : '') +
' · ' + item.exchange + '</div>';
'<div class="sub">' + formatSub(item) + '</div>';
div.addEventListener('mousedown', function (e) {
e.preventDefault();
selectItem(item);
@@ -46,6 +56,29 @@
dropdown.classList.add('show');
}
function search(q) {
if (cache.has(q)) {
renderItems(cache.get(q));
return;
}
if (abortCtrl) {
abortCtrl.abort();
}
abortCtrl = new AbortController();
fetch('/api/symbols/search?q=' + encodeURIComponent(q), {
signal: abortCtrl.signal,
})
.then(function (r) { return r.json(); })
.then(function (items) {
cache.set(q, items);
renderItems(items);
})
.catch(function (err) {
if (err && err.name === 'AbortError') return;
hideDropdown();
});
}
input.addEventListener('input', function () {
hiddenThs.value = '';
hiddenName.value = '';
@@ -59,11 +92,8 @@
}
clearTimeout(timer);
timer = setTimeout(function () {
fetch('/api/symbols/search?q=' + encodeURIComponent(q))
.then(function (r) { return r.json(); })
.then(renderItems)
.catch(function () { hideDropdown(); });
}, 300);
search(q);
}, 120);
});
input.addEventListener('blur', function () {
@@ -73,9 +103,7 @@
input.addEventListener('focus', function () {
const q = input.value.trim();
if (q && !hiddenThs.value) {
fetch('/api/symbols/search?q=' + encodeURIComponent(q))
.then(function (r) { return r.json(); })
.then(renderItems);
search(q);
}
});
}
+108 -13
View File
@@ -3,7 +3,9 @@
展示同花顺合约代码(ag2608);行情默认新浪,机构用户可通过环境变量启用同花顺 iFinD。
"""
import re
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import date
from typing import Optional
@@ -64,6 +66,9 @@ PRODUCTS = [
_MAIN_CACHE: dict[str, tuple[float, dict]] = {}
_CACHE_TTL = 300
_main_index_lock = threading.Lock()
_main_index: dict[str, dict] = {}
_main_index_ts = 0.0
def build_ths_code(product: dict, year: int, month: int) -> str:
@@ -164,14 +169,16 @@ def ths_to_sina_code(ths_code: str) -> Optional[str]:
def _make_symbol_item(product: dict, year: int, month: int, volume: float) -> dict:
ths = build_ths_code(product, year, month)
name = product["name"]
return {
"name": product["name"],
"name": name,
"ths_code": ths,
"market_code": build_ths_full_code(product, year, month),
"sina_code": build_sina_code(product, year, month),
"exchange": product["exchange"],
"contract": f"主力 {ths}",
"display": f"{product['name']} 主力 {ths}",
"display": f"{name} 主力 {ths}",
"input_label": f"{name} {ths}",
"volume": volume,
}
@@ -218,6 +225,7 @@ def resolve_main_contract(product: dict) -> Optional[dict]:
"exchange": product["exchange"],
"contract": f"主力连续 {ths_main}",
"display": f"{product['name']} 主力连续 {ths_main}",
"input_label": f"{product['name']} {ths_main}",
"volume": raw.get("volume", 0),
}
@@ -226,26 +234,110 @@ def resolve_main_contract(product: dict) -> Optional[dict]:
return best
def _enrich_item(item: dict) -> dict:
out = dict(item)
if not out.get("input_label"):
out["input_label"] = f"{out.get('name', '')} {out.get('ths_code', '')}".strip()
return out
def refresh_main_index():
"""后台预热全部品种主力合约,搜索时只读本地缓存。"""
global _main_index, _main_index_ts
new_idx: dict[str, dict] = {}
with ThreadPoolExecutor(max_workers=10) as pool:
futures = {pool.submit(resolve_main_contract, p): p for p in PRODUCTS}
for fut in as_completed(futures):
product = futures[fut]
try:
main = fut.result()
if main:
new_idx[product["sina"]] = _enrich_item(main)
except Exception:
pass
with _main_index_lock:
_main_index = new_idx
_main_index_ts = time.time()
def _warm_loop():
while True:
try:
refresh_main_index()
except Exception:
pass
time.sleep(_CACHE_TTL)
def _start_warm_thread():
threading.Thread(target=_warm_loop, daemon=True).start()
def _stub_main_contract(product: dict) -> dict:
"""缓存未就绪时的快速占位(当月合约),避免首次打开搜索为空。"""
today = date.today()
return _enrich_item(_make_symbol_item(product, today.year, today.month, 0))
def _product_matches(product: dict, q_lower: str) -> bool:
name_lower = product["name"].lower()
if q_lower in name_lower:
return True
if len(q_lower) >= 2:
ths_lower = product["ths"].lower()
sina_lower = product["sina"].lower()
if q_lower in ths_lower or q_lower in sina_lower:
return True
return False
def _match_score(product: dict, q_lower: str) -> int:
name_lower = product["name"].lower()
if name_lower == q_lower:
return 200
if name_lower.startswith(q_lower):
return 150
if q_lower in name_lower:
return 100
ths_lower = product["ths"].lower()
if ths_lower == q_lower:
return 90
if ths_lower.startswith(q_lower):
return 70
if product["sina"].lower() == q_lower:
return 80
return 10
def search_symbols(query: str) -> list:
q = query.strip().lower()
q = query.strip()
if not q:
return []
results = []
q_lower = q.lower()
with _main_index_lock:
index = dict(_main_index)
index_ready = bool(index)
scored: list[tuple[int, dict]] = []
for p in PRODUCTS:
name = p["name"]
if q not in name.lower() and q not in p["ths"].lower() and q not in p["sina"].lower():
if not _product_matches(p, q_lower):
continue
main = resolve_main_contract(p)
main = index.get(p["sina"])
if not main and not index_ready:
main = _stub_main_contract(p)
if main:
results.append(main)
scored.append((_match_score(p, q_lower), main))
scored.sort(key=lambda x: -x[0])
results = [item for _, item in scored[:12]]
if not results and len(q) >= 3:
codes = ths_to_codes(query.strip())
codes = ths_to_codes(q)
if codes:
raw = fetch_raw_for_volume(codes["sina_code"])
name = raw["name"] if raw else query.strip()
results.append({
name = raw["name"] if raw else q
results.append(_enrich_item({
"name": name,
"ths_code": codes["ths_code"],
"market_code": codes["market_code"],
@@ -254,9 +346,12 @@ def search_symbols(query: str) -> list:
"contract": codes["ths_code"],
"display": f"{name} ({codes['ths_code']})",
"volume": raw.get("volume", 0) if raw else 0,
})
}))
return results[:12]
return results
_start_warm_thread()
def get_price(market_code: str, sina_code: str = "") -> Optional[float]:
+13 -7
View File
@@ -10,7 +10,7 @@
<form action="{{ url_for('add_plan') }}" method="post" class="form-compact">
<div class="form-line line-3">
<div class="symbol-wrap">
<input type="text" class="symbol-input" placeholder="中文名或同花顺代码" autocomplete="off" required>
<input type="text" class="symbol-input" placeholder="品种" autocomplete="off" required>
<input type="hidden" name="symbol" required>
<input type="hidden" name="symbol_name">
<input type="hidden" name="market_code" required>
@@ -23,10 +23,11 @@
<option value="long">做多</option>
<option value="short">做空</option>
</select>
<input name="zone_lower" type="number" step="0.0001" placeholder="区间下限" required>
<input name="decision_reason" type="text" placeholder="决策理由">
</div>
<div class="form-line line-3">
<input name="zone_upper" type="number" step="0.0001" placeholder="区间限" required>
<div class="form-line line-4">
<input name="zone_lower" type="number" step="0.0001" placeholder="决策区间限" required>
<input name="zone_upper" type="number" step="0.0001" placeholder="决策区间上限" required>
<input name="stop_loss" type="number" step="0.0001" placeholder="止损" required>
<input name="take_profit" type="number" step="0.0001" placeholder="止盈" required>
</div>
@@ -44,7 +45,11 @@
{% if p.status == 'planned' %}<span class="badge planned">待触发</span>
{% else %}<span class="badge active">已激活</span>{% endif %}
</div>
<div>{{ p.zone_lower }}~{{ p.zone_upper }} 损{{ p.stop_loss }} 盈{{ p.take_profit }}</div>
<div>
区间{{ p.zone_lower }}~{{ p.zone_upper }}
{% if p.decision_reason %} · {{ p.decision_reason }}{% endif %}
· 损{{ p.stop_loss }} 盈{{ p.take_profit }}
</div>
<a href="{{ url_for('del_plan', pid=p.id) }}" class="btn-del" onclick="return confirm('删除?')"></a>
</div>
{% else %}
@@ -65,7 +70,7 @@
</form>
<div class="card-scroll">
<table>
<thead><tr><th>日期</th><th>品种</th><th>方向</th><th>区间</th><th>状态</th></tr></thead>
<thead><tr><th>日期</th><th>品种</th><th>方向</th><th>决策区间</th><th>决策理由</th><th>状态</th></tr></thead>
<tbody>
{% for p in history %}
<tr>
@@ -73,6 +78,7 @@
<td>{{ p.symbol_name or p.symbol }}</td>
<td><span class="badge dir">{{ '多' if p.direction == 'long' else '空' }}</span></td>
<td>{{ p.zone_lower }}~{{ p.zone_upper }}</td>
<td>{{ p.decision_reason or '—' }}</td>
<td>
{% if p.status == 'closed' %}<span class="badge profit">完成</span>
{% elif p.status == 'expired' %}<span class="badge expired">失效</span>
@@ -80,7 +86,7 @@
</td>
</tr>
{% else %}
<tr><td colspan="5" class="text-muted">暂无历史</td></tr>
<tr><td colspan="6" class="text-muted">暂无历史</td></tr>
{% endfor %}
</tbody>
</table>
+11 -3
View File
@@ -7,7 +7,15 @@
<div class="card-body">
<form id="review-form" action="{{ url_for('add_review') }}" method="post" enctype="multipart/form-data" class="form-compact form-compact-review line-tight">
<div class="form-line line-4">
<input name="symbol" placeholder="品种 ag2608" required>
<div class="symbol-wrap">
<input type="text" class="symbol-input" placeholder="品种" autocomplete="off" required>
<input type="hidden" name="symbol" required>
<input type="hidden" name="symbol_name">
<input type="hidden" name="market_code" required>
<input type="hidden" name="sina_code">
<div class="symbol-dropdown"></div>
<div class="symbol-selected"></div>
</div>
<select name="direction" required>
<option value="">方向</option>
<option value="long">做多</option>
@@ -101,7 +109,7 @@
{% for r in reviews %}
<tr>
<td>{{ r.close_time[:16] if r.close_time else '' }}</td>
<td>{{ r.symbol }}</td>
<td>{{ r.symbol_name or r.symbol }}</td>
<td><span class="badge dir">{{ '多' if r.direction == 'long' else '空' }}</span></td>
<td>
{% if r.pnl and r.pnl > 0 %}<span class="badge profit">{{ r.pnl }}</span>
@@ -111,7 +119,7 @@
<td>{% if r.is_emotion %}<span class="badge loss">情绪</span>{% else %}-{% endif %}</td>
<td>
<button type="button" class="btn-link review-view-btn" data-review='{{ {
"symbol": r.symbol, "direction": "做多" if r.direction=="long" else "做空",
"symbol": r.symbol_name or r.symbol, "symbol_code": r.symbol, "direction": "做多" if r.direction=="long" else "做空",
"entry_price": r.entry_price, "stop_loss": r.stop_loss, "take_profit": r.take_profit,
"close_price": r.close_price, "lots": r.lots,
"open_time": r.open_time, "close_time": r.close_time,