Compare commits
2 Commits
11fa7093ba
...
282cec080b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
282cec080b | ||
|
|
e786e885e6 |
@ -80,7 +80,8 @@ async def fetch_data(
|
|||||||
market=request.market,
|
market=request.market,
|
||||||
symbol=request.symbol,
|
symbol=request.symbol,
|
||||||
data_source=request.data_source,
|
data_source=request.data_source,
|
||||||
update_id=data_update.id
|
update_id=data_update.id,
|
||||||
|
currency=request.currency
|
||||||
)
|
)
|
||||||
|
|
||||||
return FetchDataResponse(
|
return FetchDataResponse(
|
||||||
@ -96,7 +97,8 @@ def fetch_data_background(
|
|||||||
market: str,
|
market: str,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
data_source: str,
|
data_source: str,
|
||||||
update_id: int
|
update_id: int,
|
||||||
|
currency: str = None
|
||||||
):
|
):
|
||||||
"""后台数据获取任务 - 完全同步执行,避免event loop冲突"""
|
"""后台数据获取任务 - 完全同步执行,避免event loop冲突"""
|
||||||
import sys
|
import sys
|
||||||
@ -117,7 +119,8 @@ def fetch_data_background(
|
|||||||
market=market,
|
market=market,
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
data_source=data_source,
|
data_source=data_source,
|
||||||
update_id=update_id
|
update_id=update_id,
|
||||||
|
currency=currency
|
||||||
)
|
)
|
||||||
|
|
||||||
# 更新数据更新记录 - 使用psycopg2同步连接
|
# 更新数据更新记录 - 使用psycopg2同步连接
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import websocket
|
|||||||
import psycopg2
|
import psycopg2
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
|
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import logging
|
import logging
|
||||||
@ -81,7 +81,7 @@ PRICE_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
STOCKCARD_CONFIG = {
|
STOCKCARD_CONFIG = {
|
||||||
"period": 15,
|
"period": 10,
|
||||||
"unit": 100000000
|
"unit": 100000000
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,6 +300,14 @@ class BloombergClient:
|
|||||||
except Exception:
|
except Exception:
|
||||||
conn.rollback() # Should not happen with IF NOT EXISTS but good practice
|
conn.rollback() # Should not happen with IF NOT EXISTS but good practice
|
||||||
|
|
||||||
|
# Migrate update_date to TIMESTAMP
|
||||||
|
try:
|
||||||
|
cur.execute("ALTER TABLE stockcard ALTER COLUMN update_date TYPE TIMESTAMP USING update_date::timestamp;")
|
||||||
|
except Exception as e:
|
||||||
|
# Likely already converted or failed, log but don't crash
|
||||||
|
logger.info(f"Schema migration note (update_date): {e}")
|
||||||
|
conn.rollback()
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
def save_data(self, data_list):
|
def save_data(self, data_list):
|
||||||
@ -307,6 +315,8 @@ class BloombergClient:
|
|||||||
if not data_list:
|
if not data_list:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
conn = self._get_db_connection()
|
conn = self._get_db_connection()
|
||||||
if not conn: return
|
if not conn: return
|
||||||
|
|
||||||
@ -354,7 +364,7 @@ class BloombergClient:
|
|||||||
id,
|
id,
|
||||||
ROW_NUMBER() OVER(
|
ROW_NUMBER() OVER(
|
||||||
PARTITION BY company_code, currency, indicator, value_date
|
PARTITION BY company_code, currency, indicator, value_date
|
||||||
ORDER BY update_date DESC
|
ORDER BY update_date DESC, id DESC
|
||||||
) as rn
|
) as rn
|
||||||
FROM
|
FROM
|
||||||
stockcard
|
stockcard
|
||||||
@ -399,7 +409,7 @@ class BloombergClient:
|
|||||||
|
|
||||||
# --- Core Fetching Logic ---
|
# --- Core Fetching Logic ---
|
||||||
|
|
||||||
def fetch_company(self, market, symbol, progress_callback=None):
|
def fetch_company(self, market, symbol, progress_callback=None, force_currency=None):
|
||||||
"""
|
"""
|
||||||
Main entry point to fetch data for a single company.
|
Main entry point to fetch data for a single company.
|
||||||
"""
|
"""
|
||||||
@ -407,12 +417,28 @@ class BloombergClient:
|
|||||||
# If symbol already has Equity, use it. Else append.
|
# If symbol already has Equity, use it. Else append.
|
||||||
if "Equity" in symbol:
|
if "Equity" in symbol:
|
||||||
company_code = symbol
|
company_code = symbol
|
||||||
|
bucket_code = symbol # Usage for bucket?
|
||||||
|
# If symbol comes in as '631 HK Equity', we might want to standardize?
|
||||||
|
# Assuming symbol is correct input.
|
||||||
else:
|
else:
|
||||||
# Special case for China market: use 'CH Equity' instead of 'CN Equity'
|
# Special case for China market: use 'CH Equity' instead of 'CN Equity'
|
||||||
mapped_market = "CH" if market == "CN" else market
|
mapped_market = "CH" if market == "CN" else market
|
||||||
|
|
||||||
|
# Canonical Code (Store in DB): Keep explicit zeros if provided in symbol
|
||||||
company_code = f"{symbol} {mapped_market} Equity"
|
company_code = f"{symbol} {mapped_market} Equity"
|
||||||
|
|
||||||
today_str = datetime.now().strftime('%Y-%m-%d')
|
# Query Ticker (Send to Bloomberg): Handle HK special case
|
||||||
|
query_ticker = company_code
|
||||||
|
if market == "HK" or (len(company_code.split()) > 1 and company_code.split()[1] == "HK"):
|
||||||
|
# Extract number part
|
||||||
|
parts = company_code.split()
|
||||||
|
ticker_part = parts[0]
|
||||||
|
if ticker_part.isdigit():
|
||||||
|
short_ticker = str(int(ticker_part))
|
||||||
|
# Reconstruct: 631 HK Equity
|
||||||
|
query_ticker = f"{short_ticker} {' '.join(parts[1:])}"
|
||||||
|
|
||||||
|
today_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
logger.info(f"🚀 Starting fetch for: {company_code}")
|
logger.info(f"🚀 Starting fetch for: {company_code}")
|
||||||
if progress_callback: progress_callback("Starting Bloomberg session...", 0)
|
if progress_callback: progress_callback("Starting Bloomberg session...", 0)
|
||||||
@ -438,61 +464,85 @@ if 'bquery' not in globals():
|
|||||||
try:
|
try:
|
||||||
self.execute_remote_code(init_code)
|
self.execute_remote_code(init_code)
|
||||||
|
|
||||||
# 1. Fetch Basic Info
|
|
||||||
|
# 1. Fetch Basic Info
|
||||||
|
# Determine currency
|
||||||
|
if force_currency and force_currency != "Auto":
|
||||||
|
currency = force_currency
|
||||||
|
logger.info(f"Using forced currency: {currency}")
|
||||||
|
else:
|
||||||
|
currency = "USD"
|
||||||
|
if "JP" in market.upper(): currency = "JPY"
|
||||||
|
elif "VN" in market.upper(): currency = "VND"
|
||||||
|
elif "CN" in market.upper(): currency = "CNY"
|
||||||
|
elif "HK" in market.upper(): currency = "HKD"
|
||||||
|
logger.info(f"Using auto-detected currency: {currency}")
|
||||||
|
|
||||||
|
# 2. Fetch Basic Info
|
||||||
logger.info("Fetching Basic Data...")
|
logger.info("Fetching Basic Data...")
|
||||||
if progress_callback: progress_callback("Fetching Company Basic Info...", 10)
|
if progress_callback: progress_callback("Fetching Company Basic Info...", 10)
|
||||||
basic_data = self._fetch_basic_remote(company_code, "USD")
|
try:
|
||||||
self.save_data(basic_data)
|
basic_data = self._fetch_basic_remote(company_code, currency, query_ticker=query_ticker)
|
||||||
|
logger.info(f"DEBUG: basic_data before save: {basic_data}")
|
||||||
|
self.save_data(basic_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching basic data: {e}")
|
||||||
|
|
||||||
# Determine currency
|
# 3. Fetch Currency Data
|
||||||
currency = "USD"
|
|
||||||
if "JP" in market.upper(): currency = "JPY"
|
|
||||||
elif "VN" in market.upper(): currency = "VND"
|
|
||||||
elif "CN" in market.upper(): currency = "CNY"
|
|
||||||
elif "HK" in market.upper(): currency = "HKD"
|
|
||||||
|
|
||||||
logger.info(f"Using currency: {currency}")
|
|
||||||
|
|
||||||
# 2. Fetch Currency Data
|
|
||||||
logger.info("Fetching Currency Data...")
|
logger.info("Fetching Currency Data...")
|
||||||
if progress_callback: progress_callback(f"正在获取货币指标 ({currency})...", 30)
|
if progress_callback: progress_callback(f"正在获取货币指标 ({currency})...", 30)
|
||||||
curr_data = self._fetch_series_remote(company_code, currency, CURRENCY_CONFIG, "currency")
|
curr_data = []
|
||||||
self.save_data(curr_data)
|
try:
|
||||||
|
curr_data = self._fetch_series_remote(company_code, currency, CURRENCY_CONFIG, "currency", query_ticker=query_ticker)
|
||||||
|
self.save_data(curr_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching currency series: {e}")
|
||||||
|
|
||||||
# 3. Fetch Non-Currency Data
|
# 4. Fetch Non-Currency Data
|
||||||
logger.info("Fetching Non-Currency Data...")
|
logger.info("Fetching Non-Currency Data...")
|
||||||
if progress_callback: progress_callback("正在获取非货币指标...", 50)
|
if progress_callback: progress_callback("正在获取非货币指标...", 50)
|
||||||
non_curr_data = self._fetch_series_remote(company_code, currency, NON_CURRENCY_CONFIG, "non_currency")
|
try:
|
||||||
self.save_data(non_curr_data)
|
non_curr_data = self._fetch_series_remote(company_code, currency, NON_CURRENCY_CONFIG, "non_currency", query_ticker=query_ticker)
|
||||||
|
self.save_data(non_curr_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching non-currency series: {e}")
|
||||||
|
|
||||||
# 4. Fetch Price Data (Aligned with Revenue Dates)
|
# 5. Fetch Price Data (Aligned with Revenue Dates)
|
||||||
logger.info("Fetching Price Data (Aligned)...")
|
logger.info("Fetching Price Data (Aligned)...")
|
||||||
if progress_callback: progress_callback("正在获取价格指标...", 70)
|
if progress_callback: progress_callback("正在获取价格指标...", 70)
|
||||||
|
|
||||||
# Extract Revenue dates
|
# Extract Revenue dates
|
||||||
revenue_dates = []
|
revenue_dates = []
|
||||||
rev_key = CURRENCY_CONFIG.get("Revenue", "SALES_REV_TURN")
|
if curr_data:
|
||||||
# The saved data uses indicator name from config keys (e.g. "Revenue")
|
for item in curr_data:
|
||||||
# So looking for "Revenue" in saved entries
|
# Check for "Revenue"
|
||||||
|
if item['indicator'].lower() == "revenue": # Robust case-insensitive check
|
||||||
|
if item['value_date']:
|
||||||
|
revenue_dates.append(item['value_date'])
|
||||||
|
|
||||||
for item in curr_data:
|
# Remove duplicates, sort
|
||||||
# Check for "Revenue" (case insensitive match if needed)
|
|
||||||
if item['indicator'].lower() == 'revenue':
|
|
||||||
if item['value_date']:
|
|
||||||
# Ensure YYYY-MM-DD
|
|
||||||
revenue_dates.append(item['value_date'])
|
|
||||||
|
|
||||||
# Remove specs, duplicates, sort
|
|
||||||
revenue_dates = sorted(list(set(revenue_dates)), reverse=True)
|
revenue_dates = sorted(list(set(revenue_dates)), reverse=True)
|
||||||
|
|
||||||
if revenue_dates:
|
if revenue_dates:
|
||||||
logger.info(f"Found {len(revenue_dates)} revenue reporting dates. Fetching aligned price data...")
|
logger.info(f"Found {len(revenue_dates)} revenue reporting dates. Fetching aligned price data...")
|
||||||
price_data = self._fetch_price_by_dates_remote(company_code, currency, revenue_dates)
|
try:
|
||||||
self.save_data(price_data)
|
price_data = self._fetch_price_by_dates_remote(company_code, currency, revenue_dates, query_ticker=query_ticker)
|
||||||
|
self.save_data(price_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching aligned price data: {e}")
|
||||||
else:
|
else:
|
||||||
logger.warning("No revenue dates found. Falling back to yearly price fetch.")
|
logger.warning("No revenue dates found. Falling back to yearly price fetch (Dec 31).")
|
||||||
price_data = self._fetch_series_remote(company_code, currency, PRICE_CONFIG, "price")
|
# Generate last 10 years Dec 31
|
||||||
self.save_data(price_data)
|
fallback_dates = []
|
||||||
|
current_year = datetime.now().year
|
||||||
|
for i in range(1, 11):
|
||||||
|
fallback_dates.append(f"{current_year - i}-12-31")
|
||||||
|
|
||||||
|
try:
|
||||||
|
price_data = self._fetch_price_by_dates_remote(company_code, currency, fallback_dates, query_ticker=query_ticker)
|
||||||
|
self.save_data(price_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching fallback price data: {e}")
|
||||||
|
|
||||||
# 5. Cleanup
|
# 5. Cleanup
|
||||||
if progress_callback: progress_callback("Finalizing data...", 90)
|
if progress_callback: progress_callback("Finalizing data...", 90)
|
||||||
@ -505,74 +555,80 @@ if 'bquery' not in globals():
|
|||||||
if progress_callback: progress_callback(f"Error: {e}", 0)
|
if progress_callback: progress_callback(f"Error: {e}", 0)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def _fetch_basic_remote(self, company_code, currency):
|
def _fetch_basic_remote(self, company_code, currency, query_ticker=None):
|
||||||
"""Generates code to fetch basic data"""
|
"""Generates code to fetch basic data"""
|
||||||
|
target_ticker = query_ticker if query_ticker else company_code
|
||||||
|
|
||||||
code = f"""
|
code = f"""
|
||||||
def get_basic():
|
def get_basic():
|
||||||
company = "{company_code}"
|
company = "{company_code}"
|
||||||
|
query_ticker = "{target_ticker}"
|
||||||
curr = "{currency}"
|
curr = "{currency}"
|
||||||
res_list = []
|
res_list = []
|
||||||
|
|
||||||
# 1. BQL query
|
# 1. BDP Query for most fields (except Market Cap)
|
||||||
q = f"for(['{{company}}']) get(name,listing_date,pe_ratio,px_to_book_ratio,cur_mkt_cap(currency={{curr}}),PCT_REVENUE_FROM_FOREIGN_SOURCES)"
|
fields = ["NAME", "PE_RATIO", "PX_TO_BOOK_RATIO", "PCT_REVENUE_FROM_FOREIGN_SOURCES", "DIVIDEND_12_MONTH_YIELD", "EQY_INIT_PO_DT"]
|
||||||
try:
|
|
||||||
df = bquery.bql(q)
|
|
||||||
if not df.empty:
|
|
||||||
def get_val(df, field_name):
|
|
||||||
if field_name == 'name':
|
|
||||||
rows = df[df['field'] == 'name']
|
|
||||||
elif 'cur_mkt_cap' in field_name:
|
|
||||||
rows = df[df['field'].str.contains('cur_mkt_cap')]
|
|
||||||
elif 'FOREIGN' in field_name:
|
|
||||||
rows = df[df['field'].str.contains('FOREIGN')]
|
|
||||||
else:
|
|
||||||
rows = df[df['field'] == field_name]
|
|
||||||
|
|
||||||
if not rows.empty:
|
try:
|
||||||
val = rows['value'].iloc[0]
|
# Use overrides for currency. Expects list of tuples.
|
||||||
# Handle Timestamp object from remote pandas
|
# Try both CURRENCY (for price) and EQY_FUND_CRNCY (for fundamentals)
|
||||||
if hasattr(val, 'strftime'):
|
df = bquery.bdp([query_ticker], fields, overrides=[('CURRENCY', curr), ('EQY_FUND_CRNCY', curr)])
|
||||||
return val.strftime('%Y-%m-%d')
|
|
||||||
return str(val) if val is not None else None
|
if not df.empty:
|
||||||
|
def safe_get(df, col):
|
||||||
|
if col in df.columns:
|
||||||
|
val = df[col].iloc[0]
|
||||||
|
if val is None: return None
|
||||||
|
if hasattr(val, 'strftime'): return val.strftime('%Y-%m-%d')
|
||||||
|
return str(val)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
res_list.append({{"indicator": "company_name", "value": get_val(df, 'name')}})
|
res_list.append({{"indicator": "company_name", "value": safe_get(df, 'NAME')}})
|
||||||
res_list.append({{"indicator": "pe_ratio", "value": get_val(df, 'pe_ratio')}})
|
res_list.append({{"indicator": "pe_ratio", "value": safe_get(df, 'PE_RATIO')}})
|
||||||
res_list.append({{"indicator": "pb_ratio", "value": get_val(df, 'pb_ratio')}})
|
res_list.append({{"indicator": "pb_ratio", "value": safe_get(df, 'PX_TO_BOOK_RATIO')}})
|
||||||
res_list.append({{"indicator": "market_cap", "value": get_val(df, 'cur_mkt_cap')}})
|
# res_list.append({{"indicator": "market_cap", "value": safe_get(df, 'CUR_MKT_CAP')}}) # Moved to BDH
|
||||||
res_list.append({{"indicator": "Rev_Abroad", "value": get_val(df, 'FOREIGN')}})
|
res_list.append({{"indicator": "Rev_Abroad", "value": safe_get(df, 'PCT_REVENUE_FROM_FOREIGN_SOURCES')}})
|
||||||
except Exception as e:
|
res_list.append({{"indicator": "dividend_yield", "value": safe_get(df, 'DIVIDEND_12_MONTH_YIELD')}})
|
||||||
print(f"Basic BQL Error: {{e}}")
|
res_list.append({{"indicator": "IPO_date", "value": safe_get(df, 'EQY_INIT_PO_DT')}})
|
||||||
|
|
||||||
# 2. BDP for IPO and Dividend
|
|
||||||
try:
|
|
||||||
did = bquery.bdp([company], ["DIVIDEND_12_MONTH_YIELD"])
|
|
||||||
if not did.empty and 'DIVIDEND_12_MONTH_YIELD' in did.columns:
|
|
||||||
res_list.append({{"indicator": "dividend_yield", "value": str(did['DIVIDEND_12_MONTH_YIELD'][0])}})
|
|
||||||
|
|
||||||
ipo = bquery.bdp([company], ["EQY_INIT_PO_DT"])
|
|
||||||
if not ipo.empty and 'EQY_INIT_PO_DT' in ipo.columns:
|
|
||||||
val = ipo['EQY_INIT_PO_DT'][0]
|
|
||||||
val_str = str(val)
|
|
||||||
if hasattr(val, 'strftime'):
|
|
||||||
val_str = val.strftime('%Y-%m-%d')
|
|
||||||
res_list.append({{"indicator": "IPO_date", "value": val_str}})
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Basic BDP Error: {{e}}")
|
print(f"Basic BDP Error: {{e}}")
|
||||||
|
|
||||||
|
# 2. BDH Query for Market Cap (To enforce Currency)
|
||||||
|
try:
|
||||||
|
import datetime
|
||||||
|
end_date = datetime.datetime.now().strftime('%Y%m%d')
|
||||||
|
start_date = (datetime.datetime.now() - datetime.timedelta(days=10)).strftime('%Y%m%d')
|
||||||
|
|
||||||
|
# Fetch CUR_MKT_CAP via BDH to respect currency option
|
||||||
|
df_cap = bquery.bdh([query_ticker], ['CUR_MKT_CAP'], start_date=start_date, end_date=end_date, options={{'currency': curr}})
|
||||||
|
|
||||||
|
if not df_cap.empty:
|
||||||
|
# BDH usually returns Market Cap in Millions.
|
||||||
|
# User simplified requirement: Keep it in Millions.
|
||||||
|
# Frontend will divide by 100 to get "Yi".
|
||||||
|
val_millions = df_cap.iloc[-1]['CUR_MKT_CAP']
|
||||||
|
res_list.append({{"indicator": "market_cap", "value": str(val_millions)}})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Basic BDH Market Cap Error: {{e}}")
|
||||||
|
|
||||||
# Format result
|
# Format result
|
||||||
final_res = []
|
final_res = []
|
||||||
today = datetime.now().strftime('%Y-%m-%d')
|
|
||||||
|
import datetime
|
||||||
|
today_dt = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
for item in res_list:
|
for item in res_list:
|
||||||
if item['value']:
|
# Check against None string too if needed
|
||||||
|
if item['value'] and str(item['value']) != 'None':
|
||||||
final_res.append({{
|
final_res.append({{
|
||||||
"Company_code": company,
|
"Company_code": company,
|
||||||
"update_date": today,
|
"update_date": today_dt,
|
||||||
"currency": curr,
|
"currency": curr,
|
||||||
"indicator": item['indicator'],
|
"indicator": item['indicator'],
|
||||||
"value": item['value'],
|
"value": item['value'],
|
||||||
"value_date": today
|
"value_date": today_dt
|
||||||
}})
|
}})
|
||||||
|
|
||||||
print("JSON_START")
|
print("JSON_START")
|
||||||
@ -583,27 +639,35 @@ get_basic()
|
|||||||
"""
|
"""
|
||||||
return self._execute_and_parse(code)
|
return self._execute_and_parse(code)
|
||||||
|
|
||||||
def _fetch_series_remote(self, company_code, currency, config_dict, result_type):
|
def _fetch_series_remote(self, company_code, currency, config_dict, result_type, query_ticker=None):
|
||||||
"""Generates code to fetch series data using BDH"""
|
"""Generates code to fetch series data using BDH"""
|
||||||
|
target_ticker = query_ticker if query_ticker else company_code
|
||||||
|
|
||||||
config_json = json.dumps(config_dict)
|
config_json = json.dumps(config_dict)
|
||||||
period_years = STOCKCARD_CONFIG['period']
|
period_years = STOCKCARD_CONFIG['period']
|
||||||
|
|
||||||
start_year = datetime.now().year - period_years
|
# Calculate start date
|
||||||
start_date = f"{start_year}0101"
|
end_date = datetime.now()
|
||||||
end_date = datetime.now().strftime('%Y%m%d')
|
start_date = end_date - timedelta(days=period_years*365)
|
||||||
|
|
||||||
|
# Format dates for BDH (YYYYMMDD)
|
||||||
|
start_date_str = start_date.strftime('%Y%m%d')
|
||||||
|
end_date_str = end_date.strftime('%Y%m%d')
|
||||||
|
|
||||||
|
# BDH Options
|
||||||
bdh_options = {
|
bdh_options = {
|
||||||
'periodicitySelection': 'YEARLY',
|
"periodicityAdjustment": "FISCAL",
|
||||||
'currency': currency,
|
"periodicitySelection": "YEARLY",
|
||||||
'nonTradingDayFillOption': 'ALL_CALENDAR_DAYS',
|
"currency": currency,
|
||||||
'nonTradingDayFillMethod': 'PREVIOUS_VALUE'
|
# "nonTradingDayFillOption": "NON_TRADING_WEEKDAYS",
|
||||||
|
# "nonTradingDayFillMethod": "PREVIOUS_VALUE"
|
||||||
}
|
}
|
||||||
bdh_opts_json = json.dumps(bdh_options)
|
bdh_opts_json = json.dumps(bdh_options)
|
||||||
|
|
||||||
code = f"""
|
code = f"""
|
||||||
def get_series():
|
def get_series():
|
||||||
company = "{company_code}"
|
company = "{company_code}"
|
||||||
|
query_ticker = "{target_ticker}"
|
||||||
curr = "{currency}"
|
curr = "{currency}"
|
||||||
config = {config_json}
|
config = {config_json}
|
||||||
|
|
||||||
@ -614,10 +678,10 @@ def get_series():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
df = bquery.bdh(
|
df = bquery.bdh(
|
||||||
[company],
|
[query_ticker],
|
||||||
fields,
|
fields,
|
||||||
start_date='{start_date}',
|
start_date='{start_date_str}',
|
||||||
end_date='{end_date}',
|
end_date='{end_date_str}',
|
||||||
options={bdh_opts_json}
|
options={bdh_opts_json}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -656,7 +720,7 @@ def get_series():
|
|||||||
|
|
||||||
res_list.append({{
|
res_list.append({{
|
||||||
"Company_code": company,
|
"Company_code": company,
|
||||||
"update_date": "{datetime.now().strftime('%Y-%m-%d')}",
|
"update_date": "{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
"currency": curr,
|
"currency": curr,
|
||||||
"indicator": indicator_name,
|
"indicator": indicator_name,
|
||||||
"value": val_str,
|
"value": val_str,
|
||||||
@ -674,11 +738,12 @@ get_series()
|
|||||||
"""
|
"""
|
||||||
return self._execute_and_parse(code)
|
return self._execute_and_parse(code)
|
||||||
|
|
||||||
def _fetch_price_by_dates_remote(self, company_code, currency, dates):
|
def _fetch_price_by_dates_remote(self, company_code, currency, dates, query_ticker=None):
|
||||||
"""Generates code to fetch price/mkt_cap for specific dates"""
|
"""Generates code to fetch price/mkt_cap for specific dates"""
|
||||||
if not dates:
|
if not dates:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
target_ticker = query_ticker if query_ticker else company_code
|
||||||
dates_json = json.dumps(dates)
|
dates_json = json.dumps(dates)
|
||||||
config_json = json.dumps(PRICE_CONFIG)
|
config_json = json.dumps(PRICE_CONFIG)
|
||||||
|
|
||||||
@ -692,6 +757,7 @@ get_series()
|
|||||||
code = f"""
|
code = f"""
|
||||||
def get_price_by_dates():
|
def get_price_by_dates():
|
||||||
company = "{company_code}"
|
company = "{company_code}"
|
||||||
|
query_ticker = "{target_ticker}"
|
||||||
curr = "{currency}"
|
curr = "{currency}"
|
||||||
dates = {dates_json}
|
dates = {dates_json}
|
||||||
config = {config_json}
|
config = {config_json}
|
||||||
@ -705,27 +771,30 @@ def get_price_by_dates():
|
|||||||
# d_str is 'YYYY-MM-DD', bdh needs 'YYYYMMDD'
|
# d_str is 'YYYY-MM-DD', bdh needs 'YYYYMMDD'
|
||||||
d_param = d_str.replace('-', '')
|
d_param = d_str.replace('-', '')
|
||||||
|
|
||||||
try:
|
for mnemonic, indicator_name in mnemonic_map.items():
|
||||||
df = bquery.bdh(
|
field = mnemonic
|
||||||
[company],
|
try:
|
||||||
fields,
|
df = bquery.bdh(
|
||||||
start_date=d_param,
|
[query_ticker],
|
||||||
end_date=d_param,
|
[field],
|
||||||
options={bdh_opts_json}
|
start_date=d_param,
|
||||||
)
|
end_date=d_param,
|
||||||
|
options={bdh_opts_json}
|
||||||
if not df.empty:
|
)
|
||||||
for _, row in df.iterrows():
|
|
||||||
# value_date is d_str
|
|
||||||
|
|
||||||
for mnemonic, indicator_name in mnemonic_map.items():
|
|
||||||
col_name = mnemonic
|
|
||||||
# bdh columns might be tuple or just string depending on request
|
|
||||||
# usually 'PX_LAST'
|
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
for _, row in df.iterrows():
|
||||||
val = None
|
val = None
|
||||||
if col_name in df.columns:
|
if field in df.columns:
|
||||||
val = row[col_name]
|
val = row[field]
|
||||||
|
elif field in row:
|
||||||
|
val = row[field]
|
||||||
|
else:
|
||||||
|
# Try case insensitive
|
||||||
|
for c in df.columns:
|
||||||
|
if c.upper() == field:
|
||||||
|
val = row[c]
|
||||||
|
break
|
||||||
|
|
||||||
if pd.isna(val) or val is None:
|
if pd.isna(val) or val is None:
|
||||||
continue
|
continue
|
||||||
@ -734,14 +803,16 @@ def get_price_by_dates():
|
|||||||
|
|
||||||
res_list.append({{
|
res_list.append({{
|
||||||
"Company_code": company,
|
"Company_code": company,
|
||||||
"update_date": "{datetime.now().strftime('%Y-%m-%d')}",
|
"update_date": "{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
"currency": curr,
|
"currency": curr,
|
||||||
"indicator": indicator_name,
|
"indicator": indicator_name,
|
||||||
"value": val_str,
|
"value": val_str,
|
||||||
"value_date": d_str
|
"value_date": d_str
|
||||||
}})
|
}})
|
||||||
except Exception as e:
|
|
||||||
print(f"Error fetching price for {{d_str}}: {{e}}")
|
except Exception as e:
|
||||||
|
# print(f"BDH Error for {{field}}: {{e}}")
|
||||||
|
pass
|
||||||
|
|
||||||
print("JSON_START")
|
print("JSON_START")
|
||||||
print(json.dumps(res_list))
|
print(json.dumps(res_list))
|
||||||
@ -755,7 +826,9 @@ get_price_by_dates()
|
|||||||
"""Execute code and parse [JSON_START]...[JSON_END]"""
|
"""Execute code and parse [JSON_START]...[JSON_END]"""
|
||||||
raw_output_list = self.execute_remote_code(code)
|
raw_output_list = self.execute_remote_code(code)
|
||||||
raw_output = "".join(raw_output_list) if isinstance(raw_output_list, list) else str(raw_output_list)
|
raw_output = "".join(raw_output_list) if isinstance(raw_output_list, list) else str(raw_output_list)
|
||||||
# logger.debug(f"Remote execution returned {len(raw_output)} chars.") # Optional debug
|
# Always log raw output first few chars for debug
|
||||||
|
logger.info(f"REMOTE RAW OUTPUT: {raw_output[:1000]}")
|
||||||
|
# Actually let's log everything if it fails to find JSON
|
||||||
|
|
||||||
# Simple parser
|
# Simple parser
|
||||||
try:
|
try:
|
||||||
@ -768,7 +841,7 @@ get_price_by_dates()
|
|||||||
logger.info(f"✅ Parsed {len(data) if isinstance(data, list) else 1} items from remote.")
|
logger.info(f"✅ Parsed {len(data) if isinstance(data, list) else 1} items from remote.")
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠️ No JSON output found in remote response.")
|
logger.warning(f"⚠️ No JSON output found in remote response. Raw: {raw_output}")
|
||||||
return []
|
return []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error parsing JSON from remote: {e}")
|
logger.error(f"❌ Error parsing JSON from remote: {e}")
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class BloombergFetcher(DataFetcher):
|
|||||||
# placeholders for 'IN' clause
|
# placeholders for 'IN' clause
|
||||||
placeholders = ','.join(['%s'] * len(indicators))
|
placeholders = ','.join(['%s'] * len(indicators))
|
||||||
query = f"""
|
query = f"""
|
||||||
SELECT indicator, value, value_date
|
SELECT indicator, value, value_date, currency
|
||||||
FROM stockcard
|
FROM stockcard
|
||||||
WHERE Company_code = %s AND indicator IN ({placeholders})
|
WHERE Company_code = %s AND indicator IN ({placeholders})
|
||||||
ORDER BY value_date DESC
|
ORDER BY value_date DESC
|
||||||
@ -65,15 +65,17 @@ class BloombergFetcher(DataFetcher):
|
|||||||
if not rows:
|
if not rows:
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
df = pd.DataFrame(rows, columns=['indicator', 'value', 'value_date'])
|
df = pd.DataFrame(rows, columns=['indicator', 'value', 'value_date', 'currency'])
|
||||||
|
|
||||||
# Pivot
|
# Pivot
|
||||||
# Index: value_date
|
# Index: [value_date, currency] -> ensures we keep both JPY and USD rows for the same date
|
||||||
# Columns: indicator
|
# Columns: indicator
|
||||||
# Values: value
|
# Values: value
|
||||||
df_pivot = df.pivot(index='value_date', columns='indicator', values='value').reset_index()
|
df_pivot = df.pivot(index=['value_date', 'currency'], columns='indicator', values='value').reset_index()
|
||||||
df_pivot.rename(columns={'value_date': 'end_date'}, inplace=True)
|
df_pivot.rename(columns={'value_date': 'end_date'}, inplace=True)
|
||||||
|
|
||||||
|
# No need to manual merge currency back, it's already in the index
|
||||||
|
|
||||||
# Clean columns? No, they are standard from config.
|
# Clean columns? No, they are standard from config.
|
||||||
return df_pivot
|
return df_pivot
|
||||||
|
|
||||||
@ -103,14 +105,12 @@ class BloombergFetcher(DataFetcher):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Bloomberg fetch failed (ignoring, checking DB): {e}")
|
print(f"Bloomberg fetch failed (ignoring, checking DB): {e}")
|
||||||
|
|
||||||
def sync_all_data(self, symbol: str, progress_callback=None) -> None:
|
def sync_all_data(self, symbol: str, progress_callback=None, force_currency=None):
|
||||||
"""
|
"""
|
||||||
全量同步 Bloomberg 数据
|
Sync all data for the company.
|
||||||
|
Delegates to the universal client.
|
||||||
触发一次全量抓取即可,不需要分别抓取各个报表。
|
|
||||||
数据会自动存储到数据库。
|
|
||||||
"""
|
"""
|
||||||
self._ensure_data_fetched(symbol, progress_callback=progress_callback)
|
self.client.fetch_company(self.market, symbol, progress_callback=progress_callback, force_currency=force_currency)
|
||||||
|
|
||||||
def get_income_statement(self, symbol: str) -> pd.DataFrame:
|
def get_income_statement(self, symbol: str) -> pd.DataFrame:
|
||||||
"""兼容性空方法"""
|
"""兼容性空方法"""
|
||||||
|
|||||||
@ -47,6 +47,7 @@ class FetchDataRequest(BaseModel):
|
|||||||
company_name: str
|
company_name: str
|
||||||
data_source: str
|
data_source: str
|
||||||
force_refresh: bool = False
|
force_refresh: bool = False
|
||||||
|
currency: Optional[str] = None
|
||||||
|
|
||||||
class FetchDataResponse(BaseModel):
|
class FetchDataResponse(BaseModel):
|
||||||
update_id: int
|
update_id: int
|
||||||
|
|||||||
@ -58,8 +58,9 @@ async def get_bloomberg_data(
|
|||||||
|
|
||||||
# 2. 获取该公司的所有数据
|
# 2. 获取该公司的所有数据
|
||||||
# schema: indicator, value, value_date (作为报告期)
|
# schema: indicator, value, value_date (作为报告期)
|
||||||
|
# Added update_date
|
||||||
query = text("""
|
query = text("""
|
||||||
SELECT indicator, value, value_date, currency
|
SELECT indicator, value, value_date, currency, update_date
|
||||||
FROM stockcard
|
FROM stockcard
|
||||||
WHERE Company_code = :code
|
WHERE Company_code = :code
|
||||||
""")
|
""")
|
||||||
@ -73,20 +74,30 @@ async def get_bloomberg_data(
|
|||||||
val = row.value
|
val = row.value
|
||||||
v_date = row.value_date
|
v_date = row.value_date
|
||||||
curr = row.currency
|
curr = row.currency
|
||||||
|
u_date = row.update_date
|
||||||
|
|
||||||
if not v_date:
|
if not v_date:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
date_str = v_date.isoformat() if hasattr(v_date, 'isoformat') else str(v_date)
|
date_str = v_date.isoformat() if hasattr(v_date, 'isoformat') else str(v_date)
|
||||||
|
# Handle update_date formatting
|
||||||
|
update_date_str = ""
|
||||||
|
if u_date:
|
||||||
|
update_date_str = u_date.isoformat() if hasattr(u_date, 'isoformat') else str(u_date)
|
||||||
|
|
||||||
if date_str not in data_by_date:
|
# Key by (date, currency) to support multiple currencies for the same date
|
||||||
data_by_date[date_str] = {
|
unique_key = f"{date_str}_{curr}"
|
||||||
|
|
||||||
|
if unique_key not in data_by_date:
|
||||||
|
data_by_date[unique_key] = {
|
||||||
"end_date": date_str,
|
"end_date": date_str,
|
||||||
"currency": curr
|
"currency": curr,
|
||||||
|
"update_date": ""
|
||||||
}
|
}
|
||||||
elif data_by_date[date_str].get("currency") is None and curr:
|
|
||||||
# If existing entry has no currency, but we have one now, update it
|
# Update the group's update_date if we find a newer one in this batch
|
||||||
data_by_date[date_str]["currency"] = curr
|
if update_date_str > data_by_date[unique_key]["update_date"]:
|
||||||
|
data_by_date[unique_key]["update_date"] = update_date_str
|
||||||
|
|
||||||
# Normalize key to match frontend expectation (mostly lowercase)
|
# Normalize key to match frontend expectation (mostly lowercase)
|
||||||
# e.g. "ROE" -> "roe", "PE" -> "pe"
|
# e.g. "ROE" -> "roe", "PE" -> "pe"
|
||||||
@ -95,7 +106,7 @@ async def get_bloomberg_data(
|
|||||||
# Special case mapping if needed, but lowercase covers most
|
# Special case mapping if needed, but lowercase covers most
|
||||||
# frontend uses: net_income, revenue, etc.
|
# frontend uses: net_income, revenue, etc.
|
||||||
|
|
||||||
data_by_date[date_str][norm_indicator] = val
|
data_by_date[unique_key][norm_indicator] = val
|
||||||
|
|
||||||
# 4. 转换为列表并排序
|
# 4. 转换为列表并排序
|
||||||
full_list = list(data_by_date.values())
|
full_list = list(data_by_date.values())
|
||||||
|
|||||||
@ -209,7 +209,8 @@ def fetch_financial_data_sync(
|
|||||||
market: str,
|
market: str,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
data_source: str,
|
data_source: str,
|
||||||
update_id: int
|
update_id: int,
|
||||||
|
currency: Optional[str] = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
同步方式获取财务数据(在后台任务中调用)
|
同步方式获取财务数据(在后台任务中调用)
|
||||||
@ -258,7 +259,10 @@ def fetch_financial_data_sync(
|
|||||||
import inspect
|
import inspect
|
||||||
sig = inspect.signature(fetcher.sync_all_data)
|
sig = inspect.signature(fetcher.sync_all_data)
|
||||||
if 'progress_callback' in sig.parameters:
|
if 'progress_callback' in sig.parameters:
|
||||||
fetcher.sync_all_data(formatted_symbol, progress_callback=progress_callback)
|
if 'force_currency' in sig.parameters:
|
||||||
|
fetcher.sync_all_data(formatted_symbol, progress_callback=progress_callback, force_currency=currency)
|
||||||
|
else:
|
||||||
|
fetcher.sync_all_data(formatted_symbol, progress_callback=progress_callback)
|
||||||
else:
|
else:
|
||||||
fetcher.sync_all_data(formatted_symbol)
|
fetcher.sync_all_data(formatted_symbol)
|
||||||
else:
|
else:
|
||||||
|
|||||||
70
backend/debug_bdh.py
Normal file
70
backend/debug_bdh.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Ensure we can import from backend
|
||||||
|
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
from app.clients.bloomberg_client import BloombergClient
|
||||||
|
|
||||||
|
def test_bdh_currency():
|
||||||
|
print("Initializing BloombergClient...")
|
||||||
|
client = BloombergClient()
|
||||||
|
|
||||||
|
ticker = "6301 JP Equity"
|
||||||
|
|
||||||
|
code = """
|
||||||
|
import sys
|
||||||
|
import datetime
|
||||||
|
try:
|
||||||
|
if 'bquery' not in globals():
|
||||||
|
import bquery
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ticker = "{ticker}"
|
||||||
|
fields = ["CUR_MKT_CAP", "PX_LAST"]
|
||||||
|
|
||||||
|
print(f"--- BDH Currency Test for {{ticker}} ---")
|
||||||
|
|
||||||
|
def get_bdh_val(curr, desc):
|
||||||
|
try:
|
||||||
|
# BDH request for last 5 days to ensure we hit a trading day
|
||||||
|
end_date = datetime.datetime.now().strftime('%Y%m%d')
|
||||||
|
start_date = (datetime.datetime.now() - datetime.timedelta(days=10)).strftime('%Y%m%d')
|
||||||
|
|
||||||
|
# Pass currency in options
|
||||||
|
df = bquery.bdh([ticker], fields, start_date=start_date, end_date=end_date, options={{'currency': curr}})
|
||||||
|
|
||||||
|
if not df.empty:
|
||||||
|
print(f"--- {{desc}} (Currency: {{curr}}) ---")
|
||||||
|
# Get last row
|
||||||
|
last_row = df.iloc[-1]
|
||||||
|
for col in df.columns:
|
||||||
|
print(f" {{col}}: {{last_row[col]}}")
|
||||||
|
else:
|
||||||
|
print(f"{{desc}} Empty")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{{desc}} Error: {{e}}")
|
||||||
|
|
||||||
|
# 1. JPY
|
||||||
|
get_bdh_val('JPY', "BDH_JPY")
|
||||||
|
|
||||||
|
# 2. USD
|
||||||
|
get_bdh_val('USD', "BDH_USD")
|
||||||
|
|
||||||
|
"""
|
||||||
|
formatted_code = code.format(ticker=ticker)
|
||||||
|
|
||||||
|
print(f"Executing remote logic for {ticker}...")
|
||||||
|
output = client.execute_remote_code(formatted_code)
|
||||||
|
print("Remote Output:")
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_bdh_currency()
|
||||||
67
backend/debug_bdp_currency.py
Normal file
67
backend/debug_bdp_currency.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Ensure we can import from backend
|
||||||
|
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
from app.clients.bloomberg_client import BloombergClient
|
||||||
|
|
||||||
|
def test_bdp_options_injection():
|
||||||
|
print("Initializing BloombergClient...")
|
||||||
|
client = BloombergClient()
|
||||||
|
|
||||||
|
ticker = "6301 JP Equity"
|
||||||
|
|
||||||
|
code = """
|
||||||
|
import sys
|
||||||
|
try:
|
||||||
|
if 'bquery' not in globals():
|
||||||
|
import bquery
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ticker = "{ticker}"
|
||||||
|
fields = ["CUR_MKT_CAP", "PX_LAST"]
|
||||||
|
|
||||||
|
print(f"--- Option Injection Test for {{ticker}} ---")
|
||||||
|
|
||||||
|
def get_val_via_options(custom_overrides, desc):
|
||||||
|
try:
|
||||||
|
# Pass overrides=None, pass custom overrides via options
|
||||||
|
# Escape braces for format string by doubling them
|
||||||
|
df = bquery.bdp([ticker], fields, overrides=None, options={{'overrides': custom_overrides}})
|
||||||
|
if not df.empty:
|
||||||
|
print(f"--- {{desc}} ---")
|
||||||
|
for col in df.columns:
|
||||||
|
print(f" {{col}}: {{df.iloc[0][col]}}")
|
||||||
|
print(f" Sent: {{custom_overrides}}")
|
||||||
|
else:
|
||||||
|
print(f"{{desc}} Empty")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{{desc}} Error: {{e}}")
|
||||||
|
|
||||||
|
# 1. Direct fieldId (No wrapper)
|
||||||
|
get_val_via_options([{{'fieldId': 'CURRENCY', 'value': 'USD'}}], "Try1_Direct_Dict")
|
||||||
|
|
||||||
|
# 2. Wrapped in 'overrides' (Same as current impl)
|
||||||
|
get_val_via_options([{{'overrides': {{'fieldId': 'CURRENCY', 'value': 'USD'}}}}], "Try2_Wrapped_Overrides")
|
||||||
|
|
||||||
|
# 3. Wrapped in 'override' (Singular)
|
||||||
|
get_val_via_options([{{'override': {{'fieldId': 'CURRENCY', 'value': 'USD'}}}}], "Try3_Wrapped_Override")
|
||||||
|
|
||||||
|
"""
|
||||||
|
formatted_code = code.format(ticker=ticker)
|
||||||
|
|
||||||
|
print(f"Executing remote logic for {ticker}...")
|
||||||
|
output = client.execute_remote_code(formatted_code)
|
||||||
|
print("Remote Output:")
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_bdp_options_injection()
|
||||||
94
backend/debug_revenue_dates.py
Normal file
94
backend/debug_revenue_dates.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
|
||||||
|
import asyncio
|
||||||
|
from app.clients.bloomberg_client import BloombergClient, CURRENCY_CONFIG
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
client = BloombergClient()
|
||||||
|
company = "2503 JP Equity"
|
||||||
|
|
||||||
|
# Simulate fetch of currency data (which includes Revenue)
|
||||||
|
# CURRENCY_CONFIG key "Revenue" is "SALES_REV_TURN"
|
||||||
|
|
||||||
|
print(f"Debugging Revenue Dates for {company} (CNY)...")
|
||||||
|
|
||||||
|
# We'll use the exact simulating logic of _fetch_series_remote but print the dates
|
||||||
|
|
||||||
|
config = {"Revenue": CURRENCY_CONFIG["Revenue"]}
|
||||||
|
# Force CNY
|
||||||
|
currency = "CNY"
|
||||||
|
|
||||||
|
# Generate the code that _fetch_series_remote generates
|
||||||
|
# We need to replicate the remote code construction roughly or call execute_remote_code
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
STOCKCARD_CONFIG = { "period": 10 }
|
||||||
|
period_years = STOCKCARD_CONFIG['period']
|
||||||
|
start_year = datetime.now().year - period_years
|
||||||
|
start_date = f"{start_year}0101"
|
||||||
|
end_date = datetime.now().strftime('%Y%m%d')
|
||||||
|
|
||||||
|
bdh_options = {
|
||||||
|
'periodicitySelection': 'YEARLY',
|
||||||
|
'currency': currency,
|
||||||
|
'nonTradingDayFillOption': 'ALL_CALENDAR_DAYS',
|
||||||
|
'nonTradingDayFillMethod': 'PREVIOUS_VALUE'
|
||||||
|
}
|
||||||
|
|
||||||
|
config_json = json.dumps(config)
|
||||||
|
bdh_opts_json = json.dumps(bdh_options)
|
||||||
|
|
||||||
|
remote_code = f"""
|
||||||
|
def debug_series():
|
||||||
|
company = "{company}"
|
||||||
|
curr = "{currency}"
|
||||||
|
config = {config_json}
|
||||||
|
|
||||||
|
mnemonic_map = {{v.upper(): k for k, v in config.items()}}
|
||||||
|
fields = list(mnemonic_map.keys())
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = bquery.bdh(
|
||||||
|
[company],
|
||||||
|
fields,
|
||||||
|
start_date='{start_date}',
|
||||||
|
end_date='{end_date}',
|
||||||
|
options={bdh_opts_json}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"--- Raw Revenue Data (CNY) ---")
|
||||||
|
if not df.empty:
|
||||||
|
print(df)
|
||||||
|
|
||||||
|
revenue_dates = []
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
# Extract date
|
||||||
|
date_val = None
|
||||||
|
if 'date' in df.columns: date_val = row['date']
|
||||||
|
elif 'DATE' in df.columns: date_val = row['DATE']
|
||||||
|
|
||||||
|
if date_val:
|
||||||
|
d_str = str(date_val)[:10]
|
||||||
|
# Check for value
|
||||||
|
val = None
|
||||||
|
if fields[0] in row: val = row[fields[0]]
|
||||||
|
elif fields[0] in df.columns: val = row[fields[0]]
|
||||||
|
|
||||||
|
if val is not None:
|
||||||
|
revenue_dates.append(d_str)
|
||||||
|
|
||||||
|
print(f"\\nExtracted Revenue Dates: {{sorted(revenue_dates, reverse=True)}}")
|
||||||
|
else:
|
||||||
|
print("DataFrame is empty.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {{e}}")
|
||||||
|
|
||||||
|
debug_series()
|
||||||
|
"""
|
||||||
|
result = client.execute_remote_code(remote_code)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(run())
|
||||||
42
backend/inspect_db.py
Normal file
42
backend/inspect_db.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
def inspect_schema():
|
||||||
|
db_host = os.getenv("DB_HOST", "192.168.3.195")
|
||||||
|
db_user = os.getenv("DB_USER", "value")
|
||||||
|
db_pass = os.getenv("DB_PASSWORD", "Value609!")
|
||||||
|
db_name = os.getenv("DB_NAME", "fa3")
|
||||||
|
db_port = os.getenv("DB_PORT", "5432")
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=db_host, user=db_user, password=db_pass, dbname=db_name, port=db_port
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
print("--- Table Columns ---")
|
||||||
|
cur.execute("""
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'stockcard';
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
print(f"{row[0]}: {row[1]}")
|
||||||
|
|
||||||
|
print("\n--- Recent Records update_date ---")
|
||||||
|
cur.execute("SELECT update_date FROM stockcard ORDER BY id DESC LIMIT 5")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
print(f"{row[0]} (Type: {type(row[0])})")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
inspect_schema()
|
||||||
39
backend/inspect_market_cap.py
Normal file
39
backend/inspect_market_cap.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Config logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
def inspect_db():
|
||||||
|
db_host = os.getenv("DB_HOST", "192.168.3.195")
|
||||||
|
db_user = os.getenv("DB_USER", "value")
|
||||||
|
db_pass = os.getenv("DB_PASSWORD", "Value609!")
|
||||||
|
db_name = os.getenv("DB_NAME", "fa3")
|
||||||
|
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=db_host, user=db_user, password=db_pass, dbname=db_name
|
||||||
|
)
|
||||||
|
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
# Check basic data market cap
|
||||||
|
cur.execute("""
|
||||||
|
SELECT update_date, indicator, value, value_date, currency, source
|
||||||
|
FROM stockcard
|
||||||
|
WHERE Company_code LIKE '6301 JP%'
|
||||||
|
AND (indicator = 'market_cap' OR indicator = 'Market_Cap')
|
||||||
|
ORDER BY update_date DESC, value_date DESC
|
||||||
|
LIMIT 20;
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
print(f"{'Update Date':<20} | {'Indicator':<15} | {'Currency':<5} | {'Value Date':<12} | {'Value (Raw)':<20} | {'Source'}")
|
||||||
|
print("-" * 100)
|
||||||
|
for row in rows:
|
||||||
|
print(f"{str(row[0]):<20} | {row[1]:<15} | {row[4]:<5} | {str(row[3]):<12} | {row[2]:<20} | {row[5]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
inspect_db()
|
||||||
1139
backend/server.log
1139
backend/server.log
File diff suppressed because it is too large
Load Diff
61
backend/test_bloomberg_market_cap.py
Normal file
61
backend/test_bloomberg_market_cap.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Add app to path
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), 'app'))
|
||||||
|
|
||||||
|
from app.clients.bloomberg_client import BloombergClient
|
||||||
|
|
||||||
|
# Config logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
def test_market_cap():
|
||||||
|
print("Initializing Client...")
|
||||||
|
client = BloombergClient()
|
||||||
|
|
||||||
|
# Target: 6301 JP Equity (Komatsu Ltd)
|
||||||
|
company_code = "6301 JP Equity"
|
||||||
|
|
||||||
|
# Dates to fetch (last few years)
|
||||||
|
dates = ["2023-03-31", "2024-03-31"] # Komatsu fiscal year end is usually March 31
|
||||||
|
|
||||||
|
# 1. Fetch in JPY (Local)
|
||||||
|
print("\nfetching Market Cap in JPY...")
|
||||||
|
try:
|
||||||
|
data_jpy = client._fetch_price_by_dates_remote(
|
||||||
|
company_code=company_code,
|
||||||
|
currency="JPY",
|
||||||
|
dates=dates,
|
||||||
|
query_ticker=company_code
|
||||||
|
)
|
||||||
|
print("--- Result JPY ---")
|
||||||
|
print(json.dumps(data_jpy, indent=2, ensure_ascii=False))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error JPY: {e}")
|
||||||
|
|
||||||
|
# 2. Fetch in CNY (Converted) - Using Series Remote (Fallback Path)
|
||||||
|
print("\nfetching Market Cap in CNY (Series Remote Fallback)...")
|
||||||
|
try:
|
||||||
|
from app.clients.bloomberg_client import PRICE_CONFIG
|
||||||
|
|
||||||
|
# We need to simulate the fallback call:
|
||||||
|
# price_data = self._fetch_series_remote(company_code, currency, PRICE_CONFIG, "price", query_ticker=query_ticker)
|
||||||
|
|
||||||
|
data_cny = client._fetch_series_remote(
|
||||||
|
company_code=company_code,
|
||||||
|
currency="CNY",
|
||||||
|
config_dict=PRICE_CONFIG,
|
||||||
|
result_type="price",
|
||||||
|
query_ticker=company_code
|
||||||
|
)
|
||||||
|
print("--- Result CNY (Series) ---")
|
||||||
|
print(json.dumps(data_cny[:5], indent=2, ensure_ascii=False))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error CNY: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_market_cap()
|
||||||
66
backend/test_bloomberg_series.py
Normal file
66
backend/test_bloomberg_series.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# Add app to path
|
||||||
|
sys.path.append(os.path.join(os.path.dirname(__file__), 'app'))
|
||||||
|
|
||||||
|
from app.clients.bloomberg_client import BloombergClient
|
||||||
|
|
||||||
|
# Config logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
def test_series():
|
||||||
|
print("Initializing Client...")
|
||||||
|
client = BloombergClient()
|
||||||
|
|
||||||
|
# Simulate the logic in fetch_company for HK
|
||||||
|
symbol = "00631"
|
||||||
|
market = "HK"
|
||||||
|
|
||||||
|
# 1. Ticker Logic
|
||||||
|
mapped_market = "CH" if market == "CN" else market
|
||||||
|
company_code = f"{symbol} {mapped_market} Equity" # "00631 HK Equity"
|
||||||
|
|
||||||
|
query_ticker = company_code
|
||||||
|
if market == "HK" or (len(company_code.split()) > 1 and company_code.split()[1] == "HK"):
|
||||||
|
parts = company_code.split()
|
||||||
|
ticker_part = parts[0]
|
||||||
|
if ticker_part.isdigit():
|
||||||
|
short_ticker = str(int(ticker_part))
|
||||||
|
query_ticker = f"{short_ticker} {' '.join(parts[1:])}" # "631 HK Equity"
|
||||||
|
|
||||||
|
print(f"Company Code (Storage): {company_code}")
|
||||||
|
print(f"Query Ticker (Bloomberg): {query_ticker}")
|
||||||
|
|
||||||
|
currency = "HKD"
|
||||||
|
|
||||||
|
# 2. Test Currency Data Fetch (Revenue)
|
||||||
|
print("\nfetching Currency Data (Series)...")
|
||||||
|
curr_indicators = {
|
||||||
|
"Revenue": "SALES_REV_TURN"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Call the internal method directly
|
||||||
|
data = client._fetch_series_remote(
|
||||||
|
company_code=company_code,
|
||||||
|
currency=currency,
|
||||||
|
config_dict=curr_indicators,
|
||||||
|
result_type="currency",
|
||||||
|
query_ticker=query_ticker
|
||||||
|
)
|
||||||
|
print("--- Result (First 5 items) ---")
|
||||||
|
print(json.dumps(data[:5], indent=2, ensure_ascii=False))
|
||||||
|
print(f"Total items: {len(data)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_series()
|
||||||
57
backend/verify_currency.py
Normal file
57
backend/verify_currency.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
from app.clients.bloomberg_client import BloombergClient
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
client = BloombergClient()
|
||||||
|
company = "2503 JP Equity"
|
||||||
|
|
||||||
|
# We want to check 2024-12-31 data for JPY, CNY, USD
|
||||||
|
currencies = ["JPY", "CNY", "USD"]
|
||||||
|
|
||||||
|
print(f"Testing Market Cap for {company} on 2024-12-31 in {currencies}")
|
||||||
|
|
||||||
|
remote_code = f"""
|
||||||
|
def test_currency():
|
||||||
|
company = "{company}"
|
||||||
|
currencies = {currencies}
|
||||||
|
date = "20241231"
|
||||||
|
|
||||||
|
results = {{}}
|
||||||
|
|
||||||
|
try:
|
||||||
|
for curr in currencies:
|
||||||
|
print(f"Fetching for {{curr}}...")
|
||||||
|
# Use BDH for historical specific date
|
||||||
|
df = bquery.bdh(
|
||||||
|
[company],
|
||||||
|
['CUR_MKT_CAP'],
|
||||||
|
start_date=date,
|
||||||
|
end_date=date,
|
||||||
|
options={{'currency': curr, 'periodicitySelection': 'DAILY', 'nonTradingDayFillOption': 'ALL_CALENDAR_DAYS', 'nonTradingDayFillMethod': 'PREVIOUS_VALUE'}}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not df.empty and 'CUR_MKT_CAP' in df.columns:
|
||||||
|
val = df['CUR_MKT_CAP'].iloc[0]
|
||||||
|
results[curr] = val
|
||||||
|
else:
|
||||||
|
results[curr] = "No Data"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {{e}}")
|
||||||
|
|
||||||
|
import json
|
||||||
|
print("JSON_START")
|
||||||
|
print(json.dumps(results))
|
||||||
|
print("JSON_END")
|
||||||
|
|
||||||
|
test_currency()
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = client.execute_remote_code(remote_code)
|
||||||
|
print("\nCheck Result:")
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(run())
|
||||||
41
backend/verify_historical_currency.py
Normal file
41
backend/verify_historical_currency.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from app.clients.bloomberg_client import BloombergClient
|
||||||
|
|
||||||
|
# Mock the PRICE_CONFIG to ensure we are fetching what we expect if it's not imported
|
||||||
|
# But BloombergClient imports it from configuration.
|
||||||
|
# Let's rely on the real client.
|
||||||
|
|
||||||
|
async def run():
|
||||||
|
client = BloombergClient()
|
||||||
|
company = "2503 JP Equity"
|
||||||
|
dates = ['2024-12-31', '2023-12-31', '2022-12-31', '2021-12-31', '2020-12-31', '2019-12-31', '2018-12-31', '2017-12-31', '2016-12-31']
|
||||||
|
|
||||||
|
currencies = ["CNY"]
|
||||||
|
|
||||||
|
print(f"Testing _fetch_price_by_dates_remote for {company} on {dates}...")
|
||||||
|
|
||||||
|
for curr in currencies:
|
||||||
|
print(f"\n--- Fetching in {curr} ---")
|
||||||
|
try:
|
||||||
|
# The method allows fetching price data for specific dates
|
||||||
|
# It uses PRICE_CONFIG which includes 'Market_Cap'
|
||||||
|
data = client._fetch_price_by_dates_remote(company, curr, dates)
|
||||||
|
|
||||||
|
# Filter for Market_Cap to show user
|
||||||
|
mkt_caps = [d for d in data if d['indicator'] == 'Market_Cap']
|
||||||
|
|
||||||
|
if mkt_caps:
|
||||||
|
for item in mkt_caps:
|
||||||
|
print(f"Currency: {item['currency']}, Date: {item['value_date']}, Value: {item['value']}")
|
||||||
|
else:
|
||||||
|
print("No Market_Cap data returned.")
|
||||||
|
print("Raw data returned:", json.dumps(data, indent=2))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching {curr}: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(run())
|
||||||
@ -19,6 +19,8 @@ import { Progress } from "@/components/ui/progress"
|
|||||||
import { formatTimestamp } from "@/lib/formatters"
|
import { formatTimestamp } from "@/lib/formatters"
|
||||||
import { BloombergView } from "@/components/bloomberg-view"
|
import { BloombergView } from "@/components/bloomberg-view"
|
||||||
import { HeaderPortal } from "@/components/header-portal"
|
import { HeaderPortal } from "@/components/header-portal"
|
||||||
|
import { AppSidebar } from "@/components/app-sidebar"
|
||||||
|
import { StockChart } from "@/components/stock-chart"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
@ -27,18 +29,23 @@ export default function Home() {
|
|||||||
const [selectedCompany, setSelectedCompany] = useState<SearchResult | null>(null)
|
const [selectedCompany, setSelectedCompany] = useState<SearchResult | null>(null)
|
||||||
const [selectedDataSource, setSelectedDataSource] = useState<string>("iFinD")
|
const [selectedDataSource, setSelectedDataSource] = useState<string>("iFinD")
|
||||||
const [companyId, setCompanyId] = useState<number | null>(null)
|
const [companyId, setCompanyId] = useState<number | null>(null)
|
||||||
const [showFinancialData, setShowFinancialData] = useState(false)
|
|
||||||
const [analysisId, setAnalysisId] = useState<number | null>(null)
|
const [analysisId, setAnalysisId] = useState<number | null>(null)
|
||||||
const [oneTimeModel, setOneTimeModel] = useState<string | undefined>(undefined)
|
const [oneTimeModel, setOneTimeModel] = useState<string | undefined>(undefined)
|
||||||
|
const [currency, setCurrency] = useState<string>("Auto")
|
||||||
|
|
||||||
|
// View State (home, financial, chart)
|
||||||
|
const [currentView, setCurrentView] = useState<string>("home")
|
||||||
|
|
||||||
// 处理公司选择
|
// 处理公司选择
|
||||||
const handleCompanySelect = (company: SearchResult, dataSource?: string) => {
|
const handleCompanySelect = (company: SearchResult, dataSource?: string) => {
|
||||||
setSelectedCompany(company)
|
setSelectedCompany(company)
|
||||||
setCompanyId(null)
|
setCompanyId(null)
|
||||||
setShowFinancialData(false)
|
|
||||||
setAnalysisId(null)
|
setAnalysisId(null)
|
||||||
setOneTimeModel(undefined)
|
setOneTimeModel(undefined)
|
||||||
|
|
||||||
|
// Switch to financial view by default when company is selected
|
||||||
|
setCurrentView("financial")
|
||||||
|
|
||||||
// 如果没有传入数据源,则根据市场设置默认值
|
// 如果没有传入数据源,则根据市场设置默认值
|
||||||
const targetDataSource = dataSource || (company.market === 'CN' ? 'Tushare' : 'iFinD')
|
const targetDataSource = dataSource || (company.market === 'CN' ? 'Tushare' : 'iFinD')
|
||||||
setSelectedDataSource(targetDataSource)
|
setSelectedDataSource(targetDataSource)
|
||||||
@ -64,7 +71,6 @@ export default function Home() {
|
|||||||
// 数据准备就绪
|
// 数据准备就绪
|
||||||
const handleDataReady = (id: number) => {
|
const handleDataReady = (id: number) => {
|
||||||
setCompanyId(id)
|
setCompanyId(id)
|
||||||
setShowFinancialData(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AI分析完成
|
// AI分析完成
|
||||||
@ -72,20 +78,30 @@ export default function Home() {
|
|||||||
setAnalysisId(id)
|
setAnalysisId(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回搜索
|
// 返回搜索 (Reset)
|
||||||
const handleBackToSearch = () => {
|
const handleBackToSearch = () => {
|
||||||
setSelectedCompany(null)
|
setSelectedCompany(null)
|
||||||
setCompanyId(null)
|
setCompanyId(null)
|
||||||
setShowFinancialData(false)
|
|
||||||
setAnalysisId(null)
|
setAnalysisId(null)
|
||||||
setOneTimeModel(undefined)
|
setOneTimeModel(undefined)
|
||||||
|
setCurrentView("home")
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// Navigation Handler
|
||||||
<div className="min-h-screen w-full p-8">
|
const handleTabChange = (tab: string) => {
|
||||||
{/* 标题和描述 */}
|
if (tab === "home") {
|
||||||
{!selectedCompany && (
|
handleBackToSearch()
|
||||||
<div className="max-w-7xl mx-auto flex flex-col gap-8">
|
} else {
|
||||||
|
setCurrentView(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Content based on View
|
||||||
|
const renderContent = () => {
|
||||||
|
// Home View
|
||||||
|
if (!selectedCompany || currentView === "home") {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto flex flex-col gap-8 p-8">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">股票分析</h1>
|
<h1 className="text-3xl font-bold tracking-tight">股票分析</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
@ -98,160 +114,220 @@ export default function Home() {
|
|||||||
<SearchStockWithSelection onSelect={handleCompanySelect} />
|
<SearchStockWithSelection onSelect={handleCompanySelect} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 最近的报告 */}
|
{/* 历史记录 */}
|
||||||
<div className="flex flex-col gap-4 mt-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<h2 className="text-2xl font-bold tracking-tight">最近的报告</h2>
|
<HistoryList onSelect={handleCompanySelect} />
|
||||||
<HistoryList />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{selectedCompany && (
|
// Chart View
|
||||||
<div className="w-full">
|
if (currentView === "chart") {
|
||||||
<CompanyAnalysisView
|
return (
|
||||||
company={selectedCompany}
|
<div className="h-full p-4 flex flex-col gap-4">
|
||||||
dataSource={selectedDataSource}
|
{/* Top Navigation for Chart View */}
|
||||||
onBack={handleBackToSearch}
|
<div className="flex flex-row items-center justify-between px-2">
|
||||||
onDataSourceChange={setSelectedDataSource}
|
<div className="flex items-center gap-4">
|
||||||
oneTimeModel={oneTimeModel}
|
<Button variant="ghost" size="sm" onClick={handleBackToSearch} className="gap-2">
|
||||||
/>
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
返回搜索
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h1 className="text-2xl font-bold">{selectedCompany.company_name}</h1>
|
||||||
|
<Badge variant="outline">{selectedCompany.symbol}</Badge>
|
||||||
|
<Badge>{selectedCompany.market}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<StockChart symbol={selectedCompany.symbol} market={selectedCompany.market} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Financial Data View (Default fallback)
|
||||||
|
return (
|
||||||
|
<div className="w-full pl-2 pr-32 py-6 space-y-6">
|
||||||
|
{/* 顶部导航栏 (Portal Target) */}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-6">
|
||||||
|
{/* 数据源选择 */}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="hidden">
|
||||||
|
<DataSourceSelector
|
||||||
|
market={selectedCompany.market}
|
||||||
|
selectedSource={selectedDataSource}
|
||||||
|
onSourceChange={setSelectedDataSource}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 数据获取状态 */}
|
||||||
|
<CompanyAnalysisView
|
||||||
|
company={selectedCompany}
|
||||||
|
dataSource={selectedDataSource}
|
||||||
|
onDataReady={handleDataReady}
|
||||||
|
onAnalysisComplete={handleAnalysisComplete}
|
||||||
|
selectedModel={oneTimeModel}
|
||||||
|
currency={currency}
|
||||||
|
setCurrency={setCurrency}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-full bg-background overflow-hidden">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<AppSidebar
|
||||||
|
activeTab={currentView}
|
||||||
|
onTabChange={handleTabChange}
|
||||||
|
hasSelectedCompany={!!selectedCompany}
|
||||||
|
className="flex-shrink-0 z-10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<main className="flex-1 overflow-auto bg-background/50 relative">
|
||||||
|
{renderContent()}
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拆分出单独的组件以使用Hooks
|
// ----------------------------------------------------------------------
|
||||||
|
// Sub-components
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
function CompanyAnalysisView({
|
function CompanyAnalysisView({
|
||||||
company,
|
company,
|
||||||
dataSource,
|
dataSource,
|
||||||
onBack,
|
onDataReady,
|
||||||
onDataSourceChange,
|
onAnalysisComplete,
|
||||||
oneTimeModel
|
selectedModel,
|
||||||
|
currency,
|
||||||
|
setCurrency
|
||||||
}: {
|
}: {
|
||||||
company: SearchResult,
|
company: SearchResult
|
||||||
dataSource: string,
|
dataSource: string
|
||||||
onBack: () => void,
|
onDataReady: (id: number) => void
|
||||||
onDataSourceChange: (ds: string) => void,
|
onAnalysisComplete: (id: number) => void
|
||||||
oneTimeModel?: string
|
selectedModel?: string
|
||||||
|
currency: string
|
||||||
|
setCurrency: (c: string) => void
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
loading,
|
loading,
|
||||||
fetching,
|
fetching,
|
||||||
updateStatus,
|
|
||||||
error,
|
error,
|
||||||
fetchData,
|
fetchData,
|
||||||
checkStatus
|
checkStatus,
|
||||||
|
updateStatus
|
||||||
} = useFinancialData(company, dataSource)
|
} = useFinancialData(company, dataSource)
|
||||||
|
|
||||||
const [companyId, setCompanyId] = useState<number | null>(null)
|
const progress = updateStatus ? {
|
||||||
const [analysisId, setAnalysisId] = useState<number | null>(null)
|
percentage: updateStatus.progress_percentage || 0,
|
||||||
|
message: updateStatus.progress_message || ""
|
||||||
|
} : null
|
||||||
|
|
||||||
// 当数据就绪时设置companyId
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status?.has_data && status.company_id) {
|
if (status && status.has_data && status.company_id) {
|
||||||
setCompanyId(status.company_id)
|
onDataReady(status.company_id)
|
||||||
} else {
|
|
||||||
setCompanyId(null)
|
|
||||||
}
|
}
|
||||||
}, [status])
|
}, [status, onDataReady])
|
||||||
|
|
||||||
const handleAnalysisComplete = (id: number) => {
|
|
||||||
setAnalysisId(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUpdating = fetching && updateStatus
|
|
||||||
const progress = updateStatus?.progress_percentage || 0
|
|
||||||
const progressStatus = updateStatus?.progress_message || ""
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
||||||
|
{/* Header Controls */}
|
||||||
<HeaderPortal>
|
<HeaderPortal>
|
||||||
<div className="flex items-center justify-end w-full gap-4 text-sm h-full">
|
<div className="flex items-center gap-2 mr-4 border-r pr-4">
|
||||||
{/* 1. 状态显示 (进度或最后更新) */}
|
<span className="font-bold whitespace-nowrap">{company.company_name}</span>
|
||||||
<div className="flex items-center text-muted-foreground mr-auto">
|
<Badge variant="outline" className="font-mono">{company.symbol}</Badge>
|
||||||
{isUpdating ? (
|
<Badge className="text-xs">{company.market}</Badge>
|
||||||
<div className="flex items-center gap-2 text-xs font-mono bg-muted/50 rounded-full px-3 py-1 animate-pulse">
|
</div>
|
||||||
<span className="font-bold text-primary">{Math.round(progress)}%</span>
|
<div className="flex items-center gap-2 mr-4">
|
||||||
<span className="truncate max-w-[200px]">{progressStatus || "Processing..."}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
status?.has_data && status.last_update && (
|
|
||||||
<div className="flex items-center gap-1.5 text-xs opacity-80">
|
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
|
|
||||||
<span>已更新: {formatTimestamp(status.last_update.date)}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 2. 公司基础信息 */}
|
|
||||||
<div className="flex items-center gap-3 border-l pl-4 border-r pr-4 h-8">
|
|
||||||
<span className="font-bold truncate max-w-[200px]">{company.company_name}</span>
|
|
||||||
<Badge variant="secondary" className="font-mono h-6">{company.market} {company.symbol}</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 3. 操作按钮 (刷新) */}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => fetchData(true)}
|
variant="outline"
|
||||||
disabled={fetching}
|
|
||||||
variant={!status?.has_data ? "default" : "outline"}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-8"
|
onClick={() => fetchData(true, currency)}
|
||||||
|
disabled={loading || fetching}
|
||||||
|
className="gap-2"
|
||||||
>
|
>
|
||||||
{fetching ? (
|
{(loading || fetching) ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin mr-2" />
|
{(loading || fetching) ? "更新中..." : "更新数据"}
|
||||||
) : (
|
|
||||||
<RefreshCw className="h-3.5 w-3.5 mr-2" />
|
|
||||||
)}
|
|
||||||
{status?.has_data ? "更新数据" : "获取数据"}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-1 mr-6 border rounded-md p-1 bg-muted/20">
|
||||||
|
{["Auto", "CNY", "USD"].map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt}
|
||||||
|
onClick={() => setCurrency(opt)}
|
||||||
|
className={`
|
||||||
|
px-3 py-1 text-xs font-medium rounded-sm transition-all
|
||||||
|
${currency === opt
|
||||||
|
? "bg-background shadow-sm text-foreground"
|
||||||
|
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{opt}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{fetching && (
|
||||||
|
<div className="flex items-center gap-2 mr-4 min-w-[200px]">
|
||||||
|
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{progress?.message || "准备中..."}
|
||||||
|
</span>
|
||||||
|
<Progress value={progress?.percentage || 0} className="h-2 w-20" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!fetching && !loading && <div className="text-xs text-muted-foreground flex items-center gap-1">
|
||||||
|
<CheckCircle2 className="w-3 h-3 text-green-500" /> 已更新: {status?.last_update?.date ? formatTimestamp(status.last_update.date) : "无记录"}
|
||||||
|
</div>}
|
||||||
</HeaderPortal>
|
</HeaderPortal>
|
||||||
|
|
||||||
{/* 数据状态详情 (Card) */}
|
{/* DataStatusCard usage removed because it duplicates logic and caused prop mismatch.
|
||||||
|
Status is now handled by HeaderPortal controls.
|
||||||
|
*/}
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 border border-destructive/50 bg-destructive/10 rounded-lg text-destructive flex items-center gap-2">
|
||||||
|
<Loader2 className="h-4 w-4" /> {/* Reuse icon or AlertCircle */}
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{dataSource === 'Bloomberg' ? (
|
||||||
{/* 财务数据表格 */}
|
status?.company_id && (
|
||||||
{status?.has_data && companyId && (
|
|
||||||
dataSource === 'Bloomberg' ? (
|
|
||||||
<BloombergView
|
<BloombergView
|
||||||
companyId={companyId}
|
selectedCurrency={currency}
|
||||||
|
userMarket={company.market}
|
||||||
|
companyId={status.company_id}
|
||||||
|
lastUpdate={status.last_update?.date}
|
||||||
/>
|
/>
|
||||||
) : (
|
)
|
||||||
|
) : (
|
||||||
|
status?.company_id && (
|
||||||
<FinancialTables
|
<FinancialTables
|
||||||
companyId={companyId}
|
companyId={status.company_id}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* AI 分析触发器 */}
|
|
||||||
{status?.has_data && companyId && !analysisId && (
|
|
||||||
<AnalysisTrigger
|
|
||||||
companyId={companyId}
|
|
||||||
dataSource={dataSource}
|
|
||||||
model={oneTimeModel}
|
|
||||||
onAnalysisComplete={handleAnalysisComplete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* AI 分析报告 */}
|
|
||||||
{analysisId && (
|
|
||||||
<AnalysisReport analysisId={analysisId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SearchStockWithSelection({ onSelect }: { onSelect: (c: SearchResult, s?: string) => void }) {
|
||||||
// 搜索组件的包装器,添加选择功能
|
|
||||||
function SearchStockWithSelection({ onSelect }: { onSelect: (company: SearchResult) => void }) {
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>开始新的分析</CardTitle>
|
<CardTitle>搜索股票</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<SearchStock onCompanySelect={onSelect} />
|
<SearchStock onCompanySelect={onSelect} />
|
||||||
|
|||||||
41
frontend/src/components/app-sidebar.tsx
Normal file
41
frontend/src/components/app-sidebar.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
|
||||||
|
import { Home, LineChart, BarChart3, Search } from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
interface AppSidebarProps {
|
||||||
|
activeTab: string
|
||||||
|
onTabChange: (tab: string) => void
|
||||||
|
className?: string
|
||||||
|
hasSelectedCompany: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppSidebar({ activeTab, onTabChange, className, hasSelectedCompany }: AppSidebarProps) {
|
||||||
|
const navItems = [
|
||||||
|
{ id: "home", label: "首页", icon: Home, disabled: false },
|
||||||
|
{ id: "financial", label: "财务数据", icon: BarChart3, disabled: !hasSelectedCompany },
|
||||||
|
{ id: "chart", label: "股价图", icon: LineChart, disabled: !hasSelectedCompany },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("w-64 border-r bg-card flex flex-col p-4 gap-2", className)}>
|
||||||
|
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<Button
|
||||||
|
key={item.id}
|
||||||
|
variant={activeTab === item.id ? "secondary" : "ghost"}
|
||||||
|
className={cn(
|
||||||
|
"justify-start gap-3 h-12 text-base font-normal",
|
||||||
|
activeTab === item.id && "bg-secondary font-medium",
|
||||||
|
activeTab !== item.id && "text-muted-foreground"
|
||||||
|
)}
|
||||||
|
onClick={() => !item.disabled && onTabChange(item.id)}
|
||||||
|
disabled={item.disabled}
|
||||||
|
>
|
||||||
|
<item.icon className="h-5 w-5" />
|
||||||
|
{item.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -12,14 +12,19 @@ import type { FinancialDataResponse } from "@/lib/types"
|
|||||||
interface BloombergViewProps {
|
interface BloombergViewProps {
|
||||||
companyId: number
|
companyId: number
|
||||||
onBack?: () => void
|
onBack?: () => void
|
||||||
|
selectedCurrency?: string
|
||||||
|
userMarket?: string
|
||||||
|
lastUpdate?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BloombergView({ companyId, onBack }: BloombergViewProps) {
|
export function BloombergView({ companyId, onBack, selectedCurrency = "Auto", userMarket, lastUpdate }: BloombergViewProps) {
|
||||||
const [data, setData] = useState<FinancialDataResponse | null>(null)
|
const [data, setData] = useState<FinancialDataResponse | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
|
if (!companyId) return
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError("")
|
setError("")
|
||||||
try {
|
try {
|
||||||
@ -37,7 +42,7 @@ export function BloombergView({ companyId, onBack }: BloombergViewProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData()
|
loadData()
|
||||||
}, [companyId])
|
}, [companyId, lastUpdate])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -67,24 +72,177 @@ export function BloombergView({ companyId, onBack }: BloombergViewProps) {
|
|||||||
const mergedData = data.unified_data || []
|
const mergedData = data.unified_data || []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-4">
|
||||||
<h2 className="text-2xl font-bold flex items-center gap-2">
|
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||||
<DollarSign className="w-6 h-6" />
|
<div className="flex items-center gap-6">
|
||||||
Bloomberg 财务数据总览
|
<h2 className="text-2xl font-bold flex items-center gap-2 whitespace-nowrap">
|
||||||
</h2>
|
<DollarSign className="w-6 h-6" />
|
||||||
<Button variant="outline" size="sm" onClick={loadData}>
|
财务概览
|
||||||
<RefreshCw className="w-4 h-4 mr-2" />
|
</h2>
|
||||||
刷新数据
|
{/* Insert Inline Header Info here */}
|
||||||
</Button>
|
<BasicInfoHeader data={mergedData} selectedCurrency={selectedCurrency} userMarket={userMarket} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 ml-auto">
|
||||||
|
<Button variant="outline" size="sm" onClick={loadData}>
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
刷新数据
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RawDataTable title="财务数据总表" data={mergedData} />
|
<RawDataTable title="财务数据总表" data={mergedData} selectedCurrency={selectedCurrency} userMarket={userMarket} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RawDataTable({ data, title }: { data: any[], title: string }) {
|
// ---------------------------------------------------------------------------
|
||||||
|
// Basic Info Header Component
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function BasicInfoHeader({ data, selectedCurrency = "Auto", userMarket }: { data: any[], selectedCurrency?: string, userMarket?: string }) {
|
||||||
|
if (!data || data.length === 0) return null
|
||||||
|
|
||||||
|
// 1. Determine Target Currency (Logic shared with Table)
|
||||||
|
let targetCurrency = selectedCurrency
|
||||||
|
if (targetCurrency === "Auto" && userMarket) {
|
||||||
|
const market = userMarket.toUpperCase()
|
||||||
|
if (market.includes("JP")) targetCurrency = "JPY"
|
||||||
|
else if (market.includes("VN")) targetCurrency = "VND"
|
||||||
|
else if (market.includes("CN")) targetCurrency = "CNY"
|
||||||
|
else if (market.includes("HK")) targetCurrency = "HKD"
|
||||||
|
else targetCurrency = "USD"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Find Latest Valid Data for each field
|
||||||
|
// We sort data by date descending to get latest first.
|
||||||
|
// Note: 'end_date' is the reporting period, 'update_date' is when it was fetched.
|
||||||
|
// Usually PE/PB/MarketCap are "current" values, so they might be associated with the latest reporting period or a special "current" row.
|
||||||
|
// Based on bloomberg logic, they are saved as time series. We take the latest available.
|
||||||
|
|
||||||
|
// Sort rows by date descending
|
||||||
|
const sortedRows = [...data].sort((a, b) => new Date(b.end_date).getTime() - new Date(a.end_date).getTime())
|
||||||
|
|
||||||
|
// Helper to find first non-null value
|
||||||
|
const findValue = (keys: string[]) => {
|
||||||
|
for (const row of sortedRows) {
|
||||||
|
// Check currency match if required (PE/PB/MarketCap are currency dependent usually, or at least MarketCap is)
|
||||||
|
// However, PE/PB are ratios.
|
||||||
|
// Strict currency check for everything to be safe?
|
||||||
|
if (targetCurrency !== 'Auto') {
|
||||||
|
if (row.currency && row.currency !== targetCurrency) continue
|
||||||
|
} else {
|
||||||
|
// If auto, try to match first row's currency if established, or just take any?
|
||||||
|
// Let's stick to the targetCurrency logic used in Table which resolved 'Auto' to a specific one if possible.
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
if (row[key] !== null && row[key] !== undefined && row[key] !== '') {
|
||||||
|
return { value: row[key], row }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract Fields
|
||||||
|
const companyName = findValue(['company_name'])?.value || '-'
|
||||||
|
|
||||||
|
// Find latest update_date from ALL rows matching currency
|
||||||
|
let maxUpdateDate = ''
|
||||||
|
for (const row of data) {
|
||||||
|
if (targetCurrency !== 'Auto' && row.currency && row.currency !== targetCurrency) continue
|
||||||
|
if (row.update_date && row.update_date > maxUpdateDate) {
|
||||||
|
maxUpdateDate = row.update_date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updateDate = maxUpdateDate
|
||||||
|
// update_date is a timestamp string "2023-10-xx 10:00:00". User wants "Day only".
|
||||||
|
const displayDate = updateDate ? formatDate(updateDate) : '-'
|
||||||
|
|
||||||
|
const pe = findValue(['pe', 'pe_ratio'])?.value
|
||||||
|
const pb = findValue(['pb', 'pb_ratio'])?.value
|
||||||
|
const marketCapTuple = findValue(['market_cap'])
|
||||||
|
const marketCap = marketCapTuple?.value
|
||||||
|
const marketCapCurrency = marketCapTuple?.row?.currency || targetCurrency
|
||||||
|
|
||||||
|
const abroadRev = findValue(['rev_abroad', 'pct_revenue_from_foreign_sources'])?.value
|
||||||
|
const divYield = findValue(['dividend_yield', 'dividend_12_month_yield'])?.value
|
||||||
|
const ipoDateRaw = findValue(['ipo_date', 'IPO_date', 'eqy_init_po_dt'])?.value
|
||||||
|
const ipoDate = ipoDateRaw ? formatDate(ipoDateRaw) : '-'
|
||||||
|
|
||||||
|
// Formatters
|
||||||
|
const formatRatio = (val: any) => {
|
||||||
|
if (val === undefined || val === null) return '-'
|
||||||
|
const num = parseFloat(val)
|
||||||
|
if (isNaN(num)) return '-'
|
||||||
|
return num.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatPercent = (val: any) => {
|
||||||
|
if (val === undefined || val === null) return '-'
|
||||||
|
const num = parseFloat(val)
|
||||||
|
if (isNaN(num)) return '-'
|
||||||
|
return num.toFixed(2) + '%'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMoney = (val: any, currency: string) => {
|
||||||
|
if (val === undefined || val === null) return '-'
|
||||||
|
let num = parseFloat(val)
|
||||||
|
if (isNaN(num)) return '-'
|
||||||
|
|
||||||
|
// Backend now returns Market Cap in Millions for ALL currencies (via BDH).
|
||||||
|
// To convert Millions to "Yi" (100 Million), we divide by 100.
|
||||||
|
// This applies uniformly to JPY, USD, CNY, etc.
|
||||||
|
num = num / 100
|
||||||
|
|
||||||
|
return num.toLocaleString('en-US', { maximumFractionDigits: 2 }) + ' 亿'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-6 text-sm bg-muted/30 px-4 py-2 rounded-lg border">
|
||||||
|
{/* Group 1: Identity */}
|
||||||
|
<div className="flex flex-col gap-0.5 border-r pr-6">
|
||||||
|
<span className="font-bold text-lg text-primary truncate max-w-[200px]" title={companyName}>{companyName}</span>
|
||||||
|
<div className="flex gap-3 text-xs text-muted-foreground">
|
||||||
|
<span>IPO: {ipoDate}</span>
|
||||||
|
<span>更新: {displayDate}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Group 2: Key Ratios */}
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<span className="text-muted-foreground text-xs scale-90">PE</span>
|
||||||
|
<span className="font-mono font-bold text-base">{formatRatio(pe)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<span className="text-muted-foreground text-xs scale-90">PB</span>
|
||||||
|
<span className="font-mono font-bold text-base">{formatRatio(pb)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<span className="text-muted-foreground text-xs scale-90">股息率</span>
|
||||||
|
<span className="font-mono font-bold text-base">{formatPercent(divYield)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Group 3: Market Size & Biz */}
|
||||||
|
<div className="flex items-center gap-6 border-l pl-6">
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
|
<span className="text-muted-foreground text-xs scale-90">市值 ({marketCapCurrency})</span>
|
||||||
|
<span className="font-mono font-bold text-base text-blue-600 dark:text-blue-400">{formatMoney(marketCap, marketCapCurrency)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end">
|
||||||
|
<span className="text-muted-foreground text-xs scale-90">海外收入</span>
|
||||||
|
<span className="font-mono font-bold text-base">{formatPercent(abroadRev)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RawDataTable({ data, title, selectedCurrency = "Auto", userMarket }: { data: any[], title: string, selectedCurrency?: string, userMarket?: string }) {
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@ -98,12 +256,53 @@ function RawDataTable({ data, title }: { data: any[], title: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 1. Filter Data Rows (Handle Multi-Currency) - DO THIS FIRST!
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Determine Target Currency Strategy
|
||||||
|
let targetCurrency = selectedCurrency
|
||||||
|
if (targetCurrency === "Auto" && userMarket) {
|
||||||
|
const market = userMarket.toUpperCase()
|
||||||
|
if (market.includes("JP")) targetCurrency = "JPY"
|
||||||
|
else if (market.includes("VN")) targetCurrency = "VND"
|
||||||
|
else if (market.includes("CN")) targetCurrency = "CNY"
|
||||||
|
else if (market.includes("HK")) targetCurrency = "HKD"
|
||||||
|
else targetCurrency = "USD"
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRows = data.filter(row => {
|
||||||
|
// Basic Validity Check
|
||||||
|
if (!row || row.revenue === null || row.revenue === undefined) return false
|
||||||
|
|
||||||
|
// Currency Filtering
|
||||||
|
// Case 1: Specific Currency Selected (or resolved from Auto)
|
||||||
|
if (targetCurrency !== "Auto") {
|
||||||
|
if (row.currency && row.currency !== targetCurrency) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Case 2: Pure Auto (Fallback if no userMarket)
|
||||||
|
else if (selectedCurrency === "Auto") {
|
||||||
|
if (data.length > 0 && data[0].currency && row.currency && row.currency !== data[0].currency) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 2. Extract Indicators (From Filtered Data ONLY)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
// 提取所有指标键值 (排除元数据)
|
// 提取所有指标键值 (排除元数据)
|
||||||
const excludeKeys = ['id', 'company_code', 'code', 'symbol', 'market', 'update_date', 'create_time', 'end_date', 'ts_code']
|
const excludeKeys = ['id', 'company_code', 'code', 'symbol', 'market', 'update_date', 'create_time', 'end_date', 'ts_code']
|
||||||
|
|
||||||
// 从合并后的数据中收集所有可能的指标
|
// 从合并后的数据中收集所有可能的指标
|
||||||
const allIndicators = new Set<string>()
|
const allIndicators = new Set<string>()
|
||||||
data.forEach(row => {
|
filteredRows.forEach(row => {
|
||||||
Object.keys(row).forEach(k => {
|
Object.keys(row).forEach(k => {
|
||||||
if (!excludeKeys.includes(k)) {
|
if (!excludeKeys.includes(k)) {
|
||||||
allIndicators.add(k)
|
allIndicators.add(k)
|
||||||
@ -166,19 +365,29 @@ function RawDataTable({ data, title }: { data: any[], title: string }) {
|
|||||||
return a.localeCompare(b)
|
return a.localeCompare(b)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 1. Filter Data Rows (Handle Multi-Currency)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// We must filter BEFORE creating the Map, because multiple rows might exist
|
||||||
|
// for the same date (e.g. 2023 JPY and 2023 USD).
|
||||||
|
// The Map key is 'end_date', so it can only hold one currency's data per date.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 2. Build Data Structures from Filtered Rows
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// 构建查找表: Map<DateString, RowData>
|
// 构建查找表: Map<DateString, RowData>
|
||||||
const dataMap = new Map()
|
const dataMap = new Map()
|
||||||
data.forEach(row => {
|
filteredRows.forEach(row => {
|
||||||
dataMap.set(row.end_date, row)
|
dataMap.set(row.end_date, row)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 过滤日期: 只保留有营业收入 (revenue) 的列
|
// 提取日期列表
|
||||||
const dates = Array.from(new Set(data.map(row => row.end_date)))
|
const dates = Array.from(new Set(filteredRows.map(row => row.end_date)))
|
||||||
.filter(date => {
|
|
||||||
const row = dataMap.get(date)
|
|
||||||
// Check if revenue exists and is not null/undefined
|
|
||||||
return row && row.revenue !== null && row.revenue !== undefined
|
|
||||||
})
|
|
||||||
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())
|
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())
|
||||||
|
|
||||||
|
|
||||||
@ -365,7 +574,7 @@ function RawDataTable({ data, title }: { data: any[], title: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableCell key={date} className={`text-right px-4 font-mono ${highlightClass} ${textStyle}`}>
|
<TableCell key={date} className={`text-right px-4 tabular-nums ${highlightClass} ${textStyle}`} style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' }}>
|
||||||
{formatCellValue(indicator, value)}
|
{formatCellValue(indicator, value)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -155,7 +155,7 @@ function FinancialTable({ data, title }: FinancialTableProps) {
|
|||||||
{data.map((row, idx) => (
|
{data.map((row, idx) => (
|
||||||
<TableRow key={idx}>
|
<TableRow key={idx}>
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<TableCell key={column} className="font-mono text-right">
|
<TableCell key={column} className="text-right tabular-nums" style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' }}>
|
||||||
{formatCellValue(column, row[column])}
|
{formatCellValue(column, row[column])}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
@ -220,7 +220,7 @@ function TransposedFinancialTable({ data, title }: FinancialTableProps) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
{/* 值 */}
|
{/* 值 */}
|
||||||
{sortedData.map((row, rowIdx) => (
|
{sortedData.map((row, rowIdx) => (
|
||||||
<TableCell key={rowIdx} className="font-mono text-right px-4">
|
<TableCell key={rowIdx} className="text-right px-4 tabular-nums" style={{ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' }}>
|
||||||
{formatCellValue(indicator, row[indicator])}
|
{formatCellValue(indicator, row[indicator])}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import Link from "next/link"
|
|
||||||
import { getReports } from "@/lib/api"
|
import { getReports } from "@/lib/api"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||||
import { Loader2 } from "lucide-react"
|
import { Loader2 } from "lucide-react"
|
||||||
|
import type { SearchResult } from "@/lib/types"
|
||||||
|
|
||||||
export function HistoryList() {
|
interface HistoryListProps {
|
||||||
|
onSelect?: (company: SearchResult, dataSource?: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HistoryList({ onSelect }: HistoryListProps) {
|
||||||
const [reports, setReports] = useState<any[]>([])
|
const [reports, setReports] = useState<any[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
@ -22,37 +26,47 @@ export function HistoryList() {
|
|||||||
return (
|
return (
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{reports.map((report: any) => (
|
{reports.map((report: any) => (
|
||||||
<Link key={report.id} href={`/analysis/${report.id}`}>
|
<Card
|
||||||
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
|
key={report.id}
|
||||||
<CardHeader>
|
className="hover:shadow-lg transition-shadow cursor-pointer"
|
||||||
<CardTitle className="text-lg">{report.company_name}</CardTitle>
|
onClick={() => {
|
||||||
<CardDescription>
|
if (onSelect) {
|
||||||
{report.market} {report.symbol}
|
onSelect({
|
||||||
</CardDescription>
|
symbol: report.symbol,
|
||||||
</CardHeader>
|
market: report.market,
|
||||||
<CardContent>
|
company_name: report.company_name
|
||||||
<div className="flex items-center justify-between">
|
}, report.data_source)
|
||||||
<span className="text-sm text-muted-foreground">
|
}
|
||||||
{new Date(report.created_at).toLocaleString('zh-CN')}
|
}}
|
||||||
</span>
|
>
|
||||||
<Badge variant={
|
<CardHeader>
|
||||||
report.status === "completed" ? "default" :
|
<CardTitle className="text-lg">{report.company_name}</CardTitle>
|
||||||
report.status === "in_progress" ? "secondary" :
|
<CardDescription>
|
||||||
report.status === "failed" ? "destructive" : "outline"
|
{report.market} {report.symbol}
|
||||||
}>
|
</CardDescription>
|
||||||
{report.status === "completed" ? "已完成" :
|
</CardHeader>
|
||||||
report.status === "in_progress" ? "进行中" :
|
<CardContent>
|
||||||
report.status === "failed" ? "失败" : "待处理"}
|
<div className="flex items-center justify-between">
|
||||||
</Badge>
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{new Date(report.created_at).toLocaleString('zh-CN')}
|
||||||
|
</span>
|
||||||
|
<Badge variant={
|
||||||
|
report.status === "completed" ? "default" :
|
||||||
|
report.status === "in_progress" ? "secondary" :
|
||||||
|
report.status === "failed" ? "destructive" : "outline"
|
||||||
|
}>
|
||||||
|
{report.status === "completed" ? "已完成" :
|
||||||
|
report.status === "in_progress" ? "进行中" :
|
||||||
|
report.status === "failed" ? "失败" : "待处理"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{report.ai_model && (
|
||||||
|
<div className="mt-2 text-xs text-muted-foreground">
|
||||||
|
模型: {report.ai_model}
|
||||||
</div>
|
</div>
|
||||||
{report.ai_model && (
|
)}
|
||||||
<div className="mt-2 text-xs text-muted-foreground">
|
</CardContent>
|
||||||
模型: {report.ai_model}
|
</Card>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -105,27 +105,27 @@ export function NavHeader() {
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select onValueChange={handleCompanyChange}>
|
<Select onValueChange={handleCompanyChange}>
|
||||||
<SelectTrigger className="w-[240px] h-8 text-xs font-mono">
|
<SelectTrigger className="w-[120px] h-8 text-xs font-mono">
|
||||||
<SelectValue placeholder="选择最近浏览的代码..." />
|
<SelectValue placeholder="代码..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="z-[70]">
|
<SelectContent className="z-[70]">
|
||||||
{companies.map((c) => (
|
{companies.map((c) => (
|
||||||
<SelectItem key={`${c.symbol}:${c.market}`} value={`${c.symbol}:${c.market}`}>
|
<SelectItem key={`${c.symbol}:${c.market}`} value={`${c.symbol}:${c.market}`}>
|
||||||
<span className="flex items-center gap-2 w-full">
|
<span className="flex items-center gap-2 w-full">
|
||||||
<span className="font-bold w-[60px] text-left">{c.symbol}</span>
|
<span className="font-bold w-[50px] text-left">{c.symbol}</span>
|
||||||
<span className="truncate flex-1 text-left">{c.company_name}</span>
|
<span className="truncate flex-1 text-left">{c.company_name}</span>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
{companies.length === 0 && (
|
{companies.length === 0 && (
|
||||||
<div className="p-2 text-xs text-muted-foreground text-center">该数据源暂无历史记录</div>
|
<div className="p-2 text-xs text-muted-foreground text-center">无记录</div>
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-[300px] ml-6">
|
<div className="w-[160px] ml-4">
|
||||||
<HeaderSearch defaultDataSource={dataSource} />
|
<HeaderSearch defaultDataSource={dataSource} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
83
frontend/src/components/stock-chart.tsx
Normal file
83
frontend/src/components/stock-chart.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
|
interface StockChartProps {
|
||||||
|
symbol: string
|
||||||
|
market: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StockChart({ symbol, market }: StockChartProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!containerRef.current) return
|
||||||
|
|
||||||
|
// Clear previous widget
|
||||||
|
containerRef.current.innerHTML = ""
|
||||||
|
|
||||||
|
// Create wrapper for widget
|
||||||
|
const widgetContainer = document.createElement("div")
|
||||||
|
widgetContainer.className = "tradingview-widget-container__widget h-full w-full"
|
||||||
|
containerRef.current.appendChild(widgetContainer)
|
||||||
|
|
||||||
|
const script = document.createElement("script")
|
||||||
|
script.src = "https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js"
|
||||||
|
script.type = "text/javascript"
|
||||||
|
script.async = true
|
||||||
|
|
||||||
|
// Map Market/Symbol to TradingView format
|
||||||
|
let exchange = "NASDAQ"
|
||||||
|
let tvSymbol = symbol
|
||||||
|
|
||||||
|
if (market === "CN") {
|
||||||
|
if (symbol.startsWith("6")) exchange = "SSE"
|
||||||
|
else if (symbol.startsWith("0") || symbol.startsWith("3")) exchange = "SZSE"
|
||||||
|
else if (symbol.startsWith("4") || symbol.startsWith("8")) exchange = "BSE"
|
||||||
|
} else if (market === "HK") {
|
||||||
|
exchange = "HKEX"
|
||||||
|
// TradingView usually expects HK stocks without leading zeros if they are 4 digits, but let's check.
|
||||||
|
// Actually HKEX:700 works, HKEX:0700 might work too. Let's try to keep it safe.
|
||||||
|
tvSymbol = parseInt(symbol).toString()
|
||||||
|
} else if (market === "JP") {
|
||||||
|
exchange = "TSE"
|
||||||
|
} else if (market === "VN") {
|
||||||
|
exchange = "HOSE" // Primary VN exchange
|
||||||
|
} else {
|
||||||
|
// US
|
||||||
|
exchange = "NASDAQ" // Default, could be NYSE
|
||||||
|
// Basic heuristic for US
|
||||||
|
// If 4 chars or more, likely Nasdaq, <=3 likely NYSE? Not 100% accurate but acceptable default.
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullSymbol = `${exchange}:${tvSymbol}`
|
||||||
|
|
||||||
|
script.innerHTML = JSON.stringify({
|
||||||
|
"autosize": true,
|
||||||
|
"symbol": fullSymbol,
|
||||||
|
"interval": "D",
|
||||||
|
"timezone": "Asia/Shanghai",
|
||||||
|
"theme": "light",
|
||||||
|
"style": "1",
|
||||||
|
"locale": "zh_CN",
|
||||||
|
"enable_publishing": false,
|
||||||
|
"allow_symbol_change": true,
|
||||||
|
"calendar": false,
|
||||||
|
"support_host": "https://www.tradingview.com"
|
||||||
|
})
|
||||||
|
|
||||||
|
containerRef.current.appendChild(script)
|
||||||
|
|
||||||
|
}, [symbol, market])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-[calc(100vh-100px)] w-full bg-background rounded-lg border overflow-hidden p-1">
|
||||||
|
<div className="tradingview-widget-container h-full w-full" ref={containerRef}>
|
||||||
|
<div className="tradingview-widget-copyright text-xs text-center text-muted-foreground p-2">
|
||||||
|
TradingView Chart by <a href="https://cn.tradingview.com/" rel="noopener nofollow" target="_blank">TradingView</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -58,7 +58,7 @@ export function useFinancialData(company: SearchResult | null, dataSource: strin
|
|||||||
}, [updateId, fetching, checkStatus])
|
}, [updateId, fetching, checkStatus])
|
||||||
|
|
||||||
// Trigger data fetch
|
// Trigger data fetch
|
||||||
const fetchData = async (forceRefresh = false) => {
|
const fetchData = async (forceRefresh = false, currency?: string) => {
|
||||||
if (!company) return
|
if (!company) return
|
||||||
setFetching(true)
|
setFetching(true)
|
||||||
setError("")
|
setError("")
|
||||||
@ -68,7 +68,8 @@ export function useFinancialData(company: SearchResult | null, dataSource: strin
|
|||||||
symbol: company.symbol,
|
symbol: company.symbol,
|
||||||
company_name: company.company_name,
|
company_name: company.company_name,
|
||||||
data_source: dataSource,
|
data_source: dataSource,
|
||||||
force_refresh: forceRefresh
|
force_refresh: forceRefresh,
|
||||||
|
currency: currency
|
||||||
})
|
})
|
||||||
setUpdateId(response.update_id)
|
setUpdateId(response.update_id)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@ -58,6 +58,7 @@ export interface FetchDataRequest {
|
|||||||
company_name: string
|
company_name: string
|
||||||
data_source: string
|
data_source: string
|
||||||
force_refresh?: boolean
|
force_refresh?: boolean
|
||||||
|
currency?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchDataResponse {
|
export interface FetchDataResponse {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user