├── requirements.txt ├── README.md └── financial_agent.py /requirements.txt: -------------------------------------------------------------------------------- 1 | phidata 2 | python-dotenv 3 | yfinance 4 | packaging 5 | duckduckgo-search 6 | fastapi 7 | uvicorn 8 | groq 9 | openai 10 | streamlit 11 | pandas 12 | plotly 13 | googlesearch-python 14 | pycountry 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | First Of All A Big Thanks To Krish Naik Sir! 2 | 3 | # **🌟 Financial Agentic AI: Your Ultimate Stock Market Companion 🌟** 4 | 5 | **Financial Agentic AI** redefines financial analysis by integrating cutting-edge AI with real-time web search capabilities. This multi-agent system brings together financial insights and web intelligence to deliver precise, actionable, and up-to-date stock market information, all tailored to your needs. 6 | 7 | --- 8 | 9 | ## **✨ Highlights** 10 | 11 | - **🤖 Intelligent Agents**: A robust team of AI agents designed for specialized roles, ensuring accurate and comprehensive results. 12 | - **📊 Financial Insights**: Dive deep into stock prices, company news, analyst recommendations, and historical trends with advanced tools. 13 | - **🌐 Real-Time Web Search**: Augment your financial knowledge with live updates from Google Search and DuckDuckGo, ensuring nothing escapes your radar. 14 | - **📈 Unified System**: Experience seamless integration of financial and web-based intelligence, empowering your stock market decisions like never before. 15 | 16 | --- 17 | 18 | ## **🔧 Features** 19 | 20 | ### **1. Web Search Agent** 21 | - **Purpose**: Scours the web for relevant, real-time data. 22 | - **Tools**: 23 | - **Google Search**: Fixed language and results for consistency. 24 | - **DuckDuckGo**: Privacy-conscious searches with maximum results tailored to your query. 25 | - **Capabilities**: 26 | - Always includes sources for transparency. 27 | - Displays results in Markdown format for readability. 28 | 29 | ### **2. Financial Insights Agent** 30 | - **Purpose**: Analyzes stock market trends and financial data. 31 | - **Tools**: 32 | - **YFinanceTools**: Tracks stock prices, company news, analyst recommendations, and historical prices. 33 | - **Capabilities**: 34 | - Provides structured insights in easy-to-read tables. 35 | - Combines data visualization and textual summaries for clarity. 36 | 37 | ### **3. Multi-Agent Integration** 38 | - **Purpose**: Combines the power of Web Search and Financial Insights agents. 39 | - **Capabilities**: 40 | - Delivers a holistic view of the stock market. 41 | - Responds to queries with multi-dimensional insights for better decision-making. 42 | 43 | --- 44 | 45 | ## **🔍 How It Works** 46 | 47 | 1. **Agent-Based Architecture**: 48 | - Build highly specialized AI agents using the `phi.agent` framework. 49 | 50 | 2. **Real-Time Data Processing**: 51 | - Access live financial and web-based information using APIs. 52 | 53 | 3. **Intuitive Interaction**: 54 | - Get responses enriched with tables, Markdown formatting, and always backed by credible sources. 55 | 56 | 4. **Query Example**: 57 | - **Question**: "Summarize analyst recommendations and share the latest news for Nvidia." 58 | - **Response**: A structured table highlighting analyst sentiments, news trends, and real-time updates. 59 | 60 | --- 61 | 62 | ## **🚀 Quickstart** 63 | 64 | ### **Step 1: Clone the Repository** 65 | ```bash 66 | git clone https://github.com/yourusername/financial-agentic-ai.git 67 | cd financial-agentic-ai 68 | ``` 69 | 70 | ### **Step 2: Install Dependencies** 71 | ```bash 72 | pip install -r requirements.txt 73 | ``` 74 | 75 | ### **Step 3: Set Up Environment Variables** 76 | - Create a `.env` file with the following keys: 77 | ```env 78 | GROQ_API_KEY=your_groq_api_key 79 | ``` 80 | 81 | ### **Step 4: Run the Script** 82 | ```bash 83 | python financial_agent.py 84 | ``` 85 | 86 | --- 87 | 88 | ## **📚 Key Technologies** 89 | - **phi.agent**: Modular AI agent framework. 90 | - **Groq Model**: AI model powering intelligent decision-making. 91 | - **YFinanceTools**: Real-time financial data analysis. 92 | - **Google Search & DuckDuckGo APIs**: Live web search integration. 93 | 94 | --- 95 | 96 | ## **🎯 Use Cases** 97 | - Financial analysts seeking reliable and actionable insights. 98 | - Stock market enthusiasts looking for real-time updates. 99 | - AI researchers exploring multi-agent architectures. 100 | 101 | --- 102 | 103 | ## **💡 Why Choose Financial Agentic AI?** 104 | - **Dynamic Intelligence**: Combines financial and web insights in a single response. 105 | - **Transparency**: Always includes sources and structured outputs for trustworthiness. 106 | - **Customizable**: Fully adaptable to various financial scenarios. 107 | 108 | -------------------------------------------------------------------------------- /financial_agent.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import pandas as pd 3 | import plotly.graph_objects as go 4 | import plotly.express as px 5 | from datetime import datetime, timedelta 6 | from phi.agent.agent import Agent 7 | from phi.model.groq import Groq 8 | from phi.tools.yfinance import YFinanceTools 9 | from phi.tools.duckduckgo import DuckDuckGo 10 | from phi.tools.googlesearch import GoogleSearch 11 | import yfinance as yf 12 | 13 | # Get API key from Streamlit secrets 14 | GROQ_API_KEY = st.secrets["GROQ_API_KEY"] 15 | 16 | # Enhanced stock symbol mappings 17 | COMMON_STOCKS = { 18 | # US Stocks 19 | 'NVIDIA': 'NVDA', 20 | 'APPLE': 'AAPL', 21 | 'GOOGLE': 'GOOGL', 22 | 'MICROSOFT': 'MSFT', 23 | 'TESLA': 'TSLA', 24 | 'AMAZON': 'AMZN', 25 | 'META': 'META', 26 | 'NETFLIX': 'NFLX', 27 | # Indian Stocks - NSE 28 | 'TCS': 'TCS.NS', 29 | 'RELIANCE': 'RELIANCE.NS', 30 | 'INFOSYS': 'INFY.NS', 31 | 'WIPRO': 'WIPRO.NS', 32 | 'HDFC': 'HDFCBANK.NS', 33 | 'TATAMOTORS': 'TATAMOTORS.NS', 34 | 'ICICIBANK': 'ICICIBANK.NS', 35 | 'SBIN': 'SBIN.NS', 36 | 'MARUTI': 'MARUTI.NS', 37 | 'BHARTIARTL': 'BHARTIARTL.NS', 38 | 'HCLTECH': 'HCLTECH.NS', 39 | 'ITC': 'ITC.NS', 40 | 'AXISBANK': 'AXISBANK.NS' 41 | } 42 | 43 | # Page configuration 44 | st.set_page_config( 45 | page_title="Advanced Stock Market Analysis", 46 | page_icon="📈", 47 | layout="wide", 48 | initial_sidebar_state="expanded" 49 | ) 50 | 51 | # Custom CSS with improved styling 52 | st.markdown(""" 53 | 104 | """, unsafe_allow_html=True) 105 | 106 | # Initialize session state 107 | if 'agents_initialized' not in st.session_state: 108 | st.session_state.agents_initialized = False 109 | st.session_state.watchlist = set() 110 | st.session_state.analysis_history = [] 111 | st.session_state.last_refresh = None 112 | 113 | def initialize_agents(): 114 | """Initialize all agent instances with improved error handling""" 115 | if not st.session_state.agents_initialized: 116 | try: 117 | st.session_state.web_agent = Agent( 118 | name="Web Search Agent", 119 | role="Search the web for the information", 120 | model=Groq(api_key=GROQ_API_KEY), 121 | tools=[ 122 | GoogleSearch(fixed_language='english', fixed_max_results=5), 123 | DuckDuckGo(fixed_max_results=5) 124 | ], 125 | instructions=['Always include sources and verification'], 126 | show_tool_calls=True, 127 | markdown=True 128 | ) 129 | 130 | st.session_state.finance_agent = Agent( 131 | name="Financial AI Agent", 132 | role="Providing financial insights", 133 | model=Groq(api_key=GROQ_API_KEY), 134 | tools=[ 135 | YFinanceTools( 136 | stock_price=True, 137 | company_news=True, 138 | analyst_recommendations=True, 139 | historical_prices=True 140 | ) 141 | ], 142 | instructions=["Provide detailed analysis with data visualization"], 143 | show_tool_calls=True, 144 | markdown=True 145 | ) 146 | 147 | st.session_state.multi_ai_agent = Agent( 148 | name='A Stock Market Agent', 149 | role='A comprehensive assistant specializing in stock market analysis', 150 | model=Groq(api_key=GROQ_API_KEY), 151 | team=[st.session_state.web_agent, st.session_state.finance_agent], 152 | instructions=["Provide comprehensive analysis with multiple data sources"], 153 | show_tool_calls=True, 154 | markdown=True 155 | ) 156 | 157 | st.session_state.agents_initialized = True 158 | return True 159 | except Exception as e: 160 | st.error(f"Error initializing agents: {str(e)}") 161 | return False 162 | 163 | def get_symbol_from_name(stock_name): 164 | """Enhanced function to fetch stock symbol from full stock name""" 165 | try: 166 | # Clean up input 167 | stock_name = stock_name.strip().upper() 168 | 169 | # First check if it's in our common stocks dictionary 170 | if stock_name in COMMON_STOCKS: 171 | return COMMON_STOCKS[stock_name] 172 | 173 | # Check if it's already a valid symbol 174 | ticker = yf.Ticker(stock_name) 175 | try: 176 | info = ticker.info 177 | if info and 'symbol' in info: 178 | return stock_name 179 | except: 180 | pass 181 | 182 | # Try Indian stock market (NSE) 183 | try: 184 | indian_symbol = f"{stock_name}.NS" 185 | ticker = yf.Ticker(indian_symbol) 186 | info = ticker.info 187 | if info and 'symbol' in info: 188 | return indian_symbol 189 | except: 190 | # Try BSE 191 | try: 192 | bse_symbol = f"{stock_name}.BO" 193 | ticker = yf.Ticker(bse_symbol) 194 | info = ticker.info 195 | if info and 'symbol' in info: 196 | return bse_symbol 197 | except: 198 | pass 199 | 200 | st.error(f"Could not find valid symbol for {stock_name}") 201 | return None 202 | except Exception as e: 203 | st.error(f"Error processing {stock_name}: {str(e)}") 204 | return None 205 | 206 | def get_stock_data(symbol, period="1y"): 207 | """Enhanced function to fetch stock data with proper cache handling""" 208 | try: 209 | # Create a new ticker instance 210 | stock = yf.Ticker(symbol) 211 | 212 | # Fetch data with error handling 213 | try: 214 | info = stock.info 215 | if not info: 216 | raise ValueError("No data retrieved for symbol") 217 | except Exception as info_error: 218 | # If .NS suffix is missing for Indian stocks, try adding it 219 | if not symbol.endswith('.NS') and not symbol.endswith('.BO'): 220 | try: 221 | indian_symbol = f"{symbol}.NS" 222 | stock = yf.Ticker(indian_symbol) 223 | info = stock.info 224 | symbol = indian_symbol 225 | except: 226 | # Try Bombay Stock Exchange 227 | try: 228 | bse_symbol = f"{symbol}.BO" 229 | stock = yf.Ticker(bse_symbol) 230 | info = stock.info 231 | symbol = bse_symbol 232 | except: 233 | raise info_error 234 | else: 235 | raise info_error 236 | 237 | # Fetch historical data 238 | hist = stock.history(period=period, interval="1d", auto_adjust=True) 239 | 240 | if hist.empty: 241 | raise ValueError("No historical data available") 242 | 243 | return info, hist 244 | except Exception as e: 245 | st.error(f"Error fetching data for {symbol}: {str(e)}") 246 | return None, None 247 | 248 | def create_price_chart(hist_data, symbol): 249 | """Create an interactive price chart using plotly""" 250 | fig = go.Figure() 251 | 252 | # Add candlestick chart 253 | fig.add_trace(go.Candlestick( 254 | x=hist_data.index, 255 | open=hist_data['Open'], 256 | high=hist_data['High'], 257 | low=hist_data['Low'], 258 | close=hist_data['Close'], 259 | name='Price' 260 | )) 261 | 262 | # Add moving averages 263 | ma20 = hist_data['Close'].rolling(window=20).mean() 264 | ma50 = hist_data['Close'].rolling(window=50).mean() 265 | 266 | fig.add_trace(go.Scatter(x=hist_data.index, y=ma20, name='20 Day MA', line=dict(color='orange'))) 267 | fig.add_trace(go.Scatter(x=hist_data.index, y=ma50, name='50 Day MA', line=dict(color='blue'))) 268 | 269 | fig.update_layout( 270 | title=f'{symbol} Stock Price', 271 | yaxis_title='Price', 272 | template='plotly_white', 273 | xaxis_rangeslider_visible=False, 274 | height=600 275 | ) 276 | 277 | return fig 278 | 279 | def create_volume_chart(hist_data): 280 | """Create enhanced volume chart using plotly""" 281 | # Calculate volume moving average 282 | volume_ma = hist_data['Volume'].rolling(window=20).mean() 283 | 284 | fig = go.Figure() 285 | 286 | # Add volume bars 287 | fig.add_trace(go.Bar( 288 | x=hist_data.index, 289 | y=hist_data['Volume'], 290 | name='Volume', 291 | marker_color='rgba(31, 119, 180, 0.3)' 292 | )) 293 | 294 | # Add volume moving average 295 | fig.add_trace(go.Scatter( 296 | x=hist_data.index, 297 | y=volume_ma, 298 | name='20 Day Volume MA', 299 | line=dict(color='red') 300 | )) 301 | 302 | fig.update_layout( 303 | title='Trading Volume Analysis', 304 | yaxis_title='Volume', 305 | template='plotly_white', 306 | height=400 307 | ) 308 | 309 | return fig 310 | 311 | def format_large_number(number): 312 | """Format large numbers into readable format""" 313 | if number >= 1e12: 314 | return f"${number/1e12:.2f}T" 315 | elif number >= 1e9: 316 | return f"${number/1e9:.2f}B" 317 | elif number >= 1e6: 318 | return f"${number/1e6:.2f}M" 319 | else: 320 | return f"${number:,.2f}" 321 | 322 | def display_metrics(info): 323 | """Display enhanced key metrics in a grid""" 324 | col1, col2, col3, col4 = st.columns(4) 325 | 326 | with col1: 327 | st.markdown('
', unsafe_allow_html=True) 328 | market_cap = info.get('marketCap', 'N/A') 329 | if market_cap != 'N/A': 330 | market_cap = format_large_number(market_cap) 331 | st.metric("Market Cap", market_cap) 332 | st.markdown('
', unsafe_allow_html=True) 333 | 334 | with col2: 335 | st.markdown('
', unsafe_allow_html=True) 336 | pe_ratio = info.get('trailingPE', 'N/A') 337 | if pe_ratio != 'N/A': 338 | pe_ratio = f"{pe_ratio:.2f}" 339 | st.metric("P/E Ratio", pe_ratio) 340 | st.markdown('
', unsafe_allow_html=True) 341 | 342 | with col3: 343 | st.markdown('
', unsafe_allow_html=True) 344 | high = info.get('fiftyTwoWeekHigh', 'N/A') 345 | if high != 'N/A': 346 | high = f"${high:.2f}" 347 | st.metric("52 Week High", high) 348 | st.markdown('
', unsafe_allow_html=True) 349 | 350 | with col4: 351 | st.markdown('
', unsafe_allow_html=True) 352 | low = info.get('fiftyTwoWeekLow', 'N/A') 353 | if low != 'N/A': 354 | low = f"${low:.2f}" 355 | st.metric("52 Week Low", low) 356 | st.markdown('
', unsafe_allow_html=True) 357 | 358 | def main(): 359 | # Sidebar 360 | with st.sidebar: 361 | st.header("📊 Analysis Options") 362 | analysis_type = st.selectbox( 363 | "Choose Analysis Type", 364 | ["Comprehensive Analysis", "Technical Analysis", "Fundamental Analysis", 365 | "News Analysis", "Sentiment Analysis"] 366 | ) 367 | 368 | # Add market selection 369 | market = st.selectbox( 370 | "Select Market", 371 | ["US Market", "Indian Market (NSE)", "Indian Market (BSE)"] 372 | ) 373 | 374 | st.markdown("---") 375 | 376 | # Watchlist section 377 | st.markdown("### 📋 Watchlist") 378 | watchlist_symbol = st.text_input("Add to Watchlist", 379 | help="Enter stock name or symbol (e.g., NVIDIA, TCS)") 380 | if st.button("Add to Watchlist"): 381 | symbol = get_symbol_from_name(watchlist_symbol) 382 | if symbol: 383 | st.session_state.watchlist.add(symbol) 384 | st.success(f"Added {symbol} to watchlist") 385 | 386 | if st.session_state.watchlist: 387 | st.markdown("#### Current Watchlist") 388 | for symbol in st.session_state.watchlist: 389 | col1, col2 = st.columns([3, 1]) 390 | with col1: 391 | st.write(symbol) 392 | with col2: 393 | if st.button("❌", key=f"remove_{symbol}"): 394 | st.session_state.watchlist.remove(symbol) 395 | st.experimental_rerun() 396 | 397 | # Main content 398 | st.markdown('

