├── 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 | TradeSense Logo 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 | 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 |
99 |

{error}

100 |
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 | 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 |
33 | 105 | 106 | {/* Mobile Navigation */} 107 | 108 | 109 |
110 | TradeSense Logo 115 | 116 | TradeSense 117 | 118 | 121 |
122 | 123 |
124 | {routes.map((route: AppRoute) => { 125 | if (route.children && route.name === "News") { 126 | return ( 127 | 128 | 129 | {route.name} 130 | 131 | 132 | 133 | {route.children.map((child: AppRoute) => ( 134 | setMobileMenuOpen(false)} 138 | className="block text-sm text-white hover:text-gray-300 py-2" 139 | > 140 | {child.name} 141 | 142 | ))} 143 | 144 | 145 | ) 146 | } 147 | 148 | return ( 149 | setMobileMenuOpen(false)} 153 | className="block text-sm font-medium text-white hover:text-gray-300 py-2" 154 | > 155 | {route.name} 156 | 157 | ) 158 | })} 159 |
160 |
161 |
162 |
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 | ![TradeSense Home](https://github.com/wangwanlu09/TradeSense_AiTradeAgent/blob/main/Home.png?raw=true) 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 | ![TradeSense Overview News](https://github.com/wangwanlu09/TradeSense_AiTradeAgent/blob/main/Overview%20News%20Analysis.png?raw=true) 14 | 15 | ![TradeSense Market Trend](https://github.com/wangwanlu09/TradeSense_AiTradeAgent/blob/main/Market%20Trend.png?raw=true) 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 | ![TradeSense Business NewsP2](https://github.com/wangwanlu09/TradeSense_AiTradeAgent/blob/main/Business%20NewsP2.jpg?raw=true) 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 | ![TradeSense News Analysis](https://github.com/wangwanlu09/TradeSense_AiTradeAgent/blob/main/News%20Analysis.png?raw=true) 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 | ![TradeSense Tech](https://github.com/wangwanlu09/TradeSense_AiTradeAgent/blob/main/Tech.png?raw=true) 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 | ![TradeSense Business News](https://github.com/wangwanlu09/TradeSense_AiTradeAgent/blob/main/Business%20News.png?raw=true) 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 | ![TradeSense Recommendation P3](https://github.com/wangwanlu09/TradeSense_AiTradeAgent/blob/main/Recommendation%20P3.png?raw=true) 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 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | {stockRecommendations.map((stock, i) => ( 137 | 138 | 139 | 140 | 141 | 144 | 156 | 157 | ))} 158 | 159 |
SymbolNamePriceChangeSignal
{stock.symbol}{stock.name}${stock.price !== undefined ? stock.price.toFixed(2) : "N/A"}= 0 ? 'text-green-500' : 'text-red-500'}`}> 142 | {stock.change !== undefined ? `${stock.change >= 0 ? '+' : ''}${stock.change.toFixed(2)}%` : 'N/A'} 143 | 145 | 153 | {stock.final_signal} 154 | 155 |
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 | 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 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | {cryptoRecommendations.map((crypto, i) => ( 137 | 138 | 139 | 140 | 141 | 144 | 156 | 157 | ))} 158 | 159 |
SymbolNamePriceChangeSignal
{crypto.symbol}{crypto.name}${crypto.price !== undefined ? crypto.price.toFixed(2) : "N/A"}= 0 ? 'text-green-500' : 'text-red-500'}`}> 142 | {crypto.change !== undefined ? `${crypto.change >= 0 ? '+' : ''}${crypto.change.toFixed(2)}%` : 'N/A'} 143 | 145 | 153 | {crypto.final_signal} 154 | 155 |
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 | 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 | --------------------------------------------------------------------------------