mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-26 08:48:05 +00:00
feat: add Atlas Terminal — Next.js 14 + FastAPI full-stack migration
Complete migration from Streamlit to Next.js 14 App Router + FastAPI backend. Frontend (Next.js 14): - 10 pages: Overview, Research, Valuation, Technical, Markets, Earnings, News, Portfolio, Filings, Settings - Terminal Noir dark theme with custom Tailwind config - TradingView Lightweight Charts for candlestick/volume - Valuation: DCF, Sensitivity Matrix, Monte Carlo, Tornado, Reverse DCF - Financial Statements table with YoY growth badges and margin rows - SEC EDGAR inline filing viewer with section tabs - News split-view with iframe article embedding - Technical Analysis with RSI, MACD, Bollinger, Fibonacci, Moving Averages - Earnings beat/miss visualization - AI Copilot chat panel with Gemini integration Backend (FastAPI): - 13 routers: market_data, financials, valuation, technical, earnings, insider, edgar, news, portfolio, analysis, chat, estimates, fx - Services: DCF engine, Monte Carlo simulation, sensitivity analysis, risk metrics, SEC parser, technical indicators - yfinance + yahooquery data sources with fallback pattern - SQLite caching layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
56a9561f71
commit
b2acda81ee
@@ -0,0 +1,132 @@
|
||||
"""Text cleaning and chunking utilities for LLM payloads.
|
||||
|
||||
Provides aggressive HTML-stripping, whitespace normalisation, and
|
||||
intelligent splitting of long text into sequential chunks that avoid
|
||||
cutting mid-sentence when possible.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def clean_text_for_llm(html_content: str) -> str:
|
||||
"""Strip HTML, collapse whitespace, and remove non-ASCII for LLM input.
|
||||
|
||||
Removes ``<table>``, ``<img>``, ``<style>``, ``<script>``, ``<svg>``,
|
||||
and ``<math>`` elements before extracting text. Drops page-number-only
|
||||
lines and other layout artefacts.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
html_content:
|
||||
Raw HTML (or already-plain text with residual tags).
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Clean, single-line-ish text suitable for an LLM prompt.
|
||||
"""
|
||||
if not html_content or not html_content.strip():
|
||||
return ""
|
||||
try:
|
||||
soup = BeautifulSoup(html_content, "lxml")
|
||||
for tag in soup.find_all(["table", "img", "style", "script", "svg", "math"]):
|
||||
tag.decompose()
|
||||
text = soup.get_text(separator=" ")
|
||||
except Exception:
|
||||
text = re.sub(r"<[^>]+>", " ", html_content)
|
||||
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
text = " ".join(text.split())
|
||||
text = re.sub(r"[^\x20-\x7E\n]", " ", text)
|
||||
text = re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
lines: List[str] = []
|
||||
for line in text.split("\n"):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if re.fullmatch(r"\d+", line) or re.fullmatch(r"[\.\-\s\-]+", line):
|
||||
continue
|
||||
if re.match(r"^(page\s+\d+|\d+)\s*$", line, re.IGNORECASE) and len(line) < 20:
|
||||
continue
|
||||
lines.append(line)
|
||||
|
||||
result = " ".join(lines)
|
||||
result = re.sub(r"\s+", " ", result).strip()
|
||||
return result
|
||||
|
||||
|
||||
def smart_chunk(
|
||||
section: str,
|
||||
max_chars: int = 10_000,
|
||||
head_ratio: float = 0.5,
|
||||
) -> str:
|
||||
"""Truncate *section* to *max_chars* keeping head and tail portions.
|
||||
|
||||
When the text exceeds the limit the middle is replaced with a brief
|
||||
``[ ... middle omitted ... ]`` marker. Approximately
|
||||
``head_ratio * max_chars`` characters come from the start and the
|
||||
remainder from the end.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
section:
|
||||
Full text to be trimmed.
|
||||
max_chars:
|
||||
Hard character budget (default 10 000 ~= 2.5k tokens).
|
||||
head_ratio:
|
||||
Fraction of the budget allocated to the leading portion.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Text guaranteed to be at most *max_chars* characters long.
|
||||
"""
|
||||
if not section or len(section) <= max_chars:
|
||||
return section
|
||||
head_size = int(max_chars * head_ratio)
|
||||
tail_size = max_chars - head_size - 100
|
||||
return section[:head_size] + " [ ... middle omitted ... ] " + section[-tail_size:]
|
||||
|
||||
|
||||
def _split_into_chunks(
|
||||
text: str,
|
||||
max_chars: int = 22_000,
|
||||
min_chunk: int = 5_000,
|
||||
) -> List[str]:
|
||||
"""Split *text* into sequential chunks without cutting mid-sentence.
|
||||
|
||||
Prefers breaking at paragraph boundaries (double newlines). Each chunk
|
||||
is at most *max_chars* characters; the algorithm avoids creating a
|
||||
trailing fragment shorter than *min_chunk* unless it is the only chunk.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text:
|
||||
The document to split.
|
||||
max_chars:
|
||||
Maximum characters per chunk.
|
||||
min_chunk:
|
||||
Minimum look-back distance when searching for a break point.
|
||||
|
||||
Returns
|
||||
-------
|
||||
List[str]
|
||||
Non-empty stripped chunks in document order.
|
||||
"""
|
||||
if not text or len(text) <= max_chars:
|
||||
return [text] if text and text.strip() else []
|
||||
chunks: List[str] = []
|
||||
start = 0
|
||||
while start < len(text):
|
||||
end = min(start + max_chars, len(text))
|
||||
if end < len(text):
|
||||
break_at = text.rfind("\n\n", start, end + 1)
|
||||
if break_at > start + min_chunk:
|
||||
end = break_at + 2
|
||||
chunks.append(text[start:end].strip())
|
||||
start = end
|
||||
return [c for c in chunks if c]
|
||||
Reference in New Issue
Block a user