From eae534af2ede4eb7c2805874b8c7f8db7e3ad0b3 Mon Sep 17 00:00:00 2001 From: guopengfa Date: Sat, 17 Jan 2026 19:35:19 +0800 Subject: [PATCH 1/5] =?UTF-8?q?make=20manual=20positions=20can=20add=20sam?= =?UTF-8?q?e=20stock=20and=20symbal=20to=20difference=20group=20(=E4=BD=BF?= =?UTF-8?q?=E8=B5=84=E9=87=91=E7=9B=91=E6=8E=A7=E5=8F=AF=E4=BB=A5=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=9B=B8=E5=90=8C=E8=82=A1=E7=A5=A8=E5=88=B0=E4=B8=8D?= =?UTF-8?q?=E5=90=8C=E5=88=86=E7=BB=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend_api_python/app/routes/portfolio.py | 2 +- backend_api_python/migrations/init.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend_api_python/app/routes/portfolio.py b/backend_api_python/app/routes/portfolio.py index d21e3bf25..210dd2b83 100644 --- a/backend_api_python/app/routes/portfolio.py +++ b/backend_api_python/app/routes/portfolio.py @@ -255,7 +255,7 @@ def add_position(): INSERT INTO qd_manual_positions (user_id, market, symbol, name, side, quantity, entry_price, entry_time, notes, tags, group_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) - ON CONFLICT(user_id, market, symbol, side) DO UPDATE SET + ON CONFLICT(user_id, market, symbol, side, group_name) DO UPDATE SET name = excluded.name, quantity = excluded.quantity, entry_price = excluded.entry_price, diff --git a/backend_api_python/migrations/init.sql b/backend_api_python/migrations/init.sql index 10bd86dee..9ad67710d 100644 --- a/backend_api_python/migrations/init.sql +++ b/backend_api_python/migrations/init.sql @@ -438,7 +438,7 @@ CREATE TABLE IF NOT EXISTS qd_manual_positions ( group_name VARCHAR(100) DEFAULT '', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), - UNIQUE(user_id, market, symbol, side) + UNIQUE(user_id, market, symbol, side, group_name) ); CREATE INDEX IF NOT EXISTS idx_manual_positions_user_id ON qd_manual_positions(user_id); From b07ed7a9ee78130cc625d325dde4942c47a58f2d Mon Sep 17 00:00:00 2001 From: guopengfa Date: Sun, 18 Jan 2026 12:49:40 +0800 Subject: [PATCH 2/5] hide logging --- backend_api_python/app/data_sources/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend_api_python/app/data_sources/base.py b/backend_api_python/app/data_sources/base.py index 9b54d242f..5eff84f81 100644 --- a/backend_api_python/app/data_sources/base.py +++ b/backend_api_python/app/data_sources/base.py @@ -140,10 +140,10 @@ def log_result( if klines: latest_time = datetime.fromtimestamp(klines[-1]['time']) time_diff = (datetime.now() - latest_time).total_seconds() - logger.info( - f"{self.name}: {symbol} 获取 {len(klines)} 条数据, " - f"最新时间: {latest_time}, 延迟: {time_diff:.0f}秒" - ) + # logger.info( + # f"{self.name}: {symbol} 获取 {len(klines)} 条数据, " + # f"最新时间: {latest_time}, 延迟: {time_diff:.0f}秒" + # ) # 检查数据是否过旧 max_diff = TIMEFRAME_SECONDS.get(timeframe, 3600) * 2 From cf4ce60ba534030cf42868804a1b1311e99e868a Mon Sep 17 00:00:00 2001 From: guopengfa Date: Sun, 18 Jan 2026 18:55:41 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix=20bug:=20when=20same=20symbal=20and=20s?= =?UTF-8?q?ame=20code=20and=20same=20name=20create=20html=20report=20will?= =?UTF-8?q?=20can=20not=20open=20the=20on=20collection,=20add=20symbal=20c?= =?UTF-8?q?ode=20name=20and=20group=20name=20to=20unique=20id=20(=E5=BD=93?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E8=82=A1=E7=A5=A8=E5=9C=A8=E4=B8=8D=E9=80=9A?= =?UTF-8?q?=E5=88=86=E7=BB=84=EF=BC=8C=E7=94=9F=E6=88=90=E5=87=BA=E6=9D=A5?= =?UTF-8?q?=E7=9A=84html=E6=8A=A5=E5=91=8A=E5=8F=AA=E8=83=BD=E5=B1=95?= =?UTF-8?q?=E5=BC=80=E4=B8=80=E4=B8=AA=E6=8A=A5=E5=91=8A)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/services/portfolio_monitor.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/backend_api_python/app/services/portfolio_monitor.py b/backend_api_python/app/services/portfolio_monitor.py index 0b63334a6..1a9f6205a 100644 --- a/backend_api_python/app/services/portfolio_monitor.py +++ b/backend_api_python/app/services/portfolio_monitor.py @@ -91,7 +91,7 @@ def _get_positions_for_monitor(position_ids: List[int] = None, user_id: int = No placeholders = ','.join(['?' for _ in position_ids]) cur.execute( f""" - SELECT id, market, symbol, name, side, quantity, entry_price + SELECT id, market, symbol, name, side, quantity, entry_price, group_name FROM qd_manual_positions WHERE user_id = ? AND id IN ({placeholders}) """, @@ -100,7 +100,7 @@ def _get_positions_for_monitor(position_ids: List[int] = None, user_id: int = No else: cur.execute( """ - SELECT id, market, symbol, name, side, quantity, entry_price + SELECT id, market, symbol, name, side, quantity, entry_price, group_name FROM qd_manual_positions WHERE user_id = ? """, @@ -115,7 +115,8 @@ def _get_positions_for_monitor(position_ids: List[int] = None, user_id: int = No symbol = row.get('symbol') entry_price = float(row.get('entry_price') or 0) quantity = float(row.get('quantity') or 0) - side = row.get('side') or 'long' + side = row.get('side') or 'long', + group_name = row.get('group_name') # Get current price (use realtime price API) current_price = 0 @@ -143,7 +144,8 @@ def _get_positions_for_monitor(position_ids: List[int] = None, user_id: int = No 'entry_price': entry_price, 'current_price': current_price, 'pnl': round(pnl, 2), - 'pnl_percent': pnl_percent + 'pnl_percent': pnl_percent, + 'group_name': group_name }) return positions @@ -168,6 +170,7 @@ def _run_ai_analysis(positions: List[Dict[str, Any]], config: Dict[str, Any]) -> market = pos.get('market') symbol = pos.get('symbol') name = pos.get('name') or symbol + group_name = pos.get('group_name') if not market or not symbol: continue @@ -193,6 +196,7 @@ def _run_ai_analysis(positions: List[Dict[str, Any]], config: Dict[str, Any]) -> 'market': market, 'symbol': symbol, 'name': name, + 'group_name': group_name, 'entry_price': pos.get('entry_price'), 'current_price': pos.get('current_price'), 'pnl': pos.get('pnl'), @@ -454,6 +458,7 @@ def _build_html_report( symbol = pa.get('symbol', '') name = pa.get('name', symbol) market = pa.get('market', '') + group_name = pa.get('group_name', '') if pa.get('error'): html += f''' @@ -538,7 +543,7 @@ def _build_html_report( ''' # Generate unique ID for collapsible sections (use symbol hash to avoid special chars) - section_id_base = hashlib.md5(f"{symbol}_{market}".encode()).hexdigest()[:8] + section_id_base = hashlib.md5(f"{symbol}_{market}_{group_name}".encode()).hexdigest()[:8] # Collapsible: Trader Analysis if trader_reasoning: From be8994c2d548e98c12ebace5ace3653cdf7acb54 Mon Sep 17 00:00:00 2001 From: guopengfa Date: Sun, 18 Jan 2026 20:26:46 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix=20bug=EF=BC=9Ahtml=20report=20direction?= =?UTF-8?q?=20right.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend_api_python/app/services/portfolio_monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend_api_python/app/services/portfolio_monitor.py b/backend_api_python/app/services/portfolio_monitor.py index 1a9f6205a..0c35f0311 100644 --- a/backend_api_python/app/services/portfolio_monitor.py +++ b/backend_api_python/app/services/portfolio_monitor.py @@ -115,7 +115,7 @@ def _get_positions_for_monitor(position_ids: List[int] = None, user_id: int = No symbol = row.get('symbol') entry_price = float(row.get('entry_price') or 0) quantity = float(row.get('quantity') or 0) - side = row.get('side') or 'long', + side = row.get('side') or 'long' group_name = row.get('group_name') # Get current price (use realtime price API) From 433e966860bffab09955dcd7e2551ac935c2b6fa Mon Sep 17 00:00:00 2001 From: guopengfa Date: Sun, 18 Jan 2026 23:01:57 +0800 Subject: [PATCH 5/5] capture signal error. --- backend_api_python/app/services/signal_notifier.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend_api_python/app/services/signal_notifier.py b/backend_api_python/app/services/signal_notifier.py index ce2c8b32d..8f4175348 100644 --- a/backend_api_python/app/services/signal_notifier.py +++ b/backend_api_python/app/services/signal_notifier.py @@ -24,6 +24,7 @@ import os import smtplib import time +import traceback from datetime import datetime, timezone from email.message import EmailMessage from typing import Any, Dict, List, Optional, Tuple @@ -482,6 +483,7 @@ def _notify_browser( return True, "" except Exception as e: logger.warning(f"browser notify persist failed: {e}") + logger.error('browser.error', traceback=traceback.format_exc()) return False, str(e) def _notify_webhook( @@ -571,6 +573,7 @@ def _post_once(timeout: float) -> requests.Response: return False, f"http_{resp2.status_code}:{(resp2.text or '')[:300]}" return False, f"http_{resp.status_code}:{(resp.text or '')[:300]}" except Exception as e: + logger.error('webhook.error', traceback=traceback.format_exc()) return False, str(e) def _notify_discord(self, *, url: str, payload: Dict[str, Any], fallback_text: str) -> Tuple[bool, str]: @@ -646,6 +649,7 @@ def _post(payload_json: Dict[str, Any]) -> requests.Response: pass return False, f"http_{resp.status_code}:{(resp.text or '')[:300]}" except Exception as e: + logger.error('discord.error', traceback=traceback.format_exc()) return False, str(e) def _notify_telegram( @@ -680,6 +684,7 @@ def _notify_telegram( return True, "" return False, f"http_{resp.status_code}:{(resp.text or '')[:300]}" except Exception as e: + logger.error('telegram.error', traceback=traceback.format_exc()) return False, str(e) def _notify_email(self, *, to_email: str, subject: str, body_text: str, body_html: str = "") -> Tuple[bool, str]: @@ -718,6 +723,7 @@ def _notify_email(self, *, to_email: str, subject: str, body_text: str, body_htm server.send_message(msg) return True, "" except Exception as e: + logger.error('email.error', traceback=traceback.format_exc()) return False, str(e) def _notify_phone(self, *, to_phone: str, body: str) -> Tuple[bool, str]: @@ -734,6 +740,7 @@ def _notify_phone(self, *, to_phone: str, body: str) -> Tuple[bool, str]: return True, "" return False, f"http_{resp.status_code}:{(resp.text or '')[:300]}" except Exception as e: + logger.error('phone.error', traceback=traceback.format_exc()) return False, str(e)