Commit 0d1afdc8 authored by xeron56's avatar xeron56

Initial DohasecuritiesStockAi snapshot

parents
# Azure OpenAI
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/
AZURE_OPENAI_DEPLOYMENT_NAME=
# OPENAI_API_VERSION=2024-10-21 # optional, required for non-v1 API
# LLM Providers (set the one you use)
OPENAI_API_KEY=
GOOGLE_API_KEY=
ANTHROPIC_API_KEY=
XAI_API_KEY=
DEEPSEEK_API_KEY=
DASHSCOPE_API_KEY=
DASHSCOPE_CN_API_KEY=
ZHIPU_API_KEY=
ZHIPU_CN_API_KEY=
MINIMAX_API_KEY=
MINIMAX_CN_API_KEY=
OPENROUTER_API_KEY=
MISTRAL_API_KEY=
MOONSHOT_API_KEY=
GROQ_API_KEY=
NVIDIA_API_KEY=
# Dhaka Stock Exchange data (required by the Bangladesh default profile).
# Sign in to the bundled Doha Securities OMS web UI and provide its current
# OAuth access token at runtime. It is short-lived; never commit it.
DSE_ACCESS_TOKEN=
# Alternative: leave DSE_ACCESS_TOKEN empty and authenticate automatically.
DSE_EMAIL_OR_PHONE=
DSE_PASSWORD=
# Optional mobile-flow overrides; these defaults match the supplied login script.
#DSE_AUTH_GRANT_TYPE=urn:ietf:params:oauth:grant-type:mobile-application
#DSE_AUTH_CLIENT_ID=oms
#DSE_AUTH_SCOPE=android
# FRED (Federal Reserve macro data: rates, inflation, labor, growth). Free key: https://fred.stlouisfed.org/docs/api/api_key.html
#FRED_API_KEY=
# Optional: a custom OpenAI-compatible endpoint (vLLM, LM Studio, llama.cpp,
# relay). Select provider "openai_compatible" and set the base URL; the key is
# optional (local servers need none).
#OPENAI_COMPATIBLE_API_KEY=
# AWS Bedrock (provider "bedrock", install with: pip install ".[bedrock]").
# Auth: either a Bedrock API key (bearer token, no AWS access keys) OR the AWS
# credential chain (env keys / ~/.aws/credentials / IAM role / AWS_PROFILE). Set
# the region either way; a bearer token takes precedence when both are present.
#AWS_BEARER_TOKEN_BEDROCK=
#AWS_DEFAULT_REGION=us-west-2
#AWS_PROFILE=
# Optional: point at a remote Ollama server. When unset, defaults to
# the local instance at http://localhost:11434/v1. Convention follows
# the broader Ollama ecosystem; both the CLI dropdown and programmatic
# client pick this up.
#OLLAMA_BASE_URL=http://your-ollama-host:11434/v1
# Optional: override DEFAULT_CONFIG without editing code.
# Any TRADINGAGENTS_* variable below, when set, replaces the matching key
# in dohasecuritiesstockai/default_config.py. Values are coerced to the type of
# the existing default (bool / int / str), so "true"/"3" work as expected.
# In the CLI, setting the LLM provider / models / backend URL / language
# also skips the matching interactive selection step (useful for
# OpenAI-compatible endpoints like opencode or LM Studio, and unattended runs).
# Bangladesh defaults use OpenRouter with Gemini models. For native Gemini use:
# TRADINGAGENTS_LLM_PROVIDER=google
# TRADINGAGENTS_DEEP_THINK_LLM=gemini-3.1-pro-preview
# TRADINGAGENTS_QUICK_THINK_LLM=gemini-3.5-flash
# Or explicitly retain the OpenRouter defaults:
#TRADINGAGENTS_LLM_PROVIDER=openrouter
#TRADINGAGENTS_DEEP_THINK_LLM=google/gemini-3.1-pro-preview
#TRADINGAGENTS_QUICK_THINK_LLM=google/gemini-3.5-flash
#TRADINGAGENTS_LLM_BACKEND_URL=
#TRADINGAGENTS_OUTPUT_LANGUAGE=English
#TRADINGAGENTS_MAX_DEBATE_ROUNDS=1
#TRADINGAGENTS_MAX_RISK_ROUNDS=1
#TRADINGAGENTS_CHECKPOINT_ENABLED=false
# Sampling temperature (lower = less run-to-run variation on models that
# honor it). Unset leaves each provider at its default. See the README
# "Reproducibility" note — no setting makes LLM output fully deterministic.
#TRADINGAGENTS_TEMPERATURE=0.0
# LLM SDK retry budget forwarded to every provider. Unset leaves each SDK at its
# own default (usually 2). Raise it to ride out bursty 429 rate-limit throttling
# on rate-limited deployments (e.g. Azure OpenAI) instead of aborting the run.
#TRADINGAGENTS_LLM_MAX_RETRIES=6
# Optional OpenRouter gateway fallbacks, tried in order after the selected model.
# Use openrouter/free to retain zero-cost routing when a specific free host is down.
#TRADINGAGENTS_OPENROUTER_FALLBACK_MODELS=openrouter/free
# Bangladesh/DSE feature gates and gateway settings.
#TRADINGAGENTS_MARKET_PROFILE=bangladesh_dse
#TRADINGAGENTS_SOCIAL_MEDIA_ENABLED=false
#TRADINGAGENTS_MACRO_DATA_ENABLED=false
#TRADINGAGENTS_PREDICTION_MARKETS_ENABLED=false
#TRADINGAGENTS_DSE_GATEWAY_URL=https://gateway.dohasecurities.com.bd
#TRADINGAGENTS_DSE_REQUEST_TIMEOUT=30
#TRADINGAGENTS_DSE_VERIFY_SSL=true
#TRADINGAGENTS_DSE_BENCHMARK_TICKER=DSEX
# Run the interactive CLI first, then prepare and open its exact result in the
# bundled Angular dashboard. The first UI launch runs npm ci + a production build.
#TRADINGAGENTS_OPEN_UI_AFTER_ANALYSIS=false
#TRADINGAGENTS_API_HOST=127.0.0.1
#TRADINGAGENTS_API_PORT=8000
# Provider-specific reasoning/thinking depth (optional; unset = provider
# default). Setting one also skips the matching interactive prompt.
#TRADINGAGENTS_OPENAI_REASONING_EFFORT=medium
#TRADINGAGENTS_GOOGLE_THINKING_LEVEL=high
#TRADINGAGENTS_ANTHROPIC_EFFORT=high
name: CI
on:
push:
branches: [main]
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: tests (py${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install (with dev extras)
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Run test suite
run: pytest -q
smoke-install:
name: clean-install smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Fresh install (no dev extras) and import
run: |
python -m pip install --upgrade pip
pip install .
# Catches undeclared runtime deps (e.g. #994 python-dotenv): a bare
# install must import the package and the CLI module.
python -c "import dohasecuritiesstockai, cli.main; print('clean-install import OK')"
lint:
name: ruff (strict, full repo)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ruff
run: pip install "ruff>=0.15"
- name: Lint the repository
# The repo is fully clean under the strict select, so we lint everything
# (results/ and worklog/ are excluded via pyproject extend-exclude).
run: ruff check .
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
frontend/
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Local brokerage frontend checkout is intentionally not published here.
/web-ui/
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
# Cache
**/data_cache/
# Enterprise env file (secrets) and generated run reports
.env.enterprise
reports/
[submodule "timesfm"]
path = timesfm
url = https://github.com/google-research/timesfm.git
This diff is collapsed.
/.angular/
/dist/
/node_modules/
/.DS_Store
# DSE AI Analysis UI
Independent Angular client for the TradingAgents DSE REST API. It does not
depend on, import from, or modify the brokerage `web-ui` application.
## Run locally
The normal user flow starts here automatically after the Python CLI completes:
```bash
# From the repository root; .env can enable this permanently.
python -m cli.main --open-ui
```
That flow serves the production Angular bundle and opens the exact completed
ticker/date. For frontend development with live rebuilds, use two terminals:
From the `TradingAgents` directory, start the Python API:
```bash
python -m tradingagents.api
```
Then start this UI in another terminal:
```bash
cd analysis-ui
npm install
npm start
```
The development client calls `http://127.0.0.1:8000/api/v1`. Production builds
use same-origin `/api/v1`; change `src/environments/environment.prod.ts` if the
API is hosted elsewhere.
The UI only requests DSE reads and analysis jobs. Brokerage order, portfolio,
cash, transfer, and other mutation endpoints are not exposed by the API.
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"analysis-ui": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/analysis-ui",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [],
"allowedCommonJsDependencies": ["sockjs-client"],
"styles": ["src/styles.scss"],
"scripts": []
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"sourceMap": false,
"outputHashing": "all",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"budgets": [
{
"type": "initial",
"maximumWarning": "1mb",
"maximumError": "2mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "15kb",
"maximumError": "20kb"
}
]
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "analysis-ui:build:production"
},
"development": {
"buildTarget": "analysis-ui:build:development"
}
},
"defaultConfiguration": "development"
}
}
}
},
"cli": {
"analytics": false
}
}
This diff is collapsed.
{
"name": "tradingagents-dse-analysis-ui",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "ng serve --host 0.0.0.0 --port 4200",
"build": "ng build",
"build:production": "ng build --configuration production"
},
"dependencies": {
"@angular/common": "^21.2.18",
"@angular/compiler": "^21.2.18",
"@angular/core": "^21.2.18",
"@angular/forms": "^21.2.18",
"@angular/platform-browser": "^21.2.18",
"@stomp/stompjs": "^7.1.1",
"chart.js": "^4.5.1",
"lightweight-charts": "^4.2.3",
"rxjs": "~7.8.1",
"sockjs-client": "^1.6.1",
"tslib": "^2.8.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "^21.2.19",
"@angular/cli": "^21.2.19",
"@angular/compiler-cli": "^21.2.18",
"@types/sockjs-client": "^1.5.4",
"typescript": "~5.9.3"
}
}
This diff is collapsed.
This diff is collapsed.
import { CommonModule } from '@angular/common';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
ViewChild,
signal,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import {
ArcElement,
Chart,
DoughnutController,
Tooltip,
} from 'chart.js';
import { Subscription, switchMap, takeWhile, timer } from 'rxjs';
import {
AnalysisJob,
AnalysisLanguage,
BilingualText,
FactorStatus,
StockAnalysis,
StockOption,
} from './stock-analysis.model';
import { StockAnalysisService } from './stock-analysis.service';
import { TimesFmPredictionComponent } from './timesfm-prediction.component';
Chart.register(DoughnutController, ArcElement, Tooltip);
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, FormsModule, TimesFmPredictionComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
readonly predictionMode =
new URLSearchParams(window.location.search).get('view') === 'timesfm';
readonly Math = Math;
readonly stocks = signal<StockOption[]>([]);
readonly analysis = signal<StockAnalysis | null>(null);
readonly language = signal<AnalysisLanguage>('en');
readonly loadingStocks = signal<boolean>(true);
readonly loadingAnalysis = signal<boolean>(false);
readonly jobMessage = signal<string>('');
readonly errorMessage = signal<string>('');
readonly fullReportOpen = signal<boolean>(false);
readonly subscriptions: Subscription[] = [];
selectedSymbol = 'GP';
private scoreCanvas: ElementRef<HTMLCanvasElement> | null = null;
private scoreChart: Chart<'doughnut'> | null = null;
@ViewChild('scoreCanvas')
set scoreCanvasRef(canvas: ElementRef<HTMLCanvasElement> | undefined) {
this.scoreCanvas = canvas ?? null;
this.renderScoreChart();
}
constructor(
private readonly stockAnalysisService: StockAnalysisService,
private readonly titleService: Title,
private readonly cdr: ChangeDetectorRef,
) {}
ngOnInit(): void {
if (this.predictionMode) return;
this.titleService.setTitle('DSE AI Stock Analysis');
const query = new URLSearchParams(window.location.search);
const requestedSymbol = query.get('symbol')?.trim().toUpperCase();
const requestedDate = query.get('date')?.trim();
if (requestedSymbol) this.selectedSymbol = requestedSymbol;
this.loadStocks();
if (requestedDate && /^\d{4}-\d{2}-\d{2}$/.test(requestedDate)) {
this.loadAnalysis(this.selectedSymbol, requestedDate);
} else {
this.loadLatest(this.selectedSymbol);
}
}
ngAfterViewInit(): void {
this.renderScoreChart();
}
ngOnDestroy(): void {
this.scoreChart?.destroy();
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
}
setLanguage(language: AnalysisLanguage): void {
this.language.set(language);
document.documentElement.lang = language;
}
copy(value: BilingualText): string {
return value[this.language()];
}
selectStock(): void {
this.fullReportOpen.set(false);
this.loadLatest(this.selectedSymbol);
}
runAnalysis(force = false): void {
this.loadingAnalysis.set(true);
this.errorMessage.set('');
this.jobMessage.set(
this.language() === 'bn'
? 'বিশ্লেষণ সারিতে যোগ করা হচ্ছে…'
: 'Queueing analysis…',
);
this.subscriptions.push(
this.stockAnalysisService.createAnalysis(this.selectedSymbol, force).subscribe({
next: (job) => this.pollJob(job),
error: () => this.handleError('The analysis job could not be started.'),
}),
);
}
toggleFullReport(): void {
this.fullReportOpen.update((open) => !open);
if (!this.fullReportOpen()) return;
setTimeout(() => {
document.getElementById('full-analysis')?.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
});
}
factorStatus(status: FactorStatus): string {
const labels: Record<FactorStatus, BilingualText> = {
positive: { en: 'Positive', bn: 'ইতিবাচক' },
caution: { en: 'Watch', bn: 'নজর রাখুন' },
negative: { en: 'Concern', bn: 'উদ্বেগ' },
neutral: { en: 'Neutral', bn: 'নিরপেক্ষ' },
};
return this.copy(labels[status]);
}
formatTaka(value: number | null): string {
return value === null
? '—'
: `৳${value.toLocaleString('en-BD', { maximumFractionDigits: 1 })}`;
}
rawState(report: StockAnalysis): string {
return JSON.stringify(report.agent_reports.raw_state, null, 2);
}
private loadStocks(): void {
this.loadingStocks.set(true);
this.subscriptions.push(
this.stockAnalysisService.getStocks().subscribe({
next: (stocks) => {
this.stocks.set(stocks);
this.loadingStocks.set(false);
this.cdr.markForCheck();
},
error: () => {
this.loadingStocks.set(false);
this.handleError('The DSE stock list could not be loaded.');
},
}),
);
}
private loadLatest(symbol: string): void {
this.loadAnalysis(symbol);
}
private loadAnalysis(symbol: string, analysisDate?: string): void {
this.loadingAnalysis.set(true);
this.errorMessage.set('');
this.jobMessage.set('');
const request = analysisDate
? this.stockAnalysisService.getAnalysis(symbol, analysisDate)
: this.stockAnalysisService.getLatest(symbol);
this.subscriptions.push(
request.subscribe({
next: (report) => this.applyAnalysis(report),
error: () => {
this.loadingAnalysis.set(false);
this.analysis.set(null);
this.errorMessage.set(
this.language() === 'bn'
? 'এই শেয়ারের কোনো সম্পূর্ণ বিশ্লেষণ নেই। নতুন বিশ্লেষণ চালান।'
: 'No completed analysis exists for this stock yet. Run a new analysis.',
);
this.cdr.markForCheck();
},
}),
);
}
private pollJob(initialJob: AnalysisJob): void {
this.jobMessage.set(initialJob.message);
this.subscriptions.push(
timer(0, 2000)
.pipe(
switchMap(() => this.stockAnalysisService.getJob(initialJob.job_id)),
takeWhile(
(job) => job.status === 'queued' || job.status === 'running',
true,
),
)
.subscribe({
next: (job) => {
this.jobMessage.set(job.message);
if (job.status === 'completed') this.loadLatest(job.symbol);
if (job.status === 'failed') {
this.handleError(job.message || 'The analysis failed.');
}
this.cdr.markForCheck();
},
error: () =>
this.handleError('The analysis job status could not be checked.'),
}),
);
}
private applyAnalysis(report: StockAnalysis): void {
this.selectedSymbol = report.symbol;
this.analysis.set(report);
this.loadingAnalysis.set(false);
this.jobMessage.set('');
this.errorMessage.set('');
const query = new URLSearchParams({
symbol: report.symbol,
date: report.analysis_date,
});
window.history.replaceState(null, '', `${window.location.pathname}?${query}`);
this.cdr.markForCheck();
setTimeout(() => this.renderScoreChart());
}
private renderScoreChart(): void {
const report = this.analysis();
if (!this.scoreCanvas || !report) return;
this.scoreChart?.destroy();
this.scoreChart = new Chart(this.scoreCanvas.nativeElement, {
type: 'doughnut',
data: {
labels: ['Score', 'Remaining'],
datasets: [
{
data: [report.fundamental_score, 100 - report.fundamental_score],
backgroundColor: ['#527c9e', '#ebe6dc'],
borderWidth: 0,
hoverOffset: 0,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '78%',
animation: { duration: 450 },
plugins: {
legend: { display: false },
tooltip: { enabled: false },
},
},
});
}
private handleError(message: string): void {
this.loadingAnalysis.set(false);
this.jobMessage.set('');
this.errorMessage.set(message);
this.cdr.markForCheck();
}
}
export type AnalysisLanguage = 'en' | 'bn';
export type FactorStatus = 'positive' | 'caution' | 'negative' | 'neutral';
export type JobStatus = 'queued' | 'running' | 'completed' | 'failed';
export interface BilingualText {
en: string;
bn: string;
}
export interface StockOption {
symbol: string;
name: string;
sector: string;
latest_price: number | null;
change_percent: number | null;
}
export interface MarketSnapshot {
latest_price: number;
change: number | null;
change_percent: number | null;
previous_close: number | null;
fifty_two_week_low: number | null;
fifty_two_week_high: number | null;
as_of: string;
}
export interface ScoreMetric {
key: string;
label: BilingualText;
display_value: string;
score: number;
}
export interface FactorCard {
key: string;
status: FactorStatus;
title: BilingualText;
subtitle: BilingualText;
explanation: BilingualText;
metrics: ScoreMetric[];
}
export interface ValuationMethod {
key: string;
label: BilingualText;
value: number | null;
available: boolean;
}
export interface ValuationSummary {
verdict: 'looks_cheap' | 'fair' | 'looks_expensive' | 'insufficient_data';
verdict_label: BilingualText;
current_price: number;
rough_estimate: number | null;
fair_range_low: number | null;
fair_range_high: number | null;
confidence: 'low' | 'medium' | 'high';
summary: BilingualText;
methods: ValuationMethod[];
}
export interface ReportSection {
key: string;
title: BilingualText;
summary: BilingualText;
bullets: BilingualText[];
}
export interface AgentReports {
market_report: string;
news_report: string;
fundamentals_report: string;
investment_plan: string;
final_trade_decision: string;
raw_state: Record<string, unknown>;
}
export interface StockAnalysis {
schema_version: '1.0';
analysis_id: string;
symbol: string;
company_name: string;
sector: string;
analysis_date: string;
generated_at: string;
market: MarketSnapshot;
fundamental_score: number;
score_label: BilingualText;
headline: BilingualText;
takeaways: BilingualText[];
in_depth_title: BilingualText;
in_depth_snippet: BilingualText;
valuation: ValuationSummary;
factors: FactorCard[];
report_sections: ReportSection[];
agent_reports: AgentReports;
disclaimer: BilingualText;
}
export interface AnalysisJob {
job_id: string;
symbol: string;
analysis_date: string;
status: JobStatus;
created_at: string;
updated_at: string;
analysis_url: string | null;
message: string;
}
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';
import { AnalysisJob, StockAnalysis, StockOption } from './stock-analysis.model';
@Injectable({ providedIn: 'root' })
export class StockAnalysisService {
private readonly apiUrl = environment.analysisApiUrl;
constructor(private readonly http: HttpClient) {}
getStocks(): Observable<StockOption[]> {
return this.http.get<StockOption[]>(`${this.apiUrl}/stocks`);
}
getLatest(symbol: string): Observable<StockAnalysis> {
return this.http.get<StockAnalysis>(
`${this.apiUrl}/analyses/${encodeURIComponent(symbol)}/latest`,
);
}
getAnalysis(symbol: string, analysisDate: string): Observable<StockAnalysis> {
return this.http.get<StockAnalysis>(
`${this.apiUrl}/analyses/${encodeURIComponent(symbol)}/${encodeURIComponent(analysisDate)}`,
);
}
createAnalysis(symbol: string, force = false): Observable<AnalysisJob> {
return this.http.post<AnalysisJob>(`${this.apiUrl}/analyses`, {
symbol,
force,
});
}
getJob(jobId: string): Observable<AnalysisJob> {
return this.http.get<AnalysisJob>(
`${this.apiUrl}/analyses/jobs/${encodeURIComponent(jobId)}`,
);
}
}
<main class="prediction-page">
<header class="prediction-toolbar">
<a class="brand" href="/?view=timesfm" aria-label="TimesFM forecast home">
<span class="brand-mark">T</span>
<span>TRADINGAGENTS <strong>FORECAST LAB</strong></span>
</a>
<div class="live-pill" [class.connected]="liveStatus() === 'connected'">
<span class="status-dot"></span>
{{ liveStatus() === 'connected' ? 'DSE live' : liveStatus() === 'connecting' ? 'Connecting' : 'Historical mode' }}
</div>
</header>
@if (loading()) {
<section class="loading-state" aria-live="polite">
<span class="loader"></span>
<h1>Loading TimesFM prediction</h1>
<p>Reading the saved backtest and chart series…</p>
</section>
}
@if (errorMessage()) {
<section class="empty-state" aria-live="polite">
<span class="eyebrow">NO PREDICTION RUN</span>
<h1>Generate the forecast first.</h1>
<p>{{ errorMessage() }}</p>
<code>tradingagents-predict "BXPHARMA'PB" --resolution 1d --open-ui</code>
</section>
}
@if (result(); as report) {
<section class="hero">
<div>
<p class="eyebrow">TIMESFM {{ report.model.version }} · ZERO-SHOT PRICE FORECAST</p>
<div class="symbol-line">
<h1>{{ report.symbol }}</h1>
<span>{{ report.data.resolution_label }}</span>
<span>{{ report.currency }}</span>
</div>
<p class="hero-copy">
The first {{ report.data.context_points | number }} bars are the only model context.
The following {{ report.data.holdout_points | number }} real bars remain unseen until scoring.
</p>
</div>
<article class="accuracy-card">
<span>Holdout accuracy</span>
<strong>{{ report.metrics.accuracy_score | number: '1.1-2' }}<small>%</small></strong>
<p>100 − sMAPE across the full hidden half</p>
</article>
</section>
<section class="metric-grid" aria-label="Backtest metrics">
<article>
<span>Mean error</span>
<strong>{{ price(report.metrics.mae) }}</strong>
<small>MAE per bar</small>
</article>
<article>
<span>Direction calls</span>
<strong>{{ percent(report.metrics.directional_accuracy_percent) }}</strong>
<small>Up/down matched</small>
</article>
<article>
<span>80% band coverage</span>
<strong>{{ percent(report.metrics.interval_80_coverage_percent) }}</strong>
<small>Actual inside Q10–Q90</small>
</article>
<article [class.negative]="(report.metrics.skill_vs_naive_percent || 0) < 0">
<span>Vs. last-price baseline</span>
<strong>{{ signedPercent(report.metrics.skill_vs_naive_percent) }}</strong>
<small>MAE skill score</small>
</article>
<article class="live-metric">
<span>Live traded price</span>
<strong>{{ price(livePrice()) }}</strong>
<small>{{ report.live_feed.stock_code }}</small>
</article>
</section>
<section class="chart-card">
<header>
<div>
<p class="eyebrow">HISTORICAL BACKTEST + FORWARD PATH</p>
<h2>Actual candles against TimesFM</h2>
</div>
<div class="legend" aria-label="Chart legend">
<span><i class="candle"></i>Actual OHLC</span>
<span><i class="backtest"></i>Held-out prediction</span>
<span><i class="future"></i>Future forecast</span>
<span><i class="live"></i>Live actual</span>
</div>
</header>
<div #predictionChart class="prediction-chart" role="img" aria-label="DSE actual and TimesFM forecast chart"></div>
<footer>
<span>Context</span>
<strong>{{ report.data.first_timestamp | date: 'mediumDate' }} → {{ report.history[report.data.context_points - 1].time | date: 'mediumDate' }}</strong>
<span>Hidden test</span>
<strong>{{ report.backtest[0].time | date: 'mediumDate' }} → {{ report.data.last_timestamp | date: 'mediumDate' }}</strong>
<span>Next forecast</span>
<strong>{{ report.future.length }} bars</strong>
</footer>
</section>
<section class="detail-grid">
<article class="live-card">
<p class="eyebrow">REAL-TIME MATCH</p>
<h2>Prediction vs. latest trade</h2>
@if (latestLiveMatch(); as match) {
<div class="live-comparison">
<div><span>Predicted</span><strong>{{ price(match.predicted) }}</strong></div>
<div><span>Observed</span><strong>{{ price(match.actual) }}</strong></div>
<div><span>Live accuracy</span><strong>{{ match.accuracyPercent | number: '1.1-2' }}%</strong></div>
</div>
<p>Matched to the nearest forecast bar at {{ match.targetTime | date: 'medium' }}.</p>
} @else {
<div class="waiting-live">
<span class="pulse-ring"></span>
<p>Waiting for the next <code>{{ report.live_feed.topic }}</code> update for {{ report.live_feed.stock_code }}.</p>
</div>
}
</article>
<article class="model-card">
<p class="eyebrow">MODEL & HARDWARE</p>
<h2>{{ report.model.name }} {{ report.model.version }}</h2>
<dl>
<div><dt>Checkpoint</dt><dd>{{ report.model.checkpoint }}</dd></div>
<div><dt>Parameters</dt><dd>{{ report.model.parameters / 1000000 | number: '1.0-0' }}M</dd></div>
<div><dt>Compute</dt><dd>{{ report.model.gpu_name || report.model.device }}</dd></div>
<div><dt>Recursive chunks</dt><dd>{{ report.model.recursive_chunks }}</dd></div>
</dl>
</article>
</section>
<section class="table-card">
<header>
<div>
<p class="eyebrow">LAST 12 HELD-OUT BARS</p>
<h2>Point-level audit</h2>
</div>
<span>Every displayed score comes from real data withheld from the model.</span>
</header>
<div class="table-scroll">
<table>
<thead><tr><th>Time</th><th>Actual</th><th>Predicted</th><th>Absolute error</th><th>Q10–Q90</th></tr></thead>
<tbody>
@for (point of recentBacktest(); track trackBacktest($index, point)) {
<tr>
<td>{{ point.time | date: 'medium' }}</td>
<td>{{ price(point.actual) }}</td>
<td>{{ price(point.predicted) }}</td>
<td>{{ price(point.absolute_error) }}</td>
<td>{{ price(point.q10) }} – {{ price(point.q90) }}</td>
</tr>
}
</tbody>
</table>
</div>
</section>
<footer class="prediction-footer">
<span>{{ report.disclaimer }}</span>
<code>{{ report.run_id }}</code>
</footer>
}
</main>
:host {
display: block;
color: #e5e7eb;
background: #090d14;
}
.prediction-page {
--panel: #111827;
--panel-soft: #151d2b;
--line: #263244;
--muted: #94a3b8;
--amber: #f59e0b;
--violet: #a78bfa;
--cyan: #22d3ee;
min-height: 100vh;
padding: 1.25rem clamp(1rem, 3vw, 3rem) 3rem;
background:
radial-gradient(circle at 10% -10%, rgb(34 211 238 / 10%), transparent 28rem),
radial-gradient(circle at 100% 5%, rgb(167 139 250 / 9%), transparent 32rem),
#090d14;
}
.prediction-toolbar,
.hero,
.metric-grid,
.chart-card,
.detail-grid,
.table-card,
.prediction-footer,
.loading-state,
.empty-state {
width: min(100%, 1560px);
margin-inline: auto;
}
.prediction-toolbar,
.hero,
.symbol-line,
.chart-card > header,
.legend,
.metric-grid,
.live-comparison,
.table-card > header,
.prediction-footer {
display: flex;
align-items: center;
}
.prediction-toolbar {
justify-content: space-between;
margin-bottom: clamp(2rem, 5vw, 4.5rem);
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.7rem;
color: #e5e7eb;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.16em;
text-decoration: none;
}
.brand strong { color: var(--cyan); }
.brand-mark {
display: grid;
width: 2rem;
height: 2rem;
place-items: center;
border: 1px solid #155e75;
border-radius: 0.55rem;
color: var(--cyan);
background: #0c2630;
font-size: 1rem;
}
.live-pill {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.8rem;
border: 1px solid #374151;
border-radius: 2rem;
color: var(--muted);
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
}
.status-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: #64748b;
}
.live-pill.connected { color: #6ee7b7; border-color: #065f46; }
.live-pill.connected .status-dot { background: #34d399; box-shadow: 0 0 0 0.25rem rgb(52 211 153 / 12%); }
.hero {
justify-content: space-between;
gap: 3rem;
margin-bottom: 2rem;
}
.eyebrow {
margin: 0 0 0.65rem;
color: #7dd3fc;
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.symbol-line { gap: 0.75rem; flex-wrap: wrap; }
.symbol-line h1 {
margin: 0 0.5rem 0 0;
color: #f8fafc;
font-size: clamp(3.2rem, 7vw, 6.8rem);
line-height: 0.9;
letter-spacing: -0.065em;
}
.symbol-line span {
padding: 0.35rem 0.75rem;
border: 1px solid #334155;
border-radius: 2rem;
color: #cbd5e1;
background: #111827;
font-size: 0.82rem;
}
.hero-copy { max-width: 48rem; color: var(--muted); line-height: 1.65; }
.accuracy-card {
flex: 0 0 min(100%, 20rem);
padding: 1.3rem 1.5rem;
border: 1px solid #3b475b;
border-radius: 1.2rem;
background: linear-gradient(145deg, #172033, #101622);
box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 28%);
}
.accuracy-card > span { color: var(--muted); font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.1em; }
.accuracy-card strong { display: block; margin: 0.4rem 0; color: #f8fafc; font-size: 4rem; line-height: 1; }
.accuracy-card small { color: var(--cyan); font-size: 1.4rem; }
.accuracy-card p { margin: 0; color: var(--muted); font-size: 0.8rem; }
.metric-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0.75rem;
margin-bottom: 1rem;
}
.metric-grid article {
min-width: 0;
padding: 1rem 1.1rem;
border: 1px solid var(--line);
border-radius: 0.9rem;
background: var(--panel);
}
.metric-grid span, .metric-grid small { display: block; color: var(--muted); }
.metric-grid span { font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.08em; }
.metric-grid strong { display: block; overflow: hidden; margin: 0.4rem 0 0.2rem; color: #f8fafc; font-size: clamp(1.2rem, 2vw, 1.75rem); text-overflow: ellipsis; }
.metric-grid small { font-size: 0.75rem; }
.metric-grid article.negative strong { color: #fb7185; }
.metric-grid .live-metric { border-color: #155e75; background: #0d202a; }
.metric-grid .live-metric strong { color: var(--cyan); }
.chart-card,
.live-card,
.model-card,
.table-card {
border: 1px solid var(--line);
border-radius: 1.1rem;
background: rgb(17 24 39 / 88%);
}
.chart-card { overflow: hidden; margin-bottom: 1rem; }
.chart-card > header { justify-content: space-between; gap: 1.5rem; padding: 1.1rem 1.25rem; border-bottom: 1px solid var(--line); }
.chart-card h2, .detail-grid h2, .table-card h2 { margin: 0; color: #f8fafc; font-size: 1.15rem; }
.legend { justify-content: flex-end; gap: 1rem; flex-wrap: wrap; color: var(--muted); font-size: 0.76rem; }
.legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.legend i { width: 1.25rem; height: 0.16rem; border-radius: 1rem; background: #34d399; }
.legend .backtest { background: var(--amber); }
.legend .future { background: var(--violet); }
.legend .live { background: var(--cyan); }
.prediction-chart { width: 100%; min-height: 520px; }
.chart-card > footer {
display: grid;
grid-template-columns: auto 1fr auto 1fr auto auto;
gap: 0.5rem 0.8rem;
padding: 0.85rem 1.25rem;
border-top: 1px solid var(--line);
color: var(--muted);
font-size: 0.76rem;
}
.chart-card > footer strong { color: #dbe5f3; font-weight: 600; }
.detail-grid { display: grid; grid-template-columns: 1.35fr 1fr; gap: 1rem; margin-bottom: 1rem; }
.live-card, .model-card { padding: 1.3rem; }
.live-comparison { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; margin-top: 1.2rem; }
.live-comparison div { padding: 0.9rem; border: 1px solid #164e63; border-radius: 0.7rem; background: #0c2029; }
.live-comparison span { display: block; color: var(--muted); font-size: 0.75rem; }
.live-comparison strong { display: block; margin-top: 0.35rem; color: var(--cyan); font-size: 1.35rem; }
.live-card > p:last-child, .waiting-live { color: var(--muted); font-size: 0.82rem; }
.waiting-live { display: flex; align-items: center; gap: 0.8rem; min-height: 5.8rem; }
.pulse-ring { width: 0.75rem; height: 0.75rem; flex: 0 0 auto; border-radius: 50%; background: var(--cyan); box-shadow: 0 0 0 0.45rem rgb(34 211 238 / 10%); }
code { color: #bae6fd; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.model-card dl { margin: 1rem 0 0; }
.model-card dl div { display: grid; grid-template-columns: 8rem 1fr; gap: 1rem; padding: 0.65rem 0; border-top: 1px solid var(--line); }
.model-card dt { color: var(--muted); }
.model-card dd { overflow-wrap: anywhere; margin: 0; color: #e5e7eb; text-align: right; }
.table-card { overflow: hidden; margin-bottom: 1rem; }
.table-card > header { justify-content: space-between; gap: 2rem; padding: 1.2rem 1.3rem; }
.table-card > header > span { max-width: 28rem; color: var(--muted); font-size: 0.8rem; text-align: right; }
.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
th, td { padding: 0.8rem 1.3rem; border-top: 1px solid var(--line); text-align: right; white-space: nowrap; }
th:first-child, td:first-child { text-align: left; }
th { color: var(--muted); background: #0d1420; font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; }
td { color: #dbe5f3; font-size: 0.82rem; }
.prediction-footer { justify-content: space-between; gap: 2rem; padding: 1rem 0; color: var(--muted); font-size: 0.75rem; }
.prediction-footer span { max-width: 60rem; }
.loading-state, .empty-state { display: grid; min-height: 60vh; place-content: center; text-align: center; }
.loading-state h1, .empty-state h1 { margin-bottom: 0.5rem; color: #f8fafc; }
.loading-state p, .empty-state p { color: var(--muted); }
.loader { width: 2rem; height: 2rem; margin: 0 auto 1rem; border: 2px solid #334155; border-top-color: var(--cyan); border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 1050px) {
.metric-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.detail-grid { grid-template-columns: 1fr; }
.chart-card > footer { grid-template-columns: auto 1fr; }
}
@media (max-width: 720px) {
.prediction-page { padding-inline: 0.8rem; }
.brand { letter-spacing: 0.08em; }
.hero { align-items: stretch; flex-direction: column; gap: 1.5rem; }
.accuracy-card { flex-basis: auto; }
.metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.chart-card > header, .table-card > header, .prediction-footer { align-items: flex-start; flex-direction: column; }
.legend { justify-content: flex-start; }
.prediction-chart { min-height: 420px; }
.live-comparison { grid-template-columns: 1fr; }
.table-card > header > span { text-align: left; }
}
@media (max-width: 430px) {
.prediction-toolbar { align-items: flex-start; gap: 1rem; }
.brand > span:last-child { display: none; }
.metric-grid { grid-template-columns: 1fr; }
.symbol-line h1 { font-size: 3.5rem; }
}
This diff is collapsed.
export interface PredictionCandle {
time: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
segment: 'context' | 'holdout';
}
export interface ForecastPoint {
time: string;
predicted: number;
q10: number;
q50: number;
q90: number;
}
export interface BacktestPoint extends ForecastPoint {
actual: number;
error: number;
absolute_error: number;
absolute_percentage_error: number | null;
}
export interface AccuracyMetrics {
accuracy_score: number;
accuracy_definition: string;
mae: number;
rmse: number;
mape_percent: number | null;
smape_percent: number;
r_squared: number | null;
directional_accuracy_percent: number | null;
interval_80_coverage_percent: number;
naive_mae: number;
skill_vs_naive_percent: number | null;
}
export interface PredictionModelMetadata {
name: string;
version: string;
checkpoint: string;
parameters: number;
backend: 'torch';
device: string;
gpu_name: string | null;
max_context: number;
max_horizon: number;
recursive_chunks: number;
}
export interface PredictionDataMetadata {
vendor: string;
endpoint: string;
symbol: string;
requested_resolution: string;
server_resolution: string;
resolution_label: string;
first_timestamp: string;
last_timestamp: string;
total_points: number;
context_points: number;
holdout_points: number;
split_ratio: number;
target: 'close';
}
export interface LiveFeedMetadata {
enabled: boolean;
transport: 'sockjs_stomp';
url: string;
topic: string;
stock_code: string;
note: string;
}
export interface TimesFmPredictionResult {
schema_version: '1.0';
run_id: string;
generated_at: string;
symbol: string;
currency: string;
model: PredictionModelMetadata;
data: PredictionDataMetadata;
metrics: AccuracyMetrics;
history: PredictionCandle[];
backtest: BacktestPoint[];
future: ForecastPoint[];
live_feed: LiveFeedMetadata;
disclaimer: string;
}
export interface LiveStockUpdate {
stock_code: string;
ltp: number;
volume?: number;
value?: number;
trades?: number;
}
export interface LiveForecastMatch {
observedAt: Date;
targetTime: string;
actual: number;
predicted: number;
absoluteError: number;
accuracyPercent: number;
}
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment';
import { TimesFmPredictionResult } from './timesfm-prediction.model';
@Injectable({ providedIn: 'root' })
export class TimesFmPredictionService {
private readonly apiUrl = environment.analysisApiUrl;
constructor(private readonly http: HttpClient) {}
getPrediction(runId: string): Observable<TimesFmPredictionResult> {
return this.http.get<TimesFmPredictionResult>(
`${this.apiUrl}/predictions/${encodeURIComponent(runId)}`,
);
}
getLatest(
symbol?: string,
resolution?: string,
): Observable<TimesFmPredictionResult> {
let params = new HttpParams();
if (symbol) params = params.set('symbol', symbol);
if (resolution) params = params.set('resolution', resolution);
return this.http.get<TimesFmPredictionResult>(
`${this.apiUrl}/predictions/latest`,
{ params },
);
}
}
export const environment = {
production: true,
analysisApiUrl: '/api/v1',
};
export const environment = {
production: false,
analysisApiUrl: 'http://127.0.0.1:8000/api/v1',
};
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>DSE AI Stock Analysis</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Bilingual Dhaka Stock Exchange analysis powered by TradingAgents"
/>
<script>
var global = window;
</script>
</head>
<body>
<app-root></app-root>
</body>
</html>
import { provideHttpClient } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [provideHttpClient()],
}).catch((error: unknown) => console.error(error));
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
html,
body {
min-height: 100%;
margin: 0;
}
body,
button,
select {
font-family:
Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
"Noto Sans Bengali", sans-serif;
}
button,
select,
summary {
outline-color: #527c9e;
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": ["ES2022", "dom"]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
import getpass
import requests
from rich.console import Console
from rich.panel import Panel
from cli.config import CLI_CONFIG
def fetch_announcements(url: str = None, timeout: float = None) -> dict:
"""Fetch announcements from endpoint. Returns dict with announcements and settings."""
endpoint = url or CLI_CONFIG["announcements_url"]
timeout = timeout or CLI_CONFIG["announcements_timeout"]
fallback = CLI_CONFIG["announcements_fallback"]
try:
response = requests.get(endpoint, timeout=timeout)
response.raise_for_status()
data = response.json()
return {
"announcements": data.get("announcements", [fallback]),
"require_attention": data.get("require_attention", False),
}
except Exception:
return {
"announcements": [fallback],
"require_attention": False,
}
def display_announcements(console: Console, data: dict) -> None:
"""Display announcements panel. Prompts for Enter if require_attention is True."""
announcements = data.get("announcements", [])
require_attention = data.get("require_attention", False)
if not announcements:
return
content = "\n".join(announcements)
panel = Panel(
content,
border_style="cyan",
padding=(1, 2),
title="Announcements",
)
console.print(panel)
if require_attention:
getpass.getpass("Press Enter to continue...")
else:
console.print()
CLI_CONFIG = {
# Announcements
"announcements_url": "https://api.tauric.ai/v1/announcements",
"announcements_timeout": 1.0,
"announcements_fallback": "[cyan]For more information, please visit[/cyan] [link=https://github.com/TauricResearch]https://github.com/TauricResearch[/link]",
}
This diff is collapsed.
from enum import Enum
class AnalystType(str, Enum):
MARKET = "market"
# Wire value stays "social" for saved-config and string-keyed-caller
# back-compat; the user-facing label is "Sentiment Analyst".
SOCIAL = "social"
NEWS = "news"
FUNDAMENTALS = "fundamentals"
class AssetType(str, Enum):
STOCK = "stock"
CRYPTO = "crypto"
____ ___ _ _ _
| _ \ / _ \| | | | / \
| | | | | | | |_| | / _ \
| |_| | |_| | _ |/ ___ \
|____/ \___/|_| |_/_/ \_\
SECURITIES STOCK AI
import threading
from typing import Any
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.messages import AIMessage
from langchain_core.outputs import LLMResult
class StatsCallbackHandler(BaseCallbackHandler):
"""Callback handler that tracks LLM calls, tool calls, and token usage."""
def __init__(self) -> None:
super().__init__()
self._lock = threading.Lock()
self.llm_calls = 0
self.tool_calls = 0
self.tokens_in = 0
self.tokens_out = 0
def on_llm_start(
self,
serialized: dict[str, Any],
prompts: list[str],
**kwargs: Any,
) -> None:
"""Increment LLM call counter when an LLM starts."""
with self._lock:
self.llm_calls += 1
def on_chat_model_start(
self,
serialized: dict[str, Any],
messages: list[list[Any]],
**kwargs: Any,
) -> None:
"""Increment LLM call counter when a chat model starts."""
with self._lock:
self.llm_calls += 1
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
"""Extract token usage from LLM response."""
try:
generation = response.generations[0][0]
except (IndexError, TypeError):
return
usage_metadata = None
if hasattr(generation, "message"):
message = generation.message
if isinstance(message, AIMessage) and hasattr(message, "usage_metadata"):
usage_metadata = message.usage_metadata
if usage_metadata:
with self._lock:
self.tokens_in += usage_metadata.get("input_tokens", 0)
self.tokens_out += usage_metadata.get("output_tokens", 0)
def on_tool_start(
self,
serialized: dict[str, Any],
input_str: str,
**kwargs: Any,
) -> None:
"""Increment tool call counter when a tool starts."""
with self._lock:
self.tool_calls += 1
def get_stats(self) -> dict[str, Any]:
"""Return current statistics."""
with self._lock:
return {
"llm_calls": self.llm_calls,
"tool_calls": self.tool_calls,
"tokens_in": self.tokens_in,
"tokens_out": self.tokens_out,
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment