danghungithp's picture
Upload 1398 files
bec48e1 verified
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/<symbol>', 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] = '<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
@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)