import re import pandas as pd from typing import List, Optional, Tuple from pathlib import Path # Import our modular components from ..state import get_user_temp_dir, update_progress, get_pending_files, clear_pending_file from .utils import now_str, convert_html_to_pdf, cleanup_after_analysis # --- Constants for Reporting --- ORIGINAL_HTML_STYLE = """ body { margin: 20px; background: #f5f5f5; font-family: Arial, sans-serif; } .table-container { margin: 20px 0; background: white; padding: 15px; border-radius: 10px; } table { width: 100%; border-collapse: collapse; margin: 10px 0; } thead { display: table-row-group; } th, td { padding: 10px; border: 1px solid #ddd; text-align: left; } th { background: #2c3e50; color: white; } tr:nth-child(even) { background: #f9f9f9; } .header { background: #2c3e50; color: white; padding: 20px; border-radius: 10px; text-align: center; } h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; } .footer { text-align: center; margin-top: 20px; color: #7f8c8d; } .oi-strong { color: #27ae60; font-weight: bold; } .oi-weak { color: #c0392b; } """ ORIGINAL_MATCHED_HEADERS = ["Ticker", "Spot MrktCap", "Spot Volume", "Spot VTMR", "Futures Volume", "Futures VTMR", "OISS", "Funding Rate"] ORIGINAL_FUTURES_HEADERS = ["Ticker", "Market Cap", "Volume", "VTMR", "OISS", "Funding Rate"] ORIGINAL_SPOT_HEADERS = ["Ticker", "MarketCap", "Volume", "VTMR"] class SignalEngine: @staticmethod def _oi_score_and_signal(oi_change: float) -> Tuple[int, str]: if oi_change > 0.20: return 5, "Strong" if oi_change > 0.10: return 4, "Bullish" if oi_change > 0.00: return 3, "Build-Up" if oi_change > -0.10: return 2, "Weakening" if oi_change > -0.20: return 1, "Exiting" return 0, "Exiting" @staticmethod def _funding_score_and_signal(funding_val: float) -> Tuple[str, str]: if funding_val >= 0.05: return "Greed", "oi-strong" if funding_val > 0.00: return "Bullish", "oi-strong" if funding_val <= -0.05: return "Extreme Fear", "oi-weak" if funding_val < 0.00: return "Bearish", "oi-weak" return "Neutral", "" @classmethod def make_oiss(cls, oi_pct: Optional[float]) -> str: if oi_pct is None or (isinstance(oi_pct, float) and pd.isna(oi_pct)): return "-" try: oi_change = oi_pct / 100 score, signal = cls._oi_score_and_signal(oi_change) if oi_change > 0: css_class = "oi-strong" elif oi_change < 0: css_class = "oi-weak" else: css_class = "" sign = "+" if oi_change > 0 else "" if css_class: return f'{sign}{oi_change*100:.0f}% {signal}' return f"{sign}{oi_change*100:.0f}% {signal}" except Exception: return "-" @classmethod def make_funding_signal(cls, funding_pct: Optional[float]) -> str: if funding_pct is None or (isinstance(funding_pct, float) and pd.isna(funding_pct)): return "-" try: val = float(funding_pct) signal_word, css_class = cls._funding_score_and_signal(val) if css_class: return f'{val}% {signal_word}' return f'{val}% {signal_word}' except Exception: return "-" class DataProcessor: """Handles Dataframe loading, merging, and HTML generation.""" @staticmethod def load_spot(path: Path) -> pd.DataFrame: print(f" Parsing Spot File: {path.name}") try: # Explicit UTF-8 for Unicode preservation if path.suffix == '.html': df = pd.read_html(str(path), encoding='utf-8')[0] else: df = pd.read_csv(path, encoding='utf-8') df.columns = [c.lower().replace(' ', '_') for c in df.columns] col_map = { 'ticker': 'ticker', 'symbol': 'ticker', 'vtmr': 'vtmr', 'spot_vtmr': 'vtmr', 'flipping_multiple': 'vtmr', 'market_cap': 'market_cap', 'marketcap': 'market_cap', 'volume_24h': 'volume', 'volume': 'volume' } df = df.rename(columns=col_map, errors='ignore') # Normalize ticker column (Find it if it's missing) if 'ticker' not in df.columns: for col in df.columns: if 'sym' in col or 'tick' in col or 'tok' in col: df = df.rename(columns={col: 'ticker'}) break # Unicode-safe cleaning (Protects Chinese characters) if 'ticker' in df.columns: df['ticker'] = df['ticker'].apply(lambda x: str(x).strip().upper()) print(f" Extracted {len(df)} spot tokens") return df except Exception as e: print(f" Spot File Error: {e}") return pd.DataFrame() @staticmethod def _generate_table_html(title: str, df: pd.DataFrame, headers: List[str], df_cols: List[str]) -> str: if df.empty: return f'

{title}

No data found

' missing = [c for c in df_cols if c not in df.columns] df_display = df.copy() for m in missing: df_display[m] = "" df_display = df_display[df_cols] df_display.columns = headers table_html = df_display.to_html(index=False, classes='table', escape=False) return f'

{title}

{table_html}
' @staticmethod def generate_html_report(futures_df: pd.DataFrame, spot_df: pd.DataFrame) -> Optional[str]: """Merges Spot and Futures dataframes and creates the final HTML report.""" if futures_df.empty or spot_df.empty: return None futures_df = futures_df.copy() if 'oi_pct' in futures_df.columns: futures_df['oiss'] = futures_df['oi_pct'].apply(SignalEngine.make_oiss) else: futures_df['oiss'] = "-" if 'funding_pct' in futures_df.columns: futures_df['funding'] = futures_df['funding_pct'].apply(SignalEngine.make_funding_signal) else: futures_df['funding'] = "-" valid_futures = futures_df.copy() try: if 'vtmr' in valid_futures.columns: valid_futures['vtmr_display'] = valid_futures['vtmr'].apply(lambda x: f"{x:.2f}x") except Exception as e: print(f" Futures display formatting error: {e}") valid_futures['vtmr_display'] = valid_futures['vtmr'] # Suffix-based merge to prevent blank column mapping issues merged = pd.merge(spot_df, valid_futures, on='ticker', how='inner', suffixes=('_spot', '_fut')) if 'vtmr_fut' in merged.columns: merged = merged.sort_values('vtmr_fut', ascending=False) futures_only = valid_futures[~valid_futures['ticker'].isin(spot_df['ticker'])].copy() if 'vtmr' in futures_only.columns: futures_only = futures_only.sort_values('vtmr', ascending=False) spot_only = spot_df[~spot_df['ticker'].isin(merged['ticker'])].copy() if 'vtmr' in spot_only.columns: try: spot_only = spot_only.copy() spot_only.loc[:, 'sort_val'] = spot_only['vtmr'].astype(str).str.replace('x', '', case=False).astype(float) spot_only = spot_only.sort_values('sort_val', ascending=False).drop(columns=['sort_val']) except Exception as e: print(f" Spot filtering error: {e}") merged_cols = ['ticker', 'market_cap_spot', 'volume_spot', 'vtmr_spot', 'volume_fut', 'vtmr_display', 'oiss', 'funding'] futures_cols = ['ticker', 'market_cap', 'volume', 'vtmr_display', 'oiss', 'funding'] spot_cols = ['ticker', 'market_cap', 'volume', 'vtmr'] html_content = "" html_content += DataProcessor._generate_table_html("Tokens in Both Futures & Spot Markets", merged, ORIGINAL_MATCHED_HEADERS, merged_cols) html_content += DataProcessor._generate_table_html("Remaining Futures-Only Tokens", futures_only, ORIGINAL_FUTURES_HEADERS, futures_cols) html_content += DataProcessor._generate_table_html("Remaining Spot-Only Tokens", spot_only, ORIGINAL_SPOT_HEADERS, spot_cols) current_time = now_str("%d-%m-%Y %H:%M:%S") cheat_sheet_pdf_footer = """

OISS & Funding Cheat Sheet:

Why VTMR of All Sides Matter

Remaining Spot Only Tokens

Remember those remaining spot only tokens because there is plenty opportunity there too. So, check them out. Don't fade on them.

Disclaimer

This analysis was generated by you using the QuantVAT by @heisbuba. It empowers your market research but does not replace your due diligence. Verify the data, back your own instincts, and trade entirely at your own risk.
""" html = f""" Quantitative Crypto Volume-driven Data Analysis Report

Cross-Market Crypto Analysis Report

Using Both Spot & Futures Market Data

Generated on: {current_time}

{html_content} {cheat_sheet_pdf_footer} """ return html def crypto_analysis_v4(user_keys, user_id) -> None: """Main execution flow for Advanced Analysis.""" print(" ADVANCED CROSS-MARKET ANALYSIS") print(" Scanning for Futures CSV and Spot HTML files") print(" " + "=" * 40) update_progress(user_id, 10, "Locating Spot and Futures files...", "active") # Find Files pending = get_pending_files(user_id) spot_file = pending.get("spot") futures_file = pending.get("futures") if not spot_file or not futures_file: print(" Required files not found.") raise FileNotFoundError(" You Need CoinAlyze Futures PDF and Spot Market Data. Kindly Generate Spot Data And Upload Futures PDF First.") # Load Files update_progress(user_id, 40, "Loading Futures and Spot data...", "active") spot_df = DataProcessor.load_spot(spot_file) try: futures_df = pd.read_csv(futures_file, dtype={'ticker': str}, encoding='utf-8') print(f" Futures CSV Retrieved: {futures_file.name}") except Exception as e: print(f" Futures CSV Error: {e}") futures_df = pd.DataFrame() # Apply user-configured Futures VTMR filter def safe_float(val, default): try: if val is None or str(val).strip() == "": return default return float(val) except (ValueError, TypeError): return default settings = user_keys.get("engine_settings", {}) if user_keys else {} MIN_F_VTMR = safe_float(settings.get('min_f_vtmr'), 0.5) MAX_F_VTMR = safe_float(settings.get('max_f_vtmr'), 399.0) if not futures_df.empty and 'vtmr' in futures_df.columns: before_count = len(futures_df) futures_df = futures_df[(futures_df['vtmr'] >= MIN_F_VTMR) & (futures_df['vtmr'] <= MAX_F_VTMR)] print(f" Extracted {len(futures_df)} tokens out of {before_count} with VTMR filter applied") print(" " + "=" * 40) update_progress(user_id, 65, "Merging cross-market signals...", "active") html_content = DataProcessor.generate_html_report(futures_df, spot_df) if not html_content: print(" No data to generate report") raise ValueError("No matching data between spot and futures files — check both sources.") # Create PDF update_progress(user_id, 85, "Compiling PDF report...", "active") pdf_path = convert_html_to_pdf(html_content, user_id) if not pdf_path: print(" PDF conversion failed! Check API Key") raise RuntimeError("PDF conversion failed. Check your PDF-rendering API key/configuration.") print(f" PDF saved: {pdf_path}") print(" 🧹 Cleaning up source files after analysis...") cleanup_after_analysis(spot_file, futures_file) clear_pending_file(user_id, "spot") clear_pending_file(user_id, "futures") print(" 📊 Analysis completed! Source files cleaned up.")