Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
This commit is contained in:
TIANHE
2025-12-30 21:02:38 +08:00
parent 58a1133c83
commit 50939212be
23 changed files with 910 additions and 99 deletions
@@ -125,7 +125,13 @@ class FundamentalAnalyst(BaseAgent):
# Memory
situation = f"{market}:{symbol} fundamental analysis"
memories = self.get_memories(situation, n_matches=2)
memory_meta = {
"market": market,
"symbol": symbol,
"timeframe": context.get("timeframe"),
"features": context.get("memory_features") or {},
}
memories = self.get_memories(situation, n_matches=None, metadata=memory_meta)
memory_prompt = self.format_memories_for_prompt(memories)
lang_instruction = self._get_language_instruction(language)
@@ -3,6 +3,7 @@
"""
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional, List
import os
from app.utils.logger import get_logger
logger = get_logger(__name__)
@@ -36,7 +37,7 @@ class BaseAgent(ABC):
"""
pass
def get_memories(self, situation: str, n_matches: int = 2) -> List[Dict[str, Any]]:
def get_memories(self, situation: str, n_matches: Optional[int] = None, metadata: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
"""
从记忆中检索相似情况
@@ -47,8 +48,17 @@ class BaseAgent(ABC):
Returns:
匹配的历史记录列表
"""
if n_matches is None:
try:
n_matches = int(os.getenv("AGENT_MEMORY_TOP_K", "5") or 5)
except Exception:
n_matches = 5
if self.memory:
return self.memory.get_memories(situation, n_matches=n_matches)
# New memory API supports metadata; older implementations will ignore extra args.
try:
return self.memory.get_memories(situation, n_matches=n_matches, metadata=metadata)
except TypeError:
return self.memory.get_memories(situation, n_matches=n_matches)
return []
def format_memories_for_prompt(self, memories: List[Dict[str, Any]]) -> str:
@@ -62,10 +72,25 @@ class BaseAgent(ABC):
格式化的字符串
"""
if not memories:
return "无历史经验可参考。"
formatted = "历史经验参考:\n"
return "No prior experience available."
lines = ["Prior experience (most relevant first):"]
for i, mem in enumerate(memories, 1):
formatted += f"{i}. {mem.get('recommendation', 'N/A')}\n"
return formatted
rec = mem.get("recommendation") or "N/A"
res = mem.get("result") or ""
ret = mem.get("returns")
created_at = mem.get("created_at")
# Keep created_at as-is (SQLite string), but include it for traceability.
meta_bits = []
if created_at:
meta_bits.append(f"at {created_at}")
if ret is not None and ret != "":
meta_bits.append(f"returns={ret}%")
meta_s = f" ({', '.join(meta_bits)})" if meta_bits else ""
if res:
lines.append(f"{i}. {rec}{meta_s}\n outcome: {res}")
else:
lines.append(f"{i}. {rec}{meta_s}")
return "\n".join(lines)
@@ -97,7 +97,7 @@ class AgentCoordinator:
self.neutral_analyst = NeutralAnalyst()
self.safe_analyst = SafeAnalyst()
def run_analysis(self, market: str, symbol: str, language: str = 'zh-CN', model: str = None) -> Dict[str, Any]:
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.
"""
@@ -111,9 +111,13 @@ class AgentCoordinator:
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)
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
@@ -127,6 +131,7 @@ class AgentCoordinator:
"fundamental_data": fundamental_data,
"company_data": company_data,
"news_data": news_data,
"indicators": indicators,
}
context = {
@@ -134,6 +139,14 @@ class AgentCoordinator:
"symbol": symbol,
"language": language,
"model": model,
"timeframe": tf,
# Compact structured features for memory retrieval.
"memory_features": {
"timeframe": tf,
"price": (current_price or {}).get("price"),
"changePercent": (current_price or {}).get("changePercent"),
"indicators": indicators,
},
"base_data": base_data
}
@@ -492,7 +505,18 @@ class AgentCoordinator:
# Update trader memory
if 'trader' in self.memories:
self.memories['trader'].add_memory(
situation, recommendation, result, returns
situation,
recommendation,
result,
returns,
metadata={
"market": market,
"symbol": symbol,
"timeframe": None,
"features": {
"source": "manual_reflect",
}
},
)
logger.info(f"Reflection completed: {market}:{symbol}, decision={decision}, returns={returns}")
@@ -0,0 +1,91 @@
"""
Lightweight embedding utilities for local-only deployments.
We intentionally avoid heavyweight ML deps (torch/sentence-transformers) and external services.
This module provides a deterministic "hashed embedding" (similar to feature hashing):
- Tokenize text
- Hash tokens into a fixed-size dense vector
- L2 normalize
It is not as semantically strong as modern transformer embeddings, but it enables:
- Vector storage in SQLite
- Cosine similarity retrieval
- Recency/return weighted ranking
"""
from __future__ import annotations
import math
import os
import re
import struct
import hashlib
from typing import List, Optional
_TOKEN_RE = re.compile(r"[A-Za-z0-9_]+", re.UNICODE)
def _tokenize(text: str) -> List[str]:
t = (text or "").lower()
return _TOKEN_RE.findall(t)
class EmbeddingService:
"""Deterministic local embedding service."""
def __init__(self, dim: Optional[int] = None):
self.dim = int(dim or os.getenv("AGENT_MEMORY_EMBEDDING_DIM", "256") or 256)
if self.dim <= 0:
self.dim = 256
def embed(self, text: str) -> List[float]:
"""
Return a dense, L2-normalized embedding vector.
"""
vec = [0.0] * self.dim
tokens = _tokenize(text)
if not tokens:
return vec
# Feature hashing with signed counts
for tok in tokens:
h = hashlib.blake2b(tok.encode("utf-8"), digest_size=8).digest()
# Use first 8 bytes as unsigned int
v = int.from_bytes(h, "little", signed=False)
idx = v % self.dim
sign = -1.0 if ((v >> 63) & 1) else 1.0
vec[idx] += sign
# L2 normalize
norm = math.sqrt(sum(x * x for x in vec)) or 1.0
return [x / norm for x in vec]
def to_bytes(self, vec: List[float]) -> bytes:
"""
Pack float vector into little-endian float32 bytes for SQLite BLOB storage.
"""
if not vec:
return b""
return struct.pack("<" + "f" * len(vec), *[float(x) for x in vec])
def from_bytes(self, blob: bytes) -> List[float]:
if not blob:
return []
n = len(blob) // 4
if n <= 0:
return []
return list(struct.unpack("<" + "f" * n, blob[: n * 4]))
def cosine_sim(a: List[float], b: List[float]) -> float:
"""
Cosine similarity for L2-normalized vectors.
If vectors are not normalized, this becomes a scaled dot product.
"""
if not a or not b:
return 0.0
n = min(len(a), len(b))
return float(sum(a[i] * b[i] for i in range(n)))
+180 -35
View File
@@ -1,15 +1,29 @@
"""
智能体记忆系统
使用 SQLite + 简单的文本相似度匹配
Agent memory system (local-only).
This module stores agent experiences in SQLite and retrieves relevant past cases
to inject into prompts (RAG-style). It does NOT finetune model weights.
Retrieval (configurable):
- Vector similarity via deterministic local embeddings (default)
- Fallback to difflib text similarity when embeddings are missing
Ranking combines:
- similarity
- recency decay (half-life)
- optional returns weight
"""
import sqlite3
import json
import os
import math
from typing import List, Dict, Any, Optional
from datetime import datetime
from datetime import datetime, timezone
import difflib
from app.utils.logger import get_logger
from .embedding import EmbeddingService, cosine_sim
logger = get_logger(__name__)
@@ -34,6 +48,13 @@ class AgentMemory:
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)
self.half_life_days = float(os.getenv("AGENT_MEMORY_HALF_LIFE_DAYS", "30") or 30)
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):
@@ -49,22 +70,91 @@ class AgentMemory:
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 add_memory(self, situation: str, recommendation: str, result: Optional[str] = None, returns: Optional[float] = None):
def _now_utc(self) -> datetime:
return datetime.now(timezone.utc)
def _parse_ts(self, ts_val: Any) -> Optional[datetime]:
if ts_val is None:
return None
if isinstance(ts_val, datetime):
return ts_val
s = str(ts_val)
try:
return datetime.fromisoformat(s.replace("Z", ""))
except Exception:
return None
def _recency_score(self, created_at: Any) -> float:
dt = self._parse_ts(created_at)
if not dt:
return 0.0
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
age_days = max(0.0, (self._now_utc() - dt).total_seconds() / 86400.0)
hl = max(0.1, float(self.half_life_days or 30.0))
return float(math.exp(-math.log(2.0) * (age_days / hl)))
def _returns_score(self, returns: Any) -> float:
try:
r = float(returns)
except Exception:
return 0.0
return float(math.tanh(r / 10.0))
def _build_embed_text(self, situation: str, recommendation: str, result: Optional[str], features_json: Optional[str]) -> str:
return "\n".join([
f"situation: {situation or ''}",
f"recommendation: {recommendation or ''}",
f"result: {result or ''}",
f"features: {features_json or ''}",
])
def add_memory(
self,
situation: str,
recommendation: str,
result: Optional[str] = None,
returns: Optional[float] = None,
metadata: Optional[Dict[str, Any]] = None,
):
"""
添加记忆
@@ -73,15 +163,32 @@ class AgentMemory:
recommendation: 建议/决策
result: 结果描述(可选)
returns: 收益(可选)
metadata: Optional structured metadata (market/symbol/timeframe/features...)
"""
try:
meta = metadata or {}
market = (meta.get("market") or "").strip() or None
symbol = (meta.get("symbol") or "").strip() or None
timeframe = (meta.get("timeframe") or "").strip() or None
features = meta.get("features") if isinstance(meta, dict) else None
try:
features_json = json.dumps(features, ensure_ascii=False) if features is not None else None
except Exception:
features_json = None
embedding_blob = None
if self.enable_vector:
text = self._build_embed_text(situation, recommendation, result, features_json)
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)
VALUES (?, ?, ?, ?)
''', (situation, recommendation, result, returns))
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()
@@ -89,7 +196,7 @@ class AgentMemory:
except Exception as e:
logger.error(f"添加记忆失败: {e}")
def get_memories(self, current_situation: str, n_matches: int = 2) -> List[Dict[str, Any]]:
def get_memories(self, current_situation: str, n_matches: int = 5, metadata: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
"""
检索相似记忆
@@ -106,11 +213,11 @@ class AgentMemory:
# 获取所有记忆
cursor.execute('''
SELECT id, situation, recommendation, result, returns, created_at
SELECT id, situation, recommendation, result, returns, created_at, market, symbol, timeframe, features_json, embedding
FROM memories
ORDER BY created_at DESC
LIMIT 100
''')
LIMIT ?
''', (int(self.candidate_limit),))
all_memories = cursor.fetchall()
conn.close()
@@ -118,33 +225,71 @@ class AgentMemory:
if not all_memories:
return []
# 计算相似度
scored_memories = []
for mem in all_memories:
mem_id, situation, recommendation, result, returns, created_at = mem
# 使用简单的文本相似度
similarity = difflib.SequenceMatcher(
None,
current_situation.lower(),
situation.lower()
).ratio()
scored_memories.append({
meta = metadata or {}
tf = (meta.get("timeframe") or "").strip()
features = meta.get("features") if isinstance(meta, dict) else None
try:
q_features_json = json.dumps(features, ensure_ascii=False) if features is not None else None
except Exception:
q_features_json = None
query_vec = []
if self.enable_vector:
query_text = self._build_embed_text(current_situation, "", "", q_features_json)
query_vec = self.embedder.embed(query_text)
ranked = []
for row in all_memories:
(
mem_id,
situation,
recommendation,
result,
returns,
created_at,
market,
symbol,
timeframe,
features_json,
embedding_blob,
) = row
sim = 0.0
if self.enable_vector and embedding_blob:
try:
mem_vec = self.embedder.from_bytes(embedding_blob)
sim = cosine_sim(query_vec, mem_vec)
except Exception:
sim = 0.0
else:
sim = difflib.SequenceMatcher(None, (current_situation or "").lower(), (situation or "").lower()).ratio()
rec = self._recency_score(created_at)
ret = self._returns_score(returns)
score = (self.w_sim * sim) + (self.w_recency * rec) + (self.w_returns * ret)
if tf and timeframe and str(timeframe).strip() != tf:
score -= 0.15
ranked.append({
'id': mem_id,
'matched_situation': situation,
'recommendation': recommendation,
'result': result,
'returns': returns,
'similarity_score': similarity,
'created_at': created_at
'created_at': created_at,
'market': market,
'symbol': symbol,
'timeframe': timeframe,
'features_json': features_json,
'score': float(score),
'sim': float(sim),
'recency': float(rec),
})
# 按相似度排序
scored_memories.sort(key=lambda x: x['similarity_score'], reverse=True)
# 返回前 n_matches 个
return scored_memories[:n_matches]
ranked.sort(key=lambda x: x.get('score', 0.0), reverse=True)
return ranked[: max(0, int(n_matches or 0))]
except Exception as e:
logger.error(f"检索记忆失败: {e}")
@@ -91,7 +91,7 @@ class ReflectionService:
conn.commit()
conn.close()
logger.info(f"已记录分析用于反思: {market}:{symbol}, 将在 {check_days} 天后验证")
logger.info(f"Recorded analysis for reflection: {market}:{symbol}, will verify after {check_days} day(s)")
except Exception as e:
logger.error(f"记录分析失败: {e}")
@@ -148,37 +148,52 @@ class ReflectionService:
if decision == "BUY":
if actual_return > 2.0:
result_desc = "准确:买入后价格上涨"
result_desc = "Correct: price rose after BUY"
is_good_prediction = True
elif actual_return < -2.0:
result_desc = "错误:买入后价格下跌"
result_desc = "Wrong: price fell after BUY"
else:
result_desc = "中性:价格波动不大"
result_desc = "Neutral: limited price movement"
elif decision == "SELL":
if actual_return < -2.0:
result_desc = "准确:卖出后价格下跌"
result_desc = "Correct: price fell after SELL"
is_good_prediction = True
elif actual_return > 2.0:
result_desc = "错误:卖出后价格上涨"
result_desc = "Wrong: price rose after SELL"
else:
result_desc = "中性:价格波动不大"
result_desc = "Neutral: limited price movement"
else: # HOLD
if -2.0 <= actual_return <= 2.0:
result_desc = "准确:持有期间波动不大"
result_desc = "Correct: limited movement during HOLD"
is_good_prediction = True
else:
result_desc = f"偏差:持有期间出现了较大波动 ({actual_return:.2f}%)"
result_desc = f"Deviated: large movement during HOLD ({actual_return:.2f}%)"
# 4. 写入记忆系统 (Let the agent learn)
memory_situation = f"{market}:{symbol} 自动验证 (预测日期: {analysis_date})"
memory_recommendation = f"当时决策: {decision} (置信度 {confidence}), 理由: {reasoning[:50]}..."
memory_result = f"验证结果: {result_desc}, 实际收益: {actual_return:.2f}% (初始 {initial_price} -> 最新 {current_price})"
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
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. 更新记录状态
@@ -0,0 +1,67 @@
"""
Background worker for automated reflection verification.
This replaces the need for an external cron job in local deployments.
It periodically runs ReflectionService.run_verification_cycle().
Controls (env):
- ENABLE_REFLECTION_WORKER=true/false (default: false)
- REFLECTION_WORKER_INTERVAL_SEC (default: 86400)
"""
from __future__ import annotations
import os
import time
import threading
from typing import Optional
from app.utils.logger import get_logger
from .reflection import ReflectionService
logger = get_logger(__name__)
class ReflectionWorker:
def __init__(self):
self._thread: Optional[threading.Thread] = None
self._stop = threading.Event()
def start(self):
if self._thread and self._thread.is_alive():
return
interval = int(os.getenv("REFLECTION_WORKER_INTERVAL_SEC", "86400") or 86400)
interval = max(60, interval) # at least 1 minute
def _run():
logger.info(f"Reflection worker started (interval={interval}s)")
svc = ReflectionService()
# Initial small delay to avoid fighting startup spikes
time.sleep(3)
while not self._stop.is_set():
try:
svc.run_verification_cycle()
except Exception as e:
logger.error(f"Reflection worker cycle failed: {e}")
# Sleep in small steps to react to stop quickly
remaining = interval
while remaining > 0 and not self._stop.is_set():
step = min(5, remaining)
time.sleep(step)
remaining -= step
logger.info("Reflection worker stopped")
self._thread = threading.Thread(target=_run, name="ReflectionWorker", daemon=True)
self._thread.start()
def stop(self):
self._stop.set()
t = self._thread
if t and t.is_alive():
try:
t.join(timeout=5)
except Exception:
pass
@@ -32,7 +32,13 @@ class BullResearcher(BaseAgent):
# Memory
situation = f"{market}:{symbol} bull case"
memories = self.get_memories(situation, n_matches=2)
memory_meta = {
"market": market,
"symbol": symbol,
"timeframe": context.get("timeframe"),
"features": context.get("memory_features") or {},
}
memories = self.get_memories(situation, n_matches=None, metadata=memory_meta)
memory_prompt = self.format_memories_for_prompt(memories)
lang_instruction = self._get_language_instruction(language)
@@ -121,7 +127,13 @@ class BearResearcher(BaseAgent):
# Memory
situation = f"{market}:{symbol} bear case"
memories = self.get_memories(situation, n_matches=2)
memory_meta = {
"market": market,
"symbol": symbol,
"timeframe": context.get("timeframe"),
"features": context.get("memory_features") or {},
}
memories = self.get_memories(situation, n_matches=None, metadata=memory_meta)
memory_prompt = self.format_memories_for_prompt(memories)
lang_instruction = self._get_language_instruction(language)
@@ -82,27 +82,45 @@ class AgentTools:
"""Whether akshare is available at runtime."""
return bool(self._has_akshare and self._ak is not None)
def get_stock_data(self, market: str, symbol: str, days: int = 30) -> Optional[List[Dict[str, Any]]]:
def get_stock_data(self, market: str, symbol: str, days: int = 30, timeframe: str = "1d") -> Optional[List[Dict[str, Any]]]:
"""
Get daily Kline data for recent days (best-effort).
Args:
market: Market
symbol: Symbol
days: Days
days: Days (for daily) / candle count hint (best-effort for intraday)
timeframe: Kline timeframe (best-effort). Common values: 1d, 1h, 4h, 1w
Returns:
List of OHLCV dicts or None
"""
try:
klines = []
tf = (timeframe or "1d").strip().lower()
# Normalize common UI values
tf_map = {
"1d": "1d",
"1day": "1d",
"d": "1d",
"1h": "1h",
"60m": "1h",
"4h": "4h",
"240m": "4h",
"1w": "1wk",
"1wk": "1wk",
"w": "1wk",
}
tf_yf = tf_map.get(tf, "1d")
if market == 'USStock':
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=days + 5)).strftime('%Y-%m-%d')
ticker = yf.Ticker(symbol)
df = ticker.history(start=start_date, end=end_date, interval="1d")
# yfinance supports limited intervals. Fallback to 1d when unsupported.
interval = tf_yf if tf_yf in ["1d", "1h", "1wk"] else "1d"
df = ticker.history(start=start_date, end=end_date, interval=interval)
if not df.empty:
df = df.tail(days).reset_index()
@@ -121,7 +139,9 @@ class AgentTools:
exchange = self._ccxt_exchange()
symbol_pair = f'{symbol}/USDT'
start_time = int((datetime.now() - timedelta(days=days)).timestamp())
ohlcv = exchange.fetch_ohlcv(symbol_pair, '1d', since=start_time * 1000, limit=days)
# CCXT timeframes: 1d, 1h, 4h ...
ccxt_tf = tf if tf in ["1d", "1h", "4h"] else "1d"
ohlcv = exchange.fetch_ohlcv(symbol_pair, ccxt_tf, since=start_time * 1000, limit=days)
if ohlcv:
for candle in ohlcv:
klines.append({
@@ -175,7 +195,8 @@ class AgentTools:
start_date = (datetime.now() - timedelta(days=days + 5)).strftime('%Y-%m-%d')
ticker = yf.Ticker(yf_symbol)
df = ticker.history(start=start_date, end=end_date, interval="1d")
interval = tf_yf if tf_yf in ["1d", "1h", "1wk"] else "1d"
df = ticker.history(start=start_date, end=end_date, interval=interval)
if not df.empty:
df = df.tail(days).reset_index()
@@ -38,7 +38,13 @@ class TraderAgent(BaseAgent):
# Memory
situation = f"{market}:{symbol} trading decision"
memories = self.get_memories(situation, n_matches=2)
memory_meta = {
"market": market,
"symbol": symbol,
"timeframe": context.get("timeframe"),
"features": context.get("memory_features") or {},
}
memories = self.get_memories(situation, n_matches=None, metadata=memory_meta)
memory_prompt = self.format_memories_for_prompt(memories)
lang_instruction = self._get_language_instruction(language)
+5 -3
View File
@@ -37,8 +37,10 @@ class AnalysisService:
# Lazy import to avoid circular imports
from app.services.agents.coordinator import AgentCoordinator
import os
enable_memory = os.getenv("ENABLE_AGENT_MEMORY", "true").lower() == "true"
self.coordinator = AgentCoordinator(
enable_memory=True,
enable_memory=enable_memory,
max_debate_rounds=2
)
logger.info("Multi-agent coordinator initialized")
@@ -50,7 +52,7 @@ class AnalysisService:
finally:
AnalysisService._initializing = False
def analyze(self, market: str, symbol: str, language: str = 'en-US', model: str = None) -> Dict[str, Any]:
def analyze(self, market: str, symbol: str, language: str = 'en-US', model: str = None, timeframe: str = "1D") -> Dict[str, Any]:
"""
Args:
market: Market (AShare, USStock, HShare, Crypto, Forex, Futures)
@@ -79,7 +81,7 @@ class AnalysisService:
try:
logger.info(f"Run coordinator: {market}:{symbol}")
agent_result = self.coordinator.run_analysis(market, symbol, language, model=model)
agent_result = self.coordinator.run_analysis(market, symbol, language, model=model, timeframe=timeframe)
logger.info(f"Coordinator result keys: {list(agent_result.keys())}")