tosanoob commited on
Commit
33062e0
·
0 Parent(s):

feat: base chat app

Browse files
Files changed (5) hide show
  1. .gitignore +3 -0
  2. chat_app.py +274 -0
  3. env-example +12 -0
  4. requirements.txt +61 -0
  5. twelvedata_api.py +98 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .env
2
+ PLAN.md
3
+ __pycache__/
chat_app.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py (Phiên bản cuối cùng với Biểu đồ Nâng cao)
2
+
3
+ import streamlit as st
4
+ import pandas as pd
5
+ import altair as alt # <-- Thêm thư viện Altair
6
+ import google.generativeai as genai
7
+ import google.ai.generativelanguage as glm
8
+ from dotenv import load_dotenv
9
+ import os
10
+ from twelvedata_api import TwelveDataAPI
11
+ from collections import deque
12
+ from datetime import datetime
13
+
14
+ # --- 1. CẤU HÌNH BAN ĐẦU & KHỞI TẠO STATE ---
15
+ load_dotenv()
16
+ st.set_page_config(layout="wide", page_title="AI Financial Dashboard")
17
+ genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
18
+
19
+ def initialize_state():
20
+ if "initialized" in st.session_state: return
21
+ st.session_state.initialized = True
22
+ st.session_state.td_api = TwelveDataAPI(os.getenv("TWELVEDATA_API_KEY"))
23
+ st.session_state.stock_watchlist = {}
24
+ st.session_state.timeseries_cache = {}
25
+ st.session_state.active_timeseries_period = 'intraday'
26
+ st.session_state.currency_converter_state = {'from': 'USD', 'to': 'VND', 'amount': 100.0, 'result': None}
27
+ st.session_state.chat_history = []
28
+ st.session_state.active_tab = 'Danh sách mã chứng khoán'
29
+ st.session_state.chat_session = None
30
+ initialize_state()
31
+
32
+ # --- 2. TẢI DỮ LIỆU NỀN ---
33
+ @st.cache_data(show_spinner="Đang tải và chuẩn bị dữ liệu thị trường...")
34
+ def load_market_data():
35
+ td_api = st.session_state.td_api
36
+ stocks_data = td_api.get_all_stocks()
37
+ forex_data = td_api.get_forex_pairs()
38
+ forex_graph = {}
39
+ if forex_data and 'data' in forex_data:
40
+ for pair in forex_data['data']:
41
+ base, quote = pair['symbol'].split('/'); forex_graph.setdefault(base, []); forex_graph.setdefault(quote, []); forex_graph[base].append(quote); forex_graph[quote].append(base)
42
+ country_currency_map = {}
43
+ if stocks_data and 'data' in stocks_data:
44
+ for stock in stocks_data['data']:
45
+ country, currency = stock.get('country'), stock.get('currency')
46
+ if country and currency: country_currency_map[country.lower()] = currency
47
+ all_currencies = sorted(forex_graph.keys())
48
+ return stocks_data, forex_graph, country_currency_map, all_currencies
49
+ ALL_STOCKS_CACHE, FOREX_GRAPH, COUNTRY_CURRENCY_MAP, AVAILABLE_CURRENCIES = load_market_data()
50
+
51
+ # --- 3. LOGIC THỰC THI TOOL ---
52
+ def find_and_process_stock(query: str):
53
+ print(f"Hybrid searching for stock: '{query}'...")
54
+ query_lower = query.lower()
55
+ found_data = [s for s in ALL_STOCKS_CACHE.get('data', []) if query_lower in s['symbol'].lower() or query_lower in s['name'].lower()]
56
+ if not found_data:
57
+ results = st.session_state.td_api.get_stocks(symbol=query)
58
+ found_data = results.get('data', [])
59
+ if len(found_data) == 1:
60
+ stock_info = found_data[0]; symbol = stock_info['symbol']
61
+ st.session_state.stock_watchlist[symbol] = stock_info
62
+ ts_data = get_smart_time_series(symbol=symbol, time_period='intraday')
63
+ if 'values' in ts_data:
64
+ df = pd.DataFrame(ts_data['values']); df['datetime'] = pd.to_datetime(df['datetime']); df['close'] = pd.to_numeric(df['close'])
65
+ if symbol not in st.session_state.timeseries_cache: st.session_state.timeseries_cache[symbol] = {}
66
+ st.session_state.timeseries_cache[symbol]['intraday'] = df.sort_values('datetime').set_index('datetime')
67
+ st.session_state.active_tab = 'Biểu đồ thời gian'; st.session_state.active_timeseries_period = 'intraday'
68
+ return {"status": "SINGLE_STOCK_PROCESSED", "symbol": symbol, "name": stock_info.get('name', 'N/A')}
69
+ elif len(found_data) > 1: return {"status": "MULTIPLE_STOCKS_FOUND", "data": found_data[:5]}
70
+ else: return {"status": "NO_STOCKS_FOUND"}
71
+ def get_smart_time_series(symbol: str, time_period: str):
72
+ logic_map = {'intraday': {'interval': '15min', 'outputsize': 120}, '1_week': {'interval': '1h', 'outputsize': 40}, '1_month': {'interval': '1day', 'outputsize': 22}, '6_months': {'interval': '1day', 'outputsize': 120}, '1_year': {'interval': '1week', 'outputsize': 52}}
73
+ params = logic_map.get(time_period)
74
+ if not params: return {"error": f"Khoảng thời gian '{time_period}' không hợp lệ."}
75
+ return st.session_state.td_api.get_time_series(symbol=symbol, **params)
76
+ def find_conversion_path_bfs(start, end):
77
+ if start not in FOREX_GRAPH or end not in FOREX_GRAPH: return None
78
+ q = deque([(start, [start])]); visited = {start}
79
+ while q:
80
+ curr, path = q.popleft()
81
+ if curr == end: return path
82
+ for neighbor in FOREX_GRAPH.get(curr, []):
83
+ if neighbor not in visited: visited.add(neighbor); q.append((neighbor, path + [neighbor]))
84
+ return None
85
+ def convert_currency_with_bridge(amount: float, symbol: str):
86
+ try: start_currency, end_currency = symbol.upper().split('/')
87
+ except ValueError: return {"error": "Định dạng cặp tiền tệ không hợp lệ."}
88
+ path = find_conversion_path_bfs(start_currency, end_currency)
89
+ if not path: return {"error": f"Không tìm thấy đường đi quy đổi từ {start_currency} sang {end_currency}."}
90
+ current_amount = amount; steps = []
91
+ for i in range(len(path) - 1):
92
+ step_start, step_end = path[i], path[i+1]
93
+ result = st.session_state.td_api.currency_conversion(amount=current_amount, symbol=f"{step_start}/{step_end}")
94
+ if 'rate' in result and result.get('rate') is not None:
95
+ current_amount = result['amount']; steps.append({"step": f"{i+1}. {step_start} → {step_end}", "rate": result['rate'], "intermediate_amount": current_amount})
96
+ else:
97
+ inverse_result = st.session_state.td_api.currency_conversion(amount=1, symbol=f"{step_end}/{step_start}")
98
+ if 'rate' in inverse_result and inverse_result.get('rate') and inverse_result['rate'] != 0:
99
+ rate = 1 / inverse_result['rate']; current_amount *= rate; steps.append({"step": f"{i+1}. {step_start} → {step_end} (Inverse)", "rate": rate, "intermediate_amount": current_amount})
100
+ else: return {"error": f"Lỗi ở bước quy đổi từ {step_start} sang {step_end}."}
101
+ return {"status": "Success", "original_amount": amount, "final_amount": current_amount, "path_taken": path, "conversion_steps": steps}
102
+ def perform_currency_conversion(amount: float, symbol: str):
103
+ result = convert_currency_with_bridge(amount, symbol)
104
+ st.session_state.currency_converter_state.update({'result': result, 'amount': amount})
105
+ try:
106
+ from_curr, to_curr = symbol.split('/'); st.session_state.currency_converter_state.update({'from': from_curr, 'to': to_curr})
107
+ except: pass
108
+ st.session_state.active_tab = 'Quy đổi tiền tệ'
109
+ return result
110
+
111
+ # --- 4. CẤU HÌNH GEMINI ---
112
+ SYSTEM_INSTRUCTION = """Bạn là bộ não AI điều khiển một Bảng điều khiển Tài chính Tương tác. Nhiệm vụ của bạn là hiểu yêu cầu của người dùng, gọi các công cụ phù hợp, và thông báo kết quả một cách súc tích.
113
+
114
+ QUY TẮC VÀNG:
115
+ 1. **HIỂU TRƯỚC, GỌI SAU:**
116
+ * **Tên công ty:** Khi người dùng nhập một tên công ty (ví dụ: "Tập đoàn Vingroup", "Apple"), nhiệm vụ ĐẦU TIÊN của bạn là dùng tool `find_and_process_stock` để xác định mã chứng khoán chính thức.
117
+ * **Tên quốc gia:** Khi người dùng nhập tên quốc gia cho tiền tệ (ví dụ: "tiền Việt Nam"), bạn phải tự suy luận ra mã tiền tệ 3 chữ cái ("VND") TRƯỚC KHI gọi tool `perform_currency_conversion`.
118
+ 2. **HÀNH ĐỘNG VÀ THÔNG BÁO:** Vai trò của bạn là thực thi lệnh và thông báo ngắn gọn.
119
+ * **Tìm thấy 1 mã:** "Tôi đã tìm thấy [Tên công ty] ([Mã CK]) và đã tự động thêm vào danh sách theo dõi và biểu đồ của bạn."
120
+ * **Tìm thấy nhiều mã:** "Tôi tìm thấy một vài kết quả cho '[query]'. Bạn vui lòng cho biết mã chính xác bạn muốn theo dõi?"
121
+ * **Quy đổi tiền tệ:** "Đã thực hiện. Mời bạn xem kết quả chi tiết trong tab 'Quy đổi tiền tệ'."
122
+ 3. **CẤM LIỆT KÊ DỮ LIỆU:** Bảng điều khiển đã hiển thị tất cả. TUYỆT ĐỐI không lặp lại danh sách, các con số, hay dữ liệu thô trong câu trả lời của bạn.
123
+ """
124
+ @st.cache_resource
125
+ def get_model_and_tools():
126
+ find_stock_func = glm.FunctionDeclaration(name="find_and_process_stock", description="Tìm kiếm cổ phiếu theo mã hoặc tên và tự động xử lý. Dùng tool này ĐẦU TIÊN để xác định mã CK chính thức.", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'query': glm.Schema(type=glm.Type.STRING, description="Mã hoặc tên công ty, ví dụ: 'Vingroup', 'Apple'.")}, required=['query']))
127
+ get_ts_func = glm.FunctionDeclaration(name="get_smart_time_series", description="Lấy dữ liệu lịch sử giá sau khi đã biết mã CK chính thức.", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'symbol': glm.Schema(type=glm.Type.STRING), 'time_period': glm.Schema(type=glm.Type.STRING, enum=["intraday", "1_week", "1_month", "6_months", "1_year"])}, required=['symbol', 'time_period']))
128
+ currency_func = glm.FunctionDeclaration(name="perform_currency_conversion", description="Quy đổi tiền tệ sau khi đã biết mã 3 chữ cái của cặp tiền tệ nguồn/đích, ví dụ USD/VND, JPY/EUR", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'amount': glm.Schema(type=glm.Type.NUMBER), 'symbol': glm.Schema(type=glm.Type.STRING)}, required=['amount', 'symbol']))
129
+ finance_tool = glm.Tool(function_declarations=[find_stock_func, get_ts_func, currency_func])
130
+ model = genai.GenerativeModel(model_name="gemini-1.5-pro-latest", tools=[finance_tool], system_instruction=SYSTEM_INSTRUCTION)
131
+ return model
132
+ model = get_model_and_tools()
133
+ if st.session_state.chat_session is None:
134
+ st.session_state.chat_session = model.start_chat(history=[])
135
+ AVAILABLE_FUNCTIONS = {"find_and_process_stock": find_and_process_stock, "get_smart_time_series": get_smart_time_series, "perform_currency_conversion": perform_currency_conversion}
136
+
137
+ # --- 5. LOGIC HIỂN THỊ CÁC TAB ---
138
+ def get_y_axis_domain(series: pd.Series, padding_percent: float = 0.1):
139
+ if series.empty: return None
140
+ data_min, data_max = series.min(), series.max()
141
+ if pd.isna(data_min) or pd.isna(data_max): return None
142
+ data_range = data_max - data_min
143
+ if data_range == 0:
144
+ padding = abs(data_max * (padding_percent / 2))
145
+ return [data_min - padding, data_max + padding]
146
+ padding = data_range * padding_percent
147
+ return [data_min - padding, data_max + padding]
148
+
149
+ def render_watchlist_tab():
150
+ st.subheader("Danh sách theo dõi")
151
+ if not st.session_state.stock_watchlist: st.info("Chưa có cổ phiếu nào. Hãy thử tìm kiếm một mã như 'Apple' hoặc 'VNM'."); return
152
+ for symbol, stock_info in list(st.session_state.stock_watchlist.items()):
153
+ col1, col2, col3 = st.columns([4, 4, 1])
154
+ with col1: st.markdown(f"**{symbol}**"); st.caption(stock_info.get('name', 'N/A'))
155
+ with col2: st.markdown(f"**{stock_info.get('exchange', 'N/A')}**"); st.caption(f"{stock_info.get('country', 'N/A')} - {stock_info.get('currency', 'N/A')}")
156
+ with col3:
157
+ if st.button("🗑️", key=f"delete_{symbol}", help=f"Xóa {symbol}"):
158
+ st.session_state.stock_watchlist.pop(symbol, None); st.session_state.timeseries_cache.pop(symbol, None); st.rerun()
159
+ st.divider()
160
+
161
+ def render_timeseries_tab():
162
+ st.subheader("Phân tích Biểu đồ")
163
+ if not st.session_state.stock_watchlist:
164
+ st.info("Hãy thêm ít nhất một cổ phiếu vào danh sách để xem biểu đồ."); return
165
+ time_periods = {'Trong ngày': 'intraday', '1 Tuần': '1_week', '1 Tháng': '1_month', '6 Tháng': '6_months', '1 Năm': '1_year'}
166
+ period_keys = list(time_periods.keys())
167
+ period_values = list(time_periods.values())
168
+ default_index = period_values.index(st.session_state.active_timeseries_period) if st.session_state.active_timeseries_period in period_values else 0
169
+ selected_label = st.radio("Chọn khoảng thời gian:", options=period_keys, horizontal=True, index=default_index)
170
+ selected_period = time_periods[selected_label]
171
+ if st.session_state.active_timeseries_period != selected_period:
172
+ st.session_state.active_timeseries_period = selected_period
173
+ with st.spinner(f"Đang cập nhật biểu đồ..."):
174
+ for symbol in st.session_state.stock_watchlist.keys():
175
+ ts_data = get_smart_time_series(symbol, selected_period)
176
+ if 'values' in ts_data:
177
+ df = pd.DataFrame(ts_data['values']); df['datetime'] = pd.to_datetime(df['datetime']); df['close'] = pd.to_numeric(df['close'])
178
+ if symbol not in st.session_state.timeseries_cache: st.session_state.timeseries_cache[symbol] = {}
179
+ st.session_state.timeseries_cache[symbol][selected_period] = df.sort_values('datetime').set_index('datetime')
180
+ st.rerun()
181
+ all_series_data = {symbol: st.session_state.timeseries_cache[symbol][selected_period] for symbol in st.session_state.stock_watchlist.keys() if symbol in st.session_state.timeseries_cache and selected_period in st.session_state.timeseries_cache[symbol]}
182
+ if not all_series_data:
183
+ st.warning("Không có đủ dữ liệu cho khoảng thời gian đã chọn."); return
184
+ st.markdown("##### So sánh Hiệu suất Tăng trưởng (%)")
185
+ normalized_dfs = []
186
+ for symbol, df in all_series_data.items():
187
+ if not df.empty:
188
+ normalized_series = (df['close'] / df['close'].iloc[0]) * 100
189
+ normalized_df = normalized_series.reset_index(); normalized_df.columns = ['datetime', 'value']; normalized_df['symbol'] = symbol
190
+ normalized_dfs.append(normalized_df)
191
+ if normalized_dfs:
192
+ full_normalized_df = pd.concat(normalized_dfs)
193
+ y_domain = get_y_axis_domain(full_normalized_df['value'])
194
+ chart = alt.Chart(full_normalized_df).mark_line().encode(x=alt.X('datetime:T', title='Thời gian'), y=alt.Y('value:Q', scale=alt.Scale(domain=y_domain, zero=False), title='Tăng trưởng (%)'), color=alt.Color('symbol:N', title='Mã CK'), tooltip=[alt.Tooltip('symbol:N', title='Mã'), alt.Tooltip('datetime:T', title='Thời điểm', format='%Y-%m-%d %H:%M'), alt.Tooltip('value:Q', title='Tăng trưởng', format='.2f')]).interactive()
195
+ st.altair_chart(chart, use_container_width=True)
196
+ else:
197
+ st.warning("Không có dữ liệu để vẽ biểu đồ tăng trưởng.")
198
+ st.divider()
199
+ st.markdown("##### Biểu đồ Giá Thực tế")
200
+ for symbol, df in all_series_data.items():
201
+ stock_info = st.session_state.stock_watchlist.get(symbol, {})
202
+ st.markdown(f"**{symbol}** ({stock_info.get('currency', 'N/A')})")
203
+ if not df.empty:
204
+ y_domain = get_y_axis_domain(df['close'])
205
+ data_for_chart = df.reset_index()
206
+ price_chart = alt.Chart(data_for_chart).mark_line().encode(x=alt.X('datetime:T', title='Thời gian'), y=alt.Y('close:Q', scale=alt.Scale(domain=y_domain, zero=False), title='Giá'), tooltip=[alt.Tooltip('datetime:T', title='Thời điểm', format='%Y-%m-%d %H:%M'), alt.Tooltip('close:Q', title='Giá', format=',.2f')]).interactive()
207
+ st.altair_chart(price_chart, use_container_width=True)
208
+
209
+ def render_currency_tab():
210
+ st.subheader("Công cụ quy đổi tiền tệ"); state = st.session_state.currency_converter_state
211
+ col1, col2 = st.columns(2)
212
+ amount = col1.number_input("Số tiền", value=state['amount'], min_value=0.0, format="%.2f", key="conv_amount")
213
+ from_curr = col1.selectbox("Từ", options=AVAILABLE_CURRENCIES, index=AVAILABLE_CURRENCIES.index(state['from']) if state['from'] in AVAILABLE_CURRENCIES else 0, key="conv_from")
214
+ to_curr = col2.selectbox("Sang", options=AVAILABLE_CURRENCIES, index=AVAILABLE_CURRENCIES.index(state['to']) if state['to'] in AVAILABLE_CURRENCIES else 1, key="conv_to")
215
+ if st.button("Quy đổi", use_container_width=True, key="conv_btn"):
216
+ with st.spinner("Đang quy đổi..."): result = perform_currency_conversion(amount, f"{from_curr}/{to_curr}"); st.rerun()
217
+ if state['result']:
218
+ res = state['result']
219
+ if res.get('status') == 'Success': st.success(f"**Kết quả:** `{res['original_amount']:,.2f} {res['path_taken'][0]}` = `{res['final_amount']:,.2f} {res['path_taken'][-1]}`")
220
+ else: st.error(f"Lỗi: {res.get('error', 'Không rõ')}")
221
+
222
+ # --- 6. MAIN APP LAYOUT & CONTROL FLOW ---
223
+ st.title("📈 AI Financial Dashboard")
224
+
225
+ col1, col2 = st.columns([1, 1])
226
+
227
+ with col2:
228
+ right_column_container = st.container(height=600)
229
+ with right_column_container:
230
+ tab_names = ['Danh sách mã chứng khoán', 'Biểu đồ thời gian', 'Quy đổi tiền tệ']
231
+ try: default_index = tab_names.index(st.session_state.active_tab)
232
+ except ValueError: default_index = 0
233
+ st.session_state.active_tab = tab_names[default_index]
234
+
235
+ tab1, tab2, tab3 = st.tabs(tab_names)
236
+ with tab1: render_watchlist_tab()
237
+ with tab2: render_timeseries_tab()
238
+ with tab3: render_currency_tab()
239
+
240
+ with col1:
241
+ chat_container = st.container(height=600)
242
+ with chat_container:
243
+ for message in st.session_state.chat_history:
244
+ with st.chat_message(message["role"]):
245
+ st.markdown(message["parts"])
246
+
247
+ user_prompt = st.chat_input("Hỏi AI để điều khiển bảng điều khiển...")
248
+ if user_prompt:
249
+ st.session_state.chat_history.append({"role": "user", "parts": user_prompt})
250
+ st.rerun()
251
+
252
+ if st.session_state.chat_history and st.session_state.chat_history[-1]["role"] == "user":
253
+ last_user_prompt = st.session_state.chat_history[-1]["parts"]
254
+
255
+ # ***** ĐÂY LÀ PHẦN THAY ĐỔI *****
256
+ with chat_container:
257
+ with st.chat_message("model"):
258
+ with st.spinner("🤖 AI đang thực thi lệnh..."):
259
+ response = st.session_state.chat_session.send_message(last_user_prompt)
260
+ tool_calls = [part.function_call for part in response.candidates[0].content.parts if part.function_call]
261
+ while tool_calls:
262
+ tool_responses = []
263
+ for call in tool_calls:
264
+ func_name = call.name; func_args = {k: v for k, v in call.args.items()}
265
+ if func_name in AVAILABLE_FUNCTIONS:
266
+ tool_result = AVAILABLE_FUNCTIONS[func_name](**func_args)
267
+ tool_responses.append(glm.Part(function_response=glm.FunctionResponse(name=func_name, response={'result': tool_result})))
268
+ else:
269
+ tool_responses.append(glm.Part(function_response=glm.FunctionResponse(name=func_name, response={'error': f"Function '{func_name}' not found."})))
270
+ response = st.session_state.chat_session.send_message(glm.Content(parts=tool_responses))
271
+ tool_calls = [part.function_call for part in response.candidates[0].content.parts if part.function_call]
272
+
273
+ st.session_state.chat_history.append({"role": "model", "parts": response.text})
274
+ st.rerun()
env-example ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base chatbot app
2
+ # TWELVEDATA_API_KEY=
3
+ TWELVEDATA_API_KEY=
4
+ GEMINI_API_KEY=
5
+ GEMINI_MODEL=gemini-2.5-flash
6
+ NGROK_AUTH_TOKEN=
7
+ NGROK_STATIC_DOMAIN=
8
+
9
+ # Invest insight app
10
+ ALPHA_VANTAGE_API_KEY=
11
+ NEWS_API_KEY=
12
+ MARKETAUX_API_KEY=
requirements.txt ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ altair==5.5.0
2
+ annotated-types==0.7.0
3
+ attrs==25.3.0
4
+ blinker==1.9.0
5
+ cachetools==5.5.2
6
+ certifi==2025.7.14
7
+ charset-normalizer==3.4.2
8
+ click==8.2.1
9
+ gitdb==4.0.12
10
+ GitPython==3.1.45
11
+ google-ai-generativelanguage==0.6.15
12
+ google-api-core==2.25.1
13
+ google-api-python-client==2.177.0
14
+ google-auth==2.40.3
15
+ google-auth-httplib2==0.2.0
16
+ google-generativeai==0.8.5
17
+ googleapis-common-protos==1.70.0
18
+ grpcio==1.74.0
19
+ grpcio-status==1.71.2
20
+ httplib2==0.22.0
21
+ idna==3.10
22
+ Jinja2==3.1.6
23
+ jsonschema==4.25.0
24
+ jsonschema-specifications==2025.4.1
25
+ MarkupSafe==3.0.2
26
+ narwhals==1.48.1
27
+ numpy==2.3.2
28
+ packaging==25.0
29
+ pandas==2.3.1
30
+ pillow==11.3.0
31
+ proto-plus==1.26.1
32
+ protobuf==5.29.5
33
+ pyarrow==21.0.0
34
+ pyasn1==0.6.1
35
+ pyasn1_modules==0.4.2
36
+ pydantic==2.11.7
37
+ pydantic_core==2.33.2
38
+ pydeck==0.9.1
39
+ pyparsing==3.2.3
40
+ python-dateutil==2.9.0.post0
41
+ python-dotenv==1.1.1
42
+ pytz==2025.2
43
+ referencing==0.36.2
44
+ requests==2.32.4
45
+ rpds-py==0.26.0
46
+ rsa==4.9.1
47
+ setuptools==78.1.1
48
+ six==1.17.0
49
+ smmap==5.0.2
50
+ streamlit==1.47.1
51
+ tenacity==9.1.2
52
+ toml==0.10.2
53
+ tornado==6.5.1
54
+ tqdm==4.67.1
55
+ typing-inspection==0.4.1
56
+ typing_extensions==4.14.1
57
+ tzdata==2025.2
58
+ uritemplate==4.2.0
59
+ urllib3==2.5.0
60
+ watchdog==6.0.0
61
+ wheel==0.45.1
twelvedata_api.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # twelvedata_api.py
2
+ import requests
3
+ import os
4
+
5
+ class TwelveDataAPI:
6
+ def __init__(self, api_key):
7
+ self.api_key = api_key
8
+ self.base_url = "https://api.twelvedata.com"
9
+
10
+ def _make_request(self, endpoint, params):
11
+ url = f"{self.base_url}/{endpoint}"
12
+ params['apikey'] = self.api_key
13
+ try:
14
+ response = requests.get(url, params=params)
15
+ response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
16
+ return response.json()
17
+ except requests.exceptions.HTTPError as errh:
18
+ print(f"Http Error: {errh}")
19
+ return {"error": f"HTTP Error: {errh}"}
20
+ except requests.exceptions.ConnectionError as errc:
21
+ print(f"Error Connecting: {errc}")
22
+ return {"error": f"Error Connecting: {errc}"}
23
+ except requests.exceptions.Timeout as errt:
24
+ print(f"Timeout Error: {errt}")
25
+ return {"error": f"Timeout Error: {errt}"}
26
+ except requests.exceptions.RequestException as err:
27
+ print(f"Something went wrong: {err}")
28
+ return {"error": f"Something went wrong: {err}"}
29
+
30
+ def get_stocks(self, symbol: str, exchange: str = "NASDAQ", country: str = "USA"):
31
+ """
32
+ Tìm kiếm và liệt kê các cổ phiếu có mã tương tự.
33
+ Args:
34
+ symbol (str): Mã cổ phiếu để tìm kiếm.
35
+ Returns:
36
+ dict: Kết quả tìm kiếm từ TwelveData.
37
+ """
38
+ print(f"Calling TwelveData /stocks for symbol: {symbol}")
39
+ return self._make_request("stocks", {"symbol": symbol, "exchange": exchange, "country": country})
40
+
41
+ def get_time_series(self, symbol: str, interval: str = "1day", outputsize: int = 30):
42
+ """
43
+ Lấy thông tin time-series của một cổ phiếu cụ thể.
44
+ Args:
45
+ symbol (str): Mã cổ phiếu (ví dụ: VNINDEX, AAPL).
46
+ interval (str): Khoảng thời gian (ví dụ: "1min", "5min", "1day", "1week"). Mặc định "1day".
47
+ outputsize (int): Số lượng điểm dữ liệu trả về. Mặc định 30.
48
+ Returns:
49
+ dict: Dữ liệu time-series từ TwelveData.
50
+ """
51
+ print(f"Calling TwelveData /time-series for symbol: {symbol}, interval: {interval}, outputsize: {outputsize}")
52
+ outputsize = int(outputsize)
53
+ if outputsize > 120:
54
+ outputsize = 120
55
+ if outputsize < 1:
56
+ outputsize = 1
57
+ return self._make_request("time_series", {
58
+ "symbol": symbol,
59
+ "interval": interval,
60
+ "outputsize": outputsize
61
+ })
62
+
63
+ def currency_conversion(self, amount: float, symbol: str):
64
+ """
65
+ Tìm thông tin quy đổi tiền tệ giữa 2 đơn vị.
66
+ Args:
67
+ amount (float): Số lượng tiền cần quy đổi.
68
+ symbol (str): Cặp tiền tệ (ví dụ: USD/VND, EUR/GBP).
69
+ Returns:
70
+ dict: Kết quả quy đổi tiền tệ từ TwelveData.
71
+ """
72
+ print(f"Calling TwelveData /currency_conversion for amount: {amount}, symbol: {symbol}")
73
+ return self._make_request("currency_conversion", {
74
+ "amount": amount,
75
+ "symbol": symbol
76
+ })
77
+
78
+ def get_all_stocks(self, exchange: str = "NASDAQ"):
79
+ """
80
+ Lấy danh sách tất cả các cổ phiếu từ một sàn giao dịch cụ thể để cache.
81
+ Lưu ý: API miễn phí có thể giới hạn, chúng ta lấy ví dụ sàn NASDAQ.
82
+ Args:
83
+ exchange (str): Sàn giao dịch để lấy mã.
84
+ Returns:
85
+ dict: Danh sách cổ phiếu.
86
+ """
87
+ print(f"Calling TwelveData /stocks for ALL stocks")
88
+ # API có thể trả về rất nhiều dữ liệu, nên giới hạn trong demo
89
+ return self._make_request("stocks", {"exchange": exchange})
90
+
91
+ def get_forex_pairs(self):
92
+ """
93
+ Lấy tất cả các cặp quy đổi tiền tệ có sẵn.
94
+ Returns:
95
+ dict: Danh sách các cặp tiền tệ.
96
+ """
97
+ print("Calling TwelveData /forex_pairs")
98
+ return self._make_request("forex_pairs", {})