Refactor code for improved readability and consistency

- Cleaned up whitespace and formatting in various files including http.py, language.py, logger.py, safe_exec.py, and SQL migration scripts.
- Consolidated import statements and removed unnecessary blank lines.
- Updated logging configuration for better clarity.
- Enhanced the safe execution code with improved error handling and logging.
- Removed commented-out code and unnecessary variables in backfill_zero_trades.py and other scripts.
- Added a pyproject.toml for Ruff and Vulture configuration.
- Introduced requirements-dev.txt for development dependencies.
- Removed commented-out stock entries in init.sql for cleaner migration scripts.
This commit is contained in:
dienakdz
2026-04-09 14:30:51 +07:00
parent 103055b3df
commit 87f2845483
157 changed files with 19026 additions and 17773 deletions
+303 -238
View File
@@ -7,14 +7,12 @@ Features:
2. Retrieve similar historical patterns
3. Track decision outcomes for learning
"""
import json
import time
import hashlib
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta
from app.utils.logger import get_logger
import json
from typing import Any, Dict, List, Optional
from app.utils.db import get_db_connection
from app.utils.logger import get_logger
logger = get_logger(__name__)
@@ -38,16 +36,16 @@ class AnalysisMemory:
Simple but effective memory system for AI analysis.
Uses PostgreSQL for persistence.
"""
def __init__(self):
self._ensure_table()
def _ensure_table(self):
"""Create memory table if not exists, and add missing columns if needed."""
try:
with get_db_connection() as db:
cur = db.cursor()
# Create table if it does not exist
cur.execute("""
CREATE TABLE IF NOT EXISTS qd_analysis_memory (
@@ -79,50 +77,50 @@ class AnalysisMemory:
feedback_at TIMESTAMP
);
""")
# Check and add missing columns (for existing tables)
cur.execute("""
DO $$
BEGIN
-- Add the user_id column if it does not exist
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_analysis_memory' AND column_name = 'user_id'
) THEN
ALTER TABLE qd_analysis_memory ADD COLUMN user_id INT;
END IF;
-- Add the raw_result column if it does not exist
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_analysis_memory' AND column_name = 'raw_result'
) THEN
ALTER TABLE qd_analysis_memory ADD COLUMN raw_result JSONB;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_analysis_memory' AND column_name = 'consensus_score'
) THEN
ALTER TABLE qd_analysis_memory ADD COLUMN consensus_score DECIMAL(24, 8);
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_analysis_memory' AND column_name = 'consensus_abs'
) THEN
ALTER TABLE qd_analysis_memory ADD COLUMN consensus_abs DECIMAL(24, 8);
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_analysis_memory' AND column_name = 'agreement_ratio'
) THEN
ALTER TABLE qd_analysis_memory ADD COLUMN agreement_ratio DECIMAL(10, 6);
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_analysis_memory' AND column_name = 'quality_multiplier'
) THEN
ALTER TABLE qd_analysis_memory ADD COLUMN quality_multiplier DECIMAL(10, 6);
@@ -150,43 +148,43 @@ class AnalysisMemory:
END IF;
END $$;
""")
#Create index
# Create index
cur.execute("""
CREATE INDEX IF NOT EXISTS idx_analysis_memory_symbol
CREATE INDEX IF NOT EXISTS idx_analysis_memory_symbol
ON qd_analysis_memory(market, symbol);
CREATE INDEX IF NOT EXISTS idx_analysis_memory_created
CREATE INDEX IF NOT EXISTS idx_analysis_memory_created
ON qd_analysis_memory(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_analysis_memory_validated
CREATE INDEX IF NOT EXISTS idx_analysis_memory_validated
ON qd_analysis_memory(validated_at) WHERE validated_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_analysis_memory_user
ON qd_analysis_memory(user_id);
""")
db.commit()
cur.close()
logger.debug("Analysis memory table ensured successfully")
except Exception as e:
logger.warning(f"Memory table creation/update skipped: {e}")
def store(self, analysis_result: Dict[str, Any], user_id: int = None) -> Optional[int]:
"""
Store an analysis result for future reference.
Args:
analysis_result: Result from FastAnalysisService.analyze()
user_id: User ID who created this analysis
Returns:
Memory ID or None if failed
"""
try:
with get_db_connection() as db:
cur = db.cursor()
# Prepare data
market = analysis_result.get("market")
symbol = analysis_result.get("symbol")
@@ -204,8 +202,9 @@ class AnalysisMemory:
consensus_abs = consensus.get("consensus_abs")
agreement_ratio = consensus.get("agreement_ratio")
quality_multiplier = consensus.get("quality_multiplier")
cur.execute("""
cur.execute(
"""
INSERT INTO qd_analysis_memory (
user_id, market, symbol, decision, confidence,
price_at_analysis, summary, reasons, scores, indicators_snapshot, raw_result,
@@ -214,43 +213,59 @@ class AnalysisMemory:
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
%s, %s, %s, %s, %s, %s, NOW())
RETURNING id
""", (
user_id, market, symbol, decision, confidence,
price, summary, reasons, scores, indicators, raw,
consensus_score, consensus_abs, agreement_ratio, quality_multiplier,
"completed", "",
))
""",
(
user_id,
market,
symbol,
decision,
confidence,
price,
summary,
reasons,
scores,
indicators,
raw,
consensus_score,
consensus_abs,
agreement_ratio,
quality_multiplier,
"completed",
"",
),
)
# Use the lastrowid attribute to get the ID (execute has already processed RETURNING internally)
memory_id = cur.lastrowid
db.commit()
cur.close()
logger.info(f"Stored analysis memory #{memory_id} for {symbol} by user {user_id}")
return memory_id
except Exception as e:
logger.error(f"Failed to store analysis memory: {e}", exc_info=True)
return None
def get_recent(self, market: str, symbol: str, days: int = 7, limit: int = 5) -> List[Dict]:
"""
Get recent analysis history for a symbol.
Args:
market: Market type
symbol: Symbol
days: Look back period
limit: Max results
Returns:
List of historical analyses
"""
try:
with get_db_connection() as db:
cur = db.cursor()
cur.execute(f"""
SELECT
cur.execute(
f"""
SELECT
id, decision, confidence, price_at_analysis,
summary, reasons, scores,
created_at, validated_at, was_correct, actual_return_pct,
@@ -260,31 +275,35 @@ class AnalysisMemory:
AND created_at > NOW() - INTERVAL '{int(days)} days'
ORDER BY created_at DESC
LIMIT %s
""", (market, symbol, limit))
""",
(market, symbol, limit),
)
rows = cur.fetchall() or []
cur.close()
results = []
for row in rows:
results.append({
"id": row['id'],
"decision": row['decision'],
"confidence": row['confidence'],
"price": float(row['price_at_analysis']) if row['price_at_analysis'] else None,
"summary": row['summary'],
"reasons": _safe_json_parse(row['reasons'], []),
"scores": _safe_json_parse(row['scores'], {}),
"status": row.get('task_status') or 'completed',
"error_message": row.get('task_error') or '',
"created_at": row['created_at'].isoformat() if row['created_at'] else None,
"updated_at": row['updated_at'].isoformat() if row.get('updated_at') else None,
"was_correct": row['was_correct'],
"actual_return_pct": float(row['actual_return_pct']) if row['actual_return_pct'] else None,
})
results.append(
{
"id": row["id"],
"decision": row["decision"],
"confidence": row["confidence"],
"price": float(row["price_at_analysis"]) if row["price_at_analysis"] else None,
"summary": row["summary"],
"reasons": _safe_json_parse(row["reasons"], []),
"scores": _safe_json_parse(row["scores"], {}),
"status": row.get("task_status") or "completed",
"error_message": row.get("task_error") or "",
"created_at": row["created_at"].isoformat() if row["created_at"] else None,
"updated_at": row["updated_at"].isoformat() if row.get("updated_at") else None,
"was_correct": row["was_correct"],
"actual_return_pct": float(row["actual_return_pct"]) if row["actual_return_pct"] else None,
}
)
return results
except Exception as e:
logger.error(f"Failed to get recent memories: {e}")
return []
@@ -292,34 +311,35 @@ class AnalysisMemory:
def get_all_history(self, user_id: int = None, page: int = 1, page_size: int = 20) -> Dict:
"""
Get all analysis history with pagination.
Args:
user_id: User ID filter (required to show only user's own history)
page: Page number (1-indexed)
page_size: Items per page
Returns:
Dict with items list and total count
"""
try:
offset = (page - 1) * page_size
with get_db_connection() as db:
cur = db.cursor()
# Build WHERE clause based on user_id
where_clause = "WHERE user_id = %s" if user_id else ""
params_count = (user_id,) if user_id else ()
# Get total count
cur.execute(f"SELECT COUNT(*) as cnt FROM qd_analysis_memory {where_clause}", params_count)
total_row = cur.fetchone()
total = total_row['cnt'] if total_row else 0
total = total_row["cnt"] if total_row else 0
# Get paginated results
params = (user_id, page_size, offset) if user_id else (page_size, offset)
cur.execute(f"""
SELECT
cur.execute(
f"""
SELECT
id, market, symbol, decision, confidence, price_at_analysis,
summary, reasons, scores, indicators_snapshot, raw_result,
created_at, validated_at, was_correct, actual_return_pct,
@@ -328,40 +348,39 @@ class AnalysisMemory:
{where_clause}
ORDER BY created_at DESC
LIMIT %s OFFSET %s
""", params)
""",
params,
)
rows = cur.fetchall() or []
cur.close()
items = []
for row in rows:
items.append({
"id": row['id'],
"market": row['market'],
"symbol": row['symbol'],
"decision": row['decision'],
"confidence": row['confidence'],
"price": float(row['price_at_analysis']) if row['price_at_analysis'] else None,
"summary": row['summary'],
"reasons": _safe_json_parse(row['reasons'], []),
"scores": _safe_json_parse(row['scores'], {}),
"indicators": _safe_json_parse(row['indicators_snapshot'], {}),
"full_result": _safe_json_parse(row['raw_result'], None),
"status": row.get('task_status') or 'completed',
"error_message": row.get('task_error') or '',
"created_at": row['created_at'].isoformat() if row['created_at'] else None,
"updated_at": row['updated_at'].isoformat() if row.get('updated_at') else None,
"was_correct": row['was_correct'],
"actual_return_pct": float(row['actual_return_pct']) if row['actual_return_pct'] else None,
})
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size
}
items.append(
{
"id": row["id"],
"market": row["market"],
"symbol": row["symbol"],
"decision": row["decision"],
"confidence": row["confidence"],
"price": float(row["price_at_analysis"]) if row["price_at_analysis"] else None,
"summary": row["summary"],
"reasons": _safe_json_parse(row["reasons"], []),
"scores": _safe_json_parse(row["scores"], {}),
"indicators": _safe_json_parse(row["indicators_snapshot"], {}),
"full_result": _safe_json_parse(row["raw_result"], None),
"status": row.get("task_status") or "completed",
"error_message": row.get("task_error") or "",
"created_at": row["created_at"].isoformat() if row["created_at"] else None,
"updated_at": row["updated_at"].isoformat() if row.get("updated_at") else None,
"was_correct": row["was_correct"],
"actual_return_pct": float(row["actual_return_pct"]) if row["actual_return_pct"] else None,
}
)
return {"items": items, "total": total, "page": page, "page_size": page_size}
except Exception as e:
logger.error(f"Failed to get all history: {e}")
return {"items": [], "total": 0, "page": page, "page_size": page_size}
@@ -369,11 +388,11 @@ class AnalysisMemory:
def delete_history(self, memory_id: int, user_id: int = None) -> bool:
"""
Delete a history record by ID.
Args:
memory_id: The ID of the analysis memory to delete
user_id: User ID to ensure user can only delete their own records
Returns:
True if deleted successfully, False otherwise
"""
@@ -393,8 +412,9 @@ class AnalysisMemory:
logger.error(f"Failed to delete memory {memory_id}: {e}")
return False
def create_pending_task(self, market: str, symbol: str, language: str, model: str, timeframe: str,
user_id: int = None) -> Optional[int]:
def create_pending_task(
self, market: str, symbol: str, language: str, model: str, timeframe: str, user_id: int = None
) -> Optional[int]:
"""Create a processing record in history before long-running analysis starts."""
try:
with get_db_connection() as db:
@@ -403,15 +423,18 @@ class AnalysisMemory:
reasons = json.dumps([])
scores = json.dumps({})
indicators = json.dumps({})
raw = json.dumps({
"market": market,
"symbol": symbol,
"language": language,
"model": model,
"timeframe": timeframe,
"task_status": "processing",
})
cur.execute("""
raw = json.dumps(
{
"market": market,
"symbol": symbol,
"language": language,
"model": model,
"timeframe": timeframe,
"task_status": "processing",
}
)
cur.execute(
"""
INSERT INTO qd_analysis_memory (
user_id, market, symbol, decision, confidence,
summary, reasons, scores, indicators_snapshot, raw_result,
@@ -420,11 +443,22 @@ class AnalysisMemory:
%s, %s, %s, %s, %s,
%s, %s, NOW(), NOW())
RETURNING id
""", (
user_id, market, symbol, "HOLD", 0,
summary, reasons, scores, indicators, raw,
"processing", "",
))
""",
(
user_id,
market,
symbol,
"HOLD",
0,
summary,
reasons,
scores,
indicators,
raw,
"processing",
"",
),
)
# PostgresCursor.execute() will consume the RETURNING result in advance from fetchone() during INSERT.
# So dont use cur.fetchone() here, just get lastrowid.
memory_id = cur.lastrowid
@@ -441,7 +475,8 @@ class AnalysisMemory:
consensus = result.get("consensus") or {}
with get_db_connection() as db:
cur = db.cursor()
cur.execute("""
cur.execute(
"""
UPDATE qd_analysis_memory
SET decision = %s,
confidence = %s,
@@ -459,23 +494,25 @@ class AnalysisMemory:
task_error = %s,
updated_at = NOW()
WHERE id = %s
""", (
result.get("decision"),
result.get("confidence"),
result.get("market_data", {}).get("current_price"),
result.get("summary"),
json.dumps(result.get("reasons", [])),
json.dumps(result.get("scores", {})),
json.dumps(result.get("indicators", {})),
json.dumps(result),
consensus.get("consensus_score"),
consensus.get("consensus_abs"),
consensus.get("agreement_ratio"),
consensus.get("quality_multiplier"),
"completed" if not result.get("error") else "failed",
str(result.get("error") or ""),
int(memory_id),
))
""",
(
result.get("decision"),
result.get("confidence"),
result.get("market_data", {}).get("current_price"),
result.get("summary"),
json.dumps(result.get("reasons", [])),
json.dumps(result.get("scores", {})),
json.dumps(result.get("indicators", {})),
json.dumps(result),
consensus.get("consensus_score"),
consensus.get("consensus_abs"),
consensus.get("agreement_ratio"),
consensus.get("quality_multiplier"),
"completed" if not result.get("error") else "failed",
str(result.get("error") or ""),
int(memory_id),
),
)
ok = cur.rowcount > 0
db.commit()
cur.close()
@@ -489,18 +526,21 @@ class AnalysisMemory:
try:
with get_db_connection() as db:
cur = db.cursor()
cur.execute("""
cur.execute(
"""
UPDATE qd_analysis_memory
SET task_status = 'failed',
task_error = %s,
summary = %s,
updated_at = NOW()
WHERE id = %s
""", (
str(error_message or "analysis failed"),
f"Analysis failed: {str(error_message or '')}",
int(memory_id),
))
""",
(
str(error_message or "analysis failed"),
f"Analysis failed: {str(error_message or '')}",
int(memory_id),
),
)
ok = cur.rowcount > 0
db.commit()
cur.close()
@@ -508,12 +548,11 @@ class AnalysisMemory:
except Exception as e:
logger.error(f"Failed to mark task failed {memory_id}: {e}")
return False
def get_similar_patterns(self, market: str, symbol: str,
current_indicators: Dict, limit: int = 3) -> List[Dict]:
def get_similar_patterns(self, market: str, symbol: str, current_indicators: Dict, limit: int = 3) -> List[Dict]:
"""
Find historical analyses with similar technical patterns.
Multi-indicator weighted similarity:
- RSI: ±15 range, weighted 0.3
- MACD signal: exact match, weighted 0.3
@@ -526,11 +565,12 @@ class AnalysisMemory:
macd_signal = str(current_indicators.get("macd", {}).get("signal") or "neutral").lower()
ma_trend = str(current_indicators.get("moving_averages", {}).get("trend") or "sideways").lower()
vol_level = str(current_indicators.get("volatility", {}).get("level") or "normal").lower()
with get_db_connection() as db:
cur = db.cursor()
cur.execute("""
SELECT
cur.execute(
"""
SELECT
id, decision, confidence, price_at_analysis,
summary, reasons, indicators_snapshot,
created_at, was_correct, actual_return_pct
@@ -540,44 +580,55 @@ class AnalysisMemory:
AND was_correct IS NOT NULL
ORDER BY validated_at DESC NULLS LAST, created_at DESC
LIMIT %s
""", (market, symbol, limit * 5))
""",
(market, symbol, limit * 5),
)
rows = cur.fetchall() or []
cur.close()
scored = []
for row in rows:
ind = _safe_json_parse(row['indicators_snapshot'], {})
ind = _safe_json_parse(row["indicators_snapshot"], {})
hist_rsi = float(ind.get("rsi", {}).get("value") or 50)
hist_macd = str(ind.get("macd", {}).get("signal") or "neutral").lower()
hist_ma = str(ind.get("moving_averages", {}).get("trend") or "sideways").lower()
hist_vol = str(ind.get("volatility", {}).get("level") or "normal").lower()
rsi_diff = abs(hist_rsi - rsi)
rsi_score = max(0, 1 - rsi_diff / 30) * 0.3
macd_score = 0.3 if hist_macd == macd_signal else 0
ma_score = 0.25 if hist_ma == ma_trend else 0
vol_score = 0.15 if hist_vol == vol_level else (0.08 if _vol_bands_similar(vol_level, hist_vol) else 0)
vol_score = (
0.15 if hist_vol == vol_level else (0.08 if _vol_bands_similar(vol_level, hist_vol) else 0)
)
sim = rsi_score + macd_score + ma_score + vol_score
if sim < 0.25:
continue
bonus = 0.1 if row['was_correct'] else 0
scored.append((sim + bonus, {
"id": row['id'],
"decision": row['decision'],
"confidence": row['confidence'],
"price": float(row['price_at_analysis']) if row['price_at_analysis'] else None,
"summary": row['summary'],
"was_correct": row['was_correct'],
"actual_return_pct": float(row['actual_return_pct']) if row['actual_return_pct'] else None,
"similarity_score": round(sim + bonus, 3),
}))
bonus = 0.1 if row["was_correct"] else 0
scored.append(
(
sim + bonus,
{
"id": row["id"],
"decision": row["decision"],
"confidence": row["confidence"],
"price": float(row["price_at_analysis"]) if row["price_at_analysis"] else None,
"summary": row["summary"],
"was_correct": row["was_correct"],
"actual_return_pct": float(row["actual_return_pct"])
if row["actual_return_pct"]
else None,
"similarity_score": round(sim + bonus, 3),
},
)
)
scored.sort(key=lambda x: -x[0])
return [p[1] for p in scored[:limit]]
except Exception as e:
logger.error(f"Failed to get similar patterns: {e}")
return []
@@ -585,7 +636,7 @@ class AnalysisMemory:
def record_feedback(self, memory_id: int, feedback: str) -> bool:
"""
Record user feedback on an analysis.
Args:
memory_id: Analysis memory ID
feedback: 'helpful' | 'not_helpful' | 'accurate' | 'inaccurate'
@@ -593,43 +644,47 @@ class AnalysisMemory:
try:
with get_db_connection() as db:
cur = db.cursor()
cur.execute("""
cur.execute(
"""
UPDATE qd_analysis_memory
SET user_feedback = %s, feedback_at = NOW()
WHERE id = %s
""", (feedback, memory_id))
""",
(feedback, memory_id),
)
db.commit()
cur.close()
return True
except Exception as e:
logger.error(f"Failed to record feedback: {e}")
return False
def validate_past_decisions(self, days_ago: int = 7) -> Dict[str, Any]:
"""
Validate historical decisions by comparing with actual price movements.
Run this periodically (e.g., daily) to build learning data.
Args:
days_ago: Validate decisions from N days ago
Returns:
Validation statistics
"""
from app.services.market_data_collector import MarketDataCollector
collector = MarketDataCollector()
stats = {
"validated": 0,
"correct": 0,
"incorrect": 0,
"errors": 0,
}
try:
with get_db_connection() as db:
cur = db.cursor()
# Get unvalidated decisions from N days ago
cur.execute(f"""
SELECT id, market, symbol, decision, price_at_analysis
@@ -639,62 +694,65 @@ class AnalysisMemory:
AND created_at > NOW() - INTERVAL '{int(days_ago + 1)} days'
LIMIT 50
""")
rows = cur.fetchall() or []
for row in rows:
try:
price_data = collector._get_price(row['market'], row['symbol'])
current_price = float(price_data.get('price', 0)) if price_data else None
price_data = collector._get_price(row["market"], row["symbol"])
current_price = float(price_data.get("price", 0)) if price_data else None
if not current_price or current_price <= 0:
continue
analysis_price = float(row['price_at_analysis'])
analysis_price = float(row["price_at_analysis"])
if analysis_price <= 0:
continue
# Calculate return
return_pct = ((current_price - analysis_price) / analysis_price) * 100
# Determine if decision was correct
decision = row['decision']
decision = row["decision"]
was_correct = False
if decision == 'BUY' and return_pct > 2: # 2% threshold
if decision == "BUY" and return_pct > 2: # 2% threshold
was_correct = True
elif decision == 'SELL' and return_pct < -2:
elif decision == "SELL" and return_pct < -2:
was_correct = True
elif decision == 'HOLD' and abs(return_pct) <= 5:
elif decision == "HOLD" and abs(return_pct) <= 5:
was_correct = True
# Update record
cur.execute("""
cur.execute(
"""
UPDATE qd_analysis_memory
SET validated_at = NOW(),
actual_return_pct = %s,
was_correct = %s
WHERE id = %s
""", (return_pct, was_correct, row['id']))
""",
(return_pct, was_correct, row["id"]),
)
stats["validated"] += 1
if was_correct:
stats["correct"] += 1
else:
stats["incorrect"] += 1
except Exception as e:
logger.warning(f"Failed to validate memory {row['id']}: {e}")
stats["errors"] += 1
db.commit()
cur.close()
except Exception as e:
logger.error(f"Validation batch failed: {e}")
accuracy = (stats["correct"] / stats["validated"] * 100) if stats["validated"] > 0 else 0
stats["accuracy_pct"] = round(accuracy, 2)
logger.info(f"Validation completed: {stats}")
return stats
@@ -706,6 +764,7 @@ class AnalysisMemory:
This is used by offline AI calibration so the system can tune itself automatically.
"""
from app.services.market_data_collector import MarketDataCollector
collector = MarketDataCollector()
stats = {
@@ -776,7 +835,7 @@ class AnalysisMemory:
logger.error(f"validate_unvalidated_older_than failed: {e}", exc_info=True)
return stats
def get_confidence_accuracy_by_bucket(
self, market: str = None, symbol: str = None, days: int = 90
) -> Dict[str, float]:
@@ -798,11 +857,14 @@ class AnalysisMemory:
params.append(symbol)
where.append(f"created_at > NOW() - INTERVAL '{int(days)} days'")
params = tuple(params) if params else ()
cur.execute(f"""
cur.execute(
f"""
SELECT confidence, was_correct
FROM qd_analysis_memory
WHERE {' AND '.join(where)}
""", params)
WHERE {" AND ".join(where)}
""",
params,
)
rows = cur.fetchall() or []
cur.close()
@@ -819,9 +881,7 @@ class AnalysisMemory:
logger.warning(f"get_confidence_accuracy_by_bucket failed: {e}")
return {}
def get_adjusted_confidence(
self, raw_confidence: int, market: str = None, symbol: str = None
) -> int:
def get_adjusted_confidence(self, raw_confidence: int, market: str = None, symbol: str = None) -> int:
"""
Adjust confidence based on historical accuracy in that bucket.
If model is overconfident (low actual accuracy), dampen. Underconfident -> boost slightly.
@@ -845,35 +905,35 @@ class AnalysisMemory:
adjusted = int(raw_confidence * factor)
return max(1, min(99, adjusted))
def get_performance_stats(self, market: str = None, symbol: str = None,
days: int = 30) -> Dict[str, Any]:
def get_performance_stats(self, market: str = None, symbol: str = None, days: int = 30) -> Dict[str, Any]:
"""
Get AI performance statistics.
Returns:
Performance metrics for display
"""
try:
with get_db_connection() as db:
cur = db.cursor()
where_clauses = ["validated_at IS NOT NULL"]
params = []
if market:
where_clauses.append("market = %s")
params.append(market)
if symbol:
where_clauses.append("symbol = %s")
params.append(symbol)
# Use f-string for interval since psycopg2 doesn't support placeholder in INTERVAL
where_clauses.append(f"created_at > NOW() - INTERVAL '{int(days)} days'")
where_sql = " AND ".join(where_clauses)
cur.execute(f"""
SELECT
cur.execute(
f"""
SELECT
COUNT(*) as total,
SUM(CASE WHEN was_correct = true THEN 1 ELSE 0 END) as correct,
AVG(actual_return_pct) as avg_return,
@@ -884,38 +944,42 @@ class AnalysisMemory:
SUM(CASE WHEN user_feedback IS NOT NULL THEN 1 ELSE 0 END) as feedback_count
FROM qd_analysis_memory
WHERE {where_sql}
""", tuple(params) if params else None)
""",
tuple(params) if params else None,
)
row = cur.fetchone()
cur.close()
if not row or not row['total']:
if not row or not row["total"]:
return {
"total_analyses": 0,
"accuracy_pct": 0,
"avg_return_pct": 0,
"user_satisfaction_pct": 0,
}
total = row['total']
correct = row['correct'] or 0
total = row["total"]
correct = row["correct"] or 0
return {
"total_analyses": total,
"accuracy_pct": round((correct / total * 100) if total > 0 else 0, 2),
"avg_return_pct": round(float(row['avg_return'] or 0), 2),
"avg_return_pct": round(float(row["avg_return"] or 0), 2),
"decision_distribution": {
"buy": row['buy_count'] or 0,
"sell": row['sell_count'] or 0,
"hold": row['hold_count'] or 0,
"buy": row["buy_count"] or 0,
"sell": row["sell_count"] or 0,
"hold": row["hold_count"] or 0,
},
"user_satisfaction_pct": round(
(row['helpful_count'] / row['feedback_count'] * 100)
if row['feedback_count'] and row['feedback_count'] > 0 else 0, 2
(row["helpful_count"] / row["feedback_count"] * 100)
if row["feedback_count"] and row["feedback_count"] > 0
else 0,
2,
),
"period_days": days,
}
except Exception as e:
logger.error(f"Failed to get performance stats: {e}")
return {
@@ -940,6 +1004,7 @@ def _vol_bands_similar(a: str, b: str) -> bool:
# Singleton
_memory_instance = None
def get_analysis_memory() -> AnalysisMemory:
"""Get singleton AnalysisMemory instance."""
global _memory_instance