Merge pull request #15 from p369349074/main

make manual positions can add same stock and symbal to difference gro…
This commit is contained in:
Quantdinger
2026-01-18 22:21:49 +07:00
committed by GitHub
5 changed files with 22 additions and 10 deletions
+4 -4
View File
@@ -140,10 +140,10 @@ class BaseDataSource(ABC):
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
+1 -1
View File
@@ -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,
@@ -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 = ?
""",
@@ -116,6 +116,7 @@ def _get_positions_for_monitor(position_ids: List[int] = None, user_id: int = No
entry_price = float(row.get('entry_price') or 0)
quantity = float(row.get('quantity') or 0)
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:
@@ -24,6 +24,7 @@ import json
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 @@ class SignalNotifier:
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 @@ class SignalNotifier:
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 @@ class SignalNotifier:
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 @@ class SignalNotifier:
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 @@ class SignalNotifier:
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 @@ class SignalNotifier:
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)
+1 -1
View File
@@ -439,7 +439,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);