Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import pandas as pd | |
| import yfinance as yf | |
| from utils import ( | |
| calculate_technical_indicators, | |
| generate_trading_signals, | |
| get_fundamental_data, | |
| predict_prices, | |
| create_price_chart, | |
| create_technical_chart, | |
| create_prediction_chart, | |
| ) | |
| import warnings | |
| warnings.filterwarnings("ignore") | |
| def analyze_stock(symbol, prediction_days=30): | |
| try: | |
| if not symbol.strip(): | |
| raise ValueError("Please enter a valid stock symbol.") | |
| if not symbol.endswith(".JK"): | |
| symbol = symbol.upper() + ".JK" | |
| stock = yf.Ticker(symbol) | |
| data = stock.history(period="6mo", interval="1d") | |
| if data.empty: | |
| raise ValueError("No price data available for this stock.") | |
| indicators = calculate_technical_indicators(data) | |
| signals = generate_trading_signals(data, indicators) | |
| fundamental_info = get_fundamental_data(stock) | |
| predictions = predict_prices(data, prediction_days=prediction_days) | |
| fig_price = create_price_chart(data, indicators) | |
| fig_technical = create_technical_chart(data, indicators) | |
| fig_prediction = create_prediction_chart(data, predictions) | |
| # kalkulasi TP1, TP2, SL | |
| last_price = data['Close'].iloc[-1] | |
| tp1 = last_price * (1 + (predictions.get("change_pct", 0) / 200)) | |
| tp2 = last_price * (1 + (predictions.get("change_pct", 0) / 100)) | |
| sl = last_price * 0.95 | |
| predictions["tp1"] = tp1 | |
| predictions["tp2"] = tp2 | |
| predictions["sl"] = sl | |
| return fundamental_info, indicators, signals, fig_price, fig_technical, fig_prediction, predictions | |
| except Exception as e: | |
| print(f"Error analyzing {symbol}: {e}") | |
| empty_fig = gr.Plot.update(value=None) | |
| empty_predictions = { | |
| "high_30d": 0, | |
| "low_30d": 0, | |
| "change_pct": 0, | |
| "summary": "Prediction unavailable.", | |
| } | |
| return {}, {}, {}, empty_fig, empty_fig, empty_fig, empty_predictions | |
| def update_analysis(symbol, prediction_days): | |
| ( | |
| fundamental_info, | |
| indicators, | |
| signals, | |
| fig_price, | |
| fig_technical, | |
| fig_prediction, | |
| predictions, | |
| ) = analyze_stock(symbol, prediction_days) | |
| if not fundamental_info: | |
| return ( | |
| "Unable to fetch stock data.", | |
| gr.Plot.update(value=None), | |
| gr.Plot.update(value=None), | |
| gr.Plot.update(value=None), | |
| ) | |
| fundamentals = f""" | |
| <h4>COMPANY FUNDAMENTALS</h4> | |
| <b>Name:</b> {fundamental_info.get('name', 'N/A')} ({symbol.upper()})<br> | |
| <b>Current Price:</b> Rp{fundamental_info.get('current_price', 0):,.2f}<br> | |
| <b>Market Cap:</b> {fundamental_info.get('market_cap', 0):,}<br> | |
| <b>P/E Ratio:</b> {fundamental_info.get('pe_ratio', 0):.2f}<br> | |
| <b>Dividend Yield:</b> {fundamental_info.get('dividend_yield', 0):.2f}%<br> | |
| <b>Volume:</b> {fundamental_info.get('volume', 0):,}<br> | |
| """ | |
| details_list = "".join( | |
| [f"<li>{line.strip()}</li>" for line in signals.get("details", "").split("\n") if line.strip()] | |
| ) | |
| trading_signal = f""" | |
| <h4>TECHNICAL SIGNAL SUMMARY</h4> | |
| <b>Overall Trend:</b> {signals.get('overall', 'N/A')}<br> | |
| <b>Signal Strength:</b> {signals.get('strength', 0):.2f}%<br> | |
| <b>Support:</b> Rp{signals.get('support', 0):,.2f}<br> | |
| <b>Resistance:</b> Rp{signals.get('resistance', 0):,.2f}<br> | |
| <b>Stop Loss:</b> Rp{signals.get('stop_loss', 0):,.2f}<br><br> | |
| <b>Detailed Signals:</b> | |
| <ul style="margin-top: 8px; padding-left: 20px; line-height: 1.6;"> | |
| {details_list} | |
| </ul> | |
| """ | |
| prediction = f""" | |
| <h4>30-DAY AI FORECAST (CHRONOS-BOLT)</h4> | |
| <b>Predicted High:</b> Rp{predictions.get('high_30d', 0):,.2f}<br> | |
| <b>Predicted Low:</b> Rp{predictions.get('low_30d', 0):,.2f}<br> | |
| <b>Expected Change:</b> {predictions.get('change_pct', 0):.2f}%<br><br> | |
| <b>TP1:</b> Rp{predictions.get('tp1', 0):,.2f}<br> | |
| <b>TP2:</b> Rp{predictions.get('tp2', 0):,.2f}<br> | |
| <b>Stop Loss:</b> Rp{predictions.get('sl', 0):,.2f}<br><br> | |
| <b>Model Insight:</b><br>{predictions.get('summary', 'No analysis available')} | |
| """ | |
| # Karena custom CSS dihapus, kita akan menggunakan div sederhana tanpa class 'panel-box' dan 'triple-panel' | |
| # Gradio secara otomatis akan menata elemen-elemen ini dengan lebih standar | |
| return ( | |
| f""" | |
| <div class="panel-card" style="background: rgba(255,255,255,0.1); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.2); border-radius: 15px; padding: 0; overflow: hidden;"> | |
| <div style="display: flex; flex-direction: row; gap: 0; height: auto;"> | |
| <div style="flex: 1; min-width: 30%; padding: 20px; border-right: 1px solid rgba(255,255,255,0.1);"> | |
| <div style="margin-bottom: 15px;"> | |
| <h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: #4fc3f7;">๐ข COMPANY FUNDAMENTALS</h3> | |
| </div> | |
| <div style="line-height: 1.8; font-size: 0.95rem;"> | |
| {fundamentals.replace('<h4>COMPANY FUNDAMENTALS</h4>', '').replace('<br>', '<br>')} | |
| </div> | |
| </div> | |
| <div style="flex: 1; min-width: 30%; padding: 20px; border-right: 1px solid rgba(255,255,255,0.1);"> | |
| <div style="margin-bottom: 15px;"> | |
| <h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: #81c784;">๐ TECHNICAL SIGNAL SUMMARY</h3> | |
| </div> | |
| <div style="line-height: 1.8; font-size: 0.95rem;"> | |
| {trading_signal.replace('<h4>TECHNICAL SIGNAL SUMMARY</h4>', '').replace('<br>', '<br>').replace('<ul style="margin-top: 8px; padding-left: 20px; line-height: 1.6;">', '<ul style="margin-top: 8px; padding-left: 20px; line-height: 1.4; font-size: 0.9rem;">')} | |
| </div> | |
| </div> | |
| <div style="flex: 1; min-width: 30%; padding: 20px;"> | |
| <div style="margin-bottom: 15px;"> | |
| <h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: #ffb74d;">๐ค AI FORECAST INSIGHTS</h3> | |
| </div> | |
| <div style="line-height: 1.8; font-size: 0.95rem;"> | |
| {prediction.replace('<h4>30-DAY AI FORECAST (CHRONOS-BOLT)</h4>', '').replace('<br>', '<br>')} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """, | |
| fig_price, | |
| fig_technical, | |
| fig_prediction, | |
| ) | |
| # Theme management | |
| theme_state = gr.State(value="light") | |
| def toggle_theme(current_theme): | |
| return "dark" if current_theme == "light" else "light" | |
| def get_theme_css(theme): | |
| if theme == "dark": | |
| return """ | |
| <style> | |
| body { background-color: #1a1a1a; color: #ffffff; } | |
| .main-container { background-color: #2a2a2a !important; } | |
| .panel-modern { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| color: white !important; | |
| border-radius: 15px !important; | |
| padding: 20px !important; | |
| margin: 10px !important; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3) !important; | |
| } | |
| .panel-card { | |
| background: rgba(255,255,255,0.05) !important; | |
| backdrop-filter: blur(10px) !important; | |
| border: 1px solid rgba(255,255,255,0.1) !important; | |
| border-radius: 12px !important; | |
| padding: 16px !important; | |
| margin: 8px !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .panel-card:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.4) !important; | |
| } | |
| .header-gradient { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| -webkit-background-clip: text !important; | |
| -webkit-text-fill-color: transparent !important; | |
| background-clip: text !important; | |
| } | |
| .theme-toggle { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| border: none !important; | |
| border-radius: 25px !important; | |
| padding: 12px 20px !important; | |
| color: white !important; | |
| font-weight: bold !important; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .theme-toggle:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6) !important; | |
| } | |
| .input-modern { | |
| background: rgba(255,255,255,0.05) !important; | |
| border: 1px solid rgba(255,255,255,0.1) !important; | |
| border-radius: 10px !important; | |
| color: white !important; | |
| } | |
| .tab-content { | |
| background: rgba(255,255,255,0.02) !important; | |
| border-radius: 12px !important; | |
| padding: 20px !important; | |
| } | |
| </style> | |
| """ | |
| else: | |
| return """ | |
| <style> | |
| body { background-color: #f8fafc; color: #1a202c; } | |
| .main-container { background-color: #ffffff !important; } | |
| .panel-modern { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| color: white !important; | |
| border-radius: 15px !important; | |
| padding: 20px !important; | |
| margin: 10px !important; | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.1) !important; | |
| } | |
| .panel-card { | |
| background: rgba(255,255,255,0.9) !important; | |
| backdrop-filter: blur(10px) !important; | |
| border: 1px solid rgba(0,0,0,0.05) !important; | |
| border-radius: 12px !important; | |
| padding: 16px !important; | |
| margin: 8px !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .panel-card:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.15) !important; | |
| } | |
| .header-gradient { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| -webkit-background-clip: text !important; | |
| -webkit-text-fill-color: transparent !important; | |
| background-clip: text !important; | |
| } | |
| .theme-toggle { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| border: none !important; | |
| border-radius: 25px !important; | |
| padding: 12px 20px !important; | |
| color: white !important; | |
| font-weight: bold !important; | |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important; | |
| transition: all 0.3s ease !important; | |
| } | |
| .theme-toggle:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6) !important; | |
| } | |
| .input-modern { | |
| background: rgba(255,255,255,0.9) !important; | |
| border: 1px solid rgba(0,0,0,0.1) !important; | |
| border-radius: 10px !important; | |
| } | |
| .tab-content { | |
| background: rgba(255,255,255,0.7) !important; | |
| border-radius: 12px !important; | |
| padding: 20px !important; | |
| } | |
| </style> | |
| """ | |
| # --- Modern UI Redesign --- | |
| with gr.Blocks( | |
| title="REXPRO FINANCIAL AI DASHBOARD", | |
| css=""" | |
| .main-container { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| .header-section { | |
| text-align: center; | |
| padding: 30px 0; | |
| margin-bottom: 30px; | |
| } | |
| .control-panel { | |
| background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); | |
| border-radius: 20px; | |
| padding: 25px; | |
| margin-bottom: 30px; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.1); | |
| } | |
| """, | |
| elem_classes=["main-container"] | |
| ) as app: | |
| # Header Section | |
| with gr.Row(elem_classes=["header-section"]): | |
| gr.HTML(""" | |
| <div class="header-gradient"> | |
| <h1 style="font-size: 2.5rem; margin: 0; font-weight: 800;"> | |
| ๐ REXPRO FINANCIAL AI DASHBOARD | |
| </h1> | |
| <p style="font-size: 1.2rem; margin: 10px 0 0 0; opacity: 0.8;"> | |
| Comprehensive stock analytics powered by <strong>AI forecasting and technical analysis</strong> | |
| </p> | |
| </div> | |
| """) | |
| # Theme toggle | |
| with gr.Row(): | |
| theme_toggle = gr.Button( | |
| "๐ Switch to Dark Mode" if gr.load else "โ๏ธ Switch to Light Mode", | |
| variant="primary", | |
| size="sm", | |
| elem_classes=["theme-toggle"] | |
| ) | |
| # Control Panel | |
| with gr.Row(elem_classes=["control-panel"]): | |
| with gr.Column(scale=2): | |
| symbol = gr.Textbox( | |
| label="๐ STOCK SYMBOL (IDX)", | |
| value="BBCA", | |
| placeholder="Example: BBCA, TLKM, ADRO, BMRI", | |
| interactive=True, | |
| elem_classes=["input-modern"] | |
| ) | |
| with gr.Column(scale=1): | |
| prediction_days = gr.Slider( | |
| label="๐ฎ FORECAST PERIOD (DAYS)", | |
| minimum=5, | |
| maximum=60, | |
| step=5, | |
| value=30, | |
| interactive=True, | |
| elem_classes=["input-modern"] | |
| ) | |
| with gr.Column(scale=1): | |
| analyze_button = gr.Button( | |
| "โก RUN ANALYSIS", | |
| variant="primary", | |
| size="lg", | |
| elem_classes=["theme-toggle"] | |
| ) | |
| # Modern theme-aware CSS | |
| theme_css = gr.HTML("") | |
| gr.HTML('<div style="height: 20px;"></div>') | |
| # Enhanced report section with modern styling | |
| report_section = gr.HTML(elem_classes=["panel-modern"]) | |
| gr.HTML('<div style="height: 20px;"></div>') | |
| with gr.Tab("๐ MARKET CHARTS", elem_classes=["tab-content"]): | |
| with gr.Row(): | |
| price_chart = gr.Plot(label="๐น PRICE & MOVING AVERAGES", elem_classes=["panel-card"]) | |
| technical_chart = gr.Plot(label="๐ TECHNICAL INDICATORS OVERVIEW", elem_classes=["panel-card"]) | |
| gr.HTML('<div style="height: 20px;"></div>') | |
| prediction_chart = gr.Plot(label="๐ค AI FORECAST PROJECTION", elem_classes=["panel-card"]) | |
| analyze_button.click( | |
| fn=update_analysis, | |
| inputs=[symbol, prediction_days], | |
| outputs=[report_section, price_chart, technical_chart, prediction_chart], | |
| ) | |
| # Theme toggle functionality | |
| theme_toggle.click( | |
| fn=lambda x: toggle_theme(x), | |
| inputs=[theme_state], | |
| outputs=[theme_state] | |
| ) | |
| # Update theme CSS when theme changes | |
| theme_state.change( | |
| fn=get_theme_css, | |
| inputs=[theme_state], | |
| outputs=[theme_css] | |
| ) | |
| if __name__ == "__main__": | |
| app.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=True) |