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 @app.route('/covered_warrant', methods=['GET', 'POST']) 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}") @app.route('/') 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 ) @app.route('/stock_analysis', methods=['GET']) 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 ) @app.route('/api/stock_data') 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) @app.route('/api/update_financial_data/', methods=['POST']) 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] = '

Không có dữ liệu.

' return jsonify(tables) except Exception as e: app.logger.error(f"Error updating financial data for {symbol}: {e}") return jsonify({'error': str(e)}), 500 @app.route('/valuation', methods=['GET']) def valuation_page(): return render_template('valuation_strategy.html') @app.route('/api/data') 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)}) @app.route('/api/trade', methods=['POST']) 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")) @app.route('/analyze_groq', methods=['POST']) 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 @app.route('/market_overview') 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)