Spaces:
Running
Running
File size: 14,798 Bytes
d96399a c554446 2440232 d96399a b76820b b0da76f 346305b b76820b c87b623 84574af 346305b b0da76f 346305b cade15b 346305b 139cb64 b0da76f 139cb64 346305b b76820b c8194fa ef860b4 d2804b9 ef860b4 46085a5 c8194fa ef860b4 8b68f73 c8194fa 346305b cade15b 346305b cade15b 346305b 139cb64 346305b 15356ea 346305b cade15b 51a4f9a 346305b b76820b 346305b 283629d a9cfd25 7957d12 ef860b4 7957d12 346305b 139cb64 73a257a 346305b 139cb64 346305b b34c551 346305b c8194fa 346305b c8194fa 346305b c8194fa 346305b 4348a44 e602f8c 346305b a9cfd25 ef860b4 a9cfd25 ef860b4 a9cfd25 ef860b4 a9cfd25 ef860b4 346305b ef860b4 346305b c554446 b0da76f 3db4d87 e84f453 346305b ef860b4 b0da76f ef860b4 346305b be1a45a 346305b 0c45b0c e52590f 346305b c87b623 346305b b76820b 346305b e52590f c49c811 ba2076b 0c45b0c ba2076b b34c551 46085a5 b34c551 8b68f73 b34c551 0c45b0c 11f78d9 8b68f73 11f78d9 7abf01e 346305b e52590f 346305b c49c811 346305b c49c811 c554446 d2391fb c554446 d2391fb c554446 0c45b0c a6dd1eb c49c811 46085a5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | 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:
@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'<span class="{css_class}">{sign}{oi_change*100:.0f}%</span> {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'<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."""
@staticmethod
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>'
@staticmethod
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}"
@staticmethod
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
@staticmethod
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()
@staticmethod
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>'
@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']
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!") |