Files
DinQuant/backend_api_python/app/data_sources/base.py
T
Dinger 2e9c7cd69e feat: AI analysis engine refactor, dark theme polish & virtual position management
Core changes:
- Refactor FastAnalysisService: single LLM multi-factor analysis replaces
  7-agent pipeline; add multi-timeframe consensus, threshold calibration,
  confidence calibration, multi-model ensemble voting
- Add RAG memory injection and reflection validation (analysis_memory +
  reflection worker)
- Simplify billing config: remove unused strategy_run/backtest/portfolio_monitor,
  add ai_code_gen separate billing (different token consumption scale)
- Settings hot-reload after save, no backend restart needed

Frontend:
- Global dark theme overhaul: pure black palette replacing blue-tinted colors
  across sidebar/header/dashboard/analysis/K-line/user-manage/profile/settings/billing
- Fix USDT payment modal dark theme (portal rendering broke CSS selectors)
- Refactor position modal: direction + quantity + entry price, remove add/reduce
  logic, show raw DB values on re-open, save exactly what user inputs
- Fix Polymarket prediction market dark text
- i18n for position modal title

Backend:
- Position management: one record per symbol (DELETE+INSERT replacing
  ON CONFLICT with side), fixes PnL showing 0 when switching long/short
- MarketDataCollector data fetching optimization
- portfolio_monitor scheduled monitoring improvements
- env.example reorganized: common config first, advanced config last

Documentation:
- README architecture diagram updated to FastAnalysisService flow
- Add virtual position, AI tuning config, billing items documentation
- Add INDICATOR_DEFINITIONS_CN.md, FRONTEND_FAST_ANALYSIS.md

Made-with: Cursor
2026-03-23 23:01:04 +08:00

171 lines
5.0 KiB
Python

"""
数据源基类
定义统一的数据源接口
"""
from abc import ABC, abstractmethod
from typing import Dict, List, Any, Optional
from datetime import datetime, timedelta, timezone
from app.utils.logger import get_logger
logger = get_logger(__name__)
# K线周期映射(秒数)
TIMEFRAME_SECONDS = {
'1m': 60,
'5m': 300,
'15m': 900,
'30m': 1800,
'1H': 3600,
'4H': 14400,
'1D': 86400,
'1W': 604800
}
class BaseDataSource(ABC):
"""数据源基类"""
name: str = "base"
@abstractmethod
def get_kline(
self,
symbol: str,
timeframe: str,
limit: int,
before_time: Optional[int] = None
) -> List[Dict[str, Any]]:
"""
获取K线数据
Args:
symbol: 交易对/股票代码
timeframe: 时间周期 (1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W)
limit: 数据条数
before_time: 获取此时间之前的数据(Unix时间戳,秒)
Returns:
K线数据列表,格式:
[{"time": int, "open": float, "high": float, "low": float, "close": float, "volume": float}, ...]
"""
pass
def get_ticker(self, symbol: str) -> Dict[str, Any]:
"""
Get latest ticker for a symbol (best-effort).
This is an optional interface used by the strategy executor for fetching current price.
Implementations may return a dict compatible with CCXT `fetch_ticker` shape (e.g. {'last': ...}).
"""
raise NotImplementedError("get_ticker is not implemented for this data source")
def format_kline(
self,
timestamp: int,
open_price: float,
high: float,
low: float,
close: float,
volume: float
) -> Dict[str, Any]:
"""格式化单条K线数据"""
return {
'time': timestamp,
'open': round(float(open_price), 4),
'high': round(float(high), 4),
'low': round(float(low), 4),
'close': round(float(close), 4),
'volume': round(float(volume), 2)
}
def calculate_time_range(
self,
timeframe: str,
limit: int,
buffer_ratio: float = 1.2
) -> int:
"""
计算获取指定数量K线所需的时间范围(秒)
Args:
timeframe: 时间周期
limit: K线数量
buffer_ratio: 缓冲系数
Returns:
时间范围(秒)
"""
seconds_per_candle = TIMEFRAME_SECONDS.get(timeframe, 86400)
return int(seconds_per_candle * limit * buffer_ratio)
def filter_and_limit(
self,
klines: List[Dict[str, Any]],
limit: int,
before_time: Optional[int] = None
) -> List[Dict[str, Any]]:
"""
过滤和限制K线数据
Args:
klines: K线数据列表
limit: 最大数量
before_time: 过滤此时间之后的数据
Returns:
处理后的K线数据
"""
# 按时间排序
klines.sort(key=lambda x: x['time'])
# 过滤时间
if before_time:
klines = [k for k in klines if k['time'] < before_time]
# 限制数量(取最新的)
if len(klines) > limit:
klines = klines[-limit:]
return klines
def log_result(
self,
symbol: str,
klines: List[Dict[str, Any]],
timeframe: str
):
"""记录获取结果日志。
延迟判断:
- K 线 time 为 Unix 秒(UTC),与 datetime.now(UTC) 比较,避免本地时区误差。
- 日线/周线:最后一根通常是「上一交易日收盘」,周末/节假日可达 3~4 天,
原先用 2×86400s(48h)会在周一早盘误报;改为日线最多容忍约 5 个自然日,周线更宽。
"""
if klines:
latest_ts = int(klines[-1]["time"])
latest_utc = datetime.fromtimestamp(latest_ts, tz=timezone.utc)
now_utc = datetime.now(timezone.utc)
time_diff = (now_utc - latest_utc).total_seconds()
tf_sec = TIMEFRAME_SECONDS.get(timeframe, 3600)
if tf_sec < 86400:
# 分钟/小时级:超过约 2 根 K 未更新则告警
max_diff = tf_sec * 2
elif tf_sec == 86400:
# 日线:覆盖周末 + 短假期(约 5 个自然日)
max_diff = 5 * 86400
else:
# 周线:允许跨多周数据滞后
max_diff = max(tf_sec * 2, 21 * 86400)
if time_diff > max_diff:
logger.warning(
f"Warning: {symbol} data is delayed ({time_diff:.0f}s, "
f"latest_bar_utc={latest_utc.isoformat()}, threshold={max_diff:.0f}s, tf={timeframe})"
)
else:
logger.warning(f"{self.name}: no data for {symbol}")