Denisijcu commited on
Commit
2a0f014
·
verified ·
1 Parent(s): ae93a4e

Upload 19 files

Browse files
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # --- Entorno de Python ---
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+ .env
7
+ .venv
8
+ env/
9
+ venv/
10
+ ENV/
11
+
12
+ # --- Configuración del Búnker (Vertex) ---
13
+ # Muy importante: No subir las credenciales de los usuarios
14
+ app/settings.json
15
+ *.log
16
+
17
+ # --- Docker ---
18
+ # Ignoramos archivos de sistema de Docker si los hubiera localmente
19
+ .docker/
20
+ # Si usas bases de datos locales para n8n
21
+ n8n_data/
22
+
23
+ # --- IDEs y Sistema ---
24
+ .vscode/
25
+ .idea/
26
+ .DS_Store
27
+ Thumbs.db
28
+
29
+ # --- Distribución ---
30
+ dist/
31
+ build/
32
+ *.egg-info/
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Usamos una imagen ligera de Python
2
+ FROM python:3.10-slim
3
+
4
+ # Evita archivos .pyc y permite ver logs en tiempo real
5
+ ENV PYTHONDONTWRITEBYTECODE 1
6
+ ENV PYTHONUNBUFFERED 1
7
+
8
+ # Directorio de trabajo
9
+ WORKDIR /app
10
+
11
+ # Instalamos dependencias del sistema
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ build-essential \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Instalamos dependencias de Python
17
+ COPY requirements.txt .
18
+ RUN pip install --no-cache-dir -r requirements.txt
19
+
20
+ # Copiamos todo el código
21
+ COPY . .
22
+
23
+ # Creamos la carpeta app si no existe para asegurar las rutas
24
+ RUN mkdir -p /app/app
25
+
26
+ # Otorgamos permisos para que Hugging Face pueda escribir el settings.json si es necesario
27
+ RUN chmod -R 777 /app
28
+
29
+ # Exponemos el puerto que Hugging Face exige por defecto
30
+ EXPOSE 7860
31
+
32
+ # Comando para arrancar el Backend en el puerto 7860
33
+ # Nota: Si quieres arrancar Dashboard y Backend juntos, usaremos un script de inicio
34
+ CMD ["python", "app/main.py"]
README.md CHANGED
@@ -1,12 +0,0 @@
1
- ---
2
- title: Vertex Risk Engine
3
- emoji: 🏃
4
- colorFrom: gray
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: Vertex-Risk-Engine
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
app/__init__.py ADDED
File without changes
app/api/__init__.py ADDED
File without changes
app/api/routes.py ADDED
File without changes
app/dashboard.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import plotly.graph_objects as go
4
+ import plotly.express as px
5
+ import pandas as pd
6
+ from fpdf import FPDF
7
+ import json
8
+ import os
9
+
10
+ # --- CONFIGURACIÓN DE RUTAS Y RED ---
11
+ BACKEND_URL = os.getenv("BACKEND_URL", "http://vertex-backend:8010")
12
+ WATCHLIST_FILE = "app/watchlist.json"
13
+
14
+ def load_watchlist():
15
+ try:
16
+ if os.path.exists(WATCHLIST_FILE):
17
+ with open(WATCHLIST_FILE, "r") as f:
18
+ return json.load(f)
19
+ except Exception: pass
20
+ return ["INTC", "TSLA", "AAPL", "SAVE"]
21
+
22
+ def save_watchlist(watchlist):
23
+ os.makedirs(os.path.dirname(WATCHLIST_FILE), exist_ok=True)
24
+ with open(WATCHLIST_FILE, "w") as f:
25
+ json.dump(watchlist, f)
26
+
27
+ # --- MOTOR DE REPORTES PDF ---
28
+ class VertexReport(FPDF):
29
+ def header(self):
30
+ self.set_font('Arial', 'B', 15)
31
+ self.cell(0, 10, 'VERTEX CODERS LLC - AUDIT REPORT', 0, 1, 'C')
32
+ self.ln(10)
33
+
34
+ def generate_pdf(data):
35
+ pdf = VertexReport()
36
+ pdf.set_auto_page_break(auto=True, margin=15)
37
+ pdf.add_page()
38
+ pdf.set_font("Arial", 'B', 14)
39
+ pdf.set_fill_color(151, 231, 225)
40
+ pdf.cell(0, 12, f"AUDIT REPORT: {data.get('ticker', 'N/A')}", 1, 1, 'C', fill=True)
41
+ pdf.ln(5)
42
+ pdf.set_font("Arial", 'B', 12)
43
+ pdf.cell(0, 10, f"FINAL STATUS: {data.get('status', 'UNKNOWN')}", 0, 1)
44
+ semantic = data.get("semantic_analysis", {})
45
+ msg = str(semantic.get('summary', data.get('msg', 'No data'))).encode('latin-1', 'replace').decode('latin-1')
46
+ pdf.ln(5)
47
+ pdf.set_font("Arial", 'B', 11)
48
+ pdf.cell(0, 10, "1. EXECUTIVE VERDICT", 0, 1)
49
+ pdf.set_font("Arial", size=10)
50
+ pdf.multi_cell(0, 8, msg)
51
+ pdf.ln(5)
52
+ pdf.set_font("Arial", 'B', 11)
53
+ pdf.cell(0, 10, "2. FINANCIAL METRICS", 0, 1)
54
+ z_score = data.get("numeric_analysis", {}).get("altman_z") or data.get("z_score", 0)
55
+ pdf.cell(0, 8, f"- Altman Z-Score: {float(z_score):.2f}", 0, 1)
56
+ return pdf.output(dest='S')
57
+
58
+ # --- CONFIGURACIÓN DE LA UI ---
59
+ st.set_page_config(page_title="Vertex Risk Terminal | Némesis", page_icon="🛡️", layout="wide")
60
+
61
+ if "watchlist" not in st.session_state:
62
+ st.session_state.watchlist = load_watchlist()
63
+
64
+ # --- SIDEBAR ---
65
+ st.sidebar.title("🏢 Stock Watchlist")
66
+ new_ticker = st.sidebar.text_input("Add Ticker").upper()
67
+
68
+ if st.sidebar.button("➕ Add"):
69
+ if new_ticker and new_ticker not in st.session_state.watchlist:
70
+ st.session_state.watchlist.append(new_ticker)
71
+ save_watchlist(st.session_state.watchlist)
72
+ st.sidebar.success(f"✅ {new_ticker} saved!")
73
+ st.rerun()
74
+
75
+ selected_ticker = st.sidebar.selectbox("Analyze Company", st.session_state.watchlist)
76
+
77
+ st.sidebar.divider()
78
+ st.sidebar.subheader("📡 Bunker Status")
79
+ def check_health(url):
80
+ try: return "🟢 ONLINE" if requests.get(url, timeout=2).status_code == 200 else "🔴 OFFLINE"
81
+ except: return "🔴 OFFLINE"
82
+
83
+ st.sidebar.write(f"Backend Engine: {check_health(BACKEND_URL + '/docs')}")
84
+
85
+ # --- CUERPO PRINCIPAL ---
86
+ st.title("🛡️ Vertex Risk Terminal")
87
+ st.caption("Quantum Risk Analysis Platform | Enterprise Edition")
88
+
89
+ # REPARACIÓN DE TABS: Declaración única de las 4 pestañas
90
+ tab1, tab2, tab3, tab4, tab5 = st.tabs(["📈 Stock Audit", "🔗 Web3 Audit", "🔍 Auditoría Individual", "📊 Comparativa Vertex", "⚙️ Settings"])
91
+
92
+ with tab1:
93
+ if st.button("🚀 RUN FULL STOCK AUDIT", type="primary", use_container_width=True):
94
+ with st.spinner(f"Auditing {selected_ticker} through Némesis Engine..."):
95
+ try:
96
+ r = requests.get(f"{BACKEND_URL}/audit/{selected_ticker}", timeout=25)
97
+ r.raise_for_status()
98
+ st.session_state.last_audit = r.json()
99
+ st.rerun()
100
+ except Exception as e:
101
+ st.error(f"🔌 Connection Failure: {e}")
102
+
103
+ if "last_audit" in st.session_state:
104
+ res = st.session_state.last_audit
105
+ st.divider()
106
+ col_l, col_r = st.columns(2)
107
+ with col_l:
108
+ st.metric("FINAL STATUS", res.get("status", "UNKNOWN"))
109
+ z_val = res.get("numeric_analysis", {}).get("altman_z") or res.get("z_score", 0)
110
+ fig = go.Figure(go.Indicator(
111
+ mode="gauge+number",
112
+ value=float(z_val),
113
+ gauge={'axis': {'range': [0, 5]},
114
+ 'steps': [{'range': [0, 1.1], 'color': "lightcoral"},
115
+ {'range': [1.1, 2.9], 'color': "lightyellow"},
116
+ {'range': [2.9, 5], 'color': "lightgreen"}]}))
117
+ fig.update_layout(height=300)
118
+ st.plotly_chart(fig, use_container_width=True)
119
+ with col_r:
120
+ st.subheader("🧠 Semantic Analysis")
121
+ sem = res.get("semantic_analysis", {})
122
+ st.info(sem.get("summary", res.get("msg", "No additional data.")))
123
+ st.download_button("📥 DOWNLOAD PDF REPORT", generate_pdf(res), f"Vertex_{selected_ticker}.pdf", "application/pdf", use_container_width=True)
124
+
125
+ with tab2:
126
+ st.subheader("🔗 Web3 Smart Contract Scanner")
127
+ contract = st.text_input("Dirección del Token (0x...)")
128
+ if st.button("🔍 SCAN WEB3 ASSET", use_container_width=True):
129
+ if contract:
130
+ with st.spinner("Escaneando seguridad..."):
131
+ try:
132
+ r = requests.get(f"{BACKEND_URL}/audit_contract/{contract}", timeout=120)
133
+ res_w3 = r.json()
134
+ st.divider()
135
+ status_w3 = res_w3.get("status", "UNKNOWN")
136
+ if status_w3 == "SAFE": st.success(f"✅ STATUS: {status_w3}")
137
+ elif status_w3 == "DANGER": st.error(f"🚨 STATUS: {status_w3}")
138
+
139
+ vulns = res_w3.get("vulnerabilities", [])
140
+ if vulns:
141
+ for v in vulns: st.error(f"**{v['description']}**")
142
+ with st.expander("Ver Código Fuente"):
143
+ st.code(res_w3.get("source_preview", ""), language='solidity')
144
+ except Exception as e: st.error(f"Error: {e}")
145
+
146
+ with tab3:
147
+ st.subheader("🔍 Auditoría Individual")
148
+ st.write(f"Vigilancia activa sobre: **{selected_ticker}**")
149
+ st.info("Este módulo utiliza análisis heurístico para reportes rápidos.")
150
+
151
+ with tab4:
152
+ st.subheader("📊 Comparativa de Salud Financiera")
153
+ comparison_list = st.multiselect("Compañías:", options=st.session_state.watchlist, default=st.session_state.watchlist[:3])
154
+ if st.button("📊 GENERAR COMPARATIVA", use_container_width=True):
155
+ comp_data = []
156
+ with st.spinner("Calculando ranking..."):
157
+ for t in comparison_list:
158
+ try:
159
+ r = requests.get(f"{BACKEND_URL}/audit/{t}", timeout=10)
160
+ if r.status_code == 200:
161
+ res = r.json()
162
+ z = res.get("numeric_analysis", {}).get("altman_z") or res.get("z_score", 0)
163
+ comp_data.append({"Ticker": t, "Z-Score": float(z)})
164
+ except: continue
165
+ if comp_data:
166
+ df = pd.DataFrame(comp_data)
167
+ fig_bar = px.bar(df, x='Ticker', y='Z-Score', color='Z-Score', color_continuous_scale=['red', 'yellow', 'green'], range_y=[0, 5])
168
+ fig_bar.add_hline(y=1.1, line_dash="dash", line_color="red")
169
+ fig_bar.add_hline(y=2.9, line_dash="dash", line_color="green")
170
+ st.plotly_chart(fig_bar, use_container_width=True)
171
+
172
+ with tab5:
173
+ st.header("⚙️ Configuración del Sistema")
174
+ st.info("Configura las credenciales de Telegram para que Némesis te envíe alertas automáticas.")
175
+
176
+ # Cargar configuraciones actuales si existen
177
+ if "settings" not in st.session_state:
178
+ st.session_state.settings = {"bot_token": "", "chat_id": ""}
179
+
180
+ with st.form("settings_form"):
181
+ bot_token = st.text_input("Telegram Bot Token", value=st.session_state.settings["bot_token"], type="password", help="El token que te dio BotFather")
182
+ chat_id = st.text_input("Telegram Chat ID", value=st.session_state.settings["chat_id"], help="Tu ID de usuario o el del grupo")
183
+
184
+ if st.form_submit_button("💾 Guardar Configuración"):
185
+ st.session_state.settings = {"bot_token": bot_token, "chat_id": chat_id}
186
+ # Aquí guardaríamos en un archivo que n8n vigile
187
+ with open("app/settings.json", "w") as f:
188
+ json.dump(st.session_state.settings, f)
189
+ st.success("✅ Configuración guardada. n8n ahora usará estas credenciales.")
app/engine/__init__.py ADDED
File without changes
app/engine/risk_engine.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yfinance as yf
2
+ import pandas as pd
3
+ from fastapi import FastAPI
4
+ import uvicorn
5
+
6
+ app = FastAPI(title="Vertex Coders Risk API")
7
+
8
+ class VertexRiskEngine:
9
+ def __init__(self, ticker):
10
+ self.ticker_str = ticker
11
+ self.company = yf.Ticker(ticker)
12
+ self.scores = {}
13
+
14
+ def get_value(self, df, possible_keys):
15
+ """Busca un valor en el DataFrame intentando varias llaves posibles."""
16
+ for key in possible_keys:
17
+ if key in df.index:
18
+ return df.loc[key]
19
+ raise ValueError(f"Ninguna de las llaves {possible_keys} encontrada.")
20
+
21
+ def run_audit(self):
22
+ try:
23
+ balance = self.company.balance_sheet
24
+ financials = self.company.financials
25
+
26
+ # --- ALTMAN Z-SCORE ---
27
+ # Usamos iloc[0] para el año más reciente
28
+ working_capital = self.get_value(balance, ['Working Capital']).iloc[0]
29
+ total_assets = self.get_value(balance, ['Total Assets']).iloc[0]
30
+ retained_earnings = self.get_value(balance, ['Retained Earnings']).iloc[0]
31
+ ebit = self.get_value(financials, ['EBIT']).iloc[0]
32
+ total_liabilities = self.get_value(balance, ['Total Liabilities Net Minority Interest', 'Total Liabilities']).iloc[0]
33
+ sales = self.get_value(financials, ['Total Revenue']).iloc[0]
34
+ market_cap = self.company.info.get('marketCap', 1)
35
+
36
+ z = (1.2 * (working_capital/total_assets) +
37
+ 1.4 * (retained_earnings/total_assets) +
38
+ 3.3 * (ebit/total_assets) +
39
+ 0.6 * (market_cap/total_liabilities) +
40
+ 1.0 * (sales/total_assets))
41
+
42
+ self.scores['altman_z'] = round(z, 2)
43
+
44
+ # --- BENEISH M-SCORE (DSRI) ---
45
+ # Comparamos Año T (iloc[0]) vs Año T-1 (iloc[1])
46
+ sales_t = sales
47
+ sales_t1 = self.get_value(financials, ['Total Revenue']).iloc[1]
48
+
49
+ receivables_keys = ['Net Receivables', 'Accounts Receivable', 'Receivables']
50
+ rec_t = self.get_value(balance, receivables_keys).iloc[0]
51
+ rec_t1 = self.get_value(balance, receivables_keys).iloc[1]
52
+
53
+ dsri = (rec_t / sales_t) / (rec_t1 / sales_t1)
54
+ self.scores['m_score_dsri'] = round(dsri, 2)
55
+
56
+ return self.scores
57
+ except Exception as e:
58
+ return {"error": str(e)}
59
+
60
+ @app.get("/audit/{ticker}")
61
+ def audit_company(ticker: str):
62
+ engine = VertexRiskEngine(ticker.upper())
63
+ result = engine.run_audit()
64
+
65
+ # 1. Validar si el motor devolvió un error de ejecución
66
+ if "error" in result:
67
+ return {
68
+ "ticker": ticker.upper(),
69
+ "status": "ERROR_TECNICO",
70
+ "analysis": None,
71
+ "msg": f"No se pudo completar la auditoría: {result['error']}"
72
+ }
73
+
74
+ # 2. Extraer valores con seguridad (Type Casting)
75
+ # Forzamos a float para evitar errores de comparación con tipos desconocidos
76
+ try:
77
+ z_score = float(result.get('altman_z', 0))
78
+ m_dsri = float(result.get('m_score_dsri', 0))
79
+ except (ValueError, TypeError):
80
+ z_score = 0.0
81
+ m_dsri = 0.0
82
+
83
+ # 3. Lógica de Semáforo Robusta
84
+ # Prioridad: Rojo (Peligro inminente o posible fraude)
85
+ if z_score < 1.8 or m_dsri > 1.4:
86
+ status = "ROJO"
87
+ msg = "Riesgo crítico detectado: Posible quiebra o manipulación."
88
+ # Zona Gris
89
+ elif 1.8 <= z_score < 3.0:
90
+ status = "AMARILLO"
91
+ msg = "Empresa en zona gris. Requiere supervisión manual."
92
+ # Zona Segura
93
+ else:
94
+ status = "VERDE"
95
+ msg = "Fundamentos financieros sólidos según modelos automáticos."
96
+
97
+ return {
98
+ "ticker": ticker.upper(),
99
+ "status": status,
100
+ "analysis": result,
101
+ "msg": msg,
102
+ "engine_version": "1.0-Némesis"
103
+ }
104
+ if __name__ == "__main__":
105
+ uvicorn.run(app, host="0.0.0.0", port=8010)
app/engine/semantic.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ class VertexSemanticAgent:
8
+ def __init__(self, ticker):
9
+ self.ticker = ticker.upper()
10
+ self.api_key = os.getenv("FMP_API_KEY")
11
+ self.base_url = "https://financialmodelingprep.com/api/v4/sec_filing_segment"
12
+
13
+ def get_sec_risk_factors(self):
14
+ """
15
+ Extrae la sección 'Item 1A' (Risk Factors) del reporte más reciente.
16
+ """
17
+ params = {
18
+ "symbol": self.ticker,
19
+ "type": "10-K",
20
+ "segment": "item1a",
21
+ "apikey": self.api_key
22
+ }
23
+
24
+ try:
25
+ response = requests.get(self.base_url, params=params)
26
+ data = response.json()
27
+
28
+ if data and isinstance(data, list):
29
+ # Retornamos el texto del primer (más reciente) reporte encontrado
30
+ return data[0].get("content", "No se encontró contenido en el Item 1A.")
31
+ return "No se hallaron filings 10-K para este ticker."
32
+
33
+ except Exception as e:
34
+ return f"Error conectando con FMP: {str(e)}"
35
+
36
+ def judge_risks(self, text):
37
+ """
38
+ Aquí es donde entraría el LLM. Por ahora, hacemos un análisis de
39
+ fuerza bruta buscando palabras clave de alta peligrosidad.
40
+ """
41
+ red_flags = ["litigation", "breach", "cybersecurity", "bankruptcy", "insolvency", "investigation"]
42
+ found = [word for word in red_flags if word in text.lower()]
43
+
44
+ score = len(found)
45
+ return {
46
+ "semantic_score": score, # A más alto, más riesgo
47
+ "detected_keywords": found,
48
+ "summary": f"Se detectaron {score} factores de riesgo críticos en el texto."
49
+ }
app/engine/web3_engine.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import json
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ class VertexWeb3Engine:
9
+ def __init__(self, contract_address, network="ethereum"):
10
+ self.address = contract_address.strip()
11
+ self.network = network
12
+ self.api_key = os.getenv("ETHERSCAN_API_KEY", "").strip()
13
+
14
+ # Configuración V2 (Obligatoria en 2026)
15
+ self.networks = {
16
+ "ethereum": {"url": "https://api.etherscan.io/v2/api", "id": "1"},
17
+ "bsc": {"url": "https://api.bscscan.com/v2/api", "id": "56"},
18
+ "polygon": {"url": "https://api.polygonscan.com/v2/api", "id": "137"}
19
+ }
20
+
21
+ config = self.networks.get(network, self.networks["ethereum"])
22
+ self.base_url = config["url"]
23
+ self.chain_id = config["id"]
24
+
25
+ def get_contract_source(self):
26
+ """Descarga el código fuente usando Etherscan API V2"""
27
+ params = {
28
+ "chainid": self.chain_id, # Requerido para V2
29
+ "module": "contract",
30
+ "action": "getsourcecode",
31
+ "address": self.address,
32
+ "apikey": self.api_key
33
+ }
34
+
35
+ try:
36
+ response = requests.get(self.base_url, params=params, timeout=15)
37
+ data = response.json()
38
+
39
+ # Verificamos status de Etherscan
40
+ if data.get("status") == "1" and data.get("result"):
41
+ result = data["result"][0]
42
+ raw_source = result.get("SourceCode", "")
43
+
44
+ if not raw_source:
45
+ return {"success": False, "error": "Contract not verified"}
46
+
47
+ # Parseo corregido para evitar el error de 'slice'
48
+ source_code = self._parse_source_code(raw_source)
49
+
50
+ return {
51
+ "success": True,
52
+ "source_code": source_code,
53
+ "contract_info": {
54
+ "name": result.get("ContractName", "Unknown"),
55
+ "compiler": result.get("CompilerVersion", "Unknown")
56
+ }
57
+ }
58
+ return {"success": False, "error": data.get("result", "API Error")}
59
+ except Exception as e:
60
+ return {"success": False, "error": str(e)}
61
+
62
+ def _parse_source_code(self, raw_source):
63
+ """Maneja formatos plano y multi-archivo (JSON)"""
64
+ # Verificamos que sea un string antes de recortar
65
+ if not isinstance(raw_source, str) or not raw_source.startswith("{"):
66
+ return raw_source
67
+
68
+ try:
69
+ # FIX: Aseguramos que el recorte se haga solo si es un string de verdad
70
+ if raw_source.startswith("{{") and raw_source.endswith("}}"):
71
+ clean_json = raw_source[1:-1] # Quitamos solo una pareja de llaves
72
+ else:
73
+ clean_json = raw_source
74
+
75
+ parsed = json.loads(clean_json)
76
+
77
+ if "sources" in parsed:
78
+ all_code = []
79
+ for filename, file_data in parsed["sources"].items():
80
+ if "content" in file_data:
81
+ all_code.append(f"// FILE: {filename}\n{file_data['content']}")
82
+ return "\n\n".join(all_code)
83
+ return raw_source
84
+ except:
85
+ return raw_source
86
+
87
+ def scan_basic_vulnerabilities(self, source_code):
88
+ """Análisis de patrones de riesgo"""
89
+ if not source_code or not isinstance(source_code, str):
90
+ return []
91
+
92
+ red_flags = []
93
+ patterns = {
94
+ "selfdestruct": "CRITICAL: Contract can be destroyed.",
95
+ "mint(": "HIGH: Infinite minting risk.",
96
+ "delegatecall": "HIGH: Proxy execution risk."
97
+ }
98
+
99
+ for pattern, risk in patterns.items():
100
+ if pattern in source_code.lower():
101
+ red_flags.append({"pattern": pattern, "description": risk})
102
+ return red_flags
app/main.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from fastapi import FastAPI
4
+ import uvicorn
5
+ from engine.risk_engine import VertexRiskEngine
6
+ from engine.semantic import VertexSemanticAgent
7
+ from engine.web3_engine import VertexWeb3Engine
8
+
9
+ app = FastAPI()
10
+
11
+ # Archivo de persistencia de la watchlist
12
+ WATCHLIST_FILE = "app/watchlist.json"
13
+
14
+ @app.get("/batch_audit")
15
+ def run_batch_audit():
16
+ """Audita toda la watchlist y devuelve alertas para n8n."""
17
+ if not os.path.exists(WATCHLIST_FILE):
18
+ return {"error": "Watchlist file not found", "alerts": []}
19
+
20
+ try:
21
+ with open(WATCHLIST_FILE, "r") as f:
22
+ watchlist = json.load(f)
23
+ except Exception as e:
24
+ return {"error": f"Failed to read watchlist: {e}", "alerts": []}
25
+
26
+ results = []
27
+ for ticker in watchlist:
28
+ try:
29
+ # Reutilizamos la lógica del motor Némesis
30
+ engine = VertexRiskEngine(ticker.upper())
31
+ num_res = engine.run_audit()
32
+
33
+ sem_agent = VertexSemanticAgent(ticker.upper())
34
+ risk_text = sem_agent.get_sec_risk_factors()
35
+ sem_res = sem_agent.judge_risks(risk_text)
36
+
37
+ # Blindaje de tipos para evitar errores de comparación
38
+ z_score = float(num_res.get('altman_z', 0.0))
39
+ m_dsri = float(num_res.get('m_score_dsri', 0.0))
40
+ s_score = float(sem_res.get('semantic_score', 0.0))
41
+
42
+ # Lógica de Semáforo de Riesgo
43
+ if z_score < 1.8 or m_dsri > 1.4 or s_score > 3:
44
+ status = "RED"
45
+ elif z_score < 3.0:
46
+ status = "YELLOW"
47
+ else:
48
+ status = "GREEN"
49
+
50
+ results.append({
51
+ "ticker": ticker,
52
+ "status": status,
53
+ "z_score": z_score,
54
+ "summary": sem_res.get("summary", ""),
55
+ "alert": True if status == "RED" else False
56
+ })
57
+ except Exception as ticker_error:
58
+ results.append({"ticker": ticker, "status": "ERROR", "msg": str(ticker_error)})
59
+
60
+ critical_alerts = [r for r in results if r.get("status") in ["RED", "YELLOW"]]
61
+ return {
62
+ "total_analyzed": len(watchlist),
63
+ "critical_count": len(critical_alerts),
64
+ "alerts": critical_alerts,
65
+ "full_results": results
66
+ }
67
+
68
+ @app.get("/audit/{ticker}")
69
+ def audit_company(ticker: str):
70
+ """Auditoría individual para el tab de Stock Audit."""
71
+ try:
72
+ engine = VertexRiskEngine(ticker.upper())
73
+ num_res = engine.run_audit()
74
+
75
+ sem_agent = VertexSemanticAgent(ticker.upper())
76
+ risk_text = sem_agent.get_sec_risk_factors()
77
+ sem_res = sem_agent.judge_risks(risk_text)
78
+
79
+ # Blindaje de tipos
80
+ z_score = float(num_res.get('altman_z', 0.0))
81
+ m_dsri = float(num_res.get('m_score_dsri', 0.0))
82
+ s_score = float(sem_res.get('semantic_score', 0.0))
83
+
84
+ if z_score < 1.8 or m_dsri > 1.4 or s_score > 3:
85
+ status = "RED"
86
+ msg = "CRITICAL RISK: Red flags detected."
87
+ elif z_score < 3.0:
88
+ status = "YELLOW"
89
+ msg = "CAUTION: Monitor closely."
90
+ else:
91
+ status = "GREEN"
92
+ msg = "SAFE: Solid fundamentals."
93
+
94
+ return {
95
+ "ticker": ticker.upper(),
96
+ "status": status,
97
+ "numeric_analysis": {"altman_z": z_score, "m_score_dsri": m_dsri},
98
+ "semantic_analysis": sem_res,
99
+ "msg": msg
100
+ }
101
+ except Exception as e:
102
+ return {"status": "ERROR", "msg": str(e)}
103
+
104
+ @app.get("/audit_contract/{address}")
105
+ def audit_smart_contract(address: str):
106
+ """Auditoría de Web3 usando el nuevo motor V2."""
107
+ try:
108
+ web3_engine = VertexWeb3Engine(address)
109
+ audit_res = web3_engine.get_contract_source()
110
+
111
+ if not audit_res["success"]:
112
+ return {"status": "ERROR", "msg": audit_res["error"]}
113
+
114
+ source = audit_res["source_code"]
115
+ vulnerabilities = web3_engine.scan_basic_vulnerabilities(source)
116
+
117
+ return {
118
+ "address": address,
119
+ "status": "DANGER" if len(vulnerabilities) > 0 else "SAFE",
120
+ "vulnerabilities": vulnerabilities,
121
+ "source_preview": source[:500] + "..." # Ahora source es string y el slice no falla
122
+ }
123
+ except Exception as e:
124
+ return {"status": "ERROR", "msg": str(e)}
125
+
126
+ @app.get("/get_settings")
127
+ async def get_settings():
128
+ try:
129
+ path = "app/settings.json"
130
+ if os.path.exists(path):
131
+ with open(path, "r") as f:
132
+ content = f.read().strip()
133
+ if not content:
134
+ return {"bot_token": "", "chat_id": "", "status": "empty_file"}
135
+ return json.loads(content)
136
+ return {"bot_token": "", "chat_id": "", "status": "not_found"}
137
+ except Exception as e:
138
+ # Esto captura el error "Expecting value" y devuelve un JSON válido para n8n
139
+ return {"bot_token": "", "chat_id": "", "error": f"JSON Error: {str(e)}"}
140
+
141
+ if __name__ == "__main__":
142
+ # Hugging Face SIEMPRE usa el puerto 7860 internamente
143
+ port = int(os.environ.get("PORT", 7860))
144
+ # Importante: host="0.0.0.0" para que sea accesible desde fuera del contenedor
145
+ uvicorn.run(app, host="0.0.0.0", port=port)
app/utils/__init__.py ADDED
File without changes
app/utils/data_helper.py ADDED
File without changes
app/watchlist.json ADDED
@@ -0,0 +1 @@
 
 
1
+ ["INTC", "TSLA", "AAPL", "DELL", "NVDA"]
docker-compose.yml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ # El Cerebro - FastAPI
3
+ vertex-backend:
4
+ build: .
5
+ container_name: vertex-backend
6
+ ports:
7
+ - "8010:8010"
8
+ volumes:
9
+ - .:/app # Mapea la carpeta actual al /app del contenedor
10
+ restart: always
11
+
12
+ # El Dashboard - Streamlit
13
+ vertex-dashboard:
14
+ build: .
15
+ container_name: vertex-dashboard
16
+ command: streamlit run app/dashboard.py --server.port 8501 --server.address 0.0.0.0
17
+ ports:
18
+ - "8501:8501"
19
+ volumes:
20
+ - .:/app # Mapea la misma carpeta para que compartan el settings.json
21
+ depends_on:
22
+ - vertex-backend
23
+ restart: always
24
+
25
+ # El Vigilante - n8n
26
+ vertex-n8n:
27
+ image: n8nio/n8n:latest
28
+ container_name: vertex-n8n
29
+ ports:
30
+ - "5678:5678"
31
+ environment:
32
+ - N8N_TIMEZONE=America/New_York
33
+ volumes:
34
+ - n8n_data:/home/node/.n8n
35
+ - .:/app # Opcional: n8n también podrá leer archivos directamente si hace falta
36
+ restart: always
37
+
38
+ volumes:
39
+ n8n_data:
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pandas
4
+ yfinance
5
+ python-telegram-bot
6
+ requests
7
+ streamlit
8
+ numpy
9
+ tensorflow
10
+ keras
11
+ python-dotenv
12
+ matplotlib
13
+ plotly
14
+ scikit-learn
15
+ fpdf
test/test_engine.py ADDED
File without changes