2025-12-29 03:06:49 +08:00
"""
2026-04-06 16:47:36 +07:00
Data source base class
Define a unified data source interface
2025-12-29 03:06:49 +08:00
"""
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
from abc import ABC , abstractmethod
2026-04-09 14:30:51 +07:00
from datetime import datetime , timezone
from typing import Any , Dict , List , Optional
2025-12-29 03:06:49 +08:00
from app.utils.logger import get_logger
logger = get_logger ( __name__ )
2026-04-06 16:47:36 +07:00
# K-line cycle mapping (seconds)
2026-04-09 14:30:51 +07:00
TIMEFRAME_SECONDS = { "1m" : 60 , "5m" : 300 , "15m" : 900 , "30m" : 1800 , "1H" : 3600 , "4H" : 14400 , "1D" : 86400 , "1W" : 604800 }
2025-12-29 03:06:49 +08:00
class BaseDataSource ( ABC ):
2026-04-06 16:47:36 +07:00
"""Data source base class."""
2025-12-29 03:06:49 +08:00
name : str = "base"
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
@abstractmethod
def get_kline (
2026-04-09 14:30:51 +07:00
self , symbol : str , timeframe : str , limit : int , before_time : Optional [ int ] = None
2025-12-29 03:06:49 +08:00
) -> List [ Dict [ str , Any ]]:
"""
2026-04-06 16:47:36 +07:00
Get K-line data
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
Args:
2026-04-06 16:47:36 +07:00
symbol: trading pair/stock code
timeframe: time period (1m, 5m, 15m, 30m, 1H, 4H, 1D, 1W)
limit: number of data items
before_time: Get data before this time (Unix timestamp, seconds)
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
Returns:
2026-04-06 16:47:36 +07:00
K-line data list, format:
2025-12-29 03:06:49 +08:00
[{"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" )
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
def format_kline (
2026-04-09 14:30:51 +07:00
self , timestamp : int , open_price : float , high : float , low : float , close : float , volume : float
2025-12-29 03:06:49 +08:00
) -> Dict [ str , Any ]:
2026-04-06 16:47:36 +07:00
"""Format a single K-line record."""
2025-12-29 03:06:49 +08:00
return {
2026-04-09 14:30:51 +07:00
"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 ),
2025-12-29 03:06:49 +08:00
}
2026-04-09 14:30:51 +07:00
def calculate_time_range ( self , timeframe : str , limit : int , buffer_ratio : float = 1.2 ) -> int :
2025-12-29 03:06:49 +08:00
"""
2026-04-06 16:47:36 +07:00
Calculate the time range (seconds) required to obtain the specified number of K-lines
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
Args:
2026-04-06 16:47:36 +07:00
timeframe: time period
limit: number of K-lines
buffer_ratio: buffer coefficient
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
Returns:
2026-04-06 16:47:36 +07:00
Time range (seconds)
2025-12-29 03:06:49 +08:00
"""
seconds_per_candle = TIMEFRAME_SECONDS . get ( timeframe , 86400 )
return int ( seconds_per_candle * limit * buffer_ratio )
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
def filter_and_limit (
2026-04-09 14:30:51 +07:00
self , klines : List [ Dict [ str , Any ]], limit : int , before_time : Optional [ int ] = None
2025-12-29 03:06:49 +08:00
) -> List [ Dict [ str , Any ]]:
"""
2026-04-06 16:47:36 +07:00
Filter and limit K-line data
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
Args:
2026-04-06 16:47:36 +07:00
klines: K-line data list
limit: maximum quantity
before_time: Filter data after this time
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
Returns:
2026-04-06 16:47:36 +07:00
Processed K-line data
2025-12-29 03:06:49 +08:00
"""
2026-04-06 16:47:36 +07:00
# Sort by time
2026-04-09 14:30:51 +07:00
klines . sort ( key = lambda x : x [ "time" ])
2026-04-06 16:47:36 +07:00
# filter time
2025-12-29 03:06:49 +08:00
if before_time :
2026-04-09 14:30:51 +07:00
klines = [ k for k in klines if k [ "time" ] < before_time ]
2026-04-06 16:47:36 +07:00
# Limit quantity (take the latest)
2025-12-29 03:06:49 +08:00
if len ( klines ) > limit :
klines = klines [ - limit :]
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
return klines
2026-04-09 14:30:51 +07:00
def log_result ( self , symbol : str , klines : List [ Dict [ str , Any ]], timeframe : str ):
2026-04-06 16:47:36 +07:00
"""Record the result log.
2026-03-23 23:01:04 +08:00
2026-04-06 16:47:36 +07:00
Delayed judgment:
- K-line time is Unix seconds (UTC), compared with datetime.now(UTC) to avoid local time zone errors.
- Daily/weekly line: The last line is usually the "close of the previous trading day", and it can last 3 to 4 days on weekends/holidays.
Originally, using 2× 86400s (48h) would cause false alarms in Monday morning trading; instead, the daily line tolerates up to about 5 natural days, and the weekly line is wider.
2026-03-23 23:01:04 +08:00
"""
2025-12-29 03:06:49 +08:00
if klines :
2026-03-23 23:01:04 +08:00
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 :
2026-04-06 16:47:36 +07:00
# Minute/hour level: If it exceeds about 2 K, an alarm will be issued if it is not updated.
2026-03-23 23:01:04 +08:00
max_diff = tf_sec * 2
elif tf_sec == 86400 :
2026-04-06 16:47:36 +07:00
# Daily line: covering weekends + short holidays (about 5 calendar days)
2026-03-23 23:01:04 +08:00
max_diff = 5 * 86400
else :
2026-04-06 16:47:36 +07:00
# Weekly: Allows data lags across multiple weeks
2026-03-23 23:01:04 +08:00
max_diff = max ( tf_sec * 2 , 21 * 86400 )
2025-12-29 03:06:49 +08:00
if time_diff > max_diff :
2026-03-23 23:01:04 +08:00
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 } )"
)
2025-12-29 03:06:49 +08:00
else :
logger . warning ( f " { self . name } : no data for { symbol } " )