Add configurable data providers and localize frontend UI
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user