File size: 13,888 Bytes
346305b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cade15b
346305b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cade15b
346305b
cade15b
346305b
cade15b
346305b
 
 
 
 
51a4f9a
346305b
 
 
 
 
 
 
 
 
 
51a4f9a
346305b
 
 
 
 
 
51a4f9a
346305b
cade15b
51a4f9a
346305b
 
 
 
 
51a4f9a
346305b
 
 
 
 
 
 
 
 
 
cade15b
346305b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cade15b
346305b
 
 
 
 
 
 
 
 
 
 
 
 
4348a44
 
346305b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51a4f9a
346305b
 
 
 
 
51a4f9a
346305b
 
 
 
 
cade15b
346305b
 
 
 
15ab37c
 
346305b
 
 
 
 
 
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
import re
import datetime
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
from .utils import now_str, convert_html_to_pdf, cleanup_after_analysis
from .futures_engine import PDFParser

# --- 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 FileScanner:
    """Locates the latest Spot and Futures data files in the USER directory."""
    @staticmethod
    def find_files(user_id) -> Tuple[Optional[Path], Optional[Path]]:
        spot_file: Optional[Path] = None
        futures_file: Optional[Path] = None
        
        user_dir = get_user_temp_dir(user_id)
        if not user_dir.exists():
            return None, None

        # Get today's date for filtering
        today = datetime.datetime.now().date()
        
        # Filter for today's files only
        today_files = []
        for f in user_dir.iterdir():
            if f.is_file():
                try:
                    file_time = datetime.datetime.fromtimestamp(f.stat().st_mtime)
                    if file_time.date() == today:  # Only use today's files
                        today_files.append(f)
                except Exception:
                    continue
        
        if not today_files:
            return None, None
            
        # Sort by modification time (newest first)
        files = sorted(today_files, key=lambda x: x.stat().st_mtime, reverse=True)

        for f in files:
            name = f.name.lower()
            if not futures_file and f.suffix == ".pdf" and "futures" in name:
                futures_file = f
            elif not spot_file and f.suffix in [".csv", ".html"] and "spot" in name:
                spot_file = f
            
            if spot_file and futures_file:
                break
                
        return spot_file, futures_file

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',           # <--- Ensures VTMR isn't blank
                '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'<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
        # escape=False is critical for rendering ticker links
        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
        
        if 'oiss' not in futures_df.columns:
            futures_df['oiss'] = "-"

        valid_futures = futures_df.copy()
        try:
            if 'vtmr' in valid_futures.columns:
                valid_futures = valid_futures[valid_futures['vtmr'] >= 0.50]
                valid_futures['vtmr_display'] = valid_futures['vtmr'].apply(lambda x: f"{x:.2f}x")
        except Exception as e:
            print(f"   Futures high-quality filtering 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[spot_only['sort_val'] >= 0.50].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 = """
            <div style="margin-top: 30px; padding: 15px; background: #ecf0f1; border-radius: 8px;">
                <h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; margin-top: 0;">OISS & Funding Cheat Sheet:</h2>
                <ul style="list-style-type: none; padding-left: 0; line-height: 1.6;">                
<li><strong>(1) Bullish Squeeze:</strong> Open Interest is positive and Funding Rate is negative, meaning new capital is flowing in while most are short. Shorts rush to buy back, triggering a sharp price surge. Bro, we are bullish.</li>
<li><strong>(2) Uptrend:</strong> Open Interest is positive and Funding Rate is positive, meaning the market is rising with broad participation. Capital keeps flowing, but the trend is costly (high fees). Solid uptrend, but watch for pauses.</li>
<li><strong>(3) Short Covering / Recovery:</strong> Open Interest is negative and Funding Rate is negative, meaning shorts are closing positions, causing a temporary rebound. No real buying pressure—trend may not last.</li>
<li><strong>(4) Flatline:</strong> Open Interest is unchanged and Funding Rate is positive, meaning the market is dead with no new capital. Minimal movement, trend paused, fees still accumulate.</li>
<li><strong>(5) Bearish Dump:</strong> Open Interest is negative and Funding Rate is positive, meaning longs are exiting aggressively or facing liquidation. Price drops sharply—strong selling pressure dominates.</li><br/>
<li><strong style="color:red;">NOTE:</strong> OISS stands for <strong>Open Interest Signal Score</strong> and FUNDING stands for <strong>Funding Rate</strong>.</li>
</ul>
<h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; margin-top: 0;">Why VTMR of All Sides Matter</h2>
                <ul style="list-style-type: none; padding-left: 0; line-height: 1.6;">  
  <li>  
    <strong>(1) The Divergence Signal (Spot vs. Futures):</strong>
    <ul style="list-style-type: disc; padding-left: 20px; line-height: 1.4;">
      <li><strong> Futures VTMR &gt; Spot VTMR (The Casino):</strong> If Futures volume is huge (e.g., 8x) but Spot is low, the price is being driven by leverage and speculation. This is fragile—expect violent "wicks" and liquidation hunts.</li>
      <li><strong>Spot VTMR &gt; Futures VTMR (The Bank):</strong> If Spot volume is leading, real money is buying to own the asset, not just gamble on it. This signals genuine accumulation and a healthier, more sustainable trend.</li>
    </ul>
  </li>  
  <li>  
    <strong>(2) The Heat Check:</strong>
    <ul style="list-style-type: disc; padding-left: 20px; line-height: 1.4;">
      <li>If VTMR is Over 1.0x: The token is trading its entire Market Cap in volume. It is hyper-active and volatile. But still that doesn't guarantee pump.</li>
    </ul>
  </li>  
</ul>
</ul>
                <h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; margin-top: 20px;">Remaining Spot Only Tokens</h2>
                <p>Remember those remaining spot only tokens because there is plenty opportunity there too. So, check them out. Don't fade on them.</p>
           <h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; margin-top: 20px;">Disclaimer</h2>
                <small>This analysis was generated by you using the <strong>QuantVAT</strong> by <strong>@heisbuba</strong>. 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.</small>
            </div>
        """
        html = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <title>Quantitative Crypto Volume-driven Data Analysis Report</title>
            <meta charset="UTF-8">
            <style>{ORIGINAL_HTML_STYLE}</style>
        </head>
        <body>
            <div class="header">
                <h1>Cross-Market Crypto Analysis Report</h1>
                <p>Using Both Spot & Futures Market Data</p>
                <p><small>Generated on: {current_time}</small></p>
            </div>
            {html_content}
              {cheat_sheet_pdf_footer}
            <div class="footer">
                <p>Generated by QuantVAT | By (@heisbuba)</p>
            </div>
        </body>
        </html>
        """
        return html

def crypto_analysis_v4(user_keys, user_id) -> None:
    """Main execution flow for Advanced Analysis."""
    print("   ADVANCED QUANT CRYPTO VOLUME ANALYSIS")
    print("   Scanning for Futures PDF and Spot CSV/HTML files")
    print("   " + "=" * 50)
    
    # Find Files
    spot_file, futures_file = FileScanner.find_files(user_id)
    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.")
    
    # Parse Files
    futures_df = PDFParser.extract(futures_file)
    spot_df = DataProcessor.load_spot(spot_file)
    
    html_content = DataProcessor.generate_html_report(futures_df, spot_df)
    if html_content:
        # Create PDF
        pdf_path = convert_html_to_pdf(html_content, user_id)
        
        if pdf_path:
            print(f"   PDF saved: {pdf_path}")
            print("   🧹 Cleaning up source files after analysis...")
            cleanup_after_analysis(spot_file, futures_file)
            print("   📊 Analysis completed! Source files cleaned up.")
        else:
            print("   PDF conversion failed! Check API Key")
    else:
        print("   No data to generate report")
    print("   Advanced Analysis completed!")