🤖 Advanced Stock Market Analysis By Shivam Shukla

', 399 | unsafe_allow_html=True) 400 | 401 | # Search and Analysis Section 402 | col1, col2 = st.columns([2, 1]) 403 | with col1: 404 | stock_input = st.text_input( 405 | "Enter Stock Name or Symbol", 406 | help="Enter company name (e.g., NVIDIA) or symbol (e.g., NVDA)" 407 | ) 408 | with col2: 409 | date_range = st.selectbox( 410 | "Select Time Range", 411 | ["1 Month", "3 Months", "6 Months", "1 Year", "5 Years"], key="time_range" 412 | ) 413 | # Convert selected range to yfinance period format 414 | period_map = { 415 | "1 Month": "1mo", 416 | "3 Months": "3mo", 417 | "6 Months": "6mo", 418 | "1 Year": "1y", 419 | "5 Years": "5y" 420 | } 421 | period = period_map[date_range] 422 | 423 | if st.button("Analyze", type="primary"): 424 | if not stock_input: 425 | st.error("Please enter a stock name or symbol.") 426 | return 427 | 428 | # Convert input to symbol 429 | stock_symbol = get_symbol_from_name(stock_input) 430 | if stock_symbol: 431 | try: 432 | # Initialize agents 433 | if initialize_agents(): 434 | # Show loading spinner 435 | with st.spinner(f"Analyzing {stock_symbol}..."): 436 | # Fetch fresh stock data 437 | info, hist = get_stock_data(stock_symbol, period=period) 438 | 439 | if info and hist is not None: 440 | # Display market status 441 | market_status = "🟢 Market Open" if info.get('regularMarketOpen') else "🔴 Market Closed" 442 | st.markdown(f"
{market_status}
", unsafe_allow_html=True) 443 | 444 | # Create tabs for different sections 445 | overview_tab, charts_tab, analysis_tab, news_tab = st.tabs([ 446 | "Overview", "Charts", "AI Analysis", "News" 447 | ]) 448 | 449 | with overview_tab: 450 | # Display company info 451 | st.markdown("### Company Overview") 452 | st.write(info.get('longBusinessSummary', 'No description available.')) 453 | 454 | # Display key metrics 455 | st.markdown("### Key Metrics") 456 | display_metrics(info) 457 | 458 | # Additional company information 459 | col1, col2 = st.columns(2) 460 | with col1: 461 | st.markdown("### Company Details") 462 | st.write(f"Sector: {info.get('sector', 'N/A')}") 463 | st.write(f"Industry: {info.get('industry', 'N/A')}") 464 | st.write(f"Country: {info.get('country', 'N/A')}") 465 | st.write(f"Employees: {info.get('fullTimeEmployees', 'N/A'):,}") 466 | 467 | with col2: 468 | st.markdown("### Trading Information") 469 | st.write(f"Exchange: {info.get('exchange', 'N/A')}") 470 | st.write(f"Currency: {info.get('currency', 'N/A')}") 471 | st.write(f"Volume: {info.get('volume', 'N/A'):,}") 472 | 473 | with charts_tab: 474 | # Price chart 475 | st.markdown("### Price Analysis") 476 | price_chart = create_price_chart(hist, stock_symbol) 477 | st.plotly_chart(price_chart, use_container_width=True) 478 | 479 | # Volume chart 480 | volume_chart = create_volume_chart(hist) 481 | st.plotly_chart(volume_chart, use_container_width=True) 482 | 483 | # Technical indicators 484 | st.markdown("### Technical Indicators") 485 | col1, col2, col3 = st.columns(3) 486 | 487 | with col1: 488 | rsi = hist['Close'].diff() 489 | rsi_pos = rsi.copy() 490 | rsi_neg = rsi.copy() 491 | rsi_pos[rsi_pos < 0] = 0 492 | rsi_neg[rsi_neg > 0] = 0 493 | rsi_14_pos = rsi_pos.rolling(window=14).mean() 494 | rsi_14_neg = abs(rsi_neg.rolling(window=14).mean()) 495 | rsi_14 = 100 - (100 / (1 + rsi_14_pos / rsi_14_neg)) 496 | st.metric("RSI (14)", f"{rsi_14.iloc[-1]:.2f}") 497 | 498 | with col2: 499 | ma20 = hist['Close'].rolling(window=20).mean() 500 | ma50 = hist['Close'].rolling(window=50).mean() 501 | cross_signal = "Bullish" if ma20.iloc[-1] > ma50.iloc[-1] else "Bearish" 502 | st.metric("MA Cross Signal", cross_signal) 503 | 504 | with col3: 505 | volatility = hist['Close'].pct_change().std() * (252 ** 0.5) * 100 506 | st.metric("Annualized Volatility", f"{volatility:.2f}%") 507 | 508 | with analysis_tab: 509 | # AI Analysis 510 | st.markdown("### AI-Powered Analysis") 511 | query = f"Provide a {analysis_type.lower()} for {stock_symbol}." 512 | response = st.session_state.multi_ai_agent.print_response(query, stream=True) 513 | 514 | # Add to analysis history 515 | st.session_state.analysis_history.append({ 516 | 'symbol': stock_symbol, 517 | 'timestamp': datetime.now(), 518 | 'analysis_type': analysis_type 519 | }) 520 | 521 | with news_tab: 522 | st.markdown("### Latest News") 523 | if 'news' in info: 524 | for news_item in info['news'][:5]: 525 | with st.container(): 526 | st.markdown(f""" 527 |
528 |

{news_item['title']}

529 |

{news_item['summary']}

530 | Source: {news_item.get('source', 'Unknown')} | 531 | {datetime.fromtimestamp(news_item['providerPublishTime']).strftime('%Y-%m-%d %H:%M:%S')} 532 |
533 | """, unsafe_allow_html=True) 534 | else: 535 | st.write("No recent news available") 536 | 537 | # Add refresh button 538 | if st.button("🔄 Refresh Data"): 539 | st.session_state.last_refresh = datetime.now() 540 | st.experimental_rerun() 541 | 542 | except Exception as e: 543 | st.error(f"An error occurred: {str(e)}") 544 | 545 | # Display analysis history 546 | if st.session_state.analysis_history: 547 | st.markdown("---") 548 | st.markdown("### Recent Analysis History") 549 | history_df = pd.DataFrame(st.session_state.analysis_history) 550 | history_df['timestamp'] = history_df['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S') 551 | st.dataframe(history_df, use_container_width=True) 552 | 553 | # Footer 554 | st.markdown("---") 555 | st.markdown("### About") 556 | st.markdown(""" 557 | This advanced stock market analysis tool combines: 558 | - Real-time market data analysis 559 | - AI-powered insights and predictions 560 | - Technical and fundamental analysis 561 | - News and sentiment analysis 562 | - Interactive charts and visualizations 563 | 564 | Features: 565 | - Support for both US and Indian markets (NSE/BSE) 566 | - Company name and symbol resolution 567 | - Watchlist management 568 | - Multiple timeframe analysis 569 | - Technical indicators 570 | 571 | Use the sidebar to configure your analysis preferences and manage your watchlist. 572 | """) 573 | 574 | # Display last refresh time if available 575 | if st.session_state.last_refresh: 576 | st.markdown(f"
Last refreshed: {st.session_state.last_refresh.strftime('%Y-%m-%d %H:%M:%S')}
", 577 | unsafe_allow_html=True) 578 | 579 | if __name__ == "__main__": 580 | main() 581 | --------------------------------------------------------------------------------