Add configurable data providers and localize frontend UI

This commit is contained in:
2026-03-15 00:55:12 +08:00
parent 12de93aa30
commit d233a3f55d
38 changed files with 1936 additions and 1038 deletions

View File

@@ -29,8 +29,10 @@ from backend.tools.data_tools import (
prices_to_df,
search_line_items,
)
from backend.tools.technical_signals import StockTechnicalAnalyzer
logger = logging.getLogger(__name__)
_technical_analyzer = StockTechnicalAnalyzer()
def _to_text_response(text: str) -> ToolResponse:
@@ -108,7 +110,12 @@ def _fmt(val, fmt=".2f", suffix="") -> str:
def _resolved_date(current_date: Optional[str]) -> str:
"""Ensure we always return a concrete date string."""
return current_date or datetime.today().strftime("%Y-%m-%d")
today = datetime.today().date()
if not current_date:
return today.strftime("%Y-%m-%d")
requested_date = datetime.strptime(current_date, "%Y-%m-%d").date()
return min(requested_date, today).strftime("%Y-%m-%d")
# ==================== Fundamental Analysis Tools ====================
@@ -419,60 +426,33 @@ def analyze_trend_following(
lines.append(f"{ticker}: Insufficient price data\n")
continue
df = prices_to_df(prices)
n = len(df)
# Calculate moving averages
sma_20_win = min(20, n // 2)
sma_50_win = min(50, n - 5) if n > 25 else min(25, n - 5)
sma_200_win = min(200, n - 10) if n > 200 else None
df["SMA_20"] = df["close"].rolling(window=sma_20_win).mean()
df["SMA_50"] = df["close"].rolling(window=sma_50_win).mean()
if sma_200_win:
df["SMA_200"] = df["close"].rolling(window=sma_200_win).mean()
df["EMA_12"] = df["close"].ewm(span=min(12, n // 3)).mean()
df["EMA_26"] = df["close"].ewm(span=min(26, n // 2)).mean()
df["MACD"] = df["EMA_12"] - df["EMA_26"]
df["MACD_signal"] = df["MACD"].ewm(span=9).mean()
current_price = _safe_float(df["close"].iloc[-1])
sma_20 = _safe_float(df["SMA_20"].iloc[-1])
sma_50 = _safe_float(df["SMA_50"].iloc[-1])
sma_200 = (
_safe_float(df["SMA_200"].iloc[-1])
if "SMA_200" in df.columns
signal = _technical_analyzer.analyze(ticker, prices_to_df(prices))
distance_200ma = (
((signal.current_price - signal.ma200) / signal.ma200) * 100
if signal.ma200
else None
)
macd = _safe_float(df["MACD"].iloc[-1])
macd_signal = _safe_float(df["MACD_signal"].iloc[-1])
macd_signal_str = (
"BUY" if signal.macd > signal.macd_signal else "SELL"
)
# Determine trend
if sma_200:
trend = "BULLISH" if current_price > sma_200 else "BEARISH"
distance_200ma = ((current_price - sma_200) / sma_200) * 100
else:
trend = "UNKNOWN"
distance_200ma = None
macd_signal_str = "BUY" if macd > macd_signal else "SELL"
lines.append(f"{ticker}: ${current_price:.2f}")
lines.append(f"{ticker}: ${signal.current_price:.2f}")
lines.append(
f" SMA20: ${sma_20:.2f} | SMA50: ${sma_50:.2f} | SMA200: {f'${sma_200:.2f}' if sma_200 else 'N/A'}",
f" MA20: ${signal.ma20:.2f} | MA50: ${signal.ma50:.2f} | MA200: {f'${signal.ma200:.2f}' if signal.ma200 else 'N/A'}",
)
lines.append(
f" MACD: {macd:.3f} | Signal: {macd_signal:.3f} -> {macd_signal_str}",
f" MACD: {signal.macd:.3f} | Signal: {signal.macd_signal:.3f} -> {macd_signal_str}",
)
lines.append(
f" Long-term Trend: {trend}"
f" Long-term Trend: {signal.trend}"
+ (
f" ({distance_200ma:+.1f}% from 200MA)"
if distance_200ma
else ""
),
)
if signal.notes:
lines.append(f" Notes: {'; '.join(signal.notes)}")
lines.append("")
return _to_text_response("\n".join(lines))
@@ -515,51 +495,29 @@ def analyze_mean_reversion(
lines.append(f"{ticker}: Insufficient price data\n")
continue
df = prices_to_df(prices)
n = len(df)
signal = _technical_analyzer.analyze(ticker, prices_to_df(prices))
deviation = (
((signal.current_price - signal.bollinger_mid) / signal.bollinger_mid)
* 100
if signal.bollinger_mid > 0
else 0
)
# Bollinger Bands
window = min(20, n - 2)
df["SMA"] = df["close"].rolling(window=window).mean()
df["STD"] = df["close"].rolling(window=window).std()
df["Upper_Band"] = df["SMA"] + (2 * df["STD"])
df["Lower_Band"] = df["SMA"] - (2 * df["STD"])
# RSI
delta = df["close"].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
rs = gain / loss
df["RSI"] = 100 - (100 / (1 + rs))
current_price = _safe_float(df["close"].iloc[-1])
sma = _safe_float(df["SMA"].iloc[-1])
upper = _safe_float(df["Upper_Band"].iloc[-1])
lower = _safe_float(df["Lower_Band"].iloc[-1])
rsi = _safe_float(df["RSI"].iloc[-1])
deviation = (current_price - sma) / sma * 100
# Signal interpretation
if rsi > 70:
rsi_signal = "OVERBOUGHT"
elif rsi < 30:
rsi_signal = "OVERSOLD"
else:
rsi_signal = "NEUTRAL"
if current_price > upper:
if signal.current_price > signal.bollinger_upper > 0:
bb_signal = "ABOVE UPPER BAND (potential sell)"
elif current_price < lower:
elif 0 < signal.current_price < signal.bollinger_lower:
bb_signal = "BELOW LOWER BAND (potential buy)"
else:
bb_signal = "WITHIN BANDS"
lines.append(f"{ticker}: ${current_price:.2f}")
lines.append(f"{ticker}: ${signal.current_price:.2f}")
lines.append(
f" Bollinger: Lower ${lower:.2f} | SMA ${sma:.2f} | Upper ${upper:.2f}",
f" Bollinger: Lower ${signal.bollinger_lower:.2f} | Mid ${signal.bollinger_mid:.2f} | Upper ${signal.bollinger_upper:.2f}",
)
lines.append(f" Position: {bb_signal}")
lines.append(f" RSI: {rsi:.1f} -> {rsi_signal}")
lines.append(
f" RSI: {signal.rsi14:.1f} -> {signal.mean_reversion_signal}",
)
lines.append(f" Price Deviation from SMA: {deviation:+.1f}%")
lines.append("")
@@ -602,61 +560,30 @@ def analyze_momentum(
lines.append(f"{ticker}: Insufficient price data\n")
continue
df = prices_to_df(prices)
n = len(df)
df["returns"] = df["close"].pct_change()
signal = _technical_analyzer.analyze(ticker, prices_to_df(prices))
# Adaptive periods
short_p = min(5, n // 3)
med_p = min(10, n // 2)
long_p = min(20, n - 2)
current_price = _safe_float(df["close"].iloc[-1])
mom_5 = (
_safe_float(
(df["close"].iloc[-1] / df["close"].iloc[-short_p - 1] - 1)
* 100,
)
if n > short_p
else 0
)
mom_10 = (
_safe_float(
(df["close"].iloc[-1] / df["close"].iloc[-med_p - 1] - 1)
* 100,
)
if n > med_p
else 0
)
mom_20 = (
_safe_float(
(df["close"].iloc[-1] / df["close"].iloc[-long_p - 1] - 1)
* 100,
)
if n > long_p
else 0
)
volatility = _safe_float(
df["returns"].tail(20).std() * np.sqrt(252) * 100,
)
# Overall momentum signal
avg_mom = (mom_5 + mom_10 + mom_20) / 3
avg_mom = (
signal.momentum_5d_pct
+ signal.momentum_10d_pct
+ signal.momentum_20d_pct
) / 3
if avg_mom > 2:
signal = "STRONG BULLISH"
signal_text = "STRONG BULLISH"
elif avg_mom > 0:
signal = "BULLISH"
signal_text = "BULLISH"
elif avg_mom > -2:
signal = "BEARISH"
signal_text = "BEARISH"
else:
signal = "STRONG BEARISH"
signal_text = "STRONG BEARISH"
lines.append(f"{ticker}: ${current_price:.2f}")
lines.append(f"{ticker}: ${signal.current_price:.2f}")
lines.append(
f" 5-day: {mom_5:+.1f}% | 10-day: {mom_10:+.1f}% | 20-day: {mom_20:+.1f}%",
f" 5-day: {signal.momentum_5d_pct:+.1f}% | 10-day: {signal.momentum_10d_pct:+.1f}% | 20-day: {signal.momentum_20d_pct:+.1f}%",
)
lines.append(f" Volatility (annualized): {volatility:.1f}%")
lines.append(f" Overall: {signal}")
lines.append(
f" Volatility (annualized): {signal.annualized_volatility_pct:.1f}%",
)
lines.append(f" Overall: {signal_text}")
lines.append("")
return _to_text_response("\n".join(lines))
@@ -699,38 +626,26 @@ def analyze_volatility(
continue
df = prices_to_df(prices)
n = len(df)
df["returns"] = df["close"].pct_change()
# Adaptive windows
short_w = min(10, n // 2)
med_w = min(20, n - 2)
long_w = min(60, n - 1) if n > 30 else med_w
current_price = _safe_float(df["close"].iloc[-1])
signal = _technical_analyzer.analyze(ticker, df)
returns = df["close"].pct_change()
short_w = min(10, max(1, len(df) - 1))
med_w = min(20, max(1, len(df) - 1))
long_w = min(60, max(1, len(df) - 1))
vol_10 = _safe_float(
df["returns"].tail(short_w).std() * np.sqrt(252) * 100,
returns.tail(short_w).std() * np.sqrt(252) * 100,
)
vol_20 = _safe_float(
df["returns"].tail(med_w).std() * np.sqrt(252) * 100,
returns.tail(med_w).std() * np.sqrt(252) * 100,
)
vol_60 = _safe_float(
df["returns"].tail(long_w).std() * np.sqrt(252) * 100,
returns.tail(long_w).std() * np.sqrt(252) * 100,
)
# Risk assessment
if vol_20 > 50:
risk = "HIGH RISK"
elif vol_20 > 25:
risk = "MODERATE RISK"
else:
risk = "LOW RISK"
lines.append(f"{ticker}: ${current_price:.2f}")
lines.append(f"{ticker}: ${signal.current_price:.2f}")
lines.append(
f" 10-day Vol: {vol_10:.1f}% | 20-day Vol: {vol_20:.1f}% | 60-day Vol: {vol_60:.1f}%",
)
lines.append(f" Risk Level: {risk}")
lines.append(f" Risk Level: {signal.risk_level}")
lines.append("")
return _to_text_response("\n".join(lines))