From 4b747c5c8c11dafe84e01324e01371659e7ee3ad Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Sat, 29 Nov 2025 16:23:22 +0800 Subject: [PATCH 1/2] change default ticker for china mainland user --- frontend/src/api/system.ts | 32 +++++++++ frontend/src/app/home/home.tsx | 21 +++++- frontend/src/constants/stock.ts | 4 +- python/valuecell/config/constants.py | 18 +++++ python/valuecell/server/api/routers/system.py | 66 ++++++++++++++++++- python/valuecell/server/db/init_db.py | 18 +++++ python/valuecell/utils/i18n_utils.py | 45 +++++++++++-- 7 files changed, 195 insertions(+), 9 deletions(-) diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts index 481a11f3e..e3d8c9bd1 100644 --- a/frontend/src/api/system.ts +++ b/frontend/src/api/system.ts @@ -10,6 +10,17 @@ import type { SystemInfo, } from "@/types/system"; +export interface DefaultTicker { + ticker: string; + symbol: string; + name: string; +} + +export interface DefaultTickersResponse { + region: string; + tickers: DefaultTicker[]; +} + export const useBackendHealth = () => { return useQuery({ queryKey: ["backend-health"], @@ -102,3 +113,24 @@ export const usePublishStrategy = () => { }, }); }; + +/** + * Get region-aware default tickers for homepage display. + * Returns A-share indices for China mainland users, + * global indices for other regions. + * + * @param region - Optional region override for testing (e.g., "cn" or "default"). + * In development, you can set this to test different regions. + */ +export const useGetDefaultTickers = (region?: string) => + useQuery({ + queryKey: ["system", "default-tickers", region], + queryFn: () => { + const params = region ? `?region=${region}` : ""; + return apiClient.get>( + `system/default-tickers${params}`, + ); + }, + select: (data) => data.data, + staleTime: 1000 * 60 * 60, // Cache for 1 hour, region doesn't change frequently + }); diff --git a/frontend/src/app/home/home.tsx b/frontend/src/app/home/home.tsx index 39dee0fa1..15313f41d 100644 --- a/frontend/src/app/home/home.tsx +++ b/frontend/src/app/home/home.tsx @@ -1,6 +1,7 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useNavigate } from "react-router"; import { useAllPollTaskList } from "@/api/conversation"; +import { useGetDefaultTickers } from "@/api/system"; import ScrollContainer from "@/components/valuecell/scroll/scroll-container"; import { HOME_STOCK_SHOW } from "@/constants/stock"; import { agentSuggestions } from "@/mock/agent-data"; @@ -17,7 +18,23 @@ function Home() { const [inputValue, setInputValue] = useState(""); const { data: allPollTaskList } = useAllPollTaskList(); - const { sparklineStocks } = useSparklineStocks(HOME_STOCK_SHOW); + + // Get region-aware default tickers from API + const { data: defaultTickersData } = useGetDefaultTickers(); + + // Use API-returned tickers, fallback to hardcoded values if API fails + const stockConfig = useMemo(() => { + if (defaultTickersData?.tickers) { + return defaultTickersData.tickers.map((t) => ({ + ticker: t.ticker, + symbol: t.symbol, + })); + } + // Fallback to hardcoded values + return [...HOME_STOCK_SHOW]; + }, [defaultTickersData]); + + const { sparklineStocks } = useSparklineStocks(stockConfig); const handleAgentClick = (agentId: string) => { navigate(`/agent/${agentId}`); diff --git a/frontend/src/constants/stock.ts b/frontend/src/constants/stock.ts index 3488388b0..3583c90b1 100644 --- a/frontend/src/constants/stock.ts +++ b/frontend/src/constants/stock.ts @@ -21,7 +21,9 @@ export const RED_BADGE = { bg: "#FFEAEA", text: "#E25C5C" }; export const NEUTRAL_BADGE = { bg: "#F5F5F5", text: "#707070" }; /** - * Stock configurations for home page display + * Stock configurations for home page display. + * Used as fallback when /api/v1/system/default-tickers API fails. + * The API returns region-appropriate stocks based on user's IP location. */ export const HOME_STOCK_SHOW = [ { diff --git a/python/valuecell/config/constants.py b/python/valuecell/config/constants.py index 179a48a7d..877f1fae8 100644 --- a/python/valuecell/config/constants.py +++ b/python/valuecell/config/constants.py @@ -74,3 +74,21 @@ "zh-Hans": {"decimal": ".", "thousands": ","}, "zh-Hant": {"decimal": ".", "thousands": ","}, } + +# Region-based default tickers for homepage display +# 'cn' for China mainland users (A-share indices via akshare/baostock) +# 'default' for other regions (global indices via yfinance) +REGION_DEFAULT_TICKERS: Dict[str, List[Dict[str, str]]] = { + # China mainland users - A-share indices only + "cn": [ + {"ticker": "SSE:000001", "symbol": "上证指数", "name": "上证指数"}, + {"ticker": "SZSE:399001", "symbol": "深证成指", "name": "深证成指"}, + {"ticker": "SSE:000300", "symbol": "沪深300", "name": "沪深300指数"}, + ], + # Default for other regions - global mixed indices + "default": [ + {"ticker": "NASDAQ:IXIC", "symbol": "NASDAQ", "name": "NASDAQ Composite"}, + {"ticker": "HKEX:HSI", "symbol": "HSI", "name": "Hang Seng Index"}, + {"ticker": "SSE:000001", "symbol": "SSE", "name": "Shanghai Composite"}, + ], +} diff --git a/python/valuecell/server/api/routers/system.py b/python/valuecell/server/api/routers/system.py index 820a34330..a5fe32ae2 100644 --- a/python/valuecell/server/api/routers/system.py +++ b/python/valuecell/server/api/routers/system.py @@ -1,13 +1,36 @@ """System related API routes.""" from datetime import datetime +from typing import Optional -from fastapi import APIRouter +from fastapi import APIRouter, Request +from ....config.constants import REGION_DEFAULT_TICKERS +from ....utils.i18n_utils import detect_user_region_async from ...config.settings import get_settings from ..schemas import AppInfoData, HealthCheckData, SuccessResponse +def _get_client_ip(request: Request) -> Optional[str]: + """Extract client IP from request, considering reverse proxy headers.""" + # Check X-Forwarded-For header (common for reverse proxies) + forwarded_for = request.headers.get("X-Forwarded-For") + if forwarded_for: + # Take the first IP in the chain (original client) + return forwarded_for.split(",")[0].strip() + + # Check X-Real-IP header (nginx) + real_ip = request.headers.get("X-Real-IP") + if real_ip: + return real_ip + + # Fallback to direct client + if request.client: + return request.client.host + + return None + + def create_system_router() -> APIRouter: """Create system related routes.""" router = APIRouter(prefix="/system", tags=["System"]) @@ -45,4 +68,45 @@ async def health_check(): data=health_data, msg="Service is running normally" ) + @router.get( + "/default-tickers", + response_model=SuccessResponse[dict], + summary="Get default tickers for homepage", + description="Get region-aware default tickers based on user's IP location", + ) + async def get_default_tickers( + request: Request, + region: Optional[str] = None, + ): + """Get default tickers for homepage based on user region. + + Returns region-appropriate stock tickers that the user can access. + For China mainland users, returns A-share indices (accessible via akshare/baostock). + For other regions, returns globally accessible indices. + + Args: + request: FastAPI request object for extracting client IP. + region: Optional region override for testing (cn or default). + """ + # If region is manually specified and valid, use it directly + if region and region in REGION_DEFAULT_TICKERS: + detected_region = region + else: + # Get client IP and detect region + client_ip = _get_client_ip(request) + detected_region = await detect_user_region_async(client_ip) + + # Get default tickers for the detected region + tickers = REGION_DEFAULT_TICKERS.get( + detected_region, REGION_DEFAULT_TICKERS["default"] + ) + + return SuccessResponse.create( + data={ + "region": detected_region, + "tickers": tickers, + }, + msg="Default tickers retrieved successfully", + ) + return router diff --git a/python/valuecell/server/db/init_db.py b/python/valuecell/server/db/init_db.py index 2d88456d8..ea125cf25 100644 --- a/python/valuecell/server/db/init_db.py +++ b/python/valuecell/server/db/init_db.py @@ -256,6 +256,9 @@ def initialize_assets_with_service(self) -> bool: "NASDAQ:IXIC", # NASDAQ Composite Index "HKEX:HSI", # Hang Seng Index "SSE:000001", # Shanghai Composite Index + "SZSE:399001", # Shenzhen Composite Index + "SZSE:399006", # ChiNext Index + "SSE:000300", # Science and Technology Innovation Board Index ] try: @@ -399,6 +402,21 @@ def _get_fallback_asset_data(self, ticker: str) -> Optional[dict]: "asset_type": "index", "exchange": "SSE", }, + "SZSE:399001": { + "name": "Shenzhen Composite Index", + "asset_type": "index", + "exchange": "SZSE", + }, + "SZSE:399006": { + "name": "ChiNext Index", + "asset_type": "index", + "exchange": "SZSE", + }, + "SSE:000300": { + "name": "CSI 300 Index", + "asset_type": "index", + "exchange": "SSE", + }, "HKEX:HSI": { "name": "Hang Seng Index", "asset_type": "index", diff --git a/python/valuecell/utils/i18n_utils.py b/python/valuecell/utils/i18n_utils.py index 6e6ab6b71..cbfab2531 100644 --- a/python/valuecell/utils/i18n_utils.py +++ b/python/valuecell/utils/i18n_utils.py @@ -65,23 +65,30 @@ def detect_browser_language(accept_language_header: str) -> str: return DEFAULT_LANGUAGE -def detect_user_region() -> str: +def detect_user_region(client_ip: Optional[str] = None) -> str: """Detect user region based on IP geolocation. + Args: + client_ip: Optional client IP address. If None, uses server's IP for detection. + Returns: - Region code: 'us' for United States, 'default' for others + Region code: 'cn' for China mainland, 'default' for others """ + try: import httpx with httpx.Client(timeout=3.0) as client: - resp = client.get("https://ipapi.co/json/") + # Use client IP if provided, otherwise detect server's IP + url = f"https://ipapi.co/{client_ip}/json/" if client_ip else "https://ipapi.co/json/" + resp = client.get(url) if resp.status_code == 200: data = resp.json() country_code = data.get("country_code", "").upper() - if country_code == "US": - return "us" + # China mainland + if country_code == "CN": + return "cn" return "default" return "default" @@ -90,6 +97,34 @@ def detect_user_region() -> str: return "default" +async def detect_user_region_async(client_ip: Optional[str] = None) -> str: + """Async version of detect_user_region for use in FastAPI routes. + + Args: + client_ip: Optional client IP address. If None, uses server's IP for detection. + + Returns: + Region code: 'cn' for China mainland, 'default' for others + """ + try: + import httpx + + async with httpx.AsyncClient(timeout=3.0) as client: + url = f"https://ipapi.co/{client_ip}/json/" if client_ip else "https://ipapi.co/json/" + resp = await client.get(url) + if resp.status_code == 200: + data = resp.json() + country_code = data.get("country_code", "").upper() + + if country_code == "CN": + return "cn" + + return "default" + return "default" + except Exception: + return "default" + + def get_timezone_for_language(language: str) -> str: """Get default timezone for a language. From d103b66eec291d33bb3573ce03c6968b68dd519c Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Sat, 29 Nov 2025 16:27:44 +0800 Subject: [PATCH 2/2] lint --- python/valuecell/utils/i18n_utils.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python/valuecell/utils/i18n_utils.py b/python/valuecell/utils/i18n_utils.py index cbfab2531..65000cc0e 100644 --- a/python/valuecell/utils/i18n_utils.py +++ b/python/valuecell/utils/i18n_utils.py @@ -80,7 +80,11 @@ def detect_user_region(client_ip: Optional[str] = None) -> str: with httpx.Client(timeout=3.0) as client: # Use client IP if provided, otherwise detect server's IP - url = f"https://ipapi.co/{client_ip}/json/" if client_ip else "https://ipapi.co/json/" + url = ( + f"https://ipapi.co/{client_ip}/json/" + if client_ip + else "https://ipapi.co/json/" + ) resp = client.get(url) if resp.status_code == 200: data = resp.json() @@ -110,7 +114,11 @@ async def detect_user_region_async(client_ip: Optional[str] = None) -> str: import httpx async with httpx.AsyncClient(timeout=3.0) as client: - url = f"https://ipapi.co/{client_ip}/json/" if client_ip else "https://ipapi.co/json/" + url = ( + f"https://ipapi.co/{client_ip}/json/" + if client_ip + else "https://ipapi.co/json/" + ) resp = await client.get(url) if resp.status_code == 200: data = resp.json()