@@ -80,10 +80,23 @@ Default address: `http://localhost:5000`
|
||||
|
||||
## Database (SQLite)
|
||||
|
||||
- Default file: `backend_api_python/quantdinger.db` (override via `SQLITE_DATABASE_FILE`)
|
||||
- Default file: `backend_api_python/data/quantdinger.db` (override via `SQLITE_DATABASE_FILE`)
|
||||
- Tables are created/updated automatically on startup (see `app/utils/db.py`)
|
||||
- `qd_addon_config` exists for backward compatibility, but **this backend reads secrets from `.env` / OS env**, not from the database (see `app/utils/config_loader.py`)
|
||||
|
||||
## AI memory augmentation (local-only)
|
||||
|
||||
This backend includes a lightweight, privacy-first **memory-augmented multi-agent** system:
|
||||
|
||||
- Memory DBs (per role): `backend_api_python/data/memory/*_memory.db`
|
||||
- Reflection DB (optional auto-verify loop): `backend_api_python/data/memory/reflection_records.db`
|
||||
- API hooks:
|
||||
- `POST /api/analysis/multi` (main entry)
|
||||
- `POST /api/analysis/reflect` (manual learn from post-trade outcomes)
|
||||
- Controls: see `.env` / `env.example`:
|
||||
- `ENABLE_AGENT_MEMORY`, `AGENT_MEMORY_*`
|
||||
- `ENABLE_REFLECTION_WORKER`, `REFLECTION_WORKER_INTERVAL_SEC`
|
||||
|
||||
## Frontend integration (Vue dev server)
|
||||
|
||||
The Vue dev server proxies `/api/*` to this backend by default:
|
||||
|
||||
@@ -13,6 +13,7 @@ logger = get_logger(__name__)
|
||||
# Global singletons (avoid duplicate strategy threads).
|
||||
_trading_executor = None
|
||||
_pending_order_worker = None
|
||||
_reflection_worker = None
|
||||
|
||||
|
||||
def get_trading_executor():
|
||||
@@ -33,6 +34,39 @@ def get_pending_order_worker():
|
||||
return _pending_order_worker
|
||||
|
||||
|
||||
def get_reflection_worker():
|
||||
"""Get the reflection verification worker singleton."""
|
||||
global _reflection_worker
|
||||
if _reflection_worker is None:
|
||||
from app.services.agents.reflection_worker import ReflectionWorker
|
||||
_reflection_worker = ReflectionWorker()
|
||||
return _reflection_worker
|
||||
|
||||
|
||||
def start_reflection_worker():
|
||||
"""
|
||||
Start the reflection worker if enabled.
|
||||
|
||||
To enable it, set ENABLE_REFLECTION_WORKER=true.
|
||||
"""
|
||||
import os
|
||||
enabled = os.getenv("ENABLE_REFLECTION_WORKER", "false").lower() == "true"
|
||||
if not enabled:
|
||||
logger.info("Reflection worker is disabled. Set ENABLE_REFLECTION_WORKER=true to enable.")
|
||||
return
|
||||
|
||||
# Avoid running twice with Flask reloader
|
||||
debug = os.getenv("PYTHON_API_DEBUG", "false").lower() == "true"
|
||||
if debug:
|
||||
if os.environ.get("WERKZEUG_RUN_MAIN") != "true":
|
||||
return
|
||||
|
||||
try:
|
||||
get_reflection_worker().start()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start reflection worker: {e}")
|
||||
|
||||
|
||||
def start_pending_order_worker():
|
||||
"""Start the pending order worker (disabled by default in paper mode).
|
||||
|
||||
@@ -128,6 +162,7 @@ def create_app(config_name='default'):
|
||||
# Startup hooks.
|
||||
with app.app_context():
|
||||
start_pending_order_worker()
|
||||
start_reflection_worker()
|
||||
restore_running_strategies()
|
||||
|
||||
return app
|
||||
|
||||
@@ -51,9 +51,9 @@ class MetaSQLiteConfig(type):
|
||||
|
||||
@property
|
||||
def DATABASE_FILE(cls):
|
||||
# 默认放在 backend_api_python 根目录
|
||||
# 默认放在 backend_api_python/data 目录(更干净,也与 docker-compose 挂载一致)
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
default_path = os.path.join(base_dir, 'quantdinger.db')
|
||||
default_path = os.path.join(base_dir, 'data', 'quantdinger.db')
|
||||
return os.getenv('SQLITE_DATABASE_FILE', default_path)
|
||||
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ def multi_analysis():
|
||||
language = detect_request_language(request, body=data, default='en-US')
|
||||
model = data.get('model', None)
|
||||
use_multi_agent = data.get('use_multi_agent', None) # None -> use backend default
|
||||
timeframe = data.get('timeframe', '1D')
|
||||
|
||||
if not symbol or not market:
|
||||
return jsonify({
|
||||
@@ -101,7 +102,7 @@ def multi_analysis():
|
||||
|
||||
# Create analysis service instance (local-only; no paid credits)
|
||||
service = AnalysisService(use_multi_agent=use_multi_agent)
|
||||
result = service.analyze(market, symbol, language, model=model)
|
||||
result = service.analyze(market, symbol, language, model=model, timeframe=timeframe)
|
||||
|
||||
# Persist as "completed" history (no paid credits in local mode).
|
||||
task_id = _store_task(market, symbol, model or '', language, 'completed', result=result, error_message='')
|
||||
@@ -247,13 +248,14 @@ def stream_analysis():
|
||||
symbol = data.get('symbol', '')
|
||||
language = detect_request_language(request, body=data, default='en-US')
|
||||
use_multi_agent = data.get('use_multi_agent', None)
|
||||
timeframe = data.get('timeframe', '1D')
|
||||
|
||||
def generate():
|
||||
try:
|
||||
yield f"data: {json.dumps({'status': 'started', 'message': 'Analysis started'})}\n\n"
|
||||
|
||||
service = AnalysisService(use_multi_agent=use_multi_agent)
|
||||
result = service.analyze(market, symbol, language)
|
||||
result = service.analyze(market, symbol, language, timeframe=timeframe)
|
||||
|
||||
yield f"data: {json.dumps({'status': 'completed', 'data': result})}\n\n"
|
||||
except Exception as e:
|
||||
|
||||
@@ -95,6 +95,27 @@ CONFIG_SCHEMA = {
|
||||
{'key': 'ENABLE_AI_ANALYSIS', 'label': '启用AI分析', 'type': 'boolean', 'default': 'True'},
|
||||
]
|
||||
},
|
||||
'agent_memory': {
|
||||
'title': '记忆/反思配置',
|
||||
'items': [
|
||||
{'key': 'ENABLE_AGENT_MEMORY', 'label': '启用Agent记忆', 'type': 'boolean', 'default': 'True'},
|
||||
{'key': 'AGENT_MEMORY_ENABLE_VECTOR', 'label': '启用向量检索(本地)', 'type': 'boolean', 'default': 'True'},
|
||||
{'key': 'AGENT_MEMORY_EMBEDDING_DIM', 'label': 'Embedding维度', 'type': 'number', 'default': '256'},
|
||||
{'key': 'AGENT_MEMORY_TOP_K', 'label': '召回数量TopK', 'type': 'number', 'default': '5'},
|
||||
{'key': 'AGENT_MEMORY_CANDIDATE_LIMIT', 'label': '候选窗口大小', 'type': 'number', 'default': '500'},
|
||||
{'key': 'AGENT_MEMORY_HALF_LIFE_DAYS', 'label': '时间衰减半衰期(天)', 'type': 'number', 'default': '30'},
|
||||
{'key': 'AGENT_MEMORY_W_SIM', 'label': '相似度权重', 'type': 'number', 'default': '0.75'},
|
||||
{'key': 'AGENT_MEMORY_W_RECENCY', 'label': '时间权重', 'type': 'number', 'default': '0.20'},
|
||||
{'key': 'AGENT_MEMORY_W_RETURNS', 'label': '收益权重', 'type': 'number', 'default': '0.05'},
|
||||
]
|
||||
},
|
||||
'reflection_worker': {
|
||||
'title': '自动反思验证Worker',
|
||||
'items': [
|
||||
{'key': 'ENABLE_REFLECTION_WORKER', 'label': '启用自动验证', 'type': 'boolean', 'default': 'False'},
|
||||
{'key': 'REFLECTION_WORKER_INTERVAL_SEC', 'label': '验证周期间隔(秒)', 'type': 'number', 'default': '86400'},
|
||||
]
|
||||
},
|
||||
'ai': {
|
||||
'title': 'AI/LLM配置',
|
||||
'items': [
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())}")
|
||||
|
||||
|
||||
@@ -12,8 +12,12 @@ from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 数据库文件默认路径(兼容旧行为:backend_api_python/quantdinger.db)
|
||||
# SQLite 主库路径解析
|
||||
#
|
||||
# 目标默认行为:把主库放到 `backend_api_python/data/quantdinger.db`
|
||||
# 兼容旧行为:老版本会在 `backend_api_python/quantdinger.db` 建库
|
||||
_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
_DEFAULT_DB_FILE = os.path.join(_BASE_DIR, 'data', 'quantdinger.db')
|
||||
_LEGACY_DB_FILE = os.path.join(_BASE_DIR, 'quantdinger.db')
|
||||
|
||||
|
||||
@@ -23,13 +27,13 @@ def _get_db_file() -> str:
|
||||
|
||||
Priority:
|
||||
- SQLITE_DATABASE_FILE env (Docker 推荐:/app/data/quantdinger.db)
|
||||
- legacy default: backend_api_python/quantdinger.db
|
||||
- default: backend_api_python/data/quantdinger.db (local recommended)
|
||||
|
||||
Also performs a best-effort one-time migration:
|
||||
If the configured path doesn't exist but legacy db exists, copy legacy to configured path.
|
||||
If the configured/default path doesn't exist but legacy db exists, copy legacy to configured/default path.
|
||||
"""
|
||||
env_path = os.getenv('SQLITE_DATABASE_FILE')
|
||||
db_path = (env_path or '').strip() or _LEGACY_DB_FILE
|
||||
db_path = (env_path or '').strip() or _DEFAULT_DB_FILE
|
||||
|
||||
# Ensure parent dir exists
|
||||
parent = os.path.dirname(db_path)
|
||||
@@ -421,8 +425,9 @@ def _init_db_schema(conn):
|
||||
conn.commit()
|
||||
logger.info("Database schema initialized (SQLite)")
|
||||
|
||||
# 初始化一次
|
||||
# 初始化一次(按 db_file 维度)
|
||||
_has_initialized = False
|
||||
_initialized_db_file = None
|
||||
|
||||
class SQLiteCursor:
|
||||
"""模拟 pymysql DictCursor"""
|
||||
@@ -492,24 +497,24 @@ def get_db_connection():
|
||||
"""
|
||||
获取数据库连接 (Context Manager)
|
||||
"""
|
||||
global _has_initialized
|
||||
global _has_initialized, _initialized_db_file
|
||||
|
||||
# 简单的连接创建,不使用连接池(SQLite 文件锁机制决定了连接池意义不大)
|
||||
# 使用线程锁防止写冲突(虽然 SQLite 有 WAL 模式,但稳妥起见)
|
||||
# 注意:这里加锁粒度较大,如果是高并发场景可能会慢,但对于个人量化系统足够。
|
||||
|
||||
# 初始化表结构
|
||||
if not _has_initialized:
|
||||
# 初始化表结构(确保每个 db_file 都被初始化过)
|
||||
db_file = _get_db_file()
|
||||
if (not _has_initialized) or (_initialized_db_file != db_file):
|
||||
try:
|
||||
db_file = _get_db_file()
|
||||
conn_init = sqlite3.connect(db_file)
|
||||
_init_db_schema(conn_init)
|
||||
conn_init.close()
|
||||
_has_initialized = True
|
||||
_initialized_db_file = db_file
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize database: {e}")
|
||||
|
||||
db_file = _get_db_file()
|
||||
conn = SQLiteConnection(db_file)
|
||||
try:
|
||||
# with _db_lock: # SQLite 内部有锁,这里如果不跨线程共享连接其实不用强加锁
|
||||
@@ -523,18 +528,18 @@ def get_db_connection():
|
||||
|
||||
def get_db_connection_sync():
|
||||
"""兼容旧接口"""
|
||||
global _has_initialized
|
||||
if not _has_initialized:
|
||||
global _has_initialized, _initialized_db_file
|
||||
db_file = _get_db_file()
|
||||
if (not _has_initialized) or (_initialized_db_file != db_file):
|
||||
try:
|
||||
db_file = _get_db_file()
|
||||
conn_init = sqlite3.connect(db_file)
|
||||
_init_db_schema(conn_init)
|
||||
conn_init.close()
|
||||
_has_initialized = True
|
||||
_initialized_db_file = db_file
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize database: {e}")
|
||||
|
||||
db_file = _get_db_file()
|
||||
return SQLiteConnection(db_file)
|
||||
|
||||
def close_db_connection():
|
||||
|
||||
@@ -15,6 +15,14 @@ PYTHON_API_HOST=0.0.0.0
|
||||
PYTHON_API_PORT=5000
|
||||
PYTHON_API_DEBUG=False
|
||||
|
||||
# =========================
|
||||
# Database (SQLite)
|
||||
# =========================
|
||||
# 主库文件路径(可选)
|
||||
# - 不设置时,默认使用:backend_api_python/data/quantdinger.db
|
||||
# - Docker 推荐:/app/data/quantdinger.db
|
||||
SQLITE_DATABASE_FILE=
|
||||
|
||||
# =========================
|
||||
# Pending orders worker (optional)
|
||||
# =========================
|
||||
@@ -95,6 +103,26 @@ ENABLE_CACHE=False
|
||||
ENABLE_REQUEST_LOG=True
|
||||
ENABLE_AI_ANALYSIS=True
|
||||
|
||||
# =========================
|
||||
# Agent memory & reflection (optional)
|
||||
# =========================
|
||||
# Toggle agent memory usage in multi-agent analysis
|
||||
ENABLE_AGENT_MEMORY=true
|
||||
|
||||
# Memory retrieval settings
|
||||
AGENT_MEMORY_ENABLE_VECTOR=true
|
||||
AGENT_MEMORY_EMBEDDING_DIM=256
|
||||
AGENT_MEMORY_TOP_K=5
|
||||
AGENT_MEMORY_CANDIDATE_LIMIT=500
|
||||
AGENT_MEMORY_HALF_LIFE_DAYS=30
|
||||
AGENT_MEMORY_W_SIM=0.75
|
||||
AGENT_MEMORY_W_RECENCY=0.20
|
||||
AGENT_MEMORY_W_RETURNS=0.05
|
||||
|
||||
# Automated verification loop (replaces cron)
|
||||
ENABLE_REFLECTION_WORKER=false
|
||||
REFLECTION_WORKER_INTERVAL_SEC=86400
|
||||
|
||||
# =========================
|
||||
# OpenRouter / LLM
|
||||
# =========================
|
||||
|
||||
Reference in New Issue
Block a user