开单计划增加决策理由;品种联想加速;复盘支持品种匹配
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user