更新内容
This commit is contained in:
Binary file not shown.
+281
-34
@@ -414,19 +414,68 @@ def _extract_json_object(text):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _journal_row_lines_for_ai(idx, row, *, include_hold_duration=True):
|
||||||
|
"""把 journal 字段拼成给 AI 的文本;字段之外的事实不要指望模型自己猜。"""
|
||||||
|
def nz(v, default="无"):
|
||||||
|
if v is None:
|
||||||
|
return default
|
||||||
|
s = str(v).strip()
|
||||||
|
return s if s else default
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f"{idx}. {nz(row['coin'])} {nz(row['tf'])} | 盈亏:{nz(row['pnl'])}U | 实际RR:{nz(row['real_rr'])} | 预期RR:{nz(row['expect_rr'])}",
|
||||||
|
f" 开仓逻辑:{nz(row['entry_reason'])}",
|
||||||
|
f" 平仓/离场(交易员自述):{nz(row['exit_reason'])}",
|
||||||
|
]
|
||||||
|
if include_hold_duration:
|
||||||
|
lines.append(f" 持仓时长:{nz(row['hold_duration'])}")
|
||||||
|
ee_bits = [
|
||||||
|
nz(row["early_exit"]),
|
||||||
|
nz(row["early_exit_reason"]),
|
||||||
|
nz(row["early_exit_trigger"]),
|
||||||
|
nz(row["early_exit_note"]),
|
||||||
|
]
|
||||||
|
if any(x != "无" for x in ee_bits):
|
||||||
|
lines.append(
|
||||||
|
" 提前离场记录:"
|
||||||
|
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
|
||||||
|
)
|
||||||
|
mood_bits = f"心态标签:{nz(row['mood_issues'])}"
|
||||||
|
if row["mood_score"] is not None:
|
||||||
|
mood_bits += f" | 自评心态分:{row['mood_score']}"
|
||||||
|
lines.append(f" {mood_bits}")
|
||||||
|
if nz(row["post_breakeven_stare"]) != "无":
|
||||||
|
lines.append(f" 保本后盯盘:{nz(row['post_breakeven_stare'])}")
|
||||||
|
if nz(row["new_trade_while_occupied"]) != "无":
|
||||||
|
lines.append(f" 占用时新开仓:{nz(row['new_trade_while_occupied'])}")
|
||||||
|
if nz(row["note"]) != "无":
|
||||||
|
lines.append(f" 备注:{nz(row['note'])}")
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def ai_review(trades_text, period_title, image_paths=None):
|
def ai_review(trades_text, period_title, image_paths=None):
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
你是一位专业交易教练。下面是用户的{period_title}交易记录,请做专业、简洁、可执行的复盘:
|
你是一位专业交易教练。下面是用户的{period_title}交易记录,请做简洁、可执行的复盘(中文)。
|
||||||
1. 总体盈亏结构
|
|
||||||
2. 心态问题与执行漏洞(请给每笔交易一个1-10的心态分并简短说明)
|
【硬性规则 — 必须遵守】
|
||||||
3. 提前离场、乱开仓、扛单等行为分析
|
- 你只能根据「交易记录」里**明确出现的字段**陈述事实;禁止编造:是否触发止损、是否扛单、亏损是否扩大、图上具体结构/进出场点位等记录里**没有**的信息。
|
||||||
4. 给出具体改进建议(最多3条)
|
- 「平仓/离场」只是交易员自述摘要,不是客观成交明细;若记录未写明代币是否打到止损价、是否软件平仓等,不要断言执行路径,可用「在记录有限前提下,一种可能是……」或简短写「执行路径记录不足,无法判断」。
|
||||||
5. 若附带截图,请结合图中价格行为、结构、进出场位置一起分析(看不清时请明确说明不确定)
|
- 「提前离场」类结论必须优先依据记录中的「提前离场记录」字段;若该段全为「无」或未出现有效内容,不得写道「明显扛单」「拒不止损」「未执行硬止损」等。
|
||||||
|
- 实际RR为负只说明结果相对于预期RR不利,不等同于「风控失灵」或「止损纪律崩溃」,除非记录里另有依据。
|
||||||
|
- 禁止用语:人身攻击、夸张定性(如「致命伤」「灾难」);语气克制、对事不对人。
|
||||||
|
- 若有截图且你能辨认,再结合图讨论;看不清或无明确定位则明确说「无法从图确认」,不得虚构 K 线故事。
|
||||||
|
|
||||||
|
【输出结构】
|
||||||
|
1. 总体盈亏结构(紧扣笔数、盈亏数字与 RR,少形容词)
|
||||||
|
2. 心态与执行(每笔 1–10 分 + 一句依据;依据必须对应记录字段)
|
||||||
|
3. 行为标签(提前离场 / 乱开仓 / 扛单等):仅在有字段或自述支撑时点名;否则写「记录未勾选或未描述,不作强加」
|
||||||
|
4. 改进建议(最多 3 条,每条具体可执行)
|
||||||
|
5. 图表(若有且可读):结合价格行为简述;否则一两句说明无法看图分析
|
||||||
|
|
||||||
交易记录:
|
交易记录:
|
||||||
{trades_text}
|
{trades_text}
|
||||||
用中文输出,直接给结论与建议。
|
|
||||||
""".strip()
|
""".strip()
|
||||||
payload = {"model": AI_MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.3}}
|
payload = {"model": AI_MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.2}}
|
||||||
images = []
|
images = []
|
||||||
for p in image_paths or []:
|
for p in image_paths or []:
|
||||||
b64 = _read_image_base64(p)
|
b64 = _read_image_base64(p)
|
||||||
@@ -677,8 +726,9 @@ def generate_multi_timeframe_chart_png(
|
|||||||
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
rows = _ohlcv_to_rows(ohlcv)[-limit:]
|
||||||
title = f"{title_prefix} | {tf} x{len(rows)}"
|
title = f"{title_prefix} | {tf} x{len(rows)}"
|
||||||
points = []
|
points = []
|
||||||
marker_tfs = set(marker_timeframes or [])
|
tf_key = str(tf).strip().lower()
|
||||||
if marker_payload and tf in marker_tfs:
|
marker_tfs = {str(x).strip().lower() for x in (marker_timeframes or []) if str(x).strip()}
|
||||||
|
if marker_payload and tf_key in marker_tfs:
|
||||||
entry_idx, entry_price = _pick_marker_point(rows, marker_payload.get("entry_ts_ms"), marker_payload.get("entry_price"))
|
entry_idx, entry_price = _pick_marker_point(rows, marker_payload.get("entry_ts_ms"), marker_payload.get("entry_price"))
|
||||||
exit_idx, exit_price = _pick_marker_point(rows, marker_payload.get("exit_ts_ms"), marker_payload.get("exit_price"))
|
exit_idx, exit_price = _pick_marker_point(rows, marker_payload.get("exit_ts_ms"), marker_payload.get("exit_price"))
|
||||||
if entry_idx is not None and entry_price is not None:
|
if entry_idx is not None and entry_price is not None:
|
||||||
@@ -1398,6 +1448,66 @@ def normalize_exchange_symbol(symbol):
|
|||||||
return sym
|
return sym
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_monitor_exchange_symbol(row):
|
||||||
|
"""将监控行上的 symbol / exchange_symbol 统一到 ccxt 永续合约 symbol,便于与 fetch_positions 结果比对。"""
|
||||||
|
raw = ""
|
||||||
|
try:
|
||||||
|
if row["exchange_symbol"]:
|
||||||
|
raw = str(row["exchange_symbol"]).strip()
|
||||||
|
except (KeyError, IndexError, TypeError):
|
||||||
|
raw = ""
|
||||||
|
if not raw:
|
||||||
|
try:
|
||||||
|
raw = str(row["symbol"] or "").strip()
|
||||||
|
except (KeyError, IndexError, TypeError):
|
||||||
|
raw = ""
|
||||||
|
return normalize_exchange_symbol(raw) if raw else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _position_contract_symbol_match(position_symbol, wanted_exchange_symbol):
|
||||||
|
if not position_symbol or not wanted_exchange_symbol:
|
||||||
|
return False
|
||||||
|
a = normalize_exchange_symbol(str(position_symbol).strip())
|
||||||
|
b = normalize_exchange_symbol(str(wanted_exchange_symbol).strip())
|
||||||
|
return a == b
|
||||||
|
|
||||||
|
|
||||||
|
def _position_matches_wanted_contract(wanted_unified_sym, position_dict):
|
||||||
|
"""统一 symbol 比对;不一致时用 Gate 原始 contract 与 ccxt market.id 对齐(兼容 1000PEPE 等命名差异)。"""
|
||||||
|
if not wanted_unified_sym or not position_dict:
|
||||||
|
return False
|
||||||
|
ps = position_dict.get("symbol")
|
||||||
|
if _position_contract_symbol_match(ps, wanted_unified_sym):
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
ensure_markets_loaded()
|
||||||
|
mid = (exchange.market(wanted_unified_sym).get("id") or "").strip().upper()
|
||||||
|
info = position_dict.get("info") or {}
|
||||||
|
c_raw = str(info.get("contract") or "").strip().upper()
|
||||||
|
if mid and c_raw and mid == c_raw:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _position_row_effective_contracts(p):
|
||||||
|
"""张数:优先 ccxt contracts,否则用 Gate 原始 size/pos(避免统一层为 0 时被误判空仓)。"""
|
||||||
|
if not p:
|
||||||
|
return 0.0
|
||||||
|
info = p.get("info") or {}
|
||||||
|
for val in (p.get("contracts"), info.get("size"), info.get("pos")):
|
||||||
|
if val is None or val == "":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
x = abs(float(val))
|
||||||
|
if x > 0:
|
||||||
|
return x
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
def normalize_symbol_input(symbol):
|
def normalize_symbol_input(symbol):
|
||||||
sym = (symbol or "").strip().upper()
|
sym = (symbol or "").strip().upper()
|
||||||
if not sym:
|
if not sym:
|
||||||
@@ -1766,6 +1876,11 @@ def ensure_exchange_live_ready():
|
|||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def exchange_private_api_configured():
|
||||||
|
"""仅表示已配置密钥;与是否允许下单(LIVE_TRADING_ENABLED)无关,用于只读拉仓等。"""
|
||||||
|
return bool(GATE_API_KEY and GATE_API_SECRET)
|
||||||
|
|
||||||
|
|
||||||
def _extract_usdt_total(balance):
|
def _extract_usdt_total(balance):
|
||||||
usdt_info = balance.get("USDT", {}) if isinstance(balance, dict) else {}
|
usdt_info = balance.get("USDT", {}) if isinstance(balance, dict) else {}
|
||||||
total_map = balance.get("total", {}) if isinstance(balance, dict) else {}
|
total_map = balance.get("total", {}) if isinstance(balance, dict) else {}
|
||||||
@@ -1980,7 +2095,7 @@ def get_synced_leverage(exchange_symbol, direction):
|
|||||||
try:
|
try:
|
||||||
positions = exchange.fetch_positions([exchange_symbol])
|
positions = exchange.fetch_positions([exchange_symbol])
|
||||||
for p in positions:
|
for p in positions:
|
||||||
if p.get("symbol") != exchange_symbol:
|
if not _position_matches_wanted_contract(exchange_symbol, p):
|
||||||
continue
|
continue
|
||||||
info = p.get("info", {}) or {}
|
info = p.get("info", {}) or {}
|
||||||
side = (p.get("side") or info.get("posSide") or "").lower()
|
side = (p.get("side") or info.get("posSide") or "").lower()
|
||||||
@@ -2489,7 +2604,7 @@ def get_live_position_contracts(exchange_symbol, direction):
|
|||||||
return None
|
return None
|
||||||
total = 0.0
|
total = 0.0
|
||||||
for p in rows:
|
for p in rows:
|
||||||
if p.get("symbol") != exchange_symbol:
|
if not _position_matches_wanted_contract(exchange_symbol, p):
|
||||||
continue
|
continue
|
||||||
info = p.get("info", {}) or {}
|
info = p.get("info", {}) or {}
|
||||||
side = (p.get("side") or info.get("posSide") or "").lower()
|
side = (p.get("side") or info.get("posSide") or "").lower()
|
||||||
@@ -2513,6 +2628,110 @@ def get_live_position_contracts(exchange_symbol, direction):
|
|||||||
return total
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def _select_live_position_row(rows, exchange_symbol, direction, relax_hedge=False):
|
||||||
|
"""在 fetch_positions 结果中取与当前监控方向一致、张数最大的一条(与 get_live_position_contracts 过滤规则一致)。"""
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
candidates = []
|
||||||
|
for p in rows:
|
||||||
|
if not _position_matches_wanted_contract(exchange_symbol, p):
|
||||||
|
continue
|
||||||
|
info = p.get("info", {}) or {}
|
||||||
|
side = (p.get("side") or info.get("posSide") or "").lower()
|
||||||
|
contracts = _position_row_effective_contracts(p)
|
||||||
|
if contracts <= 0:
|
||||||
|
continue
|
||||||
|
if (not relax_hedge) and GATE_POS_MODE == "hedge":
|
||||||
|
if side and side != (direction or "").lower():
|
||||||
|
continue
|
||||||
|
candidates.append((contracts, p))
|
||||||
|
if not candidates and (not relax_hedge) and GATE_POS_MODE == "hedge":
|
||||||
|
return _select_live_position_row(rows, exchange_symbol, direction, relax_hedge=True)
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
candidates.sort(key=lambda x: x[0], reverse=True)
|
||||||
|
return candidates[0][1]
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_float(*values):
|
||||||
|
for v in values:
|
||||||
|
if v is None or v == "":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ccxt_position_metrics(position, order_leverage=None):
|
||||||
|
"""
|
||||||
|
从 ccxt 统一持仓结构解析保证金/名义/未实现盈亏(Gate 等所字段略有差异,做多键兜底)。
|
||||||
|
与 App「仓位保证金」对齐时优先用 initialMargin;缺失时再尝试 info 内字段。
|
||||||
|
"""
|
||||||
|
if not position:
|
||||||
|
return None
|
||||||
|
p = position
|
||||||
|
info = p.get("info", {}) or {}
|
||||||
|
# Gate 全仓:ccxt 的 initialMargin 常为空;collateral 来自 API 的 margin,与 App「保证金」一致
|
||||||
|
initial = _coerce_float(p.get("collateral"), p.get("initialMargin"), p.get("margin"))
|
||||||
|
if initial is None or initial <= 0:
|
||||||
|
initial = _coerce_float(
|
||||||
|
info.get("margin"),
|
||||||
|
info.get("cross_margin"),
|
||||||
|
info.get("iso_margin"),
|
||||||
|
info.get("initial_margin"),
|
||||||
|
info.get("position_margin"),
|
||||||
|
info.get("initialMargin"),
|
||||||
|
)
|
||||||
|
notional = _coerce_float(p.get("notional"), p.get("notionalValue"))
|
||||||
|
if notional is None or notional <= 0:
|
||||||
|
notional = _coerce_float(info.get("value"))
|
||||||
|
if notional is not None:
|
||||||
|
notional = abs(notional)
|
||||||
|
# 全仓且 API margin 为 0 时:用名义/杠杆粗算展示(与交易所「约占用」接近)
|
||||||
|
if (initial is None or initial <= 0) and notional and notional > 0 and order_leverage:
|
||||||
|
try:
|
||||||
|
lev = float(order_leverage)
|
||||||
|
if lev > 0:
|
||||||
|
approx = notional / lev
|
||||||
|
if approx > 0:
|
||||||
|
initial = approx
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
unrealized = _coerce_float(
|
||||||
|
p.get("unrealizedPnl"),
|
||||||
|
info.get("unrealised_pnl"),
|
||||||
|
info.get("unrealized_pnl"),
|
||||||
|
)
|
||||||
|
mark = _coerce_float(p.get("markPrice"), p.get("mark_price"), info.get("mark_price"), info.get("markPrice"))
|
||||||
|
out = {}
|
||||||
|
if initial is not None and initial > 0:
|
||||||
|
out["initial_margin"] = round(initial, 4)
|
||||||
|
if notional is not None and notional > 0:
|
||||||
|
out["notional"] = round(notional, 4)
|
||||||
|
if unrealized is not None:
|
||||||
|
out["unrealized_pnl"] = round(unrealized, 6)
|
||||||
|
if mark is not None and mark > 0:
|
||||||
|
out["mark_price"] = round(mark, 8)
|
||||||
|
return out or None
|
||||||
|
|
||||||
|
|
||||||
|
def get_live_position_exchange_metrics(exchange_symbol, direction):
|
||||||
|
ensure_markets_loaded()
|
||||||
|
if not exchange_private_api_configured() or not exchange_symbol:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
rows = exchange.fetch_positions(None, {"settle": "usdt"}) or []
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
rows = exchange.fetch_positions([exchange_symbol]) or []
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
p = _select_live_position_row(rows, exchange_symbol, direction)
|
||||||
|
return parse_ccxt_position_metrics(p)
|
||||||
|
|
||||||
|
|
||||||
def opened_at_str_to_ms(opened_at_str):
|
def opened_at_str_to_ms(opened_at_str):
|
||||||
if not opened_at_str:
|
if not opened_at_str:
|
||||||
return None
|
return None
|
||||||
@@ -3742,7 +3961,7 @@ def api_price_snapshot():
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
key_rows = conn.execute("SELECT id,symbol,monitor_type,direction,upper,lower FROM key_monitors").fetchall()
|
key_rows = conn.execute("SELECT id,symbol,monitor_type,direction,upper,lower FROM key_monitors").fetchall()
|
||||||
order_rows = conn.execute(
|
order_rows = conn.execute(
|
||||||
"SELECT id,symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'"
|
"SELECT id,symbol,exchange_symbol,direction,trigger_price,stop_loss,initial_stop_loss,take_profit,margin_capital,leverage FROM order_monitors WHERE status='active'"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -3758,6 +3977,18 @@ def api_price_snapshot():
|
|||||||
if p is not None:
|
if p is not None:
|
||||||
prices[s] = float(p)
|
prices[s] = float(p)
|
||||||
|
|
||||||
|
all_swap_positions = []
|
||||||
|
if exchange_private_api_configured():
|
||||||
|
try:
|
||||||
|
ensure_markets_loaded()
|
||||||
|
# 显式 USDT 本位;不传 symbols 拉全量,再在本地按合约对齐
|
||||||
|
all_swap_positions = exchange.fetch_positions(None, {"settle": "usdt"}) or []
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
all_swap_positions = exchange.fetch_positions() or []
|
||||||
|
except Exception:
|
||||||
|
all_swap_positions = []
|
||||||
|
|
||||||
key_prices = []
|
key_prices = []
|
||||||
for r in key_rows:
|
for r in key_rows:
|
||||||
price = prices.get(r["symbol"])
|
price = prices.get(r["symbol"])
|
||||||
@@ -3819,19 +4050,44 @@ def api_price_snapshot():
|
|||||||
pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 else 0
|
pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 else 0
|
||||||
pnl_pct = round((pnl / margin * 100), 4) if margin > 0 else 0
|
pnl_pct = round((pnl / margin * 100), 4) if margin > 0 else 0
|
||||||
rr_ratio = calc_rr_ratio(r["direction"], entry, r["initial_stop_loss"] or r["stop_loss"], r["take_profit"])
|
rr_ratio = calc_rr_ratio(r["direction"], entry, r["initial_stop_loss"] or r["stop_loss"], r["take_profit"])
|
||||||
order_prices.append({
|
ex_sym = resolve_monitor_exchange_symbol(r)
|
||||||
|
prow = _select_live_position_row(all_swap_positions, ex_sym, r["direction"])
|
||||||
|
lev_row = r["leverage"] if "leverage" in r.keys() else None
|
||||||
|
ex_metrics = parse_ccxt_position_metrics(prow, order_leverage=lev_row) if prow else None
|
||||||
|
payload = {
|
||||||
"id": r["id"],
|
"id": r["id"],
|
||||||
"symbol": r["symbol"],
|
"symbol": r["symbol"],
|
||||||
"price": round(price, 6),
|
"price": round(price, 6),
|
||||||
"float_pnl": round(pnl, 6),
|
"float_pnl": round(pnl, 6),
|
||||||
"float_pct": pnl_pct,
|
"float_pct": pnl_pct,
|
||||||
"rr_ratio": rr_ratio,
|
"rr_ratio": rr_ratio,
|
||||||
})
|
"plan_margin": round(margin, 4) if margin else None,
|
||||||
|
"exchange_initial_margin": None,
|
||||||
|
"exchange_notional": None,
|
||||||
|
"exchange_mark_price": None,
|
||||||
|
"pnl_source": "plan",
|
||||||
|
}
|
||||||
|
if ex_metrics:
|
||||||
|
if ex_metrics.get("initial_margin") is not None:
|
||||||
|
payload["exchange_initial_margin"] = ex_metrics["initial_margin"]
|
||||||
|
if ex_metrics.get("notional") is not None:
|
||||||
|
payload["exchange_notional"] = ex_metrics["notional"]
|
||||||
|
if ex_metrics.get("mark_price") is not None:
|
||||||
|
payload["exchange_mark_price"] = ex_metrics["mark_price"]
|
||||||
|
if ex_metrics.get("unrealized_pnl") is not None:
|
||||||
|
payload["float_pnl"] = round(float(ex_metrics["unrealized_pnl"]), 6)
|
||||||
|
payload["pnl_source"] = "exchange"
|
||||||
|
denom = ex_metrics.get("initial_margin") or margin
|
||||||
|
payload["float_pct"] = (
|
||||||
|
round((payload["float_pnl"] / float(denom)) * 100, 4) if denom and float(denom) > 0 else pnl_pct
|
||||||
|
)
|
||||||
|
order_prices.append(payload)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"updated_at": app_now_str(),
|
"updated_at": app_now_str(),
|
||||||
"key_prices": key_prices,
|
"key_prices": key_prices,
|
||||||
"order_prices": order_prices
|
"order_prices": order_prices,
|
||||||
|
"positions_raw_count": len(all_swap_positions),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -4840,7 +5096,11 @@ def add_journal():
|
|||||||
filename=chart_fname,
|
filename=chart_fname,
|
||||||
filename_prefix="journal",
|
filename_prefix="journal",
|
||||||
marker_payload=marker_payload,
|
marker_payload=marker_payload,
|
||||||
marker_timeframes={"15m", "5m"},
|
marker_timeframes=(
|
||||||
|
{x.strip().lower() for x in ORDER_CHART_TFS if x and str(x).strip()}
|
||||||
|
if ORDER_CHART_TFS
|
||||||
|
else {"5m", "15m", "1h", "4h"}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if saved:
|
if saved:
|
||||||
image_filename = saved
|
image_filename = saved
|
||||||
@@ -5183,15 +5443,8 @@ def ai_daily_review():
|
|||||||
|
|
||||||
text = f"【每日交易记录】{date}\n总笔数:{len(rows)}\n\n"
|
text = f"【每日交易记录】{date}\n总笔数:{len(rows)}\n\n"
|
||||||
for idx, row in enumerate(rows, 1):
|
for idx, row in enumerate(rows, 1):
|
||||||
issues = row["mood_issues"] or "无"
|
text += _journal_row_lines_for_ai(idx, row)
|
||||||
exit_one = (row["exit_reason"] or "").strip() or "无"
|
text += "\n"
|
||||||
text += (
|
|
||||||
f"{idx}. {row['coin']} {row['tf']} | 盈亏:{row['pnl']}U | RR:{row['real_rr']}\n"
|
|
||||||
f" 开仓类型:{row['entry_reason'] or '无'}\n"
|
|
||||||
f" 心态标签:{issues}\n"
|
|
||||||
f" 平仓/离场:{exit_one}\n"
|
|
||||||
f" 问题:{issues}\n\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
image_paths = []
|
image_paths = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@@ -5229,14 +5482,8 @@ def ai_weekly_review():
|
|||||||
|
|
||||||
text = f"【周交易记录】{start_date}~{end_date}\n总笔数:{len(rows)}\n\n"
|
text = f"【周交易记录】{start_date}~{end_date}\n总笔数:{len(rows)}\n\n"
|
||||||
for idx, row in enumerate(rows, 1):
|
for idx, row in enumerate(rows, 1):
|
||||||
issues = row["mood_issues"] or "无"
|
text += _journal_row_lines_for_ai(idx, row)
|
||||||
exit_one = (row["exit_reason"] or "").strip() or "无"
|
text += "\n"
|
||||||
text += (
|
|
||||||
f"{idx}. {row['coin']} {row['tf']} | 盈亏:{row['pnl']}U | RR:{row['real_rr']}\n"
|
|
||||||
f" 开仓类型:{row['entry_reason'] or '无'}\n"
|
|
||||||
f" 平仓/离场:{exit_one}\n"
|
|
||||||
f" 心态标签:{issues} | 持仓:{row['hold_duration']}\n\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
image_paths = []
|
image_paths = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
|
|||||||
@@ -282,7 +282,7 @@
|
|||||||
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
|
||||||
</label>
|
</label>
|
||||||
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
|
||||||
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(4h/1h/15m/5m 各100)
|
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
|
||||||
</label>
|
</label>
|
||||||
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
|
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
|
||||||
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
|
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
|
||||||
@@ -303,7 +303,8 @@
|
|||||||
| 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span>
|
| 盈亏比:<span id="order-rr-{{ o.id }}">{% if o.rr_ratio is not none %}1:{{ '%.2f'|format(o.rr_ratio) }}{% else %}-{% endif %}</span>
|
||||||
| 现价:<span id="order-price-{{ o.id }}">-</span>
|
| 现价:<span id="order-price-{{ o.id }}">-</span>
|
||||||
| 浮盈亏:<span id="order-pnl-{{ o.id }}">-</span>
|
| 浮盈亏:<span id="order-pnl-{{ o.id }}">-</span>
|
||||||
| 保证金:{{ o.margin_capital }}U | 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}%
|
| 计划基数:{{ o.margin_capital }}U | 所保证金:<span id="order-ex-margin-{{ o.id }}">-</span>
|
||||||
|
| 杠杆:{{ o.leverage }}x | 仓位占比:{{ o.position_ratio }}%
|
||||||
</div>
|
</div>
|
||||||
<a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
<a href="/del_order/{{ o.id }}" class="btn-del" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -1092,8 +1093,22 @@ function refreshPriceSnapshot(){
|
|||||||
(data.order_prices || []).forEach(o=>{
|
(data.order_prices || []).forEach(o=>{
|
||||||
const pEl = document.getElementById(`order-price-${o.id}`);
|
const pEl = document.getElementById(`order-price-${o.id}`);
|
||||||
if(pEl){
|
if(pEl){
|
||||||
pEl.innerText = Number(o.price).toFixed(6);
|
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
|
||||||
paintPriceTrend(pEl, `o-${o.id}`, Number(o.price));
|
const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
|
||||||
|
const decimals = hasMark ? 8 : 6;
|
||||||
|
pEl.innerText = px.toFixed(decimals);
|
||||||
|
paintPriceTrend(pEl, `o-${o.id}`, px);
|
||||||
|
}
|
||||||
|
const exM = document.getElementById(`order-ex-margin-${o.id}`);
|
||||||
|
if(exM){
|
||||||
|
const mv = o.exchange_initial_margin;
|
||||||
|
const mn = (mv === null || mv === undefined || mv === "") ? NaN : Number(mv);
|
||||||
|
if(!Number.isNaN(mn)){
|
||||||
|
exM.innerText = `${mn.toFixed(4)}U`;
|
||||||
|
} else {
|
||||||
|
const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null;
|
||||||
|
exM.innerText = (prc === 0) ? "无仓数据" : "-";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const pnlEl = document.getElementById(`order-pnl-${o.id}`);
|
const pnlEl = document.getElementById(`order-pnl-${o.id}`);
|
||||||
if(pnlEl){
|
if(pnlEl){
|
||||||
|
|||||||
Reference in New Issue
Block a user