├── frontend
├── src
│ ├── vite-env.d.ts
│ ├── index.css
│ ├── widgets
│ │ └── layout
│ │ │ ├── index.tsx
│ │ │ ├── footer.tsx
│ │ │ ├── news_sentiment.tsx
│ │ │ ├── market_trend.tsx
│ │ │ └── navbar.tsx
│ ├── pages
│ │ ├── index.tsx
│ │ ├── home.tsx
│ │ ├── crypto_news.tsx
│ │ ├── stock_news.tsx
│ │ ├── stock.tsx
│ │ └── crypto.tsx
│ ├── main.tsx
│ ├── App.tsx
│ ├── routes.tsx
│ ├── assets
│ │ └── logo.svg
│ └── services
│ │ └── api.ts
├── postcss.config.js
├── public
│ └── img
│ │ └── logo.png
├── jsconfig.json
├── tailwind.config.js
├── .gitignore
├── vite.config.ts
├── index.html
├── tsconfig.json
├── eslint.config.js
├── package.json
└── README.md
├── requirements.txt
├── Home.png
├── Tech.png
├── Market Trend.png
├── TradeSense.mp4
├── Business News.png
├── News Analysis.png
├── Recommendation.png
├── Business NewsP2.jpg
├── Recommendation P3.png
├── Recpmmendation P2.png
├── Overview News Analysis.png
├── routes
├── market.py
├── news.py
├── recommend.py
├── technical.py
└── strategy.py
├── main.py
├── services
├── trend_analyzer.py
├── technical_analysis.py
├── recommendation.py
├── gpt_client.py
├── news_analyzer.py
└── strategy_analyzer.py
├── .gitignore
└── README.md
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | redis
3 | types-redis
4 | uvicorn
5 |
--------------------------------------------------------------------------------
/Home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/Home.png
--------------------------------------------------------------------------------
/Tech.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/Tech.png
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/Market Trend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/Market Trend.png
--------------------------------------------------------------------------------
/TradeSense.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/TradeSense.mp4
--------------------------------------------------------------------------------
/Business News.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/Business News.png
--------------------------------------------------------------------------------
/News Analysis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/News Analysis.png
--------------------------------------------------------------------------------
/Recommendation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/Recommendation.png
--------------------------------------------------------------------------------
/Business NewsP2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/Business NewsP2.jpg
--------------------------------------------------------------------------------
/Recommendation P3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/Recommendation P3.png
--------------------------------------------------------------------------------
/Recpmmendation P2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/Recpmmendation P2.png
--------------------------------------------------------------------------------
/Overview News Analysis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/Overview News Analysis.png
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
--------------------------------------------------------------------------------
/frontend/public/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwanlu09/TradeSense_AiTradeAgent/HEAD/frontend/public/img/logo.png
--------------------------------------------------------------------------------
/frontend/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./src/*"]
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/frontend/src/widgets/layout/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./navbar";
2 | export * from "./news_sentiment";
3 | export * from "./market_trend";
4 | export * from "./footer";
5 |
--------------------------------------------------------------------------------
/frontend/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./home";
2 | export * from "./stock";
3 | export * from "./crypto";
4 | export * from "./stock_news";
5 | export * from "./crypto_news";
6 |
7 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | plugins: [],
11 | }
--------------------------------------------------------------------------------
/routes/market.py:
--------------------------------------------------------------------------------
1 | # routes/market.py
2 | from fastapi import APIRouter
3 | from services.trend_analyzer import analyze_market_trend
4 |
5 | router = APIRouter()
6 |
7 | @router.get("/market", tags=["Market Trend"])
8 | def market_overview():
9 | return analyze_market_trend()
10 |
11 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import './index.css'
4 | import App from './App'
5 | import { BrowserRouter } from 'react-router-dom'
6 |
7 | ReactDOM.createRoot(document.getElementById('root')!).render(
8 |
9 |
10 |
11 |
12 | ,
13 | )
14 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | // vite.config.ts
2 | import { defineConfig } from 'vite'
3 | import react from '@vitejs/plugin-react'
4 | import path from 'path'
5 | import { fileURLToPath } from 'url'
6 |
7 | const __filename = fileURLToPath(import.meta.url)
8 | const __dirname = path.dirname(__filename)
9 |
10 | export default defineConfig({
11 | plugins: [react()],
12 | resolve: {
13 | alias: {
14 | '@': path.resolve(__dirname, 'src'),
15 | },
16 | },
17 | })
18 |
19 |
20 |
--------------------------------------------------------------------------------
/routes/news.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from services.news_analyzer import fetch_and_analyze_news_by_url
3 | import os
4 |
5 | router = APIRouter()
6 |
7 | NEWS_API_KEY = os.getenv("NEWS_API_KEY")
8 |
9 | @router.get("/news",tags=["Business News"])
10 | def get_news():
11 | url = f"https://newsapi.org/v2/top-headlines?category=business&apiKey={NEWS_API_KEY}"
12 | return fetch_and_analyze_news_by_url(url)
13 |
14 | @router.get("/news/crypto",tags=["Crypto News"])
15 | def get_crypt_news():
16 | url = f"https://newsapi.org/v2/everything?q=crypto&language=en&sortBy=publishedAt&apiKey={NEWS_API_KEY}"
17 | return fetch_and_analyze_news_by_url(url)
18 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | TradeSense
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/routes/recommend.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from services.strategy_analyzer import get_recommended_stocks, get_recommended_cryptos
3 |
4 | router = APIRouter(prefix="/recommend", tags=["Recommendation"])
5 |
6 | @router.get("/stocks")
7 | def get_stock_recommendations(count: int = 10):
8 |
9 | recommendations = get_recommended_stocks()
10 | return {"recommendations": recommendations[:count] if count else recommendations}
11 |
12 | @router.get("/cryptos")
13 | def get_crypto_recommendations(count: int = 10):
14 |
15 | recommendations = get_recommended_cryptos()
16 | return {"recommendations": recommendations[:count] if count else recommendations}
17 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "lib": ["DOM", "DOM.Iterable", "ES2020"],
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "jsx": "react-jsx",
8 |
9 | // Strict type-checking and better type inference
10 | "strict": true,
11 | "esModuleInterop": true,
12 | "skipLibCheck": true,
13 | "resolveJsonModule": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "allowImportingTsExtensions": true,
16 | "isolatedModules": true,
17 | "noEmit": true,
18 |
19 | // Path alias configuration
20 | "baseUrl": ".",
21 | "paths": {
22 | "@/*": ["src/*"]
23 | }
24 | },
25 | "include": ["src", "vite.config.ts"]
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 | from fastapi.middleware.cors import CORSMiddleware
3 | from routes import news, market, technical, strategy, recommend
4 |
5 | app = FastAPI()
6 |
7 |
8 | # Configure CORS
9 | app.add_middleware(
10 | CORSMiddleware,
11 | allow_origins=["http://localhost:5173"],
12 | allow_credentials=True,
13 | allow_methods=["*"],
14 | allow_headers=["*"],
15 | )
16 |
17 |
18 | @app.get("/")
19 | def root():
20 | return {"message": "Welcome to the AI Trade Agent API"}
21 |
22 |
23 | app.include_router(news.router)
24 | app.include_router(strategy.router)
25 | app.include_router(market.router)
26 | <<<<<<< HEAD
27 | app.include_router(technical.router)
28 | app.include_router(recommend.router)
29 |
30 |
--------------------------------------------------------------------------------
/frontend/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/routes/technical.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from services.technical_analysis import get_stock_technical_indicator, get_crypto_technical_indicator
3 |
4 | router = APIRouter()
5 |
6 | @router.get("/technical/stock/{symbol}", tags=["Stock Technical"])
7 | def get_stock_technical(symbol: str):
8 | """
9 | Get technical indicators for a given stock symbol.
10 |
11 | Returns common indicators such as RSI, MA (Moving Average), and Volume.
12 | """
13 | return get_stock_technical_indicator(symbol)
14 |
15 | @router.get("/technical/crypto/{symbol}", tags=["Crypto Technical"])
16 | def get_crypto_technical(symbol: str):
17 | """
18 | Get technical indicators for a given crypto symbol.
19 |
20 | Returns common indicators such as RSI, MA (Moving Average), and Volume.
21 | """
22 | return get_crypto_technical_indicator(symbol)
23 |
24 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Routes, Route, Navigate, useLocation } from "react-router-dom";
3 | import { routes } from "./routes";
4 | import { Navbar, Footer } from "./widgets/layout";
5 | import { StockNews, CryptoNews } from './pages';
6 |
7 |
8 | function App() {
9 | const { pathname } = useLocation();
10 |
11 | return (
12 | <>
13 |
14 |
15 |
16 |
17 | {routes.map(({ path, element }, key) => (
18 |
23 | ))}
24 |
25 |
26 |
27 | } />
28 | } />
29 |
30 |
31 | {/* Default route */}
32 | } />
33 |
34 |
35 |
36 | >
37 | );
38 | }
39 |
40 | export default App;
41 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@headlessui/react": "^2.2.1",
14 | "@heroicons/react": "^2.2.0",
15 | "@tailwindcss/postcss": "^4.1.4",
16 | "axios": "^1.8.4",
17 | "framer-motion": "^12.7.4",
18 | "react": "^19.0.0",
19 | "react-dom": "^19.0.0",
20 | "react-router-dom": "^7.5.0",
21 | "recharts": "^2.15.3"
22 | },
23 | "devDependencies": {
24 | "@eslint/js": "^9.22.0",
25 | "@types/node": "^22.14.1",
26 | "@types/react": "^19.1.2",
27 | "@types/react-dom": "^19.1.2",
28 | "@vitejs/plugin-react": "^4.3.4",
29 | "autoprefixer": "^10.4.21",
30 | "concurrently": "^9.1.2",
31 | "eslint": "^9.22.0",
32 | "eslint-plugin-react-hooks": "^5.2.0",
33 | "eslint-plugin-react-refresh": "^0.4.19",
34 | "globals": "^16.0.0",
35 | "postcss": "^8.5.3",
36 | "tailwindcss": "^3.4.1",
37 | "typescript": "~5.7.2",
38 | "typescript-eslint": "^8.26.1",
39 | "vite": "^6.3.1"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/frontend/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import { Home, Stock, Crypto, StockNews, CryptoNews } from "./pages";
2 | import { ReactElement } from "react";
3 | import { RouteObject } from "react-router-dom";
4 | import { Outlet } from "react-router-dom";
5 |
6 | export interface AppRoute {
7 | name: string;
8 | path: string;
9 | element: ReactElement;
10 | children?: AppRoute[];
11 | }
12 |
13 | function NewsLayout() {
14 | return (
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export const routes: AppRoute[] = [
22 | {
23 | name: "Home",
24 | path: "/home",
25 | element: ,
26 | },
27 | {
28 | name: "News",
29 | path: "/news",
30 | element: ,
31 | children: [
32 | {
33 | name: "Stock News",
34 | path: "stock_news",
35 | element: ,
36 | },
37 | {
38 | name: "Crypto News",
39 | path: "crypto_news",
40 | element: ,
41 | },
42 | ],
43 | },
44 | {
45 | name: "Stock",
46 | path: "/stock",
47 | element: ,
48 | },
49 | {
50 | name: "Crypto",
51 | path: "/crypto",
52 | element: ,
53 | },
54 | ];
55 |
56 | // Convert to a format accepted by react-router-dom v7, supporting nested routes.
57 | export const routerConfig: RouteObject[] = routes.map(({ path, element, children }) => {
58 | const routeObj: RouteObject = { path, element };
59 |
60 | if (children) {
61 | routeObj.children = children.map(child => ({
62 | path: child.path,
63 | element: child.element,
64 | }));
65 | }
66 |
67 | return routeObj;
68 | });
69 |
70 | export default routes;
71 |
--------------------------------------------------------------------------------
/routes/strategy.py:
--------------------------------------------------------------------------------
1 | from fastapi import APIRouter
2 | from services.strategy_analyzer import analyze_stock_strategy, analyze_crypto_strategy, get_recommended_stocks, get_recommended_cryptos
3 |
4 | router = APIRouter()
5 |
6 | @router.get("/strategy/stock/{symbol}", tags=["Stock Strategy"])
7 | def get_stock_strategy(symbol: str):
8 | """
9 | Get trading strategy recommendation for a stock symbol.
10 | This includes both news sentiment analysis and technical indicators.
11 | """
12 | return analyze_stock_strategy(symbol)
13 |
14 | @router.get("/strategy/crypto/{symbol}", tags=["Crypto Strategy"])
15 | def get_crypto_strategy(symbol: str):
16 | """
17 | Get trading strategy recommendation for a crypto symbol.
18 | This includes both news sentiment analysis and technical indicators.
19 | """
20 | return analyze_crypto_strategy(symbol)
21 |
22 | @router.get("/strategy", tags=["Strategy"])
23 | def get_strategy(symbol: str, is_crypto: bool = False):
24 | """
25 | Get trading strategy recommendation based on the is_crypto flag.
26 | This endpoint is used by the frontend.
27 | """
28 | if is_crypto:
29 | return analyze_crypto_strategy(symbol)
30 | else:
31 | return analyze_stock_strategy(symbol)
32 |
33 | @router.get("/strategy/recommended-stocks", tags=["Recommendations"])
34 | def get_stock_recommendations():
35 | """
36 | Get a list of recommended stocks based on technical and sentiment analysis.
37 | """
38 | return get_recommended_stocks()
39 |
40 | @router.get("/strategy/recommended-cryptos", tags=["Recommendations"])
41 | def get_crypto_recommendations():
42 | """
43 | Get a list of recommended cryptocurrencies based on technical and sentiment analysis.
44 | """
45 | return get_recommended_cryptos()
46 |
47 |
48 |
--------------------------------------------------------------------------------
/services/trend_analyzer.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import yfinance as yf
3 |
4 | def analyze_market_trend():
5 | # stock(yfinance)
6 | index_symbols = {
7 | "S&P500": "^GSPC",
8 | "NASDAQ": "^IXIC",
9 | "Dow Jones": "^DJI"
10 | }
11 |
12 | stock_changes = {}
13 | for name, symbol in index_symbols.items():
14 | data = yf.Ticker(symbol).history(period="1d")
15 | open_price = data["Open"][-1]
16 | close_price = data["Close"][-1]
17 | pct_change = (close_price - open_price) / open_price * 100
18 | stock_changes[name] = {
19 | "current_price": f"${round(close_price, 2)}",
20 | "percentage_change": round(pct_change, 2)
21 | }
22 |
23 | stock_avg_trend = round(
24 | sum(v["percentage_change"] for v in stock_changes.values()) / len(stock_changes), 2
25 | )
26 |
27 | # crpto
28 | crypto_ids = "bitcoin,ethereum,binancecoin,solana,dogecoin"
29 | response = requests.get("https://api.coingecko.com/api/v3/coins/markets", params={
30 | "vs_currency": "usd",
31 | "ids": crypto_ids,
32 | "price_change_percentage": "24h"
33 | })
34 | crypto_data = response.json()
35 |
36 | crypto_info = {
37 | coin["id"]: {
38 | "current_price": f"${round(coin['current_price'], 2)}",
39 | "percentage_change": round(coin["price_change_percentage_24h"], 2)
40 | }
41 | for coin in crypto_data
42 | }
43 |
44 | crypto_avg_trend = round(
45 | sum(v["percentage_change"] for v in crypto_info.values()) / len(crypto_info), 2
46 | )
47 |
48 | return {
49 | "stock_market": {
50 | "indices": stock_changes,
51 | "avg_trend": stock_avg_trend
52 | },
53 | "crypto_market": {
54 | "coins": crypto_info,
55 | "avg_trend": crypto_avg_trend
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
13 |
14 | ```js
15 | export default tseslint.config({
16 | extends: [
17 | // Remove ...tseslint.configs.recommended and replace with this
18 | ...tseslint.configs.recommendedTypeChecked,
19 | // Alternatively, use this for stricter rules
20 | ...tseslint.configs.strictTypeChecked,
21 | // Optionally, add this for stylistic rules
22 | ...tseslint.configs.stylisticTypeChecked,
23 | ],
24 | languageOptions: {
25 | // other options...
26 | parserOptions: {
27 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
28 | tsconfigRootDir: import.meta.dirname,
29 | },
30 | },
31 | })
32 | ```
33 |
34 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
35 |
36 | ```js
37 | // eslint.config.js
38 | import reactX from 'eslint-plugin-react-x'
39 | import reactDom from 'eslint-plugin-react-dom'
40 |
41 | export default tseslint.config({
42 | plugins: {
43 | // Add the react-x and react-dom plugins
44 | 'react-x': reactX,
45 | 'react-dom': reactDom,
46 | },
47 | rules: {
48 | // other rules...
49 | // Enable its recommended typescript rules
50 | ...reactX.configs['recommended-typescript'].rules,
51 | ...reactDom.configs.recommended.rules,
52 | },
53 | })
54 | ```
55 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/frontend/src/widgets/layout/footer.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { routes } from '../../routes'
4 |
5 | export function Footer() {
6 | return (
7 |
8 |
9 |
10 |
r.name === "Home")?.path || "/home"} className="flex items-center mb-4 sm:mb-0 space-x-3 rtl:space-x-reverse">
11 |
12 |
TradeSense
13 |
14 |
15 | {routes.map(route => {
16 |
17 | if (route.name === "Home") return null;
18 |
19 |
20 | if (route.children && route.name === "News") {
21 | return route.children.map(child => (
22 |
23 |
24 | {child.name}
25 |
26 |
27 | ));
28 | }
29 |
30 | return (
31 |
32 |
33 | {route.name}
34 |
35 |
36 | );
37 | })}
38 |
39 |
40 |
41 |
42 | © 2025 r.name === "Home")?.path || "/home"} className="hover:underline">TradeSense. All Rights Reserved.
43 |
44 |
45 |
46 | );
47 | }
48 |
49 | export default Footer
50 |
--------------------------------------------------------------------------------
/services/technical_analysis.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import pandas as pd
3 | import numpy as np
4 | from datetime import datetime, timedelta
5 | import yfinance as yf # for stock data
6 |
7 | # --- Crypto Technical Indicators using Binance API ---
8 | def get_crypto_technical_indicator(symbol: str):
9 | """
10 | Get technical indicators for a given crypto symbol from Binance.
11 | Includes RSI, MA_20, MA_120 and Volume.
12 | """
13 | symbol = symbol.upper()
14 | binance_symbol = f"{symbol}USDT"
15 |
16 | # Fetch historical klines (candlestick) data for 120 days
17 | url = "https://api.binance.com/api/v3/klines"
18 | params = {
19 | "symbol": binance_symbol,
20 | "interval": "1d",
21 | "limit": 120
22 | }
23 | response = requests.get(url, params=params)
24 | data = response.json()
25 |
26 | if not data or isinstance(data, dict) and data.get("code"):
27 | return {"error": f"Failed to fetch data for {binance_symbol}"}
28 |
29 | # Parse close prices and volumes
30 | closes = [float(kline[4]) for kline in data]
31 | volumes = [float(kline[5]) for kline in data]
32 | df = pd.DataFrame({
33 | "close": closes,
34 | "volume": volumes
35 | })
36 |
37 | # Calculate RSI
38 | delta = df['close'].diff()
39 | gain = delta.where(delta > 0, 0)
40 | loss = -delta.where(delta < 0, 0)
41 | avg_gain = gain.rolling(window=14).mean()
42 | avg_loss = loss.rolling(window=14).mean()
43 | rs = avg_gain / avg_loss
44 | rsi = 100 - (100 / (1 + rs))
45 | latest_rsi = rsi.iloc[-1]
46 |
47 | # Moving Averages
48 | ma_20 = df['close'].rolling(window=20).mean().iloc[-1]
49 | ma_120 = df['close'].rolling(window=120).mean().iloc[-1]
50 | latest_volume = df['volume'].iloc[-1]
51 |
52 | return {
53 | "symbol": binance_symbol,
54 | "RSI": round(latest_rsi, 2),
55 | "MA_20": round(ma_20, 2),
56 | "MA_120": round(ma_120, 2),
57 | "Volume": int(latest_volume)
58 | }
59 |
60 | # --- Stock Technical Indicators using Yahoo Finance ---
61 | def get_stock_technical_indicator(symbol: str):
62 | """
63 | Get technical indicators for a given stock symbol using Yahoo Finance.
64 | Includes RSI, MA_50 and Volume.
65 | """
66 | stock = yf.Ticker(symbol)
67 | df = stock.history(period="6mo", interval="1d")
68 |
69 | if df.empty:
70 | return {"error": f"No data found for stock symbol: {symbol}"}
71 |
72 | df['close'] = df['Close']
73 |
74 | # Calculate RSI
75 | delta = df['close'].diff()
76 | gain = delta.where(delta > 0, 0)
77 | loss = -delta.where(delta < 0, 0)
78 | avg_gain = gain.rolling(window=14).mean()
79 | avg_loss = loss.rolling(window=14).mean()
80 | rs = avg_gain / avg_loss
81 | rsi = 100 - (100 / (1 + rs))
82 | latest_rsi = rsi.iloc[-1]
83 |
84 | ma_50 = df['close'].rolling(window=50).mean().iloc[-1]
85 | latest_volume = df['Volume'].iloc[-1]
86 |
87 | return {
88 | "symbol": symbol.upper(),
89 | "RSI": round(latest_rsi, 2),
90 | "MA_50": round(ma_50, 2),
91 | "Volume": int(latest_volume)
92 | }
93 |
94 |
95 |
--------------------------------------------------------------------------------
/services/recommendation.py:
--------------------------------------------------------------------------------
1 | from .strategy_analyzer import analyze_stock_strategy, analyze_crypto_strategy
2 | import requests
3 |
4 | STOCK_LIST = ["AAPL", "MSFT", "TSLA", "NVDA", "AMZN", "GOOG", "META", "NFLX", "INTC", "AMD"]
5 |
6 | def get_stock_price(symbol: str):
7 | """
8 | Get current stock price and price change from Yahoo Finance API via RapidAPI or other.
9 | (Here we simulate with dummy data or use real API if available)
10 | """
11 | url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
12 | try:
13 | res = requests.get(url)
14 | data = res.json()
15 | price = data["chart"]["result"][0]["meta"]["regularMarketPrice"]
16 | previous_close = data["chart"]["result"][0]["meta"]["previousClose"]
17 | change_percent = ((price - previous_close) / previous_close) * 100
18 | return {
19 | "price": round(price, 2),
20 | "change_percent": round(change_percent, 2)
21 | }
22 | except:
23 | return {"price": None, "change_percent": None}
24 |
25 |
26 | def recommend_top_stocks(count: int = 10):
27 | result = []
28 | for symbol in STOCK_LIST:
29 | analysis = analyze_stock_strategy(symbol) # should include news + technical
30 | if "error" not in analysis:
31 | score = calculate_stock_score(analysis)
32 | price_data = get_stock_price(symbol)
33 |
34 | result.append({
35 | "symbol": symbol,
36 | "score": score,
37 | "price": price_data["price"],
38 | "change_percent": price_data["change_percent"],
39 | "news_sentiment": analysis.get("news_sentiment", ""),
40 | "technical_summary": analysis.get("technical_summary", "")
41 | })
42 |
43 | sorted_result = sorted(result, key=lambda x: x["score"], reverse=True)
44 | return sorted_result[:count]
45 |
46 |
47 | def get_crypto_price(symbol: str):
48 | """
49 | Get current crypto price and 24h change from Binance API.
50 | """
51 | url = f"https://api.binance.com/api/v3/ticker/24hr?symbol={symbol}USDT"
52 | try:
53 | res = requests.get(url)
54 | data = res.json()
55 | price = float(data["lastPrice"])
56 | change_percent = float(data["priceChangePercent"])
57 | return {
58 | "price": round(price, 4),
59 | "change_percent": round(change_percent, 2)
60 | }
61 | except:
62 | return {"price": None, "change_percent": None}
63 |
64 |
65 | def get_top_binance_symbols(limit=20):
66 | url = "https://api.binance.com/api/v3/ticker/24hr"
67 | try:
68 | res = requests.get(url)
69 | data = res.json()
70 | usdt_pairs = [item for item in data if item["symbol"].endswith("USDT")]
71 | sorted_by_volume = sorted(usdt_pairs, key=lambda x: float(x["quoteVolume"]), reverse=True)
72 | top_symbols = [item["symbol"].replace("USDT", "") for item in sorted_by_volume[:limit]]
73 | return top_symbols
74 | except:
75 | return []
76 |
77 |
78 | def recommend_top_cryptos(count: int = 10):
79 | result = []
80 | crypto_symbols = get_top_binance_symbols(limit=30)
81 |
82 | for symbol in crypto_symbols:
83 | analysis = analyze_crypto_strategy(symbol) # should include news + technical
84 | if "error" not in analysis:
85 | score = calculate_crypto_score(analysis)
86 | price_data = get_crypto_price(symbol)
87 |
88 | result.append({
89 | "symbol": symbol,
90 | "score": score,
91 | "price": price_data["price"],
92 | "change_percent": price_data["change_percent"],
93 | "news_sentiment": analysis.get("news_sentiment", ""),
94 | "technical_summary": analysis.get("technical_summary", "")
95 | })
96 |
97 | sorted_result = sorted(result, key=lambda x: x["score"], reverse=True)
98 | return sorted_result[:count]
99 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # UV
98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | #uv.lock
102 |
103 | # poetry
104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | # This is especially recommended for binary packages to ensure reproducibility, and is more
106 | # commonly ignored for libraries.
107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 |
110 | # pdm
111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | # in version control.
115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 |
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 |
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 |
127 | # SageMath parsed files
128 | *.sage.py
129 |
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 |
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 |
143 | # Rope project settings
144 | .ropeproject
145 |
146 | # mkdocs documentation
147 | /site
148 |
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 |
154 | # Pyre type checker
155 | .pyre/
156 |
157 | # pytype static type analyzer
158 | .pytype/
159 |
160 | # Cython debug symbols
161 | cython_debug/
162 |
163 | # PyCharm
164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | # and can be added to the global gitignore or merged into this file. For a more nuclear
167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 |
170 | # Ruff stuff:
171 | .ruff_cache/
172 |
173 | # PyPI configuration file
174 | .pypirc
175 |
--------------------------------------------------------------------------------
/frontend/src/pages/home.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { motion } from 'framer-motion'
3 | import { NewsSentimentOverview } from '../widgets/layout/news_sentiment'
4 | import { MarketTrendCard } from '../widgets/layout/market_trend'
5 | import Footer from '../widgets/layout/footer'
6 | type StatItem = {
7 | name: string
8 | value: string
9 | }
10 |
11 | const stats: StatItem[] = [
12 | { name: 'Analyzed News Articles', value: '120K+' },
13 | { name: 'Market Symbols Tracked', value: '8,500+' },
14 | { name: 'AI Recommendations Made', value: '25K+' },
15 | { name: 'Avg. Daily API Calls', value: '10,000+' },
16 | ]
17 |
18 | export function Home() {
19 | return (
20 |
21 | {/* Banner Section - Limited to one screen height */}
22 |
23 | {/* Background Image */}
24 |
32 |
33 | {/* Decorative Blur Shapes */}
34 |
46 |
47 |
59 |
60 | {/* Main Banner Content */}
61 |
62 |
63 |
64 | TradeSense
65 |
66 |
67 | Empower your trading decisions with AI-driven insights from real-time news and market analysis.
68 | TradeSense helps you understand the market before it moves.
69 |
70 |
71 |
72 |
73 |
74 | {stats.map((stat) => (
75 |
76 |
{stat.name}
77 | {stat.value}
78 |
79 | ))}
80 |
81 |
82 |
83 |
84 |
85 | {/* Sentiment Analysis Section - Separate scrollable area */}
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | )
96 | }
97 |
98 | export default Home
99 |
100 |
101 |
--------------------------------------------------------------------------------
/frontend/src/pages/crypto_news.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import apiService from "../services/api";
3 |
4 | interface Article {
5 | title: string;
6 | original_title?: string;
7 | source: { name: string };
8 | publishedAt: string;
9 | url: string;
10 | azure_sentiment: {
11 | label: string;
12 | confidence_scores: {
13 | positive: number;
14 | neutral: number;
15 | negative: number;
16 | };
17 | };
18 | gpt_analysis: string;
19 | }
20 |
21 | export function CryptoNews() {
22 | const [articles, setArticles] = useState([]);
23 | const [loading, setLoading] = useState(true);
24 | const [error, setError] = useState(null);
25 |
26 | useEffect(() => {
27 | const fetchNews = async () => {
28 | try {
29 | setLoading(true);
30 | setError(null);
31 |
32 | const data = await apiService.directData.getCryptoNews();
33 |
34 | if (data && data.articles) {
35 | setArticles(data.articles);
36 | } else {
37 | setError("Failed to retrieve crypto news.");
38 | }
39 | } catch (err) {
40 | console.error("Failed to fetch crypto news:", err);
41 | setError("An error occurred while loading crypto news.");
42 | } finally {
43 | setLoading(false);
44 | }
45 | };
46 |
47 | fetchNews();
48 | }, []);
49 |
50 | const getSentimentColor = (label: string) => {
51 | switch (label.toLowerCase()) {
52 | case "positive":
53 | return "text-green-400";
54 | case "neutral":
55 | return "text-yellow-400";
56 | case "negative":
57 | return "text-red-400";
58 | default:
59 | return "text-white";
60 | }
61 | };
62 |
63 | if (loading) return Loading crypto news...
;
64 | if (error) return Error: {error}
;
65 | if (articles.length === 0) return No crypto news available
;
66 |
67 | return (
68 |
69 |
70 | Latest Crypto News
71 |
72 |
73 | {articles.map((article, idx) => (
74 |
78 |
79 |
80 | {article.title}
81 |
82 |
83 |
84 | {article.source?.name} ·{" "}
85 | {article.publishedAt
86 | ? new Date(article.publishedAt).toLocaleString()
87 | : "Unknown date"}
88 |
89 |
90 |
91 |
Insight: {" "}
92 | {article.gpt_analysis?.includes("Rate limited")
93 | ?
Insight unavailable due to rate limits.
94 | : article.gpt_analysis || "No analysis available"}
95 | {article.original_title &&
96 | article.original_title !== article.title && (
97 |
98 | ⚠️ Alternate title: {article.original_title}
99 |
100 | )}
101 |
102 |
103 | {article.azure_sentiment && (
104 |
105 |
106 | Positive: {article.azure_sentiment.confidence_scores.positive.toFixed(2)}
107 |
108 |
109 | Neutral: {article.azure_sentiment.confidence_scores.neutral.toFixed(2)}
110 |
111 |
112 | Negative: {article.azure_sentiment.confidence_scores.negative.toFixed(2)}
113 |
114 |
115 | Sentiment: {article.azure_sentiment.label}
116 |
117 |
118 | )}
119 |
120 | ))}
121 |
122 |
123 | );
124 | }
125 |
126 | export default CryptoNews;
127 |
128 |
--------------------------------------------------------------------------------
/frontend/src/pages/stock_news.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import apiService from "../services/api";
3 |
4 | interface Article {
5 | title: string;
6 | original_title?: string;
7 | source: { name: string };
8 | publishedAt: string;
9 | url: string;
10 | azure_sentiment: {
11 | label: string;
12 | confidence_scores: {
13 | positive: number;
14 | neutral: number;
15 | negative: number;
16 | };
17 | };
18 | gpt_analysis: string;
19 | }
20 |
21 | export function StockNews() {
22 | const [articles, setArticles] = useState([]);
23 | const [loading, setLoading] = useState(true);
24 | const [error, setError] = useState(null);
25 |
26 | useEffect(() => {
27 | const fetchNews = async () => {
28 | try {
29 | setLoading(true);
30 | setError(null);
31 |
32 | // 尝试使用直接方法获取新闻数据
33 | const data = await apiService.directData.getBusinessNews();
34 |
35 | if (data && data.articles) {
36 | setArticles(data.articles);
37 | } else {
38 | setError("Failed to retrieve news data.");
39 | }
40 | } catch (err) {
41 | console.error("Failed to fetch news:", err);
42 | setError("An error occurred while loading news.");
43 | } finally {
44 | setLoading(false);
45 | }
46 | };
47 |
48 | fetchNews();
49 | }, []);
50 |
51 | const getSentimentColor = (label: string) => {
52 | switch (label.toLowerCase()) {
53 | case "positive":
54 | return "text-green-400";
55 | case "neutral":
56 | return "text-yellow-400";
57 | case "negative":
58 | return "text-red-400";
59 | default:
60 | return "text-white";
61 | }
62 | };
63 |
64 | if (loading) return Loading business news...
;
65 | if (error) return Error: {error}
;
66 | if (articles.length === 0) return No business news available
;
67 |
68 | return (
69 |
70 |
71 | Latest Business News
72 |
73 |
74 | {articles.map((article, idx) => (
75 |
79 |
80 |
81 | {article.title}
82 |
83 |
84 |
85 | {article.source?.name} ·{" "}
86 | {article.publishedAt
87 | ? new Date(article.publishedAt).toLocaleString()
88 | : "Unknown date"}
89 |
90 |
91 |
92 |
Insight: {" "}
93 | {article.gpt_analysis?.includes("Rate limited")
94 | ?
Insight unavailable due to rate limits.
95 | : article.gpt_analysis || "No analysis available"}
96 | {article.original_title &&
97 | article.original_title !== article.title && (
98 |
99 | ⚠️ Alternate title: {article.original_title}
100 |
101 | )}
102 |
103 |
104 | {article.azure_sentiment && (
105 |
106 |
107 | Positive: {article.azure_sentiment.confidence_scores.positive.toFixed(2)}
108 |
109 |
110 | Neutral: {article.azure_sentiment.confidence_scores.neutral.toFixed(2)}
111 |
112 |
113 | Negative: {article.azure_sentiment.confidence_scores.negative.toFixed(2)}
114 |
115 |
116 | Sentiment: {article.azure_sentiment.label}
117 |
118 |
119 | )}
120 |
121 | ))}
122 |
123 |
124 | );
125 | }
126 |
127 | export default StockNews;
128 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/services/gpt_client.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | from dotenv import load_dotenv
4 | import time
5 | from typing import List, Dict, Optional
6 | import json
7 |
8 | # Load environment variables from .env file
9 | load_dotenv()
10 |
11 | GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
12 | ENDPOINT = "https://models.github.ai/inference"
13 | MODEL_NAME = "openai/gpt-4.1"
14 |
15 | if not GITHUB_TOKEN:
16 | raise ValueError("GITHUB_TOKEN is not set in the .env file")
17 |
18 | headers = {
19 | "Authorization": f"Bearer {GITHUB_TOKEN}",
20 | "Content-Type": "application/json"
21 | }
22 |
23 | # Global variables tracking API call status
24 | LAST_CALL_TIMESTAMP = 0
25 | CALLS_TODAY = 0
26 | MAX_CALLS_PER_DAY = 40 # Safety limit, slightly lower than actual limit to leave margin
27 | CACHE_FILE = "gpt_cache.json"
28 |
29 | # Load cache
30 | def load_gpt_cache() -> Dict:
31 | try:
32 | if os.path.exists(CACHE_FILE):
33 | with open(CACHE_FILE, 'r') as f:
34 | cache_data = json.load(f)
35 |
36 | # Reset daily counter (if it's a new day)
37 | last_date = time.strftime("%Y-%m-%d", time.localtime(cache_data.get("last_timestamp", 0)))
38 | today = time.strftime("%Y-%m-%d")
39 |
40 | if last_date != today:
41 | cache_data["calls_today"] = 0
42 |
43 | global LAST_CALL_TIMESTAMP, CALLS_TODAY
44 | LAST_CALL_TIMESTAMP = cache_data.get("last_timestamp", 0)
45 | CALLS_TODAY = cache_data.get("calls_today", 0)
46 |
47 | return cache_data.get("results", {})
48 | return {}
49 | except Exception as e:
50 | print(f"Error loading GPT cache: {e}")
51 | return {}
52 |
53 | # Save cache
54 | def save_gpt_cache(cache: Dict):
55 | try:
56 | with open(CACHE_FILE, 'w') as f:
57 | cache_data = {
58 | "results": cache,
59 | "last_timestamp": LAST_CALL_TIMESTAMP,
60 | "calls_today": CALLS_TODAY
61 | }
62 | json.dump(cache_data, f)
63 | except Exception as e:
64 | print(f"Error saving GPT cache: {e}")
65 |
66 | # Global cache
67 | gpt_cache = load_gpt_cache()
68 |
69 | def analyze_news_sentiment(news_headlines: List[str]) -> str:
70 | """
71 | Send financial news headlines to a GPT model and receive
72 | a concise, market-oriented explanation for each headline.
73 |
74 | The model is expected to provide analytical interpretations
75 | without performing sentiment classification.
76 |
77 | Args:
78 | news_headlines (List[str]): A list of news titles/headlines.
79 |
80 | Returns:
81 | str: Raw GPT response as a single text block.
82 | """
83 | global LAST_CALL_TIMESTAMP, CALLS_TODAY, gpt_cache
84 |
85 | # Generate cache key
86 | cache_key = "_".join(sorted(news_headlines))[:500] # Limit key length
87 |
88 | # Check cache
89 | if cache_key in gpt_cache:
90 | print("Using cached GPT results")
91 | return gpt_cache[cache_key]
92 |
93 | # Check API call limits
94 | current_time = time.time()
95 | today = time.strftime("%Y-%m-%d")
96 | last_call_date = time.strftime("%Y-%m-%d", time.localtime(LAST_CALL_TIMESTAMP))
97 |
98 | # If it's a new day, reset counter
99 | if last_call_date != today:
100 | CALLS_TODAY = 0
101 |
102 | # Check if daily limit exceeded
103 | if CALLS_TODAY >= MAX_CALLS_PER_DAY:
104 | print(f"Daily API call limit reached ({MAX_CALLS_PER_DAY})")
105 | return "Daily API call limit reached. Here is a general analysis:\n" + "\n".join([
106 | f"{i+1}. {title} - This news may have some impact on the market, please refer to other analysis tools for detailed information."
107 | for i, title in enumerate(news_headlines)
108 | ])
109 |
110 | prompt = (
111 | "Analyze each financial news headline below individually. For each one, provide a market-focused interpretation that highlights potential impact, risks, or opportunities. DO NOT include any introductory text like 'Certainly' or 'Here's my analysis'. DO NOT provide interpretations for multiple headlines in one answer. For each headline, only give the analysis for that specific headline.\n\n"
112 | + "\n".join([f"{i + 1}. {title}" for i, title in enumerate(news_headlines)])
113 | )
114 |
115 | payload = {
116 | "model": MODEL_NAME,
117 | "messages": [
118 | {"role": "system", "content": "You are a concise financial analyst who provides directly actionable market interpretations without introductory phrases. Format your responses with just the number and the analysis."},
119 | {"role": "user", "content": prompt}
120 | ],
121 | "temperature": 0.7,
122 | "max_tokens": 600
123 | }
124 |
125 | try:
126 | # Update call statistics
127 | LAST_CALL_TIMESTAMP = current_time
128 | CALLS_TODAY += 1
129 |
130 | response = requests.post(f"{ENDPOINT}/chat/completions", headers=headers, json=payload)
131 |
132 | if response.status_code == 200:
133 | result = response.json()["choices"][0]["message"]["content"]
134 |
135 | # Cache results
136 | gpt_cache[cache_key] = result
137 | save_gpt_cache(gpt_cache)
138 |
139 | return result
140 | else:
141 | error_msg = f"Error from GPT API: {response.status_code} - {response.text}"
142 | print(error_msg)
143 |
144 | # If rate limit error, reduce counter (since call wasn't successful)
145 | if response.status_code == 429:
146 | CALLS_TODAY -= 1
147 |
148 | raise Exception(error_msg)
149 | except Exception as e:
150 | # Save current state
151 | save_gpt_cache(gpt_cache)
152 | raise e
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/frontend/src/widgets/layout/news_sentiment.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import {
3 | PieChart,
4 | Pie,
5 | Cell,
6 | Legend,
7 | Tooltip,
8 | ResponsiveContainer,
9 | } from "recharts";
10 | import apiService from "../../services/api";
11 |
12 | interface Article {
13 | title: string;
14 | azure_sentiment?: {
15 | label: string;
16 | };
17 | }
18 |
19 | export function NewsSentimentOverview() {
20 | const [sentimentData, setSentimentData] = useState({
21 | positive: 0,
22 | neutral: 0,
23 | negative: 0,
24 | });
25 | const [date, setDate] = useState("");
26 | const [loading, setLoading] = useState(true);
27 | const [error, setError] = useState("");
28 |
29 | useEffect(() => {
30 | const fetchData = async () => {
31 | try {
32 | setLoading(true);
33 | const res = await apiService.news.getBusinessNews();
34 | const articles = res.articles;
35 |
36 | const sentimentCount = getSentimentDistribution(articles);
37 | setSentimentData(sentimentCount);
38 | setDate(new Date().toISOString().split("T")[0]);
39 | setError("");
40 | } catch (err) {
41 | console.error("Failed to fetch news sentiment:", err);
42 | setError("Failed to load data. Please try again later.");
43 | } finally {
44 | setLoading(false);
45 | }
46 | };
47 |
48 | fetchData();
49 | }, []);
50 |
51 | function getSentimentDistribution(articles: Article[]) {
52 | const sentimentCount = { positive: 0, neutral: 0, negative: 0 };
53 |
54 | articles.forEach((article: Article) => {
55 | const label = article.azure_sentiment?.label;
56 | if (label === "positive") sentimentCount.positive += 1;
57 | else if (label === "negative") sentimentCount.negative += 1;
58 | else sentimentCount.neutral += 1;
59 | });
60 |
61 | return sentimentCount;
62 | }
63 |
64 | function getOverallSentiment(data: {
65 | positive: number;
66 | neutral: number;
67 | negative: number;
68 | }) {
69 | const { positive, neutral, negative } = data;
70 | const max = Math.max(positive, neutral, negative);
71 | if (max === positive) return "Positive";
72 | if (max === negative) return "Negative";
73 | return "Neutral";
74 | }
75 |
76 | const sentimentColor = {
77 | Positive: "text-green-400",
78 | Neutral: "text-yellow-400",
79 | Negative: "text-red-400",
80 | };
81 |
82 | const pieData = [
83 | { name: "Positive", value: sentimentData.positive },
84 | { name: "Neutral", value: sentimentData.neutral },
85 | { name: "Negative", value: sentimentData.negative },
86 | ];
87 |
88 | if (loading) {
89 | return (
90 |
91 |
Loading sentiment analysis...
92 |
93 | );
94 | }
95 |
96 | if (error) {
97 | return (
98 |
101 | );
102 | }
103 |
104 | const overallSentiment = getOverallSentiment(sentimentData);
105 |
106 | return (
107 |
108 |
109 | {/* left */}
110 |
111 |
News & Sentiment Analysis
112 |
113 | Market Sentiment:{" "}
114 |
115 | {overallSentiment}
116 |
117 |
118 |
Date: {date}
119 |
120 |
121 | {/* right */}
122 |
123 |
Sentiment Distribution
124 |
125 |
126 |
127 |
136 | |
137 | |
138 | |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | );
149 | }
150 |
151 | export default NewsSentimentOverview;
152 |
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/frontend/src/widgets/layout/market_trend.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import {
4 | LineChart,
5 | Line,
6 | XAxis,
7 | YAxis,
8 | Tooltip,
9 | ResponsiveContainer,
10 | } from "recharts";
11 | import apiService from "../../services/api";
12 |
13 | // Interface for each market item (includes current price and 24h percentage change)
14 | interface MarketItem {
15 | current_price: string;
16 | percentage_change: number;
17 | }
18 |
19 | // Market section (either indices or crypto coins)
20 | interface MarketSection {
21 | [key: string]: MarketItem;
22 | }
23 |
24 | // Stock market data structure
25 | interface StockMarketData {
26 | indices: MarketSection;
27 | avg_trend: number;
28 | }
29 |
30 | // Crypto market data structure
31 | interface CryptoMarketData {
32 | coins: MarketSection;
33 | avg_trend: number;
34 | }
35 |
36 | // Overall market data structure
37 | interface MarketData {
38 | stock_market: StockMarketData;
39 | crypto_market: CryptoMarketData;
40 | }
41 |
42 | // Type guard for distinguishing stock vs crypto market
43 | function isStockMarketData(market: StockMarketData | CryptoMarketData): market is StockMarketData {
44 | return 'indices' in market;
45 | }
46 |
47 | export function MarketTrendCard() {
48 | const [data, setData] = useState(null);
49 | const [loading, setLoading] = useState(true);
50 | const [error, setError] = useState("");
51 | const navigate = useNavigate();
52 |
53 | // Fetch market data on component mount
54 | useEffect(() => {
55 | const fetchMarketData = async () => {
56 | try {
57 | setLoading(true);
58 | const res: MarketData = await apiService.market.getMarketTrend();
59 | setData(res);
60 | setError("");
61 | } catch (err) {
62 | console.error("Failed to fetch market data:", err);
63 | setError("Failed to load market trend data.");
64 | } finally {
65 | setLoading(false);
66 | }
67 | };
68 |
69 | fetchMarketData();
70 | }, []);
71 |
72 | // Render individual market section (stock or crypto)
73 | const renderMarketSection = (
74 | title: string,
75 | market: StockMarketData | CryptoMarketData
76 | ): React.ReactElement => {
77 | const items = isStockMarketData(market) ? market.indices : market.coins;
78 | const isBearish = market.avg_trend < 0;
79 | const label = isBearish ? "Bearish" : "Bullish";
80 | const color = isBearish ? "text-red-400" : "text-green-400";
81 | const route = isStockMarketData(market) ? "/stock" : "/crypto";
82 |
83 | // Prepare data for line chart
84 | const chartData = Object.entries(items).map(([name, item]) => {
85 | const price = parseFloat(item.current_price.replace('$', ''));
86 | return {
87 | name,
88 | value: item.percentage_change,
89 | displayPrice: isNaN(price) ? "N/A" : `$${price.toFixed(2)}`, // Safe price display
90 | };
91 | });
92 |
93 | return (
94 |
95 | {/* Market list */}
96 |
97 |
{title}
98 |
99 | {Object.entries(items).map(([name, item]) => {
100 | const isUp = item.percentage_change >= 0;
101 | const priceColor = isUp ? "text-green-400" : "text-red-400";
102 | const percentColor = priceColor;
103 | const arrow = isUp ? "▲" : "▼";
104 |
105 | const price = parseFloat(item.current_price.replace('$', ''));
106 | const displayPrice = isNaN(price) ? "N/A" : `$${price.toFixed(2)}`;
107 |
108 | return (
109 |
110 | {/* Left: Name */}
111 | {name}
112 |
113 | {/* Middle: Price */}
114 |
115 | {displayPrice}
116 |
117 |
118 | {/* Right: Percentage */}
119 |
120 | {arrow} {item.percentage_change.toFixed(2)}%
121 |
122 |
123 | );
124 | })}
125 |
126 |
127 | {title} Trend: {label} ({market.avg_trend.toFixed(2)}%)
128 |
129 |
navigate(route)}
132 | >
133 | About More
134 |
135 |
136 |
137 | {/* Chart section */}
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 | );
150 | };
151 |
152 | // Loading state
153 | if (loading) {
154 | return (
155 |
156 | Loading Market Trend...
157 |
158 | );
159 | }
160 |
161 | // Error state
162 | if (error || !data) {
163 | return (
164 |
165 | {error}
166 |
167 | );
168 | }
169 |
170 | // Final render
171 | return (
172 |
173 |
Market Trend
174 | {renderMarketSection("Stock Market", data.stock_market)}
175 | {renderMarketSection("Crypto Market", data.crypto_market)}
176 |
177 | );
178 | }
179 |
180 | export default MarketTrendCard;
181 |
182 |
183 |
184 |
185 |
--------------------------------------------------------------------------------
/frontend/src/widgets/layout/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { routes } from '../../routes'
4 | import { AppRoute } from '../../routes'
5 | import {
6 | Dialog,
7 | DialogPanel,
8 | Disclosure,
9 | DisclosureButton,
10 | DisclosurePanel,
11 | } from '@headlessui/react'
12 | import {
13 | Bars3Icon,
14 | XMarkIcon,
15 | ChevronDownIcon,
16 | } from '@heroicons/react/24/outline'
17 |
18 | export function Navbar() {
19 | const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
20 | const [newsMenuOpen, setNewsMenuOpen] = useState(false)
21 |
22 |
23 | useEffect(() => {
24 | if (newsMenuOpen) {
25 | const closeMenu = () => setNewsMenuOpen(false);
26 | document.addEventListener('click', closeMenu);
27 | return () => document.removeEventListener('click', closeMenu);
28 | }
29 | }, [newsMenuOpen]);
30 |
31 | return (
32 |
163 | )
164 | }
165 |
166 | export default Navbar
167 |
168 |
--------------------------------------------------------------------------------
/services/news_analyzer.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from azure.ai.textanalytics import TextAnalyticsClient
3 | from azure.core.credentials import AzureKeyCredential
4 | from dotenv import load_dotenv
5 | from typing import Optional
6 | import os
7 | import time
8 | import hashlib
9 | import json
10 | from datetime import datetime, timedelta
11 | import logging
12 | from services.gpt_client import analyze_news_sentiment # GPT analysis function
13 |
14 | # Load environment variables
15 | load_dotenv()
16 |
17 | AZURE_KEY = os.getenv("AZURE_KEY")
18 | AZURE_ENDPOINT = os.getenv("AZURE_ENDPOINT")
19 | client = TextAnalyticsClient(endpoint=AZURE_ENDPOINT, credential=AzureKeyCredential(AZURE_KEY))
20 |
21 | # Cache settings
22 | CACHE_EXPIRATION_HOURS = 6
23 | NEWS_CACHE_FILE = "news_cache.json"
24 | LAST_API_CALL_TIME = 0
25 | API_CALL_COOLDOWN = 60 # in seconds
26 |
27 | # Global cache dictionary
28 | news_cache = {}
29 |
30 | # Setup logging
31 | logging.basicConfig(level=logging.INFO)
32 |
33 | def load_cache():
34 | """Load news sentiment cache from file."""
35 | global news_cache
36 | try:
37 | if os.path.exists(NEWS_CACHE_FILE):
38 | with open(NEWS_CACHE_FILE, 'r') as f:
39 | news_cache = json.load(f)
40 | except Exception as e:
41 | logging.error(f"Error loading cache: {e}")
42 | news_cache = {}
43 |
44 | def save_cache():
45 | """Save sentiment analysis cache to file."""
46 | try:
47 | with open(NEWS_CACHE_FILE, 'w') as f:
48 | json.dump(news_cache, f)
49 | except Exception as e:
50 | logging.error(f"Error saving cache: {e}")
51 |
52 | # Load cache on startup
53 | load_cache()
54 |
55 | def cache_key(url: str, titles: list) -> str:
56 | """Generate a unique cache key based on URL and news titles."""
57 | content_hash = hashlib.md5(str(titles).encode()).hexdigest()
58 | return f"{url}_{content_hash}"
59 |
60 | def get_cached_response(key: str) -> Optional[dict]:
61 | """Retrieve a valid cache entry if it hasn't expired."""
62 | if key in news_cache:
63 | timestamp = news_cache[key]["timestamp"]
64 | expiration = datetime.fromtimestamp(timestamp) + timedelta(hours=CACHE_EXPIRATION_HOURS)
65 | if datetime.now() < expiration:
66 | return news_cache[key]["data"], news_cache[key]["gpt_results"]
67 | logging.info(f"No valid cache found for key: {key}")
68 | return None
69 |
70 | def analyze_sentiment(documents: list) -> list:
71 | """Use Azure Text Analytics API to analyze sentiment in batches."""
72 | results = []
73 | for i in range(0, len(documents), 10):
74 | batch = documents[i:i + 10]
75 | response = client.analyze_sentiment(batch, language="en")
76 | results.extend([
77 | {"label": doc.sentiment, "confidence_scores": {
78 | "positive": doc.confidence_scores.positive,
79 | "neutral": doc.confidence_scores.neutral,
80 | "negative": doc.confidence_scores.negative
81 | }}
82 | for doc in response
83 | ])
84 | return results
85 |
86 | def parse_gpt_response(gpt_response: str, expected_count: int) -> list:
87 | """Parse GPT response into a list of analysis strings with padding if needed."""
88 | explanations = []
89 |
90 | # Split the response by numbered lines (e.g., "1. ", "2. ", etc.)
91 | lines = gpt_response.strip().split('\n')
92 | current_explanation = ""
93 |
94 | for line in lines:
95 | line = line.strip()
96 | # Skip empty lines
97 | if not line:
98 | continue
99 |
100 | # Skip intro lines that don't start with a number
101 | if not (line[0].isdigit() and ". " in line[:4]):
102 | # Check if this is a continuation of a previous explanation
103 | if current_explanation:
104 | current_explanation += " " + line
105 | continue
106 |
107 | # If we have an existing explanation, add it to the list before starting a new one
108 | if current_explanation:
109 | explanations.append(clean_explanation(current_explanation))
110 |
111 | # Start a new explanation, removing the number prefix
112 | parts = line.split(". ", 1)
113 | if len(parts) > 1:
114 | current_explanation = parts[1]
115 | else:
116 | current_explanation = line
117 |
118 | # Add the last explanation if there is one
119 | if current_explanation:
120 | explanations.append(clean_explanation(current_explanation))
121 |
122 | # Pad or trim to match expected count
123 | if len(explanations) < expected_count:
124 | # Fill missing explanations with placeholder
125 | while len(explanations) < expected_count:
126 | explanations.append("No analysis available.")
127 | elif len(explanations) > expected_count:
128 | explanations = explanations[:expected_count] # Trim to match expected count
129 |
130 | return explanations
131 |
132 | def clean_explanation(text: str) -> str:
133 | """Clean up explanation text to remove unwanted patterns"""
134 | # Remove introductory phrases
135 | patterns_to_remove = [
136 | "Certainly!",
137 | "Here's a market-oriented interpretation",
138 | "Here's my analysis",
139 | "Market analysis:",
140 | "Market interpretation:",
141 | "**Interpretation:**"
142 | ]
143 |
144 | cleaned_text = text
145 | for pattern in patterns_to_remove:
146 | if cleaned_text.startswith(pattern):
147 | cleaned_text = cleaned_text[len(pattern):].strip()
148 |
149 | # Remove headline repetition (often appears when GPT repeats the headline)
150 | if "**" in cleaned_text and cleaned_text.count("**") >= 2:
151 | # Extract content between first set of ** markers
152 | headline_parts = cleaned_text.split("**", 2)
153 | if len(headline_parts) >= 3:
154 | # Check if this looks like a headline
155 | potential_headline = headline_parts[1]
156 | if len(potential_headline.split()) <= 15: # Reasonable headline length
157 | # Remove the headline part
158 | cleaned_text = "".join(headline_parts[2:]).strip()
159 | # If the next part starts with **, remove those too
160 | if cleaned_text.startswith("**"):
161 | cleaned_text = cleaned_text[2:].strip()
162 |
163 | return cleaned_text
164 |
165 | def get_gpt_analysis(titles: list, contents: list) -> tuple:
166 | """Perform GPT sentiment analysis with rate limiting."""
167 | global LAST_API_CALL_TIME
168 | current_time = time.time()
169 | if current_time - LAST_API_CALL_TIME < API_CALL_COOLDOWN:
170 | logging.warning("API rate limit exceeded. Please try again later.")
171 | return "API rate limited", ["Rate limited. Try again later."] * len(titles)
172 |
173 | try:
174 | LAST_API_CALL_TIME = current_time
175 | gpt_raw_response = analyze_news_sentiment(titles)
176 | logging.info(f"GPT Response: {gpt_raw_response}") # Log the raw response
177 | gpt_results = parse_gpt_response(gpt_raw_response, expected_count=len(titles))
178 | return gpt_raw_response, gpt_results
179 | except Exception as e:
180 | logging.error(f"Error during GPT analysis: {e}")
181 | return "API error", ["Unable to analyze news."] * len(titles)
182 |
183 | def fetch_and_analyze_news_by_url(url: str) -> dict:
184 | """Fetch news from a URL and return analyzed results with Azure and GPT sentiment."""
185 | try:
186 | response = requests.get(url)
187 | response.raise_for_status()
188 | news_data = response.json()
189 | if "articles" not in news_data:
190 | logging.error("No 'articles' key in response data.")
191 | return {"error": "No news articles found."}
192 | articles = news_data["articles"]
193 | except requests.exceptions.RequestException as e:
194 | logging.error(f"Error fetching news from {url}: {e}")
195 | return {"error": str(e)}
196 |
197 | titles = [article.get("title", "") for article in articles]
198 | descriptions = [article.get("description", "") for article in articles]
199 | contents = [
200 | f"{desc}\n{content}" if desc else content
201 | for desc, content in zip(descriptions, [article.get("content", "") for article in articles])
202 | ]
203 |
204 | cache_id = cache_key(url, titles)
205 | cached_result = get_cached_response(cache_id)
206 | if cached_result:
207 | return {"articles": cached_result[0]}
208 |
209 | azure_results = analyze_sentiment(titles)
210 | gpt_raw_response, gpt_results = get_gpt_analysis(titles, contents)
211 |
212 | for i, article in enumerate(articles):
213 | article["azure_sentiment"] = azure_results[i] if i < len(azure_results) else {}
214 | article["gpt_analysis"] = gpt_results[i] if i < len(gpt_results) else "No analysis available."
215 |
216 | news_cache[cache_id] = {
217 | "timestamp": time.time(),
218 | "data": articles,
219 | "gpt_results": gpt_results
220 | }
221 | save_cache()
222 |
223 | return {"articles": articles}
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TradeSense: AI Trade Agent
2 |
3 | TradeSense is an AI-powered trading assistant that helps users analyze financial news, market trends, and provides intelligent investment insights.
4 |
5 | 
6 |
7 | ## Background + Overview
8 |
9 | Making smart investments shouldn’t require constant monitoring of volatile markets or spending hours reading scattered financial news. TradeSense is designed to simplify that process with real-time, AI-powered trading insights based on news sentiment, technical analysis, and live market data.
10 |
11 | Imagine opening your dashboard and instantly understanding the market mood, key opportunities, and smart strategies—all backed by real-time data and intelligent recommendations. With TradeSense, trading becomes more informed, focused, and accessible.
12 |
13 | 
14 |
15 | 
16 |
17 | ### The Problem:
18 |
19 | - **Too Much Noise**: Investors are overwhelmed by fragmented news sources and conflicting signals.
20 | - **Lack of Personalization**: Most tools don’t adapt to individual risk preferences or investment styles.
21 | - **Outdated Tools**: Traditional market analysis is slow and reactive, not predictive.
22 |
23 | ### Our Solution:
24 |
25 | Powered by cutting-edge AI models and real-time financial data, TradeSense delivers personalized, intelligent trading insights across both crypto and stock markets. Whether you're a new investor overwhelmed by market noise or an experienced trader looking to enhance your strategy, TradeSense helps you make smarter, faster decisions.
26 |
27 | 
28 |
29 | By combining live news sentiment analysis with technical indicators like RSI, MA20, MA120, and volume trends, TradeSense adapts continuously to changing market conditions—ensuring every recommendation is timely, relevant, and actionable.
30 |
31 | ## Features
32 |
33 | - **News Analysis**
34 | Analyzes financial news in real time to extract sentiment, identify key topics, and surface relevant market insights.
35 |
36 | - **Technical Analysis**
37 | Provides essential technical indicators such as RSI, MA20, MA120, and volume analytics, along with basic chart pattern recognition.
38 |
39 | - **Strategy Evaluation**
40 | Evaluates market conditions and suggests trading strategies based on data-driven insights and sentiment signals.
41 |
42 | - **Market Insights**
43 | Offers real-time stock and crypto market data, highlighting major movements and emerging trends.
44 |
45 | - **Personalized Recommendations**
46 | Delivers tailored investment suggestions based on user preferences, live data, and AI-driven analysis.
47 |
48 | ## How It Works
49 |
50 | TradeSense combines real-time data retrieval, AI-powered analysis, and personalized strategy generation to deliver actionable trading insights. Here's how the system works:
51 |
52 | - **News Aggregation**
53 | The system continuously fetches and filters financial news from reliable sources across the stock and crypto markets.
54 |
55 | 
56 |
57 | - **Sentiment Analysis**
58 | AI models analyze the news content to detect market sentiment (positive, neutral, negative) and identify key financial signals.
59 |
60 | - **Strategy Evaluation**
61 | Evaluates market conditions and suggests trading strategies based on data-driven insights and sentiment signals.
62 |
63 | - **Technical Indicator Calculation**
64 | Using APIs (e.g., Binance, Yahoo Finance), TradeSense calculates indicators such as RSI, MA20, MA120, and volume trends for selected assets.
65 |
66 | - **Strategy & Trend Evaluation**
67 | Based on combined news sentiment and technical signals, the system generates strategy suggestions and highlights short-term trends.
68 |
69 | - **Personalized Recommendations**
70 | Finally, TradeSense tailors its output based on user preferences or portfolio interests, delivering concise, actionable recommendations via dashboard or chat.
71 |
72 | ## Project Structure
73 |
74 | ```
75 | ├── main.py # FastAPI application entry point
76 | ├── routes/ # API route definitions
77 | │ ├── news.py # News analysis endpoints
78 | │ ├── market.py # Market data endpoints
79 | │ ├── technical.py # Technical analysis endpoints
80 | │ ├── strategy.py # Strategy evaluation endpoints
81 | │ └── recommend.py # Recommendation endpoints
82 | ├── services/ # Business logic services
83 | │ ├── news_analyzer.py # News processing and analysis
84 | │ ├── technical_analysis.py # Technical indicators and patterns
85 | │ ├── strategy_analyzer.py # Strategy evaluation logic
86 | │ ├── trend_analyzer.py # Market trend analysis
87 | │ ├── recommendation.py # Recommendation generation
88 | │ └── gpt_client.py # GPT integration for analysis
89 | ├── frontend/ # React frontend application
90 | │ ├── src/ # Frontend source code
91 | │ └── public/ # Static assets
92 | └── requirements.txt # Python dependencies
93 | ```
94 |
95 | ## Setup Instructions
96 |
97 | ### Backend Setup
98 |
99 | 1. Clone the repository:
100 | ```bash
101 | git clone https://github.com/yourusername/ai-trade-agent.git
102 | cd ai-trade-agent
103 | ```
104 |
105 | 2. Create and activate a virtual environment:
106 | ```bash
107 | python -m venv .venv
108 | # On Windows
109 | .venv\Scripts\activate
110 | # On macOS/Linux
111 | source .venv/bin/activate
112 | ```
113 |
114 | 3. Install dependencies:
115 | ```bash
116 | pip install -r requirements.txt
117 | ```
118 |
119 | 4. Start the backend server:
120 | ```bash
121 | uvicorn main:app --reload
122 | ```
123 | The API will be available at http://localhost:8000
124 |
125 | ### Frontend Setup
126 |
127 | 1. Navigate to the frontend directory:
128 | ```bash
129 | cd frontend
130 | ```
131 |
132 | 2. Install dependencies:
133 | ```bash
134 | npm install
135 | ```
136 |
137 | 3. Start the development server:
138 | ```bash
139 | npm run dev
140 | ```
141 | The frontend will be available at http://localhost:5173
142 |
143 | ## API Endpoints
144 |
145 | - `GET /news` - Get analyzed financial news
146 | - `GET /market` - Get market data
147 | - `GET /technical` - Get technical analysis for specific securities
148 | - `GET /strategy` - Get strategy evaluations and recommendations
149 | - `GET /recommend` - Get personalized investment recommendations
150 |
151 | ## Technologies Used
152 | Here’s what powers the intelligent trading experience behind **TradeSense**:
153 |
154 | - **FastAPI ⚡**
155 | A high-performance web framework for handling API requests efficiently, enabling real-time data access and interaction with the AI trade agent.
156 |
157 | - **React + TypeScript + Tailwind CSS 💻**
158 | A modern, responsive front-end stack that ensures a smooth user interface with clean visuals, dynamic updates, and a great dashboard experience.
159 |
160 | - **Azure AI Agent Service 🤖**
161 | Powers intelligent interactions and trading recommendations using cutting-edge GPT models tailored for financial context and dialogue.
162 |
163 | - **Azure Text Analytics 🧠**
164 | Extracts sentiment and key insights from financial news, turning unstructured text into actionable intelligence.
165 |
166 | - **Redis 🔄**
167 | Used for caching frequently accessed data like market summaries or news sentiment results, improving system responsiveness.
168 |
169 | 
170 |
171 | ## Insights Deep Dive
172 |
173 | ### Real-Time Smart Analysis
174 | TradeSense retrieves the latest financial news and market data based on user input (e.g., BTC or a specific stock), and uses AI models to analyze current sentiment and trends. For example, when a user enters “AAPL?”, the system combines sentiment analysis and technical indicators to generate a real-time market summary and actionable insight.
175 |
176 | ### GPT-Powered Summaries
177 | Powered by Azure AI Agent Service with GPT models, TradeSense produces concise and insightful trend summaries.
178 | **Example output**: *“Lower stock futures signal cautious sentiment as investors await earnings reports, company results and outlooks.”*
179 |
180 | 
181 |
182 | ### Multimodal Insights
183 | By combining news sentiment, market movement (e.g., price and percent change), and technical indicators such as RSI, MA20, and MA120, TradeSense provides a comprehensive market perspective. This multi-signal approach helps users make better-informed decisions and avoid relying on a single indicator.
184 |
185 | 
186 |
187 |
188 | ## Contributing
189 |
190 | 1. Fork the repository
191 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
192 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
193 | 4. Push to the branch (`git push origin feature/amazing-feature`)
194 | 5. Open a Pull Request
195 |
196 | ## License
197 |
198 | This project is licensed under the MIT License - see the LICENSE file for details.
199 |
--------------------------------------------------------------------------------
/frontend/src/pages/stock.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import apiService from "../services/api";
3 |
4 | interface Article {
5 | title: string;
6 | gpt_analysis: "Positive" | "Negative";
7 | }
8 |
9 | interface TechnicalIndicators {
10 | RSI: string | number;
11 | MA_50: string | number;
12 | Volume: string | number;
13 | }
14 |
15 | interface SentimentData {
16 | articles: Article[];
17 | }
18 |
19 | interface StrategyResponse {
20 | technical_indicators: TechnicalIndicators;
21 | articles?: Article[];
22 | sentiment_data?: SentimentData;
23 | positive_sentiment: number;
24 | negative_sentiment: number;
25 | final_signal: "Buy" | "Sell" | "Hold";
26 | error?: string;
27 | }
28 |
29 | interface StockRecommendation {
30 | symbol: string;
31 | name: string;
32 | final_signal: "Buy" | "Sell" | "Hold";
33 | price?: number;
34 | change?: number;
35 | }
36 |
37 | export function Stock() {
38 | const [symbol, setSymbol] = useState("");
39 | const [loading, setLoading] = useState(false);
40 | const [result, setResult] = useState(null);
41 | const [error, setError] = useState(null);
42 | const [stockRecommendations, setStockRecommendations] = useState([
43 | {
44 | symbol: "AAPL",
45 | name: "Apple Inc.",
46 | final_signal: "Buy",
47 | price: 180.25,
48 | change: 1.25
49 | },
50 | {
51 | symbol: "MSFT",
52 | name: "Microsoft Corporation",
53 | final_signal: "Buy",
54 | price: 350.80,
55 | change: 0.75
56 | },
57 | {
58 | symbol: "GOOGL",
59 | name: "Alphabet Inc.",
60 | final_signal: "Hold",
61 | price: 140.10,
62 | change: -0.5
63 | },
64 | {
65 | symbol: "AMZN",
66 | name: "Amazon.com Inc.",
67 | final_signal: "Buy",
68 | price: 125.30,
69 | change: 2.15
70 | },
71 | {
72 | symbol: "TSLA",
73 | name: "Tesla Inc.",
74 | final_signal: "Sell",
75 | price: 220.45,
76 | change: -3.20
77 | }
78 | ]);
79 |
80 | // 页面加载时尝试获取推荐股票
81 | useEffect(() => {
82 | const fetchRecommendations = async () => {
83 | try {
84 | // 尝试使用直接方法获取数据
85 | const stocksData = await apiService.directData.getTopStocks();
86 | if (stocksData && stocksData.length > 0) {
87 | setStockRecommendations(stocksData);
88 | }
89 | } catch (err) {
90 | console.error("Failed to load stock recommendations:", err);
91 | // 保留默认数据,不做任何改变
92 | }
93 | };
94 |
95 | fetchRecommendations();
96 | }, []);
97 |
98 | // 点击按钮后获取用户输入股票的分析数据
99 | const fetchStrategy = async () => {
100 | setLoading(true);
101 | setError(null);
102 | setResult(null);
103 |
104 | try {
105 | const strategyRes = await apiService.strategy.getStrategyWithParams(symbol, false);
106 | setResult(strategyRes);
107 | } catch (err) {
108 | setError("Failed to fetch strategy data.");
109 | } finally {
110 | setLoading(false);
111 | }
112 | };
113 |
114 | return (
115 |
116 |
117 |
Stock Recommendation
118 |
119 | {/* 股票推荐部分 - 表格形式 */}
120 |
121 |
Top Stock Recommendations
122 |
123 | {stockRecommendations.length > 0 ? (
124 |
125 |
126 |
127 |
128 | Symbol
129 | Name
130 | Price
131 | Change
132 | Signal
133 |
134 |
135 |
136 | {stockRecommendations.map((stock, i) => (
137 |
138 | {stock.symbol}
139 | {stock.name}
140 | ${stock.price !== undefined ? stock.price.toFixed(2) : "N/A"}
141 | = 0 ? 'text-green-500' : 'text-red-500'}`}>
142 | {stock.change !== undefined ? `${stock.change >= 0 ? '+' : ''}${stock.change.toFixed(2)}%` : 'N/A'}
143 |
144 |
145 |
153 | {stock.final_signal}
154 |
155 |
156 |
157 | ))}
158 |
159 |
160 |
161 | ) : (
162 |
No recommendations available.
163 | )}
164 |
165 |
166 | {/* 股票分析输入框 */}
167 |
168 |
Analyze a Stock
169 |
170 | setSymbol(e.target.value)}
175 | className="border border-gray-700 bg-gray-800 text-white rounded p-2 flex-grow"
176 | />
177 |
181 | Submit
182 |
183 |
184 |
185 |
186 | {/* 加载或错误提示 */}
187 | {loading &&
Loading...
}
188 | {error &&
{error}
}
189 |
190 | {/* 分析结果展示 */}
191 | {result && !result.error && (
192 |
193 |
194 |
Technical Indicators
195 |
196 | RSI: {result.technical_indicators.RSI}
197 | MA_50: {result.technical_indicators.MA_50}
198 | Volume: {result.technical_indicators.Volume}
199 |
200 |
201 |
202 |
203 |
News Sentiment
204 |
205 | {(result.articles ?? result.sentiment_data?.articles)?.map((a, i) => (
206 |
207 | {a.title} -{" "}
208 |
215 | {a.gpt_analysis}
216 |
217 |
218 | ))}
219 |
220 |
221 | Positive Sentiment: {(result.positive_sentiment * 100).toFixed(1)}% | Negative Sentiment:{" "}
222 | {(result.negative_sentiment * 100).toFixed(1)}%
223 |
224 |
225 |
226 |
227 | Final Signal:{" "}
228 |
237 | {result.final_signal}
238 |
239 |
240 |
241 | )}
242 |
243 | {result?.error && (
244 |
❌ {result.error}
245 | )}
246 |
247 |
248 |
249 | );
250 | }
251 |
252 | export default Stock;
253 |
254 |
--------------------------------------------------------------------------------
/frontend/src/pages/crypto.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import apiService from "../services/api";
3 |
4 | interface Article {
5 | title: string;
6 | gpt_analysis: "Positive" | "Negative";
7 | }
8 |
9 | interface TechnicalIndicators {
10 | RSI: string | number;
11 | MA_20: string | number;
12 | Volume: string | number;
13 | }
14 |
15 | interface SentimentData {
16 | articles: Article[];
17 | }
18 |
19 | interface StrategyResponse {
20 | technical_indicators: TechnicalIndicators;
21 | articles?: Article[];
22 | sentiment_data?: SentimentData;
23 | positive_sentiment: number;
24 | negative_sentiment: number;
25 | final_signal: "Buy" | "Sell" | "Hold";
26 | error?: string;
27 | }
28 |
29 | interface CryptoRecommendation {
30 | symbol: string;
31 | name: string;
32 | final_signal: "Buy" | "Sell" | "Hold";
33 | price?: number;
34 | change?: number;
35 | }
36 |
37 | export function Crypto() {
38 | const [symbol, setSymbol] = useState("");
39 | const [loading, setLoading] = useState(false);
40 | const [result, setResult] = useState(null);
41 | const [error, setError] = useState(null);
42 | const [cryptoRecommendations, setCryptoRecommendations] = useState([
43 | {
44 | symbol: "BTC",
45 | name: "Bitcoin",
46 | final_signal: "Buy",
47 | price: 65340.75,
48 | change: 2.45
49 | },
50 | {
51 | symbol: "ETH",
52 | name: "Ethereum",
53 | final_signal: "Buy",
54 | price: 3560.20,
55 | change: 1.35
56 | },
57 | {
58 | symbol: "BNB",
59 | name: "Binance Coin",
60 | final_signal: "Hold",
61 | price: 580.50,
62 | change: -0.75
63 | },
64 | {
65 | symbol: "SOL",
66 | name: "Solana",
67 | final_signal: "Buy",
68 | price: 140.65,
69 | change: 4.20
70 | },
71 | {
72 | symbol: "XRP",
73 | name: "Ripple",
74 | final_signal: "Sell",
75 | price: 0.58,
76 | change: -2.10
77 | }
78 | ]);
79 |
80 | // 页面加载时尝试获取推荐加密货币
81 | useEffect(() => {
82 | const fetchRecommendations = async () => {
83 | try {
84 | // 尝试使用直接方法获取数据
85 | const cryptosData = await apiService.directData.getTopCryptos();
86 | if (cryptosData && cryptosData.length > 0) {
87 | setCryptoRecommendations(cryptosData);
88 | }
89 | } catch (err) {
90 | console.error("Failed to load crypto recommendations:", err);
91 | // 保留默认数据,不做任何改变
92 | }
93 | };
94 |
95 | fetchRecommendations();
96 | }, []);
97 |
98 | // 点击按钮后获取用户输入加密货币的分析数据
99 | const fetchStrategy = async () => {
100 | setLoading(true);
101 | setError(null);
102 | setResult(null);
103 |
104 | try {
105 | const strategyRes = await apiService.strategy.getStrategyWithParams(symbol, true); // true表示这是加密货币
106 | setResult(strategyRes);
107 | } catch (err) {
108 | setError("Failed to fetch strategy data.");
109 | } finally {
110 | setLoading(false);
111 | }
112 | };
113 |
114 | return (
115 |
116 |
117 |
Crypto Recommendation
118 |
119 | {/* 加密货币推荐部分 - 表格形式 */}
120 |
121 |
Top Crypto Recommendations
122 |
123 | {cryptoRecommendations.length > 0 ? (
124 |
125 |
126 |
127 |
128 | Symbol
129 | Name
130 | Price
131 | Change
132 | Signal
133 |
134 |
135 |
136 | {cryptoRecommendations.map((crypto, i) => (
137 |
138 | {crypto.symbol}
139 | {crypto.name}
140 | ${crypto.price !== undefined ? crypto.price.toFixed(2) : "N/A"}
141 | = 0 ? 'text-green-500' : 'text-red-500'}`}>
142 | {crypto.change !== undefined ? `${crypto.change >= 0 ? '+' : ''}${crypto.change.toFixed(2)}%` : 'N/A'}
143 |
144 |
145 |
153 | {crypto.final_signal}
154 |
155 |
156 |
157 | ))}
158 |
159 |
160 |
161 | ) : (
162 |
No recommendations available.
163 | )}
164 |
165 |
166 | {/* 加密货币分析输入框 */}
167 |
168 |
Analyze a Cryptocurrency
169 |
170 | setSymbol(e.target.value)}
175 | className="border border-gray-700 bg-gray-800 text-white rounded p-2 flex-grow"
176 | />
177 |
181 | Submit
182 |
183 |
184 |
185 |
186 | {/* 加载或错误提示 */}
187 | {loading &&
Loading...
}
188 | {error &&
{error}
}
189 |
190 | {/* 分析结果展示 */}
191 | {result && !result.error && (
192 |
193 |
194 |
Technical Indicators
195 |
196 | RSI: {result.technical_indicators.RSI}
197 | MA_20: {result.technical_indicators.MA_20}
198 | Volume: {result.technical_indicators.Volume}
199 |
200 |
201 |
202 |
203 |
News Sentiment
204 |
205 | {(result.articles ?? result.sentiment_data?.articles)?.map((a, i) => (
206 |
207 | {a.title} -{" "}
208 |
215 | {a.gpt_analysis}
216 |
217 |
218 | ))}
219 |
220 |
221 | Positive Sentiment: {(result.positive_sentiment * 100).toFixed(1)}% | Negative Sentiment:{" "}
222 | {(result.negative_sentiment * 100).toFixed(1)}%
223 |
224 |
225 |
226 |
227 | Final Signal:{" "}
228 |
237 | {result.final_signal}
238 |
239 |
240 |
241 | )}
242 |
243 | {result?.error && (
244 |
❌ {result.error}
245 | )}
246 |
247 |
248 |
249 | );
250 | }
251 |
252 | export default Crypto;
--------------------------------------------------------------------------------
/services/strategy_analyzer.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import pandas as pd
3 | import numpy as np
4 | from datetime import datetime, timedelta
5 | import yfinance as yf # for stock data
6 | from services.technical_analysis import get_crypto_technical_indicator, get_stock_technical_indicator
7 |
8 | # --- Sentiment Analysis (Real Implementation) ---
9 | def analyze_crypto_news_sentiment(symbol: str):
10 | """
11 | Analyze the sentiment of the latest news articles for a given cryptocurrency symbol.
12 | Returns sentiment analysis results from real news API.
13 | """
14 | import os
15 | from services.news_analyzer import fetch_and_analyze_news_by_url
16 |
17 | NEWS_API_KEY = os.getenv("NEWS_API_KEY")
18 | # Get news specific to the crypto symbol
19 | url = f"https://newsapi.org/v2/everything?q={symbol} crypto&language=en&sortBy=publishedAt&apiKey={NEWS_API_KEY}"
20 |
21 | results = fetch_and_analyze_news_by_url(url)
22 |
23 | if "error" in results:
24 | # Fallback to default data if API call fails
25 | articles = [
26 | {"title": f"{symbol} market analysis", "gpt_analysis": "Positive"},
27 | {"title": f"{symbol} price prediction", "gpt_analysis": "Neutral"},
28 | {"title": f"{symbol} trading volume increases", "gpt_analysis": "Positive"},
29 | ]
30 | return {"symbol": symbol, "articles": articles}
31 |
32 | # Transform API results to match expected format
33 | simplified_articles = []
34 | for article in results.get("articles", [])[:5]: # Limit to 5 articles
35 | sentiment = "Positive"
36 | if article.get("azure_sentiment") and article["azure_sentiment"].get("label"):
37 | azure_label = article["azure_sentiment"]["label"].lower()
38 | if azure_label == "negative":
39 | sentiment = "Negative"
40 | elif azure_label == "neutral":
41 | # For neutral, look at confidence scores to decide
42 | if article["azure_sentiment"].get("confidence_scores"):
43 | scores = article["azure_sentiment"]["confidence_scores"]
44 | if scores.get("negative", 0) > scores.get("positive", 0):
45 | sentiment = "Negative"
46 |
47 | simplified_articles.append({
48 | "title": article.get("title", ""),
49 | "gpt_analysis": sentiment
50 | })
51 |
52 | return {"symbol": symbol, "articles": simplified_articles}
53 |
54 | def analyze_stock_news_sentiment(symbol: str):
55 | """
56 | Analyze the sentiment of the latest news articles for a given stock symbol.
57 | Returns sentiment analysis results from real news API.
58 | """
59 | import os
60 | from services.news_analyzer import fetch_and_analyze_news_by_url
61 |
62 | NEWS_API_KEY = os.getenv("NEWS_API_KEY")
63 | # Get news specific to the stock symbol
64 | url = f"https://newsapi.org/v2/everything?q={symbol} stock&language=en&sortBy=publishedAt&apiKey={NEWS_API_KEY}"
65 |
66 | results = fetch_and_analyze_news_by_url(url)
67 |
68 | if "error" in results:
69 | # Fallback to default data if API call fails
70 | articles = [
71 | {"title": f"{symbol} earnings report", "gpt_analysis": "Positive"},
72 | {"title": f"{symbol} market outlook", "gpt_analysis": "Neutral"},
73 | {"title": f"{symbol} company news", "gpt_analysis": "Positive"},
74 | ]
75 | return {"symbol": symbol, "articles": articles}
76 |
77 | # Transform API results to match expected format
78 | simplified_articles = []
79 | for article in results.get("articles", [])[:5]: # Limit to 5 articles
80 | sentiment = "Positive"
81 | if article.get("azure_sentiment") and article["azure_sentiment"].get("label"):
82 | azure_label = article["azure_sentiment"]["label"].lower()
83 | if azure_label == "negative":
84 | sentiment = "Negative"
85 | elif azure_label == "neutral":
86 | # For neutral, look at confidence scores to decide
87 | if article["azure_sentiment"].get("confidence_scores"):
88 | scores = article["azure_sentiment"]["confidence_scores"]
89 | if scores.get("negative", 0) > scores.get("positive", 0):
90 | sentiment = "Negative"
91 |
92 | simplified_articles.append({
93 | "title": article.get("title", ""),
94 | "gpt_analysis": sentiment
95 | })
96 |
97 | return {"symbol": symbol, "articles": simplified_articles}
98 |
99 | # --- Strategy Analysis ---
100 | def generate_strategy_signal(symbol: str, is_crypto: bool):
101 | """
102 | Generate a strategy signal based on technical indicators and sentiment analysis.
103 | """
104 | # Fetch technical indicators
105 | if is_crypto:
106 | tech_indicators = get_crypto_technical_indicator(symbol)
107 | else:
108 | tech_indicators = get_stock_technical_indicator(symbol)
109 |
110 | if "error" in tech_indicators:
111 | return tech_indicators
112 |
113 | rsi = tech_indicators.get("RSI")
114 | ma_20 = tech_indicators.get("MA_20" if is_crypto else "MA_50")
115 | ma_120 = tech_indicators.get("MA_120" if is_crypto else "MA_50")
116 | volume = tech_indicators.get("Volume")
117 |
118 | # Generate technical signals
119 | buy_signal = False
120 | sell_signal = False
121 |
122 | # RSI Signal
123 | if rsi < 30:
124 | buy_signal = True
125 | elif rsi > 70:
126 | sell_signal = True
127 |
128 | # Moving Average Signal (Bullish crossover: MA_20 > MA_120)
129 | if ma_20 > ma_120:
130 | buy_signal = True
131 | elif ma_20 < ma_120:
132 | sell_signal = True
133 |
134 | # Volume Signal (Check if volume is above average)
135 | avg_volume = volume # You could implement a rolling average for volume
136 | if volume > avg_volume * 1.5: # Example: Volume spike threshold
137 | if not sell_signal: # Avoid double sell signal if RSI and MA are not confirming
138 | sell_signal = True
139 |
140 | # Fetch news sentiment
141 | if is_crypto:
142 | sentiment_data = analyze_crypto_news_sentiment(symbol)
143 | else:
144 | sentiment_data = analyze_stock_news_sentiment(symbol)
145 |
146 | if "articles" not in sentiment_data:
147 | return {"error": "No news articles found."}
148 |
149 | positive_sentiment = sum(1 for article in sentiment_data["articles"] if article["gpt_analysis"] == "Positive") / len(sentiment_data["articles"])
150 | negative_sentiment = 1 - positive_sentiment
151 |
152 | # Combine sentiment and technical signals
153 | if positive_sentiment > 0.6: # Positive sentiment threshold
154 | buy_signal = True
155 | elif negative_sentiment > 0.6: # Negative sentiment threshold
156 | sell_signal = True
157 |
158 | # Combine Signals
159 | if buy_signal and sell_signal:
160 | final_signal = "Hold" # If conflicting signals
161 | elif buy_signal:
162 | final_signal = "Buy"
163 | elif sell_signal:
164 | final_signal = "Sell"
165 | else:
166 | final_signal = "Hold" # No clear signal
167 |
168 | return {
169 | "symbol": symbol,
170 | "buy_signal": buy_signal,
171 | "sell_signal": sell_signal,
172 | "final_signal": final_signal,
173 | "technical_indicators": tech_indicators,
174 | "positive_sentiment": positive_sentiment,
175 | "negative_sentiment": negative_sentiment
176 | }
177 |
178 | # --- Main endpoints that will be called by routes ---
179 | def analyze_stock_strategy(symbol: str):
180 | """
181 | Analyze a stock symbol and generate a trading strategy recommendation.
182 | This function is used by the API endpoints.
183 | """
184 | return generate_strategy_signal(symbol, is_crypto=False)
185 |
186 | def analyze_crypto_strategy(symbol: str):
187 | """
188 | Analyze a cryptocurrency symbol and generate a trading strategy recommendation.
189 | This function is used by the API endpoints.
190 | """
191 | return generate_strategy_signal(symbol, is_crypto=True)
192 |
193 | # --- Strategy Recommendations for Multiple Assets ---
194 | def get_top_stock_symbols():
195 | """
196 | Fetch the top 10 stock symbols that have the highest potential based on technical and sentiment analysis.
197 | """
198 | stock_symbols = ["AAPL", "MSFT", "GOOG", "AMZN", "TSLA", "NVDA", "META", "SPY", "AMD", "NFLX"] # Sample list
199 | stock_recommendations = []
200 |
201 | for symbol in stock_symbols:
202 | strategy = generate_strategy_signal(symbol, is_crypto=False)
203 | if strategy.get("final_signal") == "Buy":
204 | stock_recommendations.append(strategy)
205 |
206 | return stock_recommendations[:10] # Return top 10 recommendations
207 |
208 | def get_top_crypto_symbols():
209 | """
210 | Fetch the top 10 cryptocurrency symbols that have the highest potential based on technical and sentiment analysis.
211 | """
212 | crypto_symbols = ["BTC", "ETH", "BNB", "ADA", "SOL", "XRP", "DOGE", "DOT", "LTC", "MATIC"] # Sample list
213 | crypto_recommendations = []
214 |
215 | for symbol in crypto_symbols:
216 | strategy = generate_strategy_signal(symbol, is_crypto=True)
217 | if strategy.get("final_signal") == "Buy":
218 | crypto_recommendations.append(strategy)
219 |
220 | return crypto_recommendations[:10] # Return top 10 recommendations
221 |
222 | # --- Main endpoints to display recommendations ---
223 | def get_recommended_stocks():
224 | """
225 | Return a list of recommended stocks based on technical and sentiment analysis.
226 | """
227 | recommendations = get_top_stock_symbols()
228 |
229 | # If no recommendations found, return some default stocks with real-time data
230 | if not recommendations:
231 | # Default stocks to show when no recommendations are available (10个知名股票)
232 | default_symbols = ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN", "TSLA", "META", "JPM", "V", "WMT"]
233 | default_recommendations = []
234 |
235 | for symbol in default_symbols:
236 | try:
237 | # Get real-time data using yfinance
238 | stock = yf.Ticker(symbol)
239 | hist = stock.history(period="3mo") # Get 3 months of history
240 |
241 | if len(hist) > 0:
242 | # Calculate technical indicators
243 | close_prices = hist['Close']
244 | volume = hist['Volume']
245 |
246 | # Calculate RSI (14-day)
247 | delta = close_prices.diff()
248 | gain = delta.where(delta > 0, 0).rolling(window=14).mean()
249 | loss = -delta.where(delta < 0, 0).rolling(window=14).mean()
250 | rs = gain / loss
251 | rsi = 100 - (100 / (1 + rs)).iloc[-1] # Get latest RSI
252 |
253 | # Calculate 50-day Moving Average
254 | ma_50 = close_prices.rolling(window=50).mean().iloc[-1]
255 |
256 | # Get latest volume
257 | latest_volume = volume.iloc[-1]
258 |
259 | # Determine signal based on actual indicators
260 | signal = "Hold" # Default signal
261 | if rsi < 30:
262 | signal = "Buy" # Oversold condition
263 | elif rsi > 70:
264 | signal = "Sell" # Overbought condition
265 | else:
266 | # Check if price is above MA
267 | latest_price = close_prices.iloc[-1]
268 | if latest_price > ma_50:
269 | signal = "Buy"
270 |
271 | # Add to recommendations
272 | default_recommendations.append({
273 | "symbol": symbol,
274 | "name": stock.info.get("shortName", symbol),
275 | "final_signal": signal,
276 | "technical_indicators": {
277 | "RSI": round(rsi, 2),
278 | "MA_50": round(ma_50, 2),
279 | "Volume": int(latest_volume)
280 | }
281 | })
282 | except Exception as e:
283 | print(f"Error fetching data for {symbol}: {e}")
284 | # Fallback in case of API error
285 | default_recommendations.append({
286 | "symbol": symbol,
287 | "name": symbol,
288 | "final_signal": "Hold",
289 | "technical_indicators": {
290 | "RSI": "N/A",
291 | "MA_50": "N/A",
292 | "Volume": "N/A"
293 | }
294 | })
295 |
296 | return default_recommendations
297 |
298 | return recommendations
299 |
300 | def get_recommended_cryptos():
301 | """
302 | Return a list of recommended cryptocurrencies based on technical and sentiment analysis.
303 | """
304 | return get_top_crypto_symbols()
305 |
306 | # You can call the above functions to get the list of top 10 stocks or cryptos for recommendations
307 |
308 |
--------------------------------------------------------------------------------
/frontend/src/services/api.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | // Define types
4 | interface MarketData {
5 | symbol: string;
6 | timeframe?: string;
7 | indicators?: string[];
8 | }
9 |
10 | // Base API URL
11 | const API_URL = "http://localhost:8000";
12 |
13 | // Create axios instance
14 | const apiClient = axios.create({
15 | baseURL: API_URL,
16 | headers: {
17 | "Content-Type": "application/json",
18 | },
19 | });
20 |
21 | // Handle request interceptor (e.g., for adding auth headers)
22 | apiClient.interceptors.request.use(
23 | (config) => {
24 | // You can add an auth token here if needed
25 | return config;
26 | },
27 | (error) => {
28 | return Promise.reject(error);
29 | }
30 | );
31 |
32 | // API service object
33 | const apiService = {
34 | // News related API
35 | news: {
36 | // Get business news
37 | getBusinessNews: async () => {
38 | try {
39 | const response = await apiClient.get("/news");
40 | return response.data;
41 | } catch (error) {
42 | console.error("Error fetching business news:", error);
43 | throw error;
44 | }
45 | },
46 |
47 | // Get crypto news
48 | getCryptoNews: async () => {
49 | try {
50 | const response = await apiClient.get("/news/crypto");
51 | return response.data;
52 | } catch (error) {
53 | console.error("Error fetching crypto news:", error);
54 | throw error;
55 | }
56 | }
57 | },
58 |
59 | // Strategy related API
60 | strategy: {
61 | // Get strategy recommendation for a stock
62 | getStockStrategy: async (symbol: string) => {
63 | try {
64 | const response = await apiClient.get(`/strategy/stock/${symbol}`);
65 | return response.data;
66 | } catch (error) {
67 | console.error(`Error fetching stock strategy for ${symbol}:`, error);
68 | throw error;
69 | }
70 | },
71 |
72 | // Get strategy recommendation for a crypto asset
73 | getCryptoStrategy: async (symbol: string) => {
74 | try {
75 | const response = await apiClient.get(`/strategy/crypto/${symbol}`);
76 | return response.data;
77 | } catch (error) {
78 | console.error(`Error fetching crypto strategy for ${symbol}:`, error);
79 | throw error;
80 | }
81 | },
82 |
83 | // Generic strategy endpoint - using query parameters
84 | getStrategyWithParams: async (symbol: string, isCrypto: boolean = false) => {
85 | try {
86 | const response = await apiClient.get(`/strategy?symbol=${symbol}&is_crypto=${isCrypto}`);
87 | return response.data;
88 | } catch (error) {
89 | console.error(`Error fetching strategy for ${symbol}:`, error);
90 | throw error;
91 | }
92 | },
93 |
94 | // Get recommended stocks list
95 | getRecommendedStocks: async () => {
96 | try {
97 | const response = await apiClient.get('/strategy/recommended-stocks');
98 | return response.data;
99 | } catch (error) {
100 | console.error('Error fetching recommended stocks:', error);
101 | throw error;
102 | }
103 | },
104 |
105 | // Get recommended cryptocurrencies list
106 | getRecommendedCryptos: async () => {
107 | try {
108 | const response = await apiClient.get('/strategy/recommended-cryptos');
109 | return response.data;
110 | } catch (error) {
111 | console.error('Error fetching recommended cryptos:', error);
112 | throw error;
113 | }
114 | }
115 | },
116 |
117 | // Market related API
118 | market: {
119 | // Get market data for a specific symbol
120 | getMarketData: async (symbol: string) => {
121 | try {
122 | const response = await apiClient.get(`/market/${symbol}`);
123 | return response.data;
124 | } catch (error) {
125 | console.error(`Error fetching market data for ${symbol}:`, error);
126 | throw error;
127 | }
128 | },
129 |
130 | // Get general market trend overview
131 | getMarketTrend: async () => {
132 | try {
133 | const response = await apiClient.get('/market');
134 | return response.data;
135 | } catch (error) {
136 | console.error('Error fetching market trend:', error);
137 | throw error;
138 | }
139 | }
140 | },
141 |
142 | // Technical analysis related API
143 | technical: {
144 | // Get technical indicators for a stock
145 | getStockTechnical: async (symbol: string) => {
146 | try {
147 | const response = await apiClient.get(`/technical/stock/${symbol}`);
148 | return response.data;
149 | } catch (error) {
150 | console.error(`Error fetching stock technical indicators for ${symbol}:`, error);
151 | throw error;
152 | }
153 | },
154 |
155 | // Get technical indicators for a crypto asset
156 | getCryptoTechnical: async (symbol: string) => {
157 | try {
158 | const response = await apiClient.get(`/technical/crypto/${symbol}`);
159 | return response.data;
160 | } catch (error) {
161 | console.error(`Error fetching crypto technical indicators for ${symbol}:`, error);
162 | throw error;
163 | }
164 | }
165 | },
166 |
167 | // Direct data fetching functions (not relying on backend)
168 | directData: {
169 | // Get top stocks with real market data
170 | getTopStocks: async () => {
171 | const stockSymbols = ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA", "NVDA", "META", "JPM", "V", "WMT"];
172 | const stocks = [];
173 |
174 | try {
175 | // First try to get data from our API
176 | try {
177 | const response = await apiClient.get('/strategy/recommended-stocks');
178 | if (response.data && Array.isArray(response.data) && response.data.length > 0) {
179 | return response.data;
180 | }
181 | } catch (error) {
182 | console.log("Backend API failed, using direct Yahoo Finance data");
183 | }
184 |
185 | // If backend fails, get data directly from Yahoo Finance
186 | for (const symbol of stockSymbols) {
187 | try {
188 | const yahooResponse = await fetch(`https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?interval=1d`);
189 | const data = await yahooResponse.json();
190 |
191 | if (data.chart && data.chart.result && data.chart.result.length > 0) {
192 | const quote = data.chart.result[0].meta;
193 | const previousClose = quote.previousClose || quote.chartPreviousClose;
194 | const currentPrice = quote.regularMarketPrice;
195 | const changePercent = previousClose ? ((currentPrice - previousClose) / previousClose) * 100 : 0;
196 |
197 | // Generate a random signal based on price movement
198 | let signal = "Hold";
199 | if (changePercent > 1.5) signal = "Buy";
200 | else if (changePercent < -1.5) signal = "Sell";
201 |
202 | stocks.push({
203 | symbol: symbol,
204 | name: quote.instrumentName || symbol,
205 | final_signal: signal,
206 | price: currentPrice,
207 | change: parseFloat(changePercent.toFixed(2))
208 | });
209 | }
210 | } catch (error) {
211 | console.error(`Error fetching data for ${symbol}:`, error);
212 | }
213 | }
214 |
215 | return stocks;
216 | } catch (error) {
217 | console.error("Error in getTopStocks:", error);
218 | throw error;
219 | }
220 | },
221 |
222 | // Get top cryptos with real market data
223 | getTopCryptos: async () => {
224 | const cryptoIds = "bitcoin,ethereum,binancecoin,solana,ripple,cardano,dogecoin,polkadot,litecoin,matic-network";
225 |
226 | try {
227 | // First try to get data from our API
228 | try {
229 | const response = await apiClient.get('/strategy/recommended-cryptos');
230 | if (response.data && Array.isArray(response.data) && response.data.length > 0) {
231 | return response.data;
232 | }
233 | } catch (error) {
234 | console.log("Backend API failed, using direct CoinGecko data");
235 | }
236 |
237 | // If backend fails, get data directly from CoinGecko
238 | const response = await fetch(`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=${cryptoIds}&price_change_percentage=24h`);
239 | const coinData = await response.json();
240 |
241 | const cryptos = coinData.map((coin: {
242 | id: string;
243 | symbol: string;
244 | name: string;
245 | current_price: number;
246 | price_change_percentage_24h: number;
247 | }) => {
248 | // Generate a random signal based on price movement
249 | let signal = "Hold";
250 | if (coin.price_change_percentage_24h > 5) signal = "Buy";
251 | else if (coin.price_change_percentage_24h < -5) signal = "Sell";
252 |
253 | const symbolMap: { [key: string]: string } = {
254 | 'bitcoin': 'BTC',
255 | 'ethereum': 'ETH',
256 | 'binancecoin': 'BNB',
257 | 'solana': 'SOL',
258 | 'ripple': 'XRP',
259 | 'cardano': 'ADA',
260 | 'dogecoin': 'DOGE',
261 | 'polkadot': 'DOT',
262 | 'litecoin': 'LTC',
263 | 'matic-network': 'MATIC'
264 | };
265 |
266 | return {
267 | symbol: symbolMap[coin.id] || coin.symbol.toUpperCase(),
268 | name: coin.name,
269 | final_signal: signal,
270 | price: coin.current_price,
271 | change: parseFloat(coin.price_change_percentage_24h.toFixed(2))
272 | };
273 | });
274 |
275 | return cryptos;
276 | } catch (error) {
277 | console.error("Error in getTopCryptos:", error);
278 | throw error;
279 | }
280 | },
281 |
282 | // Get business news directly
283 | getBusinessNews: async () => {
284 | try {
285 | // Try to get data from our API
286 | const response = await apiClient.get('/news');
287 | return response.data;
288 | } catch (error) {
289 | console.error("Error in getBusinessNews:", error);
290 |
291 | // Provide mock news data as a fallback
292 | const mockNews = [
293 | {
294 | title: "Apple Reports Record Quarterly Revenue Despite Market Challenges",
295 | source: { name: "Financial Times" },
296 | publishedAt: new Date(Date.now() - 3600000).toISOString(),
297 | url: "https://example.com/apple-quarterly-revenue",
298 | sentiment: {
299 | label: "positive",
300 | score: 0.85
301 | },
302 | gpt_analysis: "Strong performance in a challenging economic environment. Investors remain optimistic about future growth."
303 | },
304 | {
305 | title: "Tesla Delivers More Cars Than Expected in Q2",
306 | source: { name: "Bloomberg" },
307 | publishedAt: new Date(Date.now() - 7200000).toISOString(),
308 | url: "https://example.com/tesla-delivers",
309 | sentiment: {
310 | label: "positive",
311 | score: 0.78
312 | },
313 | gpt_analysis: "Tesla continues to overcome supply chain challenges, indicating strong demand for electric vehicles."
314 | },
315 | {
316 | title: "Fed Signals Interest Rate Hike in Effort to Combat Inflation",
317 | source: { name: "Wall Street Journal" },
318 | publishedAt: new Date(Date.now() - 10800000).toISOString(),
319 | url: "https://example.com/fed-rate-hike",
320 | sentiment: {
321 | label: "neutral",
322 | score: 0.52
323 | },
324 | gpt_analysis: "Expected move by the Federal Reserve as it continues to battle persistent inflation. Markets had largely priced in this decision."
325 | },
326 | {
327 | title: "Microsoft Acquires AI Startup for $2 Billion",
328 | source: { name: "CNBC" },
329 | publishedAt: new Date(Date.now() - 14400000).toISOString(),
330 | url: "https://example.com/microsoft-acquisition",
331 | sentiment: {
332 | label: "positive",
333 | score: 0.81
334 | },
335 | gpt_analysis: "Strategic acquisition to strengthen Microsoft's AI capabilities amid increasing competition in the sector."
336 | },
337 | {
338 | title: "Global Supply Chain Issues Expected to Persist Through 2023",
339 | source: { name: "Reuters" },
340 | publishedAt: new Date(Date.now() - 18000000).toISOString(),
341 | url: "https://example.com/supply-chain-issues",
342 | sentiment: {
343 | label: "negative",
344 | score: 0.67
345 | },
346 | gpt_analysis: "Ongoing challenges for manufacturers and retailers. Companies with robust logistics networks are better positioned to navigate these disruptions."
347 | },
348 | {
349 | title: "Amazon Announces New Fulfillment Centers, Creating 10,000 Jobs",
350 | source: { name: "Business Insider" },
351 | publishedAt: new Date(Date.now() - 21600000).toISOString(),
352 | url: "https://example.com/amazon-expansion",
353 | sentiment: {
354 | label: "positive",
355 | score: 0.89
356 | },
357 | gpt_analysis: "Significant expansion of Amazon's logistics network, highlighting confidence in continued e-commerce growth."
358 | }
359 | ];
360 |
361 | return mockNews;
362 | }
363 | },
364 |
365 | // Get crypto news directly
366 | getCryptoNews: async () => {
367 | try {
368 | // Try to get data from our API
369 | const response = await apiClient.get('/news/crypto');
370 | return response.data;
371 | } catch (error) {
372 | console.error("Error in getCryptoNews:", error);
373 |
374 | // Provide mock cryptocurrency news data as a fallback
375 | const mockCryptoNews = [
376 | {
377 | title: "Bitcoin Surges Past $60,000 as Institutional Adoption Grows",
378 | source: { name: "CoinDesk" },
379 | publishedAt: new Date(Date.now() - 2800000).toISOString(),
380 | url: "https://example.com/bitcoin-surge",
381 | sentiment: {
382 | label: "positive",
383 | score: 0.91
384 | },
385 | gpt_analysis: "Increased institutional investment and limited supply continue to drive Bitcoin's price appreciation."
386 | },
387 | {
388 | title: "Ethereum Completes Major Network Upgrade, Improving Scalability",
389 | source: { name: "The Block" },
390 | publishedAt: new Date(Date.now() - 5200000).toISOString(),
391 | url: "https://example.com/ethereum-upgrade",
392 | sentiment: {
393 | label: "positive",
394 | score: 0.87
395 | },
396 | gpt_analysis: "Significant technical milestone that addresses Ethereum's scaling challenges and potentially reduces transaction fees."
397 | },
398 | {
399 | title: "Regulatory Concerns Grow as Cryptocurrency Market Expands",
400 | source: { name: "Financial Times" },
401 | publishedAt: new Date(Date.now() - 9100000).toISOString(),
402 | url: "https://example.com/crypto-regulation",
403 | sentiment: {
404 | label: "neutral",
405 | score: 0.48
406 | },
407 | gpt_analysis: "Increased regulatory scrutiny is expected as cryptocurrencies become more mainstream. Clear regulations could actually benefit the industry long-term."
408 | },
409 | {
410 | title: "Binance Faces Probe from Financial Regulators",
411 | source: { name: "Bloomberg" },
412 | publishedAt: new Date(Date.now() - 12500000).toISOString(),
413 | url: "https://example.com/binance-probe",
414 | sentiment: {
415 | label: "negative",
416 | score: 0.71
417 | },
418 | gpt_analysis: "Regulatory challenges for one of the world's largest cryptocurrency exchanges. This could impact market liquidity in the short term."
419 | },
420 | {
421 | title: "Major Bank Launches Cryptocurrency Custody Service for Institutional Clients",
422 | source: { name: "Reuters" },
423 | publishedAt: new Date(Date.now() - 16700000).toISOString(),
424 | url: "https://example.com/bank-crypto-custody",
425 | sentiment: {
426 | label: "positive",
427 | score: 0.82
428 | },
429 | gpt_analysis: "Notable step in the integration of cryptocurrencies into traditional financial infrastructure, potentially lowering barriers to institutional adoption."
430 | },
431 | {
432 | title: "NFT Market Shows Signs of Cooling After Record-Breaking Quarter",
433 | source: { name: "CNBC" },
434 | publishedAt: new Date(Date.now() - 19900000).toISOString(),
435 | url: "https://example.com/nft-market-cooling",
436 | sentiment: {
437 | label: "neutral",
438 | score: 0.54
439 | },
440 | gpt_analysis: "Normalization of the NFT market after period of extraordinary growth. Focus is shifting to projects with clear utility and long-term value."
441 | }
442 | ];
443 |
444 | return mockCryptoNews;
445 | }
446 | }
447 | }
448 | };
449 |
450 | export default apiService;
451 |
452 |
--------------------------------------------------------------------------------