Spaces:
Running
Running
Download src/services/adv_analysis.py from heisbuba/quantvat: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/spaces/heisbuba/quantvat/resolve/main/src/services/adv_analysis.py
- Command line
-
hf download hf://spaces/heisbuba/quantvat/src/services/adv_analysis.py
-
curl -L -o adv_analysis.py https://huggingface.co/spaces/heisbuba/quantvat/resolve/main/src/services/adv_analysis.py
14.8 kB
| import re | |
| import datetime | |
| import html as html_lib | |
| import pandas as pd | |
| from typing import List, Optional, Tuple | |
| from pathlib import Path | |
| from string import Template | |
| # Import our modular components | |
| from ..state import get_user_temp_dir, update_progress, get_pending_files, clear_pending_file | |
| from .utils import now_str, cleanup_after_analysis, safe_float, parse_mc_or_default, parse_mc_or_none | |
| # --- Constants for Reporting --- | |
| CROSS_REPORT_TEMPLATE = Path(__file__).resolve().parents[1] / "templates" / "reports" / "cross-market-report.html" | |
| SHARED_TOKENS_CSS = Path(__file__).resolve().parents[1] / "static" / "css" / "shared_tokens.css" | |
| 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: | |
| 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" | |
| 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", "" | |
| 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'<span class="{css_class}">{sign}{oi_change*100:.0f}%</span> {signal}' | |
| return f"{sign}{oi_change*100:.0f}% {signal}" | |
| except Exception: | |
| return "-" | |
| 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'<span class="{css_class}">{val}%</span> <span style="font-size:0.8em; color:var(--report-muted);">{signal_word}</span>' | |
| return f'{val}% {signal_word}' | |
| except Exception: | |
| return "-" | |
| class DataProcessor: | |
| """Handles Dataframe loading, merging, and HTML generation.""" | |
| def format_ticker_btn(sym_val) -> str: | |
| """Helper method isolated to avoid scope issues.""" | |
| sym_clean = html_lib.escape(str(sym_val).strip().upper(), quote=True) | |
| return f'<a href="/deep-diver?ticker={sym_clean}" class="ticker-btn">{sym_clean}</a>' | |
| def format_futures_money(value) -> str: | |
| """Adds a '$' prefix to a parseable futures market_cap/volume value; leaves n/a bare.""" | |
| if pd.isna(value): | |
| return "n/a" | |
| s = str(value).strip() | |
| if s.lower() == "n/a": | |
| return "n/a" | |
| return f"${s}" | |
| def _caps_compatible(spot_cap, fut_cap, max_ratio: float = 5.0) -> bool: | |
| """Guards against cross-market ticker collisions.""" | |
| spot_val = parse_mc_or_none(spot_cap) | |
| fut_val = parse_mc_or_none(fut_cap) | |
| if spot_val is None or fut_val is None or spot_val <= 0 or fut_val <= 0: | |
| return True | |
| ratio = max(spot_val, fut_val) / min(spot_val, fut_val) | |
| return ratio <= max_ratio | |
| def load_spot(path: Path) -> pd.DataFrame: | |
| print(f" Parsing Spot File: {path.name}") | |
| try: | |
| 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') | |
| 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 | |
| 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() | |
| def _generate_table_html(title: str, df: pd.DataFrame, headers: List[str], df_cols: List[str]) -> str: | |
| if df.empty: | |
| return f'<div class="table-container"><h2>{title}</h2><p>No data found</p></div>' | |
| 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 | |
| SAFE_HTML_HEADERS = {"Ticker", "OISS", "Funding Rate"} | |
| for col in df_display.columns: | |
| if col not in SAFE_HTML_HEADERS: | |
| df_display[col] = df_display[col].apply(lambda v: html_lib.escape(str(v))) | |
| table_html = df_display.to_html(index=False, classes='table', escape=False) | |
| return f'<div class="table-container"><h2>{title}</h2>{table_html}</div>' | |
| 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'] | |
| merged = pd.merge(spot_df, valid_futures, on='ticker', how='inner', suffixes=('_spot', '_fut')) | |
| if not merged.empty and 'market_cap_spot' in merged.columns and 'market_cap_fut' in merged.columns: | |
| cap_ok = merged.apply( | |
| lambda r: DataProcessor._caps_compatible(r.get('market_cap_spot'), r.get('market_cap_fut')), | |
| axis=1 | |
| ) | |
| mismatched = merged[~cap_ok] | |
| if not mismatched.empty: | |
| print(f" ⚠️ Excluded {len(mismatched)} ticker collision(s) from cross-market merge " | |
| f"(market cap mismatch, likely different tokens sharing a symbol): " | |
| f"{sorted(mismatched['ticker'].unique().tolist())}") | |
| merged = merged[cap_ok].copy() | |
| matched_tickers = set(merged['ticker']) | |
| if 'vtmr_fut' in merged.columns: | |
| merged = merged.sort_values('vtmr_fut', ascending=False) | |
| futures_only = valid_futures[~valid_futures['ticker'].isin(matched_tickers)].copy() | |
| if 'vtmr' in futures_only.columns: | |
| futures_only = futures_only.sort_values('vtmr', ascending=False) | |
| spot_only = spot_df[~spot_df['ticker'].isin(matched_tickers)].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}") | |
| # Map button formatting via isolated staticmethod | |
| if 'ticker' in merged.columns: | |
| merged['ticker'] = merged['ticker'].apply(DataProcessor.format_ticker_btn) | |
| if 'ticker' in futures_only.columns: | |
| futures_only['ticker'] = futures_only['ticker'].apply(DataProcessor.format_ticker_btn) | |
| if 'ticker' in spot_only.columns: | |
| spot_only['ticker'] = spot_only['ticker'].apply(DataProcessor.format_ticker_btn) | |
| 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'] | |
| tables_body = "" | |
| tables_body += DataProcessor._generate_table_html("Tokens in Both Futures & Spot Markets", merged, ORIGINAL_MATCHED_HEADERS, merged_cols) | |
| tables_body += DataProcessor._generate_table_html("Remaining Futures-Only Tokens", futures_only, ORIGINAL_FUTURES_HEADERS, futures_cols) | |
| tables_body += 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_footer = """ | |
| <div style="margin-top: 30px; padding: 15px; background: var(--report-guide); border-radius: 8px;"> | |
| <h2 style="color: var(--report-head); border-bottom: 2px solid var(--report-accent); padding-bottom: 5px; margin-top: 0;">Guide to Reading the Cross‑Market Report</h2> | |
| <p>For a comprehensive guide on how to interpret the cross‑market report, please visit: <a href="https://www.quantvat.name.ng/2026/08/reading-quantvat-cross-market-report.html" target="_blank">here</a></p> | |
| </div> | |
| """ | |
| template_text = CROSS_REPORT_TEMPLATE.read_text(encoding="utf-8") | |
| final_document = Template(template_text).substitute( | |
| tokens_css=SHARED_TOKENS_CSS.read_text(encoding="utf-8"), | |
| current_time=current_time, | |
| tables_body=tables_body, | |
| cheat_sheet_footer=cheat_sheet_footer, | |
| ) | |
| return final_document | |
| def adv_analysis_v5(user_keys, user_id) -> None: | |
| """Main execution flow for Advanced Analysis.""" | |
| print(" Scanning for files to run cross-market analysis") | |
| update_progress(user_id, 10, "Locating Spot and Futures files...", "active") | |
| 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.") | |
| 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" Retrieved Futures CSV: {futures_file.name}") | |
| except Exception as e: | |
| print(f" Futures CSV Error: {e}") | |
| futures_df = pd.DataFrame() | |
| if not futures_df.empty: | |
| for col in ('market_cap', 'volume'): | |
| if col in futures_df.columns: | |
| futures_df[col] = futures_df[col].apply(DataProcessor.format_futures_money) | |
| 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) | |
| MIN_F_MC = parse_mc_or_default(settings.get('min_f_mc'), 0.0) | |
| MAX_F_MC = parse_mc_or_default(settings.get('max_f_mc'), float('inf')) | |
| 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)} futures tokens out of {before_count} with VTMR filter applied") | |
| if not futures_df.empty and 'market_cap' in futures_df.columns: | |
| before_count = len(futures_df) | |
| mc_numeric = futures_df['market_cap'].apply(parse_mc_or_none) | |
| # Rows with missing/unparseable market cap pass through unfiltered (VTMR filter already vetted them) | |
| in_range = mc_numeric.isna() | ((mc_numeric >= MIN_F_MC) & (mc_numeric <= MAX_F_MC)) | |
| futures_df = futures_df[in_range] | |
| print(f" Then extracted {len(futures_df)} futures tokens out of {before_count} with MC filter applied") | |
| 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.") | |
| update_progress(user_id, 85, "Saving report...", "active") | |
| timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") | |
| html_file = get_user_temp_dir(user_id) / f"cross-market-analysis-{timestamp}.html" | |
| try: | |
| html_file.write_text(html_content, encoding="utf-8") | |
| except Exception as e: | |
| print(f" Report save failed: {e}") | |
| raise RuntimeError(f"Could not save the report: {e}") | |
| print(f" Report saved as: {html_file.name}") | |
| cleanup_after_analysis(spot_file, futures_file, keep_spot=True) | |
| clear_pending_file(user_id, "futures") | |
| print(" Analysis completed!") |