Fix D1 open elapsed (use current bar); upsert floating P/L on dashboard.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+104
-28
@@ -339,6 +339,7 @@ def register_routes(app: Flask) -> None:
|
|||||||
"side": t.get("side"),
|
"side": t.get("side"),
|
||||||
"volume": t.get("volume"),
|
"volume": t.get("volume"),
|
||||||
"profit": t.get("profit"),
|
"profit": t.get("profit"),
|
||||||
|
"close_time": t.get("close_time"),
|
||||||
"account_name": t.get("account_name"),
|
"account_name": t.get("account_name"),
|
||||||
"machine_name": t.get("machine_name"),
|
"machine_name": t.get("machine_name"),
|
||||||
"ticket": t.get("ticket"),
|
"ticket": t.get("ticket"),
|
||||||
@@ -658,35 +659,110 @@ def register_routes(app: Flask) -> None:
|
|||||||
if not act.get("account_id"):
|
if not act.get("account_id"):
|
||||||
return jsonify({"ok": False, "error": "激活码未绑定账户"}), 400
|
return jsonify({"ok": False, "error": "激活码未绑定账户"}), 400
|
||||||
snapshot = data.get("snapshot") or {}
|
snapshot = data.get("snapshot") or {}
|
||||||
conn.execute(
|
ticket = str(data.get("ticket") or "").strip()
|
||||||
"""
|
symbol = (data.get("symbol") or "XAUUSD").upper()
|
||||||
INSERT INTO trades(
|
side = (data.get("side") or "").lower()
|
||||||
ticket, symbol, side, volume, open_price, close_price,
|
volume = float(data.get("volume") or 0)
|
||||||
open_time, close_time, profit, sl, tp,
|
open_price = float(data.get("open_price") or 0)
|
||||||
account_id, machine_id, entry_style, review_note, snapshot_json, created_at
|
close_price = (
|
||||||
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
float(data.get("close_price") or 0)
|
||||||
""",
|
if data.get("close_price") is not None
|
||||||
(
|
else None
|
||||||
str(data.get("ticket") or ""),
|
|
||||||
(data.get("symbol") or "XAUUSD").upper(),
|
|
||||||
(data.get("side") or "").lower(),
|
|
||||||
float(data.get("volume") or 0),
|
|
||||||
float(data.get("open_price") or 0),
|
|
||||||
float(data.get("close_price") or 0) if data.get("close_price") is not None else None,
|
|
||||||
data.get("open_time"),
|
|
||||||
data.get("close_time"),
|
|
||||||
float(data.get("profit") or 0) if data.get("profit") is not None else None,
|
|
||||||
data.get("sl"),
|
|
||||||
data.get("tp"),
|
|
||||||
act["account_id"],
|
|
||||||
act["machine_id"],
|
|
||||||
(data.get("entry_style") or "").strip(),
|
|
||||||
(data.get("review_note") or "").strip(),
|
|
||||||
json.dumps(snapshot, ensure_ascii=False),
|
|
||||||
database.now(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
tid = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
open_time = data.get("open_time")
|
||||||
|
close_time = data.get("close_time")
|
||||||
|
if data.get("profit") is None:
|
||||||
|
profit = None
|
||||||
|
else:
|
||||||
|
profit = float(data.get("profit") or 0)
|
||||||
|
sl = data.get("sl")
|
||||||
|
tp = data.get("tp")
|
||||||
|
entry_style = (data.get("entry_style") or "").strip()
|
||||||
|
review_note = (data.get("review_note") or "").strip()
|
||||||
|
snap_json = json.dumps(snapshot, ensure_ascii=False)
|
||||||
|
|
||||||
|
existing = None
|
||||||
|
if ticket:
|
||||||
|
existing = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, entry_style, review_note, open_time, close_time, close_price
|
||||||
|
FROM trades
|
||||||
|
WHERE account_id=? AND ticket=?
|
||||||
|
ORDER BY id DESC LIMIT 1
|
||||||
|
""",
|
||||||
|
(act["account_id"], ticket),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# 浮盈更新不覆盖已有平仓;空备注不覆盖已标注
|
||||||
|
keep_close = existing["close_time"] not in (None, "") and close_time in (None, "")
|
||||||
|
new_close_time = existing["close_time"] if keep_close else close_time
|
||||||
|
new_close_price = existing["close_price"] if keep_close else close_price
|
||||||
|
new_entry = entry_style or (existing["entry_style"] or "")
|
||||||
|
new_note = review_note or (existing["review_note"] or "")
|
||||||
|
new_open_time = open_time if open_time is not None else existing["open_time"]
|
||||||
|
if keep_close:
|
||||||
|
# 已平仓单不再被浮盈推送改写盈亏
|
||||||
|
profit = conn.execute(
|
||||||
|
"SELECT profit FROM trades WHERE id=?", (existing["id"],)
|
||||||
|
).fetchone()["profit"]
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE trades SET
|
||||||
|
symbol=?, side=?, volume=?, open_price=?, close_price=?,
|
||||||
|
open_time=?, close_time=?, profit=?, sl=?, tp=?,
|
||||||
|
machine_id=?, entry_style=?, review_note=?, snapshot_json=?
|
||||||
|
WHERE id=?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
volume,
|
||||||
|
open_price,
|
||||||
|
new_close_price,
|
||||||
|
new_open_time,
|
||||||
|
new_close_time,
|
||||||
|
profit,
|
||||||
|
sl,
|
||||||
|
tp,
|
||||||
|
act["machine_id"],
|
||||||
|
new_entry,
|
||||||
|
new_note,
|
||||||
|
snap_json,
|
||||||
|
existing["id"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
tid = int(existing["id"])
|
||||||
|
else:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO trades(
|
||||||
|
ticket, symbol, side, volume, open_price, close_price,
|
||||||
|
open_time, close_time, profit, sl, tp,
|
||||||
|
account_id, machine_id, entry_style, review_note, snapshot_json, created_at
|
||||||
|
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
ticket,
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
volume,
|
||||||
|
open_price,
|
||||||
|
close_price,
|
||||||
|
open_time,
|
||||||
|
close_time,
|
||||||
|
profit,
|
||||||
|
sl,
|
||||||
|
tp,
|
||||||
|
act["account_id"],
|
||||||
|
act["machine_id"],
|
||||||
|
entry_style,
|
||||||
|
review_note,
|
||||||
|
snap_json,
|
||||||
|
database.now(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
tid = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||||
return jsonify({"ok": True, "id": tid})
|
return jsonify({"ok": True, "id": tid})
|
||||||
|
|
||||||
@app.post("/api/v1/bars")
|
@app.post("/api/v1/bars")
|
||||||
|
|||||||
@@ -60,8 +60,8 @@
|
|||||||
<td>{{ t.symbol }}</td>
|
<td>{{ t.symbol }}</td>
|
||||||
<td><span class="tag {% if t.side=='buy' %}green{% else %}red{% endif %}">{{ t.side }}</span></td>
|
<td><span class="tag {% if t.side=='buy' %}green{% else %}red{% endif %}">{{ t.side }}</span></td>
|
||||||
<td>{{ t.volume if t.volume is not none else '—' }}</td>
|
<td>{{ t.volume if t.volume is not none else '—' }}</td>
|
||||||
<td style="color:{% if t.profit and t.profit>0 %}var(--green){% elif t.profit and t.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
<td style="color:{% if t.profit is not none and t.profit>0 %}var(--green){% elif t.profit is not none and t.profit<0 %}var(--red){% else %}inherit{% endif %}">
|
||||||
{{ t.profit if t.profit is not none else '—' }}
|
{% if t.profit is none %}—{% else %}{{ '%.2f'|format(t.profit) }}{% if not t.close_time %}<span class="muted"> 浮</span>{% endif %}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ t.account_name or '—' }}</td>
|
<td>{{ t.account_name or '—' }}</td>
|
||||||
<td class="muted">{{ t.ticket or '—' }}</td>
|
<td class="muted">{{ t.ticket or '—' }}</td>
|
||||||
@@ -149,7 +149,11 @@
|
|||||||
'<td>' + esc(t.symbol) + '</td>' +
|
'<td>' + esc(t.symbol) + '</td>' +
|
||||||
'<td><span class="tag ' + sideCls + '">' + esc(t.side) + '</span></td>' +
|
'<td><span class="tag ' + sideCls + '">' + esc(t.side) + '</span></td>' +
|
||||||
'<td>' + esc(t.volume != null ? t.volume : '—') + '</td>' +
|
'<td>' + esc(t.volume != null ? t.volume : '—') + '</td>' +
|
||||||
'<td style="color:' + profitColor(t.profit) + '">' + esc(t.profit != null ? t.profit : '—') + '</td>' +
|
'<td style="color:' + profitColor(t.profit) + '">' +
|
||||||
|
(t.profit == null || t.profit === ''
|
||||||
|
? '—'
|
||||||
|
: (Number(t.profit).toFixed(2) + (t.close_time ? '' : ' <span class="muted">浮</span>'))) +
|
||||||
|
'</td>' +
|
||||||
'<td>' + esc(t.account_name || '—') + '</td>' +
|
'<td>' + esc(t.account_name || '—') + '</td>' +
|
||||||
'<td class="muted">' + esc(t.ticket || '—') + '</td>' +
|
'<td class="muted">' + esc(t.ticket || '—') + '</td>' +
|
||||||
'<td><a href="' + detailBase + t.id + '">复盘</a></td>' +
|
'<td><a href="' + detailBase + t.id + '">复盘</a></td>' +
|
||||||
|
|||||||
@@ -288,12 +288,13 @@ class Mt5Bridge:
|
|||||||
return age < 180 # 3 分钟内有报价视为开市
|
return age < 180 # 3 分钟内有报价视为开市
|
||||||
|
|
||||||
def day_ohlc(self, symbol: str) -> dict[str, Any]:
|
def day_ohlc(self, symbol: str) -> dict[str, Any]:
|
||||||
"""最近交易日日线开高低(休市仍显示);开市时用 tick 修正最高/最低。"""
|
"""当前/最近交易日日线开高低(休市仍显示);开市时用 tick 修正最高/最低。"""
|
||||||
self.ensure_symbol(symbol)
|
self.ensure_symbol(symbol)
|
||||||
rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_D1, 0, 2)
|
rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_D1, 0, 3)
|
||||||
if rates is None or len(rates) < 1:
|
if rates is None or len(rates) < 1:
|
||||||
raise TradeError("无法读取日线,请确认品种有 D1 数据")
|
raise TradeError("无法读取日线,请确认品种有 D1 数据")
|
||||||
bar = rates[0]
|
# MT5 返回数组从旧到新:[-1] 才是当前日线
|
||||||
|
bar = rates[-1]
|
||||||
open_px = float(bar["open"])
|
open_px = float(bar["open"])
|
||||||
high = float(bar["high"])
|
high = float(bar["high"])
|
||||||
low = float(bar["low"])
|
low = float(bar["low"])
|
||||||
@@ -514,7 +515,7 @@ class Mt5Bridge:
|
|||||||
"close_price": None,
|
"close_price": None,
|
||||||
"open_time": time.time(),
|
"open_time": time.time(),
|
||||||
"close_time": None,
|
"close_time": None,
|
||||||
"profit": None,
|
"profit": 0.0,
|
||||||
"sl": sl,
|
"sl": sl,
|
||||||
"tp": tp,
|
"tp": tp,
|
||||||
"snapshot": {"event": "open"},
|
"snapshot": {"event": "open"},
|
||||||
@@ -1383,7 +1384,7 @@ class OrderApp(tk.Tk):
|
|||||||
]
|
]
|
||||||
hub_client.report_bars(symbol, name, bars)
|
hub_client.report_bars(symbol, name, bars)
|
||||||
|
|
||||||
def _hub_report(self, payload: Union[dict, list, None]) -> None:
|
def _hub_report(self, payload: Union[dict, list, None], with_bars: bool = True) -> None:
|
||||||
if hub_client is None or not is_activated() or not payload:
|
if hub_client is None or not is_activated() or not payload:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1394,13 +1395,47 @@ class OrderApp(tk.Tk):
|
|||||||
if not isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
continue
|
continue
|
||||||
hub_client.report_trade(item)
|
hub_client.report_trade(item)
|
||||||
sym = str(item.get("symbol") or self._symbol())
|
if with_bars:
|
||||||
self._hub_upload_bars(sym)
|
sym = str(item.get("symbol") or self._symbol())
|
||||||
|
self._hub_upload_bars(sym)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
threading.Thread(target=run, daemon=True).start()
|
threading.Thread(target=run, daemon=True).start()
|
||||||
|
|
||||||
|
def _hub_sync_floating(self, symbol: str) -> None:
|
||||||
|
"""定期把持仓浮盈 upsert 到中控,总览盈亏才有数。"""
|
||||||
|
if hub_client is None or not is_activated():
|
||||||
|
return
|
||||||
|
now = time.time()
|
||||||
|
last = float(getattr(self, "_last_float_sync", 0) or 0)
|
||||||
|
if now - last < 2.0:
|
||||||
|
return
|
||||||
|
self._last_float_sync = now
|
||||||
|
try:
|
||||||
|
poss = self.bridge.positions(symbol)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
if not poss:
|
||||||
|
return
|
||||||
|
reports = []
|
||||||
|
for p in poss:
|
||||||
|
reports.append({
|
||||||
|
"ticket": str(p.ticket),
|
||||||
|
"symbol": str(p.symbol),
|
||||||
|
"side": "buy" if int(p.type) == mt5.POSITION_TYPE_BUY else "sell",
|
||||||
|
"volume": float(p.volume),
|
||||||
|
"open_price": float(p.price_open),
|
||||||
|
"close_price": None,
|
||||||
|
"open_time": float(p.time),
|
||||||
|
"close_time": None,
|
||||||
|
"profit": float(p.profit) + float(p.swap),
|
||||||
|
"sl": float(p.sl),
|
||||||
|
"tp": float(p.tp),
|
||||||
|
"snapshot": {"event": "float"},
|
||||||
|
})
|
||||||
|
self._hub_report(reports, with_bars=False)
|
||||||
|
|
||||||
def _run_async(self, work) -> None:
|
def _run_async(self, work) -> None:
|
||||||
if self._busy:
|
if self._busy:
|
||||||
self.set_status("上一次操作还未完成", ok=False)
|
self.set_status("上一次操作还未完成", ok=False)
|
||||||
@@ -1597,6 +1632,7 @@ class OrderApp(tk.Tk):
|
|||||||
|
|
||||||
self._refresh_positions(symbol, digits)
|
self._refresh_positions(symbol, digits)
|
||||||
self._sync_open_buttons(symbol)
|
self._sync_open_buttons(symbol)
|
||||||
|
self._hub_sync_floating(symbol)
|
||||||
except TradeError:
|
except TradeError:
|
||||||
# 断连时仍尝试刷新日线卡片
|
# 断连时仍尝试刷新日线卡片
|
||||||
try:
|
try:
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user