feat: Multi-user system with PostgreSQL - WIP temporary save

This commit is contained in:
TIANHE
2026-01-14 05:29:55 +08:00
parent 996e3b38fe
commit 61a5e5e6aa
68 changed files with 91057 additions and 1920 deletions
@@ -97,6 +97,227 @@ class AgentCoordinator:
self.neutral_analyst = NeutralAnalyst()
self.safe_analyst = SafeAnalyst()
def run_analysis_stream(self, market: str, symbol: str, language: str = 'zh-CN', model: str = None, timeframe: str = "1D", on_progress=None):
"""
Run the full multi-agent analysis workflow with progress callbacks.
Args:
on_progress: Callback function that receives (agent_name: str, status: str, result: Optional[dict])
status can be: 'started', 'completed', 'error'
Yields:
Progress events as dicts: { 'agent': str, 'status': str, 'result': dict or None }
"""
logger.info(f"Multi-agent stream analysis start: {market}:{symbol}, model={model}, language={language}")
def emit_progress(agent: str, status: str, result: dict = None):
"""Emit progress event."""
if on_progress:
on_progress(agent, status, result)
# Build base context
from .tools import AgentTools
tools = AgentTools()
emit_progress('data_collection', 'started', None)
# 1) Base data
current_price = tools.get_current_price(market, symbol)
company_data = tools.get_company_data(market, symbol, language=language)
# Normalize timeframe
tf = (timeframe or "1D").strip()
# 2) Kline + fundamentals
kline_data = tools.get_stock_data(market, symbol, days=30, timeframe=tf)
fundamental_data = tools.get_fundamental_data(market, symbol)
indicators = tools.calculate_technical_indicators(kline_data or [])
# 3) News (Finnhub + web search)
company_name = company_data.get('name', symbol) if company_data else symbol
news_data = tools.get_news(market, symbol, days=7, company_name=company_name)
base_data = {
"market": market,
"symbol": symbol,
"current_price": current_price,
"kline_data": kline_data,
"fundamental_data": fundamental_data,
"company_data": company_data,
"news_data": news_data,
"indicators": indicators,
}
context = {
"market": market,
"symbol": symbol,
"language": language,
"model": model,
"timeframe": tf,
"memory_features": {
"timeframe": tf,
"price": (current_price or {}).get("price"),
"changePercent": (current_price or {}).get("changePercent"),
"indicators": indicators,
},
"base_data": base_data
}
emit_progress('data_collection', 'completed', None)
# Phase 1: Analysts (parallel but report individually)
logger.info("Phase 1: Analyst team")
# Fundamental Analyst
emit_progress('fundamental', 'started', None)
fundamental_report = self.fundamental_analyst.analyze(context)
emit_progress('fundamental', 'completed', fundamental_report.get('data', {}))
# Technical Analyst (market_analyst)
emit_progress('technical', 'started', None)
market_report = self.market_analyst.analyze(context)
emit_progress('technical', 'completed', market_report.get('data', {}))
# News Analyst
emit_progress('news', 'started', None)
news_report = self.news_analyst.analyze(context)
emit_progress('news', 'completed', news_report.get('data', {}))
# Sentiment Analyst
emit_progress('sentiment', 'started', None)
sentiment_report = self.sentiment_analyst.analyze(context)
emit_progress('sentiment', 'completed', sentiment_report.get('data', {}))
# Risk Analyst
emit_progress('risk', 'started', None)
risk_report = self.risk_analyst.analyze(context)
emit_progress('risk', 'completed', risk_report.get('data', {}))
# Update context with analyst outputs
context.update({
"market_report": market_report,
"fundamental_report": fundamental_report,
"news_report": news_report,
"sentiment_report": sentiment_report,
"risk_report": risk_report,
})
# Phase 2: Bull/Bear debate
logger.info("Phase 2: Research debate")
emit_progress('debate_bull', 'started', None)
bull_argument = self.bull_researcher.analyze(context)
emit_progress('debate_bull', 'completed', bull_argument.get('data', {}))
emit_progress('debate_bear', 'started', None)
bear_argument = self.bear_researcher.analyze(context)
emit_progress('debate_bear', 'completed', bear_argument.get('data', {}))
context["bull_argument"] = bull_argument
context["bear_argument"] = bear_argument
# Research manager decision
emit_progress('debate_research', 'started', None)
research_decision = self._make_research_decision(bull_argument, bear_argument, context)
context["research_decision"] = research_decision
emit_progress('debate_research', 'completed', {'research_decision': research_decision})
# Phase 3: Trader decision
logger.info("Phase 3: Trader decision")
emit_progress('trader_decision', 'started', None)
trader_result = self.trader_agent.analyze(context)
trader_plan = trader_result.get('data', {}).get('trading_plan', {})
context["trader_plan"] = trader_plan
emit_progress('trader_decision', 'completed', trader_result.get('data', {}))
# Phase 4: Risk debate
logger.info("Phase 4: Risk debate")
emit_progress('risk_debate_risky', 'started', None)
risky_result = self.risky_analyst.analyze(context)
emit_progress('risk_debate_risky', 'completed', risky_result.get('data', {}))
emit_progress('risk_debate_neutral', 'started', None)
neutral_result = self.neutral_analyst.analyze(context)
emit_progress('risk_debate_neutral', 'completed', neutral_result.get('data', {}))
emit_progress('risk_debate_safe', 'started', None)
safe_result = self.safe_analyst.analyze(context)
emit_progress('risk_debate_safe', 'completed', safe_result.get('data', {}))
# Final decision
logger.info("Phase 5: Final decision")
emit_progress('final_decision', 'started', None)
final_decision = self._make_risk_decision(risky_result, neutral_result, safe_result, trader_result, context)
emit_progress('final_decision', 'completed', final_decision)
# Generate overview
emit_progress('overview', 'started', None)
overview = self._generate_overview(context, final_decision)
emit_progress('overview', 'completed', overview)
# Record for reflection
if self.reflection_service and final_decision.get('decision') in ['BUY', 'SELL', 'HOLD']:
try:
self.reflection_service.record_analysis(
market=market,
symbol=symbol,
price=base_data.get('current_price', {}).get('price'),
decision=final_decision.get('decision'),
confidence=final_decision.get('confidence', 50),
reasoning=final_decision.get('reasoning', ''),
check_days=7
)
except Exception as e:
logger.warning(f"Record reflection failed: {e}")
# Build final result
debate_data = {
"bull": bull_argument.get('data', {}) if bull_argument.get('data') else {},
"bear": bear_argument.get('data', {}) if bear_argument.get('data') else {},
"research_decision": research_decision if research_decision else "Analyzing..."
}
trader_decision_data = trader_result.get('data', {}) if trader_result.get('data') else {
"decision": "HOLD",
"confidence": 50,
"reasoning": "Analyzing...",
"trading_plan": {},
"report": "Analyzing..."
}
risk_debate_data = {
"risky": risky_result.get('data', {}) if risky_result.get('data') else {},
"neutral": neutral_result.get('data', {}) if neutral_result.get('data') else {},
"safe": safe_result.get('data', {}) if safe_result.get('data') else {}
}
if not final_decision or (isinstance(final_decision, dict) and len(final_decision) == 0):
final_decision = {
"decision": "HOLD",
"confidence": 50,
"reasoning": "Analyzing...",
"risk_summary": {},
"recommendation": "Analyzing..."
}
result = {
"overview": overview,
"fundamental": fundamental_report.get('data', {}),
"technical": market_report.get('data', {}),
"news": news_report.get('data', {}),
"sentiment": sentiment_report.get('data', {}),
"risk": risk_report.get('data', {}),
"debate": debate_data,
"trader_decision": trader_decision_data,
"risk_debate": risk_debate_data,
"final_decision": final_decision,
"error": None
}
logger.info(f"Multi-agent stream analysis completed: {market}:{symbol}")
return result
def run_analysis(self, market: str, symbol: str, language: str = 'zh-CN', model: str = None, timeframe: str = "1D") -> Dict[str, Any]:
"""
Run the full multi-agent analysis workflow.
+118 -147
View File
@@ -1,7 +1,7 @@
"""
Agent memory system (local-only).
Agent memory system (PostgreSQL).
This module stores agent experiences in SQLite and retrieves relevant past cases
This module stores agent experiences in PostgreSQL and retrieves relevant past cases
to inject into prompts (RAG-style). It does NOT finetune model weights.
Retrieval (configurable):
@@ -14,7 +14,6 @@ Ranking combines:
- optional returns weight
"""
import sqlite3
import json
import os
import math
@@ -23,31 +22,24 @@ from datetime import datetime, timezone
import difflib
from app.utils.logger import get_logger
from app.utils.db import get_db_connection
from .embedding import EmbeddingService, cosine_sim
logger = get_logger(__name__)
class AgentMemory:
"""智能体记忆系统"""
"""Agent memory system using PostgreSQL"""
def __init__(self, agent_name: str, db_path: Optional[str] = None):
"""
初始化记忆系统
Initialize memory system.
Args:
agent_name: 智能体名称
db_path: 数据库路径(可选)
agent_name: Agent identifier (e.g., 'trader_agent', 'risk_analyst')
db_path: Deprecated parameter, kept for backward compatibility
"""
self.agent_name = agent_name
if db_path is None:
# 默认数据库路径
db_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'data', 'memory')
os.makedirs(db_dir, exist_ok=True)
db_path = os.path.join(db_dir, f'{agent_name}_memory.db')
self.db_path = db_path
self.embedder = EmbeddingService()
self.enable_vector = os.getenv("AGENT_MEMORY_ENABLE_VECTOR", "true").lower() == "true"
self.candidate_limit = int(os.getenv("AGENT_MEMORY_CANDIDATE_LIMIT", "500") or 500)
@@ -55,58 +47,6 @@ class AgentMemory:
self.w_sim = float(os.getenv("AGENT_MEMORY_W_SIM", "0.75") or 0.75)
self.w_recency = float(os.getenv("AGENT_MEMORY_W_RECENCY", "0.20") or 0.20)
self.w_returns = float(os.getenv("AGENT_MEMORY_W_RETURNS", "0.05") or 0.05)
self._init_database()
def _init_database(self):
"""初始化数据库表"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
situation TEXT NOT NULL,
recommendation TEXT NOT NULL,
result TEXT,
returns REAL,
market TEXT,
symbol TEXT,
timeframe TEXT,
features_json TEXT,
embedding BLOB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Best-effort migration for older DBs
# NOTE: For existing tables, we must add missing columns BEFORE creating indexes
# that reference them (otherwise we'll hit: "no such column: market").
cursor.execute("PRAGMA table_info(memories)")
existing_cols = {row[1] for row in cursor.fetchall() or []}
for col, ddl in {
"market": "TEXT",
"symbol": "TEXT",
"timeframe": "TEXT",
"features_json": "TEXT",
"embedding": "BLOB",
}.items():
if col not in existing_cols:
cursor.execute(f"ALTER TABLE memories ADD COLUMN {col} {ddl}")
# 创建索引(放在迁移之后,兼容旧库)
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at)
''')
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_market_symbol ON memories(market, symbol)
''')
conn.commit()
conn.close()
except Exception as e:
logger.error(f"初始化记忆数据库失败: {e}")
def _now_utc(self) -> datetime:
return datetime.now(timezone.utc)
@@ -156,13 +96,13 @@ class AgentMemory:
metadata: Optional[Dict[str, Any]] = None,
):
"""
添加记忆
Add a memory entry.
Args:
situation: 情况描述
recommendation: 建议/决策
result: 结果描述(可选)
returns: 收益(可选)
situation: Situation description
recommendation: Decision/recommendation made
result: Outcome description (optional)
returns: Return percentage (optional)
metadata: Optional structured metadata (market/symbol/timeframe/features...)
"""
try:
@@ -182,45 +122,50 @@ class AgentMemory:
vec = self.embedder.embed(text)
embedding_blob = self.embedder.to_bytes(vec)
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO memories (situation, recommendation, result, returns, market, symbol, timeframe, features_json, embedding)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (situation, recommendation, result, returns, market, symbol, timeframe, features_json, embedding_blob))
conn.commit()
conn.close()
logger.info(f"{self.agent_name} 添加新记忆")
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute(
"""
INSERT INTO qd_agent_memories
(agent_name, situation, recommendation, result, returns, market, symbol, timeframe, features_json, embedding)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(self.agent_name, situation, recommendation, result, returns, market, symbol, timeframe, features_json, embedding_blob)
)
conn.commit()
cur.close()
logger.info(f"{self.agent_name} added new memory")
except Exception as e:
logger.error(f"添加记忆失败: {e}")
logger.error(f"Failed to add memory: {e}")
def get_memories(self, current_situation: str, n_matches: int = 5, metadata: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
"""
检索相似记忆
Retrieve similar memories.
Args:
current_situation: 当前情况描述
n_matches: 返回的匹配数量
current_situation: Current situation description
n_matches: Number of matches to return
metadata: Optional metadata for filtering/weighting
Returns:
匹配的记忆列表
List of matching memory entries
"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 获取所有记忆
cursor.execute('''
SELECT id, situation, recommendation, result, returns, created_at, market, symbol, timeframe, features_json, embedding
FROM memories
ORDER BY created_at DESC
LIMIT ?
''', (int(self.candidate_limit),))
all_memories = cursor.fetchall()
conn.close()
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute(
"""
SELECT id, situation, recommendation, result, returns, created_at,
market, symbol, timeframe, features_json, embedding
FROM qd_agent_memories
WHERE agent_name = ?
ORDER BY created_at DESC
LIMIT ?
""",
(self.agent_name, int(self.candidate_limit))
)
all_memories = cur.fetchall() or []
cur.close()
if not all_memories:
return []
@@ -240,23 +185,24 @@ class AgentMemory:
ranked = []
for row in all_memories:
(
mem_id,
situation,
recommendation,
result,
returns,
created_at,
market,
symbol,
timeframe,
features_json,
embedding_blob,
) = row
mem_id = row['id']
situation = row['situation']
recommendation = row['recommendation']
result = row['result']
returns = row['returns']
created_at = row['created_at']
market = row['market']
symbol = row['symbol']
timeframe = row['timeframe']
features_json = row['features_json']
embedding_blob = row['embedding']
sim = 0.0
if self.enable_vector and embedding_blob:
try:
# Handle memoryview/bytes from PostgreSQL
if isinstance(embedding_blob, memoryview):
embedding_blob = bytes(embedding_blob)
mem_vec = self.embedder.from_bytes(embedding_blob)
sim = cosine_sim(query_vec, mem_vec)
except Exception:
@@ -292,50 +238,60 @@ class AgentMemory:
return ranked[: max(0, int(n_matches or 0))]
except Exception as e:
logger.error(f"检索记忆失败: {e}")
logger.error(f"Failed to retrieve memories: {e}")
return []
def update_memory_result(self, memory_id: int, result: str, returns: Optional[float] = None):
"""
更新记忆的结果
Update memory result.
Args:
memory_id: 记忆ID
result: 结果描述
returns: 收益
memory_id: Memory ID
result: Outcome description
returns: Return percentage
"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
UPDATE memories
SET result = ?, returns = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', (result, returns, memory_id))
conn.commit()
conn.close()
logger.info(f"{self.agent_name} 更新记忆 {memory_id}")
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute(
"""
UPDATE qd_agent_memories
SET result = ?, returns = ?, updated_at = NOW()
WHERE id = ? AND agent_name = ?
""",
(result, returns, memory_id, self.agent_name)
)
conn.commit()
cur.close()
logger.info(f"{self.agent_name} updated memory {memory_id}")
except Exception as e:
logger.error(f"更新记忆失败: {e}")
logger.error(f"Failed to update memory: {e}")
def get_statistics(self) -> Dict[str, Any]:
"""获取记忆统计信息"""
"""Get memory statistics for this agent."""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT COUNT(*) FROM memories')
total = cursor.fetchone()[0]
cursor.execute('SELECT AVG(returns) FROM memories WHERE returns IS NOT NULL')
avg_returns = cursor.fetchone()[0] or 0
cursor.execute('SELECT COUNT(*) FROM memories WHERE returns > 0')
positive = cursor.fetchone()[0]
conn.close()
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute(
'SELECT COUNT(*) as cnt FROM qd_agent_memories WHERE agent_name = ?',
(self.agent_name,)
)
total = cur.fetchone()['cnt']
cur.execute(
'SELECT AVG(returns) as avg_ret FROM qd_agent_memories WHERE agent_name = ? AND returns IS NOT NULL',
(self.agent_name,)
)
avg_returns = cur.fetchone()['avg_ret'] or 0
cur.execute(
'SELECT COUNT(*) as cnt FROM qd_agent_memories WHERE agent_name = ? AND returns > 0',
(self.agent_name,)
)
positive = cur.fetchone()['cnt']
cur.close()
return {
'total_memories': total,
@@ -344,5 +300,20 @@ class AgentMemory:
'success_rate': round(positive / total * 100, 2) if total > 0 else 0
}
except Exception as e:
logger.error(f"获取统计信息失败: {e}")
logger.error(f"Failed to get statistics: {e}")
return {}
def clear_memories(self):
"""Clear all memories for this agent (use with caution)."""
try:
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute(
'DELETE FROM qd_agent_memories WHERE agent_name = ?',
(self.agent_name,)
)
conn.commit()
cur.close()
logger.warning(f"{self.agent_name} cleared all memories")
except Exception as e:
logger.error(f"Failed to clear memories: {e}")
@@ -1,220 +1,249 @@
"""
自动反思与验证服务
用于记录分析预测,并在未来自动验证结果,实现闭环学习
Auto-reflection and verification service (PostgreSQL).
Records analysis predictions and auto-verifies results in the future
to achieve closed-loop learning for AI agents.
"""
import sqlite3
import os
import json
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from app.utils.logger import get_logger
from app.utils.db import get_db_connection
from .memory import AgentMemory
from .tools import AgentTools
logger = get_logger(__name__)
class ReflectionService:
"""反思服务:管理分析记录的存储和验证"""
"""Reflection service: manages storage and verification of analysis records."""
def __init__(self, db_path: Optional[str] = None):
if db_path is None:
# 默认数据库路径
db_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'data', 'memory')
os.makedirs(db_dir, exist_ok=True)
db_path = os.path.join(db_dir, 'reflection_records.db')
self.db_path = db_path
self.tools = AgentTools()
self._init_database()
def _init_database(self):
"""初始化数据库表"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 创建分析记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS analysis_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
market TEXT NOT NULL,
symbol TEXT NOT NULL,
initial_price REAL,
decision TEXT,
confidence INTEGER,
reasoning TEXT,
analysis_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
target_check_date TIMESTAMP,
status TEXT DEFAULT 'PENDING', -- PENDING, COMPLETED, FAILED
final_price REAL,
actual_return REAL,
check_result TEXT
)
''')
# 创建索引
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_status_date ON analysis_records(status, target_check_date)
''')
conn.commit()
conn.close()
except Exception as e:
logger.error(f"初始化反思数据库失败: {e}")
def record_analysis(self, market: str, symbol: str, price: float,
decision: str, confidence: int, reasoning: str,
check_days: int = 7):
"""
记录一次分析,以便未来验证
Initialize reflection service.
Args:
market: 市场
symbol: 代码
price: 当前价格
decision: 决策 (BUY/SELL/HOLD)
confidence: 置信度
reasoning: 理由
check_days: 几天后验证 (默认7天)
db_path: Deprecated parameter, kept for backward compatibility
"""
self.tools = AgentTools()
def record_analysis(
self,
market: str,
symbol: str,
price: float,
decision: str,
confidence: int,
reasoning: str,
check_days: int = 7
):
"""
Record an analysis for future verification.
Args:
market: Market type
symbol: Symbol code
price: Current price
decision: Decision (BUY/SELL/HOLD)
confidence: Confidence level (0-100)
reasoning: Reasoning text
check_days: Days until verification (default 7)
"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
target_date = datetime.now() + timedelta(days=check_days)
cursor.execute('''
INSERT INTO analysis_records
(market, symbol, initial_price, decision, confidence, reasoning, target_check_date)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (market, symbol, price, decision, confidence, reasoning, target_date))
conn.commit()
conn.close()
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute(
"""
INSERT INTO qd_reflection_records
(market, symbol, initial_price, decision, confidence, reasoning, target_check_date)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(market, symbol, price, decision, confidence, reasoning, target_date)
)
conn.commit()
cur.close()
logger.info(f"Recorded analysis for reflection: {market}:{symbol}, will verify after {check_days} day(s)")
except Exception as e:
logger.error(f"记录分析失败: {e}")
logger.error(f"Failed to record analysis: {e}")
def run_verification_cycle(self):
"""
执行验证周期:检查到期的记录,验证结果,并写入记忆
Execute verification cycle: check due records, verify results, and write to memory.
"""
logger.info("开始执行自动反思验证周期...")
logger.info("Starting auto-reflection verification cycle...")
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 1. 查找所有已到期且未处理的记录
cursor.execute('''
SELECT id, market, symbol, initial_price, decision, confidence, reasoning, analysis_date
FROM analysis_records
WHERE status = 'PENDING' AND target_check_date <= CURRENT_TIMESTAMP
''')
records = cursor.fetchall()
if not records:
logger.info("没有需要验证的记录")
conn.close()
return
logger.info(f"发现 {len(records)} 条待验证记录")
# 初始化记忆系统(用于写入验证结果)
trader_memory = AgentMemory('trader_agent')
for record in records:
record_id, market, symbol, initial_price, decision, confidence, reasoning, analysis_date = record
with get_db_connection() as conn:
cur = conn.cursor()
try:
# 2. 获取当前最新价格
current_price_data = self.tools.get_current_price(market, symbol)
current_price = current_price_data.get('price')
# 1. Find all due and pending records
cur.execute(
"""
SELECT id, market, symbol, initial_price, decision, confidence, reasoning, analysis_date
FROM qd_reflection_records
WHERE status = 'PENDING' AND target_check_date <= NOW()
"""
)
records = cur.fetchall() or []
if not records:
logger.info("No records to verify")
cur.close()
return
logger.info(f"Found {len(records)} records to verify")
# Initialize memory system for writing verification results
trader_memory = AgentMemory('trader_agent')
for record in records:
record_id = record['id']
market = record['market']
symbol = record['symbol']
initial_price = record['initial_price']
decision = record['decision']
confidence = record['confidence']
reasoning = record['reasoning']
analysis_date = record['analysis_date']
if not current_price:
logger.warning(f"无法获取 {market}:{symbol} 的当前价格,跳过")
continue
try:
# 2. Get current price
current_price_data = self.tools.get_current_price(market, symbol)
current_price = current_price_data.get('price')
# 3. 计算收益和结果
if not initial_price or initial_price == 0:
actual_return = 0.0
else:
actual_return = (current_price - initial_price) / initial_price * 100
# 评估结果
result_desc = ""
is_good_prediction = False
if decision == "BUY":
if actual_return > 2.0:
result_desc = "Correct: price rose after BUY"
is_good_prediction = True
elif actual_return < -2.0:
result_desc = "Wrong: price fell after BUY"
if not current_price:
logger.warning(f"Cannot get current price for {market}:{symbol}, skipping")
continue
# 3. Calculate return and result
if not initial_price or initial_price == 0:
actual_return = 0.0
else:
result_desc = "Neutral: limited price movement"
elif decision == "SELL":
if actual_return < -2.0:
result_desc = "Correct: price fell after SELL"
is_good_prediction = True
elif actual_return > 2.0:
result_desc = "Wrong: price rose after SELL"
else:
result_desc = "Neutral: limited price movement"
else: # HOLD
if -2.0 <= actual_return <= 2.0:
result_desc = "Correct: limited movement during HOLD"
is_good_prediction = True
else:
result_desc = f"Deviated: large movement during HOLD ({actual_return:.2f}%)"
actual_return = (current_price - initial_price) / initial_price * 100
# Evaluate result
result_desc = ""
is_good_prediction = False
if decision == "BUY":
if actual_return > 2.0:
result_desc = "Correct: price rose after BUY"
is_good_prediction = True
elif actual_return < -2.0:
result_desc = "Wrong: price fell after BUY"
else:
result_desc = "Neutral: limited price movement"
elif decision == "SELL":
if actual_return < -2.0:
result_desc = "Correct: price fell after SELL"
is_good_prediction = True
elif actual_return > 2.0:
result_desc = "Wrong: price rose after SELL"
else:
result_desc = "Neutral: limited price movement"
else: # HOLD
if -2.0 <= actual_return <= 2.0:
result_desc = "Correct: limited movement during HOLD"
is_good_prediction = True
else:
result_desc = f"Deviated: large movement during HOLD ({actual_return:.2f}%)"
# 4. 写入记忆系统 (Let the agent learn)
memory_situation = f"{market}:{symbol} auto-verified (analysis_date: {analysis_date})"
memory_recommendation = f"Decision: {decision} (confidence {confidence}), reasoning: {(reasoning or '')[:120]}"
memory_result = f"Verification: {result_desc}; return={actual_return:.2f}% (initial {initial_price} -> final {current_price})"
trader_memory.add_memory(
memory_situation,
memory_recommendation,
memory_result,
actual_return,
metadata={
"market": market,
"symbol": symbol,
"timeframe": "1D",
"features": {
"source": "auto_verify",
"decision": decision,
"confidence": confidence,
"initial_price": initial_price,
"final_price": current_price,
"analysis_date": str(analysis_date),
"result_desc": result_desc,
"is_good_prediction": bool(is_good_prediction),
},
}
)
# 5. 更新记录状态
cursor.execute('''
UPDATE analysis_records
SET status = 'COMPLETED', final_price = ?, actual_return = ?, check_result = ?
WHERE id = ?
''', (current_price, actual_return, result_desc, record_id))
conn.commit()
logger.info(f"验证完成 {market}:{symbol}: {result_desc}")
except Exception as inner_e:
logger.error(f"处理记录 {record_id} 失败: {inner_e}")
# 标记为失败,避免重复处理
# cursor.execute("UPDATE analysis_records SET status = 'FAILED' WHERE id = ?", (record_id,))
# conn.commit()
conn.close()
logger.info("反思验证周期结束")
# 4. Write to memory system (agent learning)
memory_situation = f"{market}:{symbol} auto-verified (analysis_date: {analysis_date})"
memory_recommendation = f"Decision: {decision} (confidence {confidence}), reasoning: {(reasoning or '')[:120]}"
memory_result = f"Verification: {result_desc}; return={actual_return:.2f}% (initial {initial_price} -> final {current_price})"
trader_memory.add_memory(
memory_situation,
memory_recommendation,
memory_result,
actual_return,
metadata={
"market": market,
"symbol": symbol,
"timeframe": "1D",
"features": {
"source": "auto_verify",
"decision": decision,
"confidence": confidence,
"initial_price": initial_price,
"final_price": current_price,
"analysis_date": str(analysis_date),
"result_desc": result_desc,
"is_good_prediction": bool(is_good_prediction),
},
}
)
# 5. Update record status
cur.execute(
"""
UPDATE qd_reflection_records
SET status = 'COMPLETED', final_price = ?, actual_return = ?, check_result = ?
WHERE id = ?
""",
(current_price, actual_return, result_desc, record_id)
)
conn.commit()
logger.info(f"Verification completed {market}:{symbol}: {result_desc}")
except Exception as inner_e:
logger.error(f"Failed to process record {record_id}: {inner_e}")
# Optionally mark as failed to avoid repeated processing
# cur.execute("UPDATE qd_reflection_records SET status = 'FAILED' WHERE id = ?", (record_id,))
# conn.commit()
cur.close()
logger.info("Reflection verification cycle completed")
except Exception as e:
logger.error(f"执行验证周期失败: {e}")
logger.error(f"Failed to execute verification cycle: {e}")
def get_pending_count(self) -> int:
"""Get count of pending verification records."""
try:
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute("SELECT COUNT(*) as cnt FROM qd_reflection_records WHERE status = 'PENDING'")
count = cur.fetchone()['cnt']
cur.close()
return count
except Exception as e:
logger.error(f"Failed to get pending count: {e}")
return 0
def get_statistics(self) -> Dict[str, Any]:
"""Get reflection statistics."""
try:
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute("SELECT COUNT(*) as cnt FROM qd_reflection_records")
total = cur.fetchone()['cnt']
cur.execute("SELECT COUNT(*) as cnt FROM qd_reflection_records WHERE status = 'PENDING'")
pending = cur.fetchone()['cnt']
cur.execute("SELECT COUNT(*) as cnt FROM qd_reflection_records WHERE status = 'COMPLETED'")
completed = cur.fetchone()['cnt']
cur.execute(
"SELECT AVG(actual_return) as avg_ret FROM qd_reflection_records WHERE status = 'COMPLETED' AND actual_return IS NOT NULL"
)
avg_return = cur.fetchone()['avg_ret'] or 0
cur.close()
return {
'total_records': total,
'pending_records': pending,
'completed_records': completed,
'average_return': round(avg_return, 2)
}
except Exception as e:
logger.error(f"Failed to get statistics: {e}")
return {}