Spaces:
Runtime error
Runtime error
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory | |
from vnstock import Vnstock | |
import pandas as pd | |
import matplotlib.pyplot as plt | |
import os | |
# API keys should be set as environment variables, not hardcoded. | |
# os.environ["GROQ_API_KEY"] = "your_groq_api_key" | |
import numpy as np | |
import pandas_ta as ta | |
import scipy.signal # For signal processing functions | |
from datetime import datetime, timedelta | |
import matplotlib.dates as mdates | |
import mplfinance as mpf | |
import numpy_financial as npf | |
from flask_cors import CORS | |
import logging | |
import google.generativeai as genai | |
import json | |
from modules.utils import ( | |
detect_candlestick_patterns, calculate_fibonacci_levels, calculate_money_flow, | |
find_double_top_bottom, detect_w_double_bottom, detect_m_double_top, detect_cup_and_handle, | |
plot_candlestick_with_fibo_patterns, get_financial_valuation, | |
calculate_dcf_valuation, calculate_ddm_valuation, calculate_nav, calculate_residual_income, calculate_eva, safe_float, analyze_financial_csv_with_groq, | |
fetch_vietstock_news, analyze_news_with_groq | |
) | |
from modules.valuation import calculate_covered_warrant_profit, black_scholes_price | |
from modules.market_news import ( | |
analyze_market_data | |
) | |
import requests | |
from bs4 import BeautifulSoup | |
# import talib # For technical analysis indicators | |
app = Flask(__name__) | |
app.secret_key = 'your-very-secret-key-2024' # Đặt secret key cho session/flash | |
CORS(app) | |
# Đường dẫn lưu biểu đồ | |
CHART_PATH_CANDLE = "static/images/stock_candle.png" | |
CHART_PATH_MONEY = "static/images/stock_money.png" | |
CHART_PATH_CANDLE_VNINDEX = "static/images/vnindex_candle.png" | |
CHART_PATH_MONEY_VNINDEX = "static/images/vnindex_money.png" | |
# Đường dẫn thư mục dữ liệu tài chính | |
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') | |
# Đường dẫn tới thư mục public của VSA | |
VSA_PUBLIC_DIR = os.path.join(os.path.dirname(__file__), 'stock-vsa-analyzer', 'public') | |
logging.basicConfig(level=logging.INFO) | |
logging.info(f"[VSA] Static dir: {VSA_PUBLIC_DIR}") | |
# HƯỚNG DẪN SỬ DỤNG BIỂU ĐỒ VSA | |
VSA_GUIDE = '' | |
def fetch_covered_warrant_info(warrant_code): | |
""" | |
Fetches covered warrant information from Vietstock. | |
""" | |
url = f"https://finance.vietstock.vn/chung-khoan-phai-sinh/{warrant_code}/cw-tong-quan.htm" | |
try: | |
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'} | |
response = requests.get(url, headers=headers, timeout=15) | |
response.raise_for_status() | |
soup = BeautifulSoup(response.content, 'html.parser') | |
info = {} | |
# Find the table with the data | |
table = soup.find('table', {'class': 'table table-striped table-bordered table-condensed'}) | |
if not table: | |
return None | |
rows = table.find_all('tr') | |
for row in rows: | |
cols = row.find_all('td') | |
if len(cols) == 2: | |
key = cols[0].text.strip() | |
value = cols[1].text.strip() | |
info[key] = value | |
return info | |
except requests.RequestException as e: | |
print(f"Error fetching {url}: {e}") | |
return None | |
def covered_warrant(): | |
warrant_code = request.args.get('warrant_code', '').upper() | |
warrant_info = None | |
calculation_results = None | |
RISK_FREE_RATE = 0.05 # Lãi suất phi rủi ro, giả định 5% | |
form_data = { | |
'stock_price': request.form.get('stock_price'), | |
'strike_price': request.form.get('strike_price'), | |
'warrant_price': request.form.get('warrant_price'), | |
'conversion_ratio': request.form.get('conversion_ratio'), | |
'volatility': request.form.get('volatility'), | |
'time_to_expiration': request.form.get('time_to_expiration') | |
} | |
if warrant_code: | |
warrant_info = fetch_covered_warrant_info(warrant_code) | |
if warrant_info: | |
# Tự động điền một số thông tin vào form | |
try: | |
# Lấy ngày đáo hạn và tính T (thời gian còn lại) | |
if 'Ngày đáo hạn' in warrant_info: | |
expiration_date = datetime.strptime(warrant_info['Ngày đáo hạn'], '%d/%m/%Y') | |
T = (expiration_date - datetime.now()).days / 365.0 | |
form_data['time_to_expiration'] = round(T, 4) if T > 0 else 0 | |
# Lấy mã CK cơ sở để tính toán độ biến động | |
if 'Mã CK cơ sở' in warrant_info: | |
underlying_symbol = warrant_info['Mã CK cơ sở'] | |
stock = Vnstock().stock(symbol=underlying_symbol, source='VCI') | |
df = stock.quote.history(start=(datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d'), end=datetime.now().strftime('%Y-%m-%d')) | |
if df is not None and not df.empty: | |
df['returns'] = np.log(df['close'] / df['close'].shift(1)) | |
volatility = df['returns'].std() * np.sqrt(252) # Độ biến động hàng năm | |
form_data['volatility'] = round(volatility, 4) | |
except Exception as e: | |
app.logger.error(f"Error auto-populating form for {warrant_code}: {e}") | |
if request.method == 'POST': | |
try: | |
stock_price = safe_float(request.form.get('stock_price')) | |
strike_price = safe_float(request.form.get('strike_price')) | |
warrant_price = safe_float(request.form.get('warrant_price')) | |
conversion_ratio = safe_float(request.form.get('conversion_ratio')) | |
volatility = safe_float(request.form.get('volatility')) | |
time_to_expiration = safe_float(request.form.get('time_to_expiration')) | |
if all([stock_price, strike_price, warrant_price, conversion_ratio, volatility, time_to_expiration is not None]): | |
# Tính toán cơ bản | |
basic_calcs = calculate_covered_warrant_profit(stock_price, strike_price, warrant_price, conversion_ratio) | |
# Tính toán Black-Scholes | |
theoretical_price = black_scholes_price(S=stock_price, X=strike_price, T=time_to_expiration, r=RISK_FREE_RATE, v=volatility, conversion_ratio=conversion_ratio) | |
calculation_results = { | |
'profit': basic_calcs['profit'], | |
'break_even_price': basic_calcs['break_even_price'], | |
'theoretical_price': theoretical_price, | |
'is_overvalued': warrant_price > theoretical_price, | |
'valuation_diff': warrant_price - theoretical_price | |
} | |
else: | |
flash('Vui lòng nhập đầy đủ và hợp lệ các giá trị để tính toán.', 'warning') | |
# Cập nhật lại form_data để hiển thị lại trên form | |
form_data.update({ | |
'stock_price': stock_price, 'strike_price': strike_price, 'warrant_price': warrant_price, | |
'conversion_ratio': conversion_ratio, 'volatility': volatility, 'time_to_expiration': time_to_expiration | |
}) | |
except Exception as e: | |
flash(f"Đã xảy ra lỗi khi tính toán: {e}", "danger") | |
app.logger.error(f"Error in covered_warrant POST: {e}") | |
return render_template('covered_warrant.html', | |
calculation_results=calculation_results, | |
form_data=form_data, | |
warrant_code=warrant_code, | |
warrant_info=warrant_info) | |
# XÓA các hàm đã tách sang modules.utils.py (detect_candlestick_patterns, calculate_fibonacci_levels, calculate_money_flow, find_double_top_bottom, detect_w_double_bottom, detect_m_double_top, detect_cup_and_handle, plot_candlestick_with_fibo_patterns, get_market_info, get_financial_valuation, calculate_dcf_valuation, calculate_ddm_valuation, calculate_nav, calculate_residual_income, calculate_eva, safe_float, analyze_financial_csv_with_groq) | |
# --- Hàm kiểm tra và cập nhật file vnindex.csv đủ 3 năm --- | |
def ensure_vnindex_csv(): | |
""" | |
Đảm bảo file vnindex.csv tồn tại và có đủ dữ liệu 3 năm gần nhất. | |
Nếu thiếu hoặc chưa có, sẽ tự động tải dữ liệu mới nhất từ Vnstock và lưu lại. | |
""" | |
csv_path = os.path.join(DATA_DIR, 'vnindex.csv') | |
today = datetime.now().date() | |
start_date = (today.replace(year=today.year - 3)).strftime('%Y-%m-%d') | |
end_date = today.strftime('%Y-%m-%d') | |
need_update = False | |
if not os.path.exists(csv_path): | |
need_update = True | |
else: | |
try: | |
df = pd.read_csv(csv_path) | |
if 'time' in df.columns: | |
df['time'] = pd.to_datetime(df['time']) | |
min_date = df['time'].min().date() | |
max_date = df['time'].max().date() | |
# Nếu thiếu dữ liệu 3 năm hoặc thiếu ngày mới nhất thì update | |
if min_date > today.replace(year=today.year - 3) or max_date < today: | |
need_update = True | |
# Nếu số dòng < 700 (ít hơn 3 năm giao dịch) thì update | |
if len(df) < 700: | |
need_update = True | |
else: | |
need_update = True | |
except Exception: | |
need_update = True | |
if need_update: | |
try: | |
stock = Vnstock().stock(symbol='VNINDEX', source='VCI') | |
df = stock.quote.history(start=start_date, end=end_date, interval='1D') | |
if df is not None and not df.empty: | |
df.to_csv(csv_path, index=False) | |
print(f"[INFO] Đã cập nhật file vnindex.csv với dữ liệu từ {start_date} đến {end_date}") | |
else: | |
print("[WARN] Không lấy được dữ liệu VNINDEX để cập nhật vnindex.csv!") | |
except Exception as e: | |
print(f"[ERROR] Lỗi khi cập nhật vnindex.csv: {e}") | |
def home(): | |
ensure_vnindex_csv() | |
csv_path = os.path.join(DATA_DIR, 'vnindex.csv') | |
if not os.path.exists(csv_path): | |
return render_template('home.html', error="Không có dữ liệu cho VNINDEX.") | |
try: | |
df = pd.read_csv(csv_path) | |
if 'time' in df.columns: | |
df['time'] = pd.to_datetime(df['time']) | |
else: | |
return render_template('home.html', error="File vnindex.csv không hợp lệ.") | |
except Exception as e: | |
return render_template('home.html', error=f"Lỗi đọc file vnindex.csv: {e}") | |
if df is None or df.empty: | |
return render_template('home.html', error="Không có dữ liệu cho VNINDEX.") | |
df_analysis = df.tail(250).reset_index(drop=True) | |
# --- Technical Analysis --- | |
fibonacci_levels, _, _ = calculate_fibonacci_levels(df_analysis) | |
candlestick_patterns = detect_candlestick_patterns(df_analysis) | |
pattern_results = {} | |
for pattern_name, pattern_data in candlestick_patterns.items(): | |
if pattern_data is not None and not pattern_data.where(pattern_data != 0).isnull().all(): | |
last_occurrence_idx = pattern_data.where(pattern_data != 0).last_valid_index() | |
if last_occurrence_idx is not None: | |
last_date = df_analysis.loc[last_occurrence_idx, 'time'] | |
pattern_results[pattern_name.replace('_', ' ').title()] = last_date | |
# --- News Fetching and Analysis --- | |
news_items = fetch_vietstock_news() | |
news_analysis = analyze_news_with_groq(news_items) | |
return render_template( | |
'home.html', | |
symbol='VNINDEX', | |
vnindex_fibonacci_levels=fibonacci_levels, | |
vnindex_pattern_results=pattern_results, | |
news_items=news_items, | |
news_analysis=news_analysis | |
) | |
def stock_analysis(): | |
symbol = request.args.get('symbol', '').strip().upper() | |
# Khởi tạo các biến DataFrame là None để tránh lỗi UnboundLocalError | |
bs_year = bs_quarter = is_year = is_quarter = cf_year = ratio_year = ratio_quarter = None | |
if not symbol: | |
# Trang trống, chỉ hiển thị form nhập mã cổ phiếu | |
return render_template('stock_analysis.html', symbol='', error=None, financial_valuation=None, dcf_value=None, ddm_value=None, nav_value=None, residual_income=None, eva=None, multiples_value=None, growth_forecast_value=None, tables={ | |
'bs_year': '', 'bs_quarter': '', 'is_year': '', 'is_quarter': '', 'cf_year': '', 'ratio_year': '', 'ratio_quarter': '' | |
}) | |
# Khi đã nhập mã, mới tải dữ liệu | |
start = request.args.get('start', '2024-01-01') | |
end = request.args.get('end', datetime.now().strftime('%Y-%m-%d')) | |
try: | |
stock = Vnstock().stock(symbol=symbol, source='VCI') | |
# Kiểm tra stock và các thuộc tính trước khi truy cập | |
if not stock or not hasattr(stock, 'quote') or not hasattr(stock.quote, 'history'): | |
raise Exception('Không lấy được dữ liệu giao dịch cho mã này (stock hoặc quote không khả dụng).') | |
df = stock.quote.history(start=start, end=end, interval='1D') | |
except Exception as e: | |
return render_template('stock_analysis.html', symbol=symbol, error=f"Không lấy được dữ liệu cho mã {symbol}: {e}", financial_valuation={'error': f'Không lấy được dữ liệu cho mã {symbol}: {e}'}, dcf_value=None, ddm_value=None, nav_value=None, residual_income=None, eva=None, multiples_value=None, growth_forecast_value=None, tables={ | |
'bs_year': '', 'bs_quarter': '', 'is_year': '', 'is_quarter': '', 'cf_year': '', 'ratio_year': '', 'ratio_quarter': '' | |
}) | |
if df is None or df.empty: | |
return render_template('stock_analysis.html', symbol=symbol, error=f"Không có dữ liệu cho mã cổ phiếu {symbol} hoặc khoảng thời gian đã chọn.", financial_valuation={'error': f'Không có dữ liệu cho mã cổ phiếu {symbol} hoặc khoảng thời gian đã chọn.'}, dcf_value=None, ddm_value=None, nav_value=None, residual_income=None, eva=None, multiples_value=None, growth_forecast_value=None, tables={ | |
'bs_year': '', 'bs_quarter': '', 'is_year': '', 'is_quarter': '', 'cf_year': '', 'ratio_year': '', 'ratio_quarter': '' | |
}) | |
# Lấy dữ liệu tài chính | |
tables = {} | |
financial_valuation = {} | |
try: | |
# Kiểm tra stock.finance trước khi lấy dữ liệu tài chính | |
if not hasattr(stock, 'finance') or stock.finance is None: | |
raise Exception(f"Không lấy được dữ liệu tài chính cho mã {symbol} (finance không khả dụng).") | |
bs_year = stock.finance.balance_sheet(yearly=True, to_df=True) | |
bs_quarter = stock.finance.balance_sheet(yearly=False, to_df=True) | |
is_year = stock.finance.income_statement(yearly=True, to_df=True) | |
is_quarter = stock.finance.income_statement(yearly=False, to_df=True) | |
cf_year = stock.finance.cash_flow(yearly=True, to_df=True) | |
ratio_year = stock.finance.ratio(yearly=True, to_df=True) | |
ratio_quarter = stock.finance.ratio(yearly=False, to_df=True) | |
# Chuyển DataFrame thành HTML | |
tables = { | |
'bs_year': bs_year.to_html(classes='table table-striped table-hover', border=0) if bs_year is not None else '', | |
'bs_quarter': bs_quarter.to_html(classes='table table-striped table-hover', border=0) if bs_quarter is not None else '', | |
'is_year': is_year.to_html(classes='table table-striped table-hover', border=0) if is_year is not None else '', | |
'is_quarter': is_quarter.to_html(classes='table table-striped table-hover', border=0) if is_quarter is not None else '', | |
'cf_year': cf_year.to_html(classes='table table-striped table-hover', border=0) if cf_year is not None else '', | |
'ratio_year': ratio_year.to_html(classes='table table-striped table-hover', border=0) if ratio_year is not None else '', | |
'ratio_quarter': ratio_quarter.to_html(classes='table table-striped table-hover', border=0) if ratio_quarter is not None else '' | |
} | |
financial_valuation = get_financial_valuation(stock) | |
except Exception as e: | |
# Nếu có lỗi khi lấy dữ liệu tài chính, vẫn tiếp tục để hiển thị dữ liệu giao dịch | |
financial_error = f"Lỗi khi tải dữ liệu tài chính: {e}" | |
print(financial_error) # Log lỗi ra console | |
# Đặt các bảng thành rỗng và truyền lỗi vào template | |
tables = {key: '' for key in ['bs_year', 'bs_quarter', 'is_year', 'is_quarter', 'cf_year', 'ratio_year', 'ratio_quarter']} | |
# Thêm lỗi này vào financial_valuation để hiển thị | |
financial_valuation = {'error': financial_error} | |
candlestick_patterns = detect_candlestick_patterns(df) | |
# No longer plotting charts on the backend | |
# plot_candlestick_with_fibo_patterns(...) is removed | |
# --- Định giá nâng cao --- | |
# Lấy dữ liệu từ các bảng tài chính đã đọc ở trên | |
dcf_value = ddm_value = nav_value = residual_income = eva = multiples_value = growth_forecast_value = None | |
try: | |
# Lấy các chỉ số cần thiết từ các DataFrame đã load | |
net_income = None | |
equity = None | |
dividend = None | |
pe = None | |
pb = None | |
roe = None | |
eps = None | |
# Lấy dòng dữ liệu mới nhất (năm gần nhất) | |
if is_year is not None and not is_year.empty: | |
# Ưu tiên các tên cột phổ biến, fallback nếu không có | |
for col in ['lợi nhuận sau thuế của cổ đông công ty mẹ (đồng)', 'lợi nhuận sau thuế', 'netincome', 'lnst']: | |
if col in is_year.columns: | |
net_income = is_year.iloc[0][col] | |
break | |
if bs_year is not None and not bs_year.empty: | |
for col in ['vốn chủ sở hữu', 'equity', 'vcshty', 'vcsht']: | |
if col in bs_year.columns: | |
equity = bs_year.iloc[0][col] | |
break | |
if ratio_year is not None and not ratio_year.empty: | |
for col in ['tỷ suất cổ tức (%)', 'dividend', 'dividend_yield']: | |
if col in ratio_year.columns: | |
dividend = ratio_year.iloc[0][col] | |
break | |
for col in ['p/e', 'pe']: | |
if col in ratio_year.columns: | |
roe = ratio_year.iloc[0][col] | |
break | |
for col in ['p/b', 'pb']: | |
if col in ratio_year.columns: | |
roe = ratio_year.iloc[0][col] | |
break | |
for col in ['eps (vnd)', 'eps']: | |
if col in ratio_year.columns: | |
eps = is_year.iloc[0][col] | |
break | |
# DCF (giả định tăng trưởng 10%, WACC 15%) | |
if net_income: | |
dcf_value = calculate_dcf_valuation(float(net_income), 0.1, 0.15) | |
# DDM | |
if dividend and eps: | |
ddm_value = calculate_ddm_valuation(float(dividend), 0.1, 0.15) | |
# NAV | |
if equity: | |
nav_value = calculate_nav(float(equity)) | |
# Residual Income | |
if net_income and equity: | |
residual_income = calculate_residual_income(float(net_income), float(equity), 0.15) | |
# EVA | |
if net_income and equity: | |
eva = calculate_eva(float(net_income), float(equity), 0, 0.15, 0.1) | |
except Exception as e: | |
print('Valuation error:', e) | |
return render_template( | |
'stock_analysis.html', | |
symbol=symbol, | |
error=None, # Lỗi đã được xử lý ở trên | |
financial_valuation=financial_valuation, | |
dcf_value=dcf_value, | |
ddm_value=ddm_value, | |
nav_value=nav_value, | |
residual_income=residual_income, | |
eva=eva, | |
multiples_value=multiples_value, | |
growth_forecast_value=growth_forecast_value, | |
tables=tables | |
) | |
def get_stock_data(): | |
symbol = request.args.get('symbol', 'VCB').upper() | |
start_date = request.args.get('start_date', (datetime.now() - timedelta(days=365*2)).strftime('%Y-%m-%d')) | |
end_date = request.args.get('end_date', datetime.now().strftime('%Y-%m-%d')) | |
df = None | |
if symbol == 'VNINDEX': | |
csv_path = os.path.join(DATA_DIR, 'vnindex.csv') | |
if os.path.exists(csv_path): | |
df = pd.read_csv(csv_path) | |
if 'time' in df.columns: | |
df['date'] = pd.to_datetime(df['time']) | |
df = df.sort_values(by='date').reset_index(drop=True) | |
else: | |
try: | |
stock = Vnstock().stock(symbol=symbol, source='VCI') | |
df = stock.quote.history(start=start_date, end=end_date, interval='1D') | |
if df is not None and not df.empty: | |
df['date'] = pd.to_datetime(df['time']) | |
df = df.sort_values(by='date').reset_index(drop=True) | |
except Exception as e: | |
return jsonify({'error': str(e)}), 500 | |
if df is None or df.empty: | |
return jsonify({'error': f'No data found for {symbol}'}), 404 | |
df_analysis = df.tail(250).reset_index(drop=True) | |
# --- Technical Analysis --- | |
fibonacci_levels, _, _ = calculate_fibonacci_levels(df_analysis) | |
candlestick_patterns = detect_candlestick_patterns(df_analysis) | |
peaks, _ = scipy.signal.find_peaks(df_analysis['close'], distance=5, prominence=df_analysis['close'].std()*0.5) | |
troughs, _ = scipy.signal.find_peaks(-df_analysis['close'], distance=5, prominence=df_analysis['close'].std()*0.5) | |
w_bottoms = detect_w_double_bottom(df_analysis) | |
m_tops = detect_m_double_top(df_analysis) | |
# --- Prepare Data for JSON Response --- | |
chart_data = [{'time': row['date'].strftime('%Y-%m-%d'), 'open': row['open'], 'high': row['high'], 'low': row['low'], 'close': row['close'], 'volume': row['volume']} for _, row in df.iterrows()] | |
signals = [] | |
for pattern_name, pattern_data in candlestick_patterns.items(): | |
if pattern_data is not None and not pattern_data.where(pattern_data != 0).isnull().all(): | |
signal_dates = df_analysis.loc[pattern_data != 0, 'date'] | |
for date in signal_dates: | |
signals.append({'date': date.strftime('%Y-%m-%d'), 'type': 'pattern', 'name': pattern_name.replace('_', ' ').title(), 'position': 'above' if 'bearish' in pattern_name or 'star' in pattern_name else 'below'}) | |
for peak_idx in peaks: | |
date = df_analysis.iloc[peak_idx]['date'] | |
signals.append({'date': date.strftime('%Y-%m-%d'), 'type': 'peak', 'name': 'Đỉnh', 'position': 'above'}) | |
for trough_idx in troughs: | |
date = df_analysis.iloc[trough_idx]['date'] | |
signals.append({'date': date.strftime('%Y-%m-%d'), 'type': 'trough', 'name': 'Đáy', 'position': 'below'}) | |
for t1, t2 in w_bottoms: | |
signals.append({'date': t2, 'type': 'pattern', 'name': '2 Đáy (W)', 'position': 'below'}) | |
for t1, t2 in m_tops: | |
signals.append({'date': t2, 'type': 'pattern', 'name': '2 Đỉnh (M)', 'position': 'above'}) | |
return jsonify({ | |
'chart_data': chart_data, | |
'signals': signals, | |
'fibonacci_levels': fibonacci_levels | |
}) | |
# --- Đăng ký blueprint VSA --- | |
from modules.vsa import vsa_bp | |
app.register_blueprint(vsa_bp) | |
from modules.home import home_bp | |
app.register_blueprint(home_bp) | |
from modules.filter_stock import filter_stock_bp | |
app.register_blueprint(filter_stock_bp) | |
def update_financial_data(symbol): | |
""" | |
API endpoint to force-reload financial data for a given stock symbol | |
and save it to CSV files. | |
""" | |
if not symbol: | |
return jsonify({'error': 'Mã cổ phiếu không được để trống.'}), 400 | |
try: | |
stock = Vnstock().stock(symbol=symbol.upper(), source='VCI') | |
if not hasattr(stock, 'finance'): | |
return jsonify({'error': f'Không thể tải dữ liệu tài chính cho mã {symbol}.'}), 500 | |
# Define which reports to fetch and where to save them | |
reports_to_fetch = { | |
'bs_year': ('balance_sheet', {'yearly': True}), | |
'bs_quarter': ('balance_sheet', {'yearly': False}), | |
'is_year': ('income_statement', {'yearly': True}), | |
'is_quarter': ('income_statement', {'yearly': False}), | |
'cf_year': ('cash_flow', {'yearly': True}), | |
'ratio_year': ('ratio', {'yearly': True}), | |
'ratio_quarter': ('ratio', {'yearly': False}), | |
} | |
# File mapping | |
file_mapping = { | |
'bs_year': 'DFbalance_sheet_year.csv', | |
'bs_quarter': 'DFbalance_sheet_quarter.csv', | |
'is_year': 'DFincome_statement_year.csv', | |
'is_quarter': 'DFincome_statement_quarter.csv', | |
'cf_year': 'dfcash_flow_year.csv', | |
'ratio_year': 'dfratio_year.csv', | |
'ratio_quarter': 'dfratio_quarter.csv', | |
} | |
tables = {} | |
for key, (func_name, kwargs) in reports_to_fetch.items(): | |
# Fetch data frame | |
df = getattr(stock.finance, func_name)(**kwargs, to_df=True) | |
if df is not None and not df.empty: | |
# Save to CSV | |
file_path = os.path.join(DATA_DIR, file_mapping[key]) | |
df.to_csv(file_path, index=False) | |
# Convert to HTML for the response | |
tables[key] = df.to_html(classes='table table-striped table-hover', border=0) | |
else: | |
tables[key] = '<p>Không có dữ liệu.</p>' | |
return jsonify(tables) | |
except Exception as e: | |
app.logger.error(f"Error updating financial data for {symbol}: {e}") | |
return jsonify({'error': str(e)}), 500 | |
def valuation_page(): | |
return render_template('valuation_strategy.html') | |
def api_valuation_data(): | |
symbol = request.args.get('symbol', '').upper() | |
market_price = safe_float(request.args.get('market_price', 0)) | |
fcf = safe_float(request.args.get('fcf', 0)) | |
growth_rate = safe_float(request.args.get('growth_rate', 0)) / 100 | |
wacc = safe_float(request.args.get('wacc', 0)) / 100 | |
dividend = safe_float(request.args.get('dividend', 0)) | |
r = safe_float(request.args.get('r', 0)) / 100 | |
g = safe_float(request.args.get('g', 0)) / 100 | |
book_value = safe_float(request.args.get('book_value', 0)) | |
eps = safe_float(request.args.get('eps', 0)) | |
sl = safe_float(request.args.get('sl', 0)) | |
tp = safe_float(request.args.get('tp', 0)) | |
p = safe_float(request.args.get('p', 0)) | |
g_win = safe_float(request.args.get('g_win', 0)) | |
l = safe_float(request.args.get('l', 0)) | |
kelly_fraction = safe_float(request.args.get('kelly_fraction', 1)) | |
# Fetch price data | |
try: | |
stock = Vnstock().stock(symbol=symbol, source='VCI') | |
df = stock.quote.history(start='2024-01-01', end=datetime.now().strftime('%Y-%m-%d'), interval='1D') | |
if df is None or df.empty: | |
return jsonify({'error': 'No data for symbol'}) | |
# DCF | |
intrinsic_dcf = calculate_dcf_valuation(fcf, growth_rate, wacc) if fcf and growth_rate and wacc else None | |
# DDM | |
intrinsic_ddm = calculate_ddm_valuation(dividend, g, r) if dividend and r and g else None | |
# P/B, P/E | |
intrinsic_pb = book_value if book_value else None | |
intrinsic_pe = eps if eps else None | |
# Chọn intrinsic value ưu tiên DCF > DDM > P/B > P/E | |
intrinsic_value = None | |
if intrinsic_dcf: intrinsic_value = intrinsic_dcf | |
elif intrinsic_ddm: intrinsic_value = intrinsic_ddm | |
elif intrinsic_pb: intrinsic_value = intrinsic_pb | |
elif intrinsic_pe: intrinsic_value = intrinsic_pe | |
# Deviation, margin of safety | |
deviation = market_price - intrinsic_value if intrinsic_value else None | |
margin_of_safety = round(100 * (intrinsic_value - market_price) / market_price, 2) if intrinsic_value and market_price else None | |
# Breakout detection: close > max(close[-20]) and volume spike | |
closes = df['close'].values | |
volumes = df['volume'].values | |
dates = df['time'].astype(str).tolist() | |
breakout = False | |
breakout_markers = [None]*len(closes) | |
for i in range(20, len(closes)): | |
if closes[i] > max(closes[i-20:i]) and volumes[i] > np.mean(volumes[i-20:i])*1.5: | |
breakout = True | |
breakout_markers[i] = closes[i] | |
# Kelly sizing | |
q = 1 - p if p else 0 | |
kelly = (p/g_win - q/l) if p and g_win and l else 0 | |
kelly_position = round(kelly * kelly_fraction, 3) if kelly else 0 | |
# Intrinsic value band for chart | |
intrinsic_series = [intrinsic_value]*len(closes) if intrinsic_value else [None]*len(closes) | |
return jsonify({ | |
'dates': dates, | |
'prices': closes.tolist(), | |
'intrinsic_series': intrinsic_series, | |
'breakout': breakout, | |
'breakout_markers': breakout_markers, | |
'intrinsic_value': intrinsic_value, | |
'deviation': deviation, | |
'margin_of_safety': margin_of_safety, | |
'kelly_position': kelly_position, | |
'sl': sl, | |
'tp': tp | |
}) | |
except Exception as e: | |
return jsonify({'error': str(e)}) | |
def api_save_trade(): | |
import csv | |
symbol = request.form.get('symbol', '').upper() | |
size = request.form.get('size', '') | |
sl = request.form.get('sl', '') | |
tp = request.form.get('tp', '') | |
config_path = os.path.join(DATA_DIR, 'trade_configs.csv') | |
row = [symbol, size, sl, tp, datetime.now().isoformat()] | |
header = ['symbol', 'size', 'sl', 'tp', 'created_at'] | |
write_header = not os.path.exists(config_path) | |
with open(config_path, 'a', newline='') as f: | |
writer = csv.writer(f) | |
if write_header: | |
writer.writerow(header) | |
writer.writerow(row) | |
return jsonify({'status': 'ok'}) | |
# Configure Google AI with an environment variable | |
if "GOOGLE_API_KEY" in os.environ: | |
genai.configure(api_key=os.environ["GOOGLE_API_KEY"]) | |
import os | |
from groq import Groq | |
# API key is now set at the top of the file. | |
client = Groq(api_key=os.environ.get("GROQ_API_KEY")) | |
def analyze_groq(): | |
symbol = request.form['symbol'] | |
user_question = request.form.get('question', None) | |
try: | |
csv_files = [ | |
('RATIO_YEAR', 'dfratio_year.csv'), | |
('RATIO_QUARTER', 'dfratio_quarter.csv'), | |
('BALANCE_SHEET_YEAR', 'DFbalance_sheet_year.csv'), | |
('BALANCE_SHEET_QUARTER', 'DFbalance_sheet_quarter.csv'), | |
('INCOME_STATEMENT_YEAR', 'DFincome_statement_year.csv'), | |
('INCOME_STATEMENT_QUARTER', 'DFincome_statement_quarter.csv'), | |
('CASH_FLOW_YEAR', 'dfcash_flow_year.csv'), | |
] | |
csv_content = '' | |
for label, fname in csv_files: | |
fpath = os.path.join(DATA_DIR, fname) | |
if os.path.exists(fpath): | |
df = pd.read_csv(fpath) | |
symbol_col = None | |
for col in ['ticker', 'cp', 'mã', 'stock', 'symbol']: | |
if col in df.columns: | |
symbol_col = col | |
break | |
if symbol_col: | |
df = df[df[symbol_col].str.upper() == symbol.upper()] | |
if not df.empty: | |
csv_content += f'{label}\n' + df.to_csv(index=False) + '\n' | |
if not csv_content: | |
return jsonify({'error': 'Không có dữ liệu tài chính cho mã này trong file CSV.'}) | |
result = analyze_financial_csv_with_groq(csv_content, user_question) | |
except Exception as e: | |
result = f"Lỗi khi gọi Groq AI: {e}" | |
return jsonify({'result': result}) | |
def safe_float(val, default=0.0): | |
try: | |
return float(val) | |
except (TypeError, ValueError): | |
return default | |
def market_overview(): | |
""" | |
Displays a market overview page with data from various sources, | |
analyzed by Groq AI. | |
""" | |
# The analyze_market_data function now fetches all necessary data internally | |
# and saves it to JSON files before performing the analysis. | |
ai_analysis = analyze_market_data() | |
# Load the data from JSON files to display on the page | |
vietnambiz_data_path = os.path.join(DATA_DIR, 'vietnambiz_data.json') | |
usd_index_path = os.path.join(DATA_DIR, 'usd_index.json') | |
market_news_path = os.path.join(DATA_DIR, 'market_news.json') | |
foreign_trading_path = os.path.join(DATA_DIR, 'foreign_trading_data.json') | |
vietnambiz_data = {} | |
if os.path.exists(vietnambiz_data_path): | |
with open(vietnambiz_data_path, 'r', encoding='utf-8') as f: | |
vietnambiz_data = json.load(f) | |
usd_index = {} | |
if os.path.exists(usd_index_path): | |
with open(usd_index_path, 'r', encoding='utf-8') as f: | |
usd_index = json.load(f) | |
market_news = [] | |
if os.path.exists(market_news_path): | |
with open(market_news_path, 'r', encoding='utf-8') as f: | |
market_news = json.load(f) | |
foreign_trading_data = {} | |
if os.path.exists(foreign_trading_path): | |
with open(foreign_trading_path, 'r', encoding='utf-8') as f: | |
foreign_trading_data = json.load(f) | |
return render_template( | |
'market_overview.html', | |
vietnambiz_data=vietnambiz_data, | |
usd_index=usd_index, | |
market_news=market_news, | |
foreign_trading_data=foreign_trading_data, | |
ai_analysis=ai_analysis | |
) | |
if __name__ == '__main__': | |
app.run(host='0.0.0.0', port=5000, debug=True) | |