# kennis_streamlit.py import streamlit as st import numpy as np import pandas as pd from io import BytesIO import plotly.graph_objects as go # ------------------ Configuración de la página ------------------ st.set_page_config(page_title="Kennis FX Markets", layout="wide", initial_sidebar_state="expanded") st.markdown( """ """, unsafe_allow_html=True, ) # ------------------ Header / Hero ------------------ col1, col2 = st.columns([4,1]) with col1: st.markdown("
Kennis FX Markets
", unsafe_allow_html=True) st.markdown("
Institutional Bayesian Intelligence Engine
", unsafe_allow_html=True) st.markdown("
Detection and strategic allocation guidance. (Edición táctica — swing / corto plazo)
", unsafe_allow_html=True) with col2: st.markdown("
Kennis FX — Tactical edition
", unsafe_allow_html=True) st.markdown("---") # ------------------ Sidebar: Inputs & opciones ------------------ st.sidebar.header("Entradas (táctica / swing)") uploaded = st.sidebar.file_uploader( "Sube CSV histórico (opcional). Columnas requeridas: date,cot_long,cot_short,fed_prob,retail_pct,target_up", type=["csv"] ) st.sidebar.markdown("O ingresa la fila live abajo:") par = st.sidebar.selectbox( "Par de divisas", [ # Majors "USD/JPY", "EUR/USD", "GBP/USD", "AUD/USD", "USD/CHF", "USD/CAD", "NZD/USD", "EUR/GBP", "EUR/JPY", "GBP/JPY", # Principales exóticas / emergentes y crosses relevantes "USD/SGD", "USD/MXN", "USD/BRL", "USD/ZAR", "USD/TRY", "USD/HKD", "EUR/TRY", "GBP/TRY" ], index=0 ) cot_long = st.sidebar.number_input("COT No-Commercial — Long", value=13662, step=1) cot_short = st.sidebar.number_input("COT No-Commercial — Short", value=13546, step=1) fed_prob = st.sidebar.number_input("FedWatch Prob (%) para bin actual", min_value=0.0, max_value=100.0, value=96.0, step=0.1) retail_pct = st.sidebar.number_input("Retail % Buy (0-100)", min_value=0.0, max_value=100.0, value=47.0, step=0.1) st.sidebar.markdown("---") st.sidebar.header("Modelo y normalización") s_cot = st.sidebar.number_input("S_cot (escala para COT)", value=50000, step=1000) prior_var = st.sidebar.number_input("Varianza prior gaussiano", value=0.5, step=0.1) retrain = st.sidebar.button("Reentrenar (si subes CSV)") # ------------------ Utilidades matemáticas ------------------ def sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) def fit_bayesian_logit(X, y, prior_var=0.5, maxiter=200, tol=1e-8): n, p = X.shape beta = np.zeros(p) prior_prec = np.eye(p) / prior_var H = np.eye(p) * 1e-6 for i in range(maxiter): eta = X.dot(beta) p_vec = sigmoid(eta) W = p_vec * (1.0 - p_vec) W = np.clip(W, 1e-8, None) H = X.T.dot(W[:, None] * X) + prior_prec grad = X.T.dot(y - p_vec) - prior_prec.dot(beta) try: delta = np.linalg.solve(H, grad) except np.linalg.LinAlgError: H += np.eye(p) * 1e-8 delta = np.linalg.solve(H, grad) beta = beta + delta if np.linalg.norm(delta) < tol: break try: cov = np.linalg.inv(H) except np.linalg.LinAlgError: cov = np.linalg.pinv(H) return beta, cov def to_excel_bytes(df): out = BytesIO() with pd.ExcelWriter(out, engine="openpyxl") as writer: df.to_excel(writer, index=False, sheet_name="signal") return out.getvalue() # ------------------ Ingesta de datos ------------------ using_demo = False if uploaded is not None: try: df = pd.read_csv(uploaded) st.info(f"CSV cargado: {uploaded.name} — {len(df)} filas") except Exception as e: st.error("Error leyendo CSV: " + str(e)) df = pd.DataFrame() using_demo = True else: using_demo = True np.random.seed(42) N = 420 cot_long_demo = np.random.normal(12000, 3000, size=N).astype(int) cot_short_demo = np.random.normal(11000, 3000, size=N).astype(int) fed_demo = np.clip(np.random.normal(60, 12, size=N), 0, 100) retail_demo = np.clip(np.random.normal(50, 12, size=N), 0, 100) net_demo = cot_long_demo - cot_short_demo z_net_demo = (net_demo - net_demo.mean()) / (net_demo.std() if net_demo.std() > 0 else 1) true_score = 0.8 * z_net_demo + 1.5 * ((fed_demo - 50) / 50) - 0.5 * ((retail_demo - 50) / 50) prob_demo = sigmoid(true_score) target_demo = (np.random.rand(N) < prob_demo).astype(int) df = pd.DataFrame({ "date": pd.date_range(end=pd.Timestamp.today(), periods=N), "cot_long": cot_long_demo, "cot_short": cot_short_demo, "fed_prob": fed_demo, "retail_pct": retail_demo, "target_up": target_demo }) st.info("Usando datos demo. Suba CSV histórico para calibración con datos reales.") # ------------------ Feature engineering ------------------ def build_features(df_in, s_cot_local=50000): df = df_in.copy() df["net"] = df["cot_long"] - df["cot_short"] df["net_scaled"] = df["net"] / float(s_cot_local) df["z_net"] = (df["net"] - df["net"].rolling(252, min_periods=1).mean()) / df["net"].rolling(252, min_periods=1).std().replace(0,1) df["delta_1w"] = df["net"].diff(5).fillna(0) df["delta_4w"] = df["net"].diff(20).fillna(0) df["z_delta_1w"] = (df["delta_1w"] - df["delta_1w"].rolling(252, min_periods=1).mean()) / df["delta_1w"].rolling(252, min_periods=1).std().replace(0,1) df["z_delta_4w"] = (df["delta_4w"] - df["delta_4w"].rolling(252, min_periods=1).mean()) / df["delta_4w"].rolling(252, min_periods=1).std().replace(0,1) df["signal_fed"] = (df["fed_prob"] - 50.0) / 50.0 df["signal_retail"] = (df["retail_pct"] - 50.0) / 50.0 return df df_feat = build_features(df, s_cot_local=s_cot) # Features used in el modelo táctico feature_cols = ["z_net", "z_delta_1w", "z_delta_4w", "signal_fed", "signal_retail"] if "target_up" in df_feat.columns: train_df = df_feat.copy() X_train = train_df[feature_cols].fillna(0).values y_train = train_df["target_up"].astype(int).values else: # Should not happen because demo includes target_up, but keep safe fallback train_df = df_feat.copy() X_train = train_df[feature_cols].fillna(0).values y_train = df_feat.get("target_up", pd.Series(np.zeros(len(train_df)))).astype(int).values if retrain and uploaded is None: st.warning("Para reentrenar debe subir un CSV histórico con etiqueta 'target_up'.") with st.spinner("Calibrando modelo bayesiano táctico (MAP + Laplace)..."): try: beta_map, cov_post = fit_bayesian_logit(X_train, y_train, prior_var=prior_var) except Exception as e: st.error("Fallo en calibración del modelo: " + str(e)) beta_map = np.zeros(len(feature_cols)) cov_post = np.eye(len(feature_cols)) * 1.0 # ------------------ Fila en vivo y predicción ------------------ live = {"cot_long": float(cot_long), "cot_short": float(cot_short), "fed_prob": float(fed_prob), "retail_pct": float(retail_pct)} aug = pd.concat([df.head(1).copy(), pd.DataFrame([live])], ignore_index=True) live_feat = build_features(aug, s_cot_local=s_cot).iloc[-1] x_live = live_feat[feature_cols].fillna(0).values net = float(live_feat["net"]) # Posterior sampling + predicción (defensiva) + calibración contra sesgo histórico sample_probs_adj = None calibration_applied = False try: cov_post = np.array(cov_post) cov_post = 0.5 * (cov_post + cov_post.T) jitter = 1e-8 * np.eye(cov_post.shape[0]) samples = np.random.multivariate_normal(beta_map, cov_post + jitter, size=4000) sample_probs = sigmoid(samples.dot(x_live)) # Calibración: centramos la predicción para neutralizar el sesgo medio en entrenamiento try: train_eta = X_train.dot(beta_map) train_mean_p = float(sigmoid(train_eta).mean()) except Exception: train_mean_p = float(sigmoid(X_train.dot(beta_map)).mean()) if X_train.size else 0.5 bias_offset = train_mean_p - 0.5 # positivo => modelo históricamente overpredicts up # ajustar sample probs y recortar entre 0 y 1 sample_probs_adj = np.clip(sample_probs - bias_offset, 0.0, 1.0) p_mean = float(sample_probs_adj.mean()) p_low = float(np.percentile(sample_probs_adj, 2.5)) p_high = float(np.percentile(sample_probs_adj, 97.5)) calibration_applied = abs(bias_offset) > 1e-6 except Exception: eta = float(x_live.dot(beta_map)) p_mean = float(sigmoid(eta)) p_low, p_high = p_mean, p_mean sample_probs_adj = np.array([p_mean]) calibration_applied = False # Índice de convicción (usando distribución calibrada si existe) dispersion = max(1e-9, float(np.std(sample_probs_adj))) conviction = min(100.0, max(0.0, 100.0 * (abs(p_mean - 0.5) * 2.0) * (1.0 / (1.0 + 5.0 * dispersion)))) conviction = round(conviction, 1) # Decisión táctica profesional (probabilidad + convicción) if conviction < 35: decision = "ESPERAR (NO OPERAR)" decision_flag = "wait" elif p_mean >= 0.60: decision = "COMPRAR (LONG)" decision_flag = "buy" elif p_mean <= 0.40: decision = "VENDER (SHORT)" decision_flag = "sell" else: decision = "ESPERAR (NO OPERAR)" decision_flag = "wait" # Descomposición de contribuciones contrib_raw = np.array(beta_map) * np.array(x_live) if np.sum(np.abs(contrib_raw)) > 0: contrib_pct = 100.0 * contrib_raw / np.sum(np.abs(contrib_raw)) else: contrib_pct = np.zeros_like(contrib_raw) # ------------------ Módulo Intradía (Híbrido Profesional) ------------------ st.markdown("---") st.markdown("## Intraday Execution Engine") activate_intraday = st.checkbox("Activar módulo intradía (requiere bias macro activo)") if activate_intraday: # Solo permitir intradía si bias macro activo y convicción suficiente if decision_flag == "wait" or conviction < 35: st.warning("Bias macro insuficiente o convicción baja. Intradía desactivado hasta nueva señal.") intraday_signal = "NO TRADE" intraday_info = {} else: # Parámetros de la demo (reemplazar por feed real en producción) st.markdown("### Parámetros intradía (demo)") # Simulación de datos intradía (1m-like series) — sustituir por feed real np.random.seed(7) M = 300 intraday_returns = np.random.normal(0, 0.0008, M) # retornos pequeños intraday_prices = 100.0 + np.cumsum(intraday_returns) intraday_volume = np.random.randint(80, 200, M) df_intraday = pd.DataFrame({ "price": intraday_prices, "volume": intraday_volume }) # VWAP df_intraday["cum_vol"] = df_intraday["volume"].cumsum() df_intraday["cum_pv"] = (df_intraday["price"] * df_intraday["volume"]).cumsum() # evitar división por cero df_intraday["vwap"] = df_intraday["cum_pv"] / df_intraday["cum_vol"].replace(0, np.nan) df_intraday["vwap"].fillna(method="ffill", inplace=True) current_price = float(df_intraday["price"].iloc[-1]) current_vwap = float(df_intraday["vwap"].iloc[-1]) # ATR proxy (rolling mean absolute diff) df_intraday["returns_abs"] = df_intraday["price"].diff().abs() atr = float(df_intraday["returns_abs"].rolling(14, min_periods=1).mean().iloc[-1]) # Session high / low session_high = float(df_intraday["price"].max()) session_low = float(df_intraday["price"].min()) # Tick imbalance proxy up_ticks = int((df_intraday["price"].diff() > 0).sum()) down_ticks = int((df_intraday["price"].diff() < 0).sum()) denom = max(1, up_ticks + down_ticks) imbalance = float((up_ticks - down_ticks) / denom) # Confirmación estructural simple intraday_signal = "NO TRADE" if decision_flag == "buy": # criterio: precio por encima de VWAP y imbalance positivo if (current_price > current_vwap) and (imbalance > 0): intraday_signal = "LONG CONFIRMADO" elif decision_flag == "sell": if (current_price < current_vwap) and (imbalance < 0): intraday_signal = "SHORT CONFIRMADO" intraday_info = { "current_price": current_price, "current_vwap": current_vwap, "atr": atr, "session_high": session_high, "session_low": session_low, "imbalance": imbalance, "signal": intraday_signal } # Mostrar resultados intradía st.markdown("### Estado intradía (demo)") colA, colB, colC = st.columns(3) with colA: st.metric("Precio actual", f"{current_price:.5f}") st.metric("VWAP", f"{current_vwap:.5f}") with colB: st.metric("ATR (proxy)", f"{atr:.6f}") st.metric("Imbalance", f"{imbalance:.3f}") with colC: st.metric("Session High", f"{session_high:.5f}") st.metric("Session Low", f"{session_low:.5f}") if intraday_signal == "LONG CONFIRMADO": st.success("Señal Intradía: LONG confirmado con VWAP + Imbalance") elif intraday_signal == "SHORT CONFIRMADO": st.error("Señal Intradía: SHORT confirmado con VWAP + Imbalance") else: st.info("No hay confirmación intradía aún. Esperar estructura o mayor desequilibrio.") # Plan de ejecución sugerido (demo) st.markdown("### Plan de Ejecución (demo)") if intraday_signal in ["LONG CONFIRMADO", "SHORT CONFIRMADO"]: st.markdown(f""" - Entrada en dirección del bias macro ({decision}). - Stop técnico ≈ 1.2 × ATR (≈ {1.2*atr:.6f}). - Target inicial ≥ 1.5 × riesgo. - Tamaño sugerido: 0.5–1% del capital. """) else: st.markdown("Esperar ruptura estructural o mayor desequilibrio de flujo.") else: intraday_signal = "NO TRADE" intraday_info = {} # ------------------ Narrativa simplificada + técnica (en español) ------------------ def narrative_tactical_simplificada(p_mean, p_low, p_high, conviction, decision_flag, x_live, contrib_pct, feature_cols, par, calibration_applied, using_demo): """ Versión legible para usuario promedio + explicación técnica desplegable. Devuelve (texto_simple, texto_tecnico). """ prob_text = f"P({par} ↑) ≈ {p_mean*100:.1f}% (IC95%: {p_low*100:.1f}%–{p_high*100:.1f}%). Convicción: {conviction}/100." if decision_flag == "buy": simple = ( "Recomendación: **Comprar / Abrir posición larga (táctica)**.\n\n" "Por qué (simple): la probabilidad de subida es alta y hay soporte estructural. " "Se sugiere entrar de forma gradual y controlar el riesgo con stops." ) elif decision_flag == "sell": simple = ( "Recomendación: **Vender / Abrir posición corta (táctica)**.\n\n" "Por qué (simple): la probabilidad favorece la baja y la convicción es suficiente para una operación táctica. " "Usa stops ajustados y tamaño reducido." ) else: simple = ( "Recomendación: **No operar (esperar)**.\n\n" "Por qué (simple): la probabilidad está cerca de equilibrio o la convicción es baja; mejor esperar confirmación." ) # Texto técnico fed_s = x_live[3] if len(x_live) > 3 else 0.0 cot_s = x_live[0] if len(x_live) > 0 else 0.0 retail_s = x_live[4] if len(x_live) > 4 else 0.0 tech_lines = [] tech_lines.append(f"Resumen técnico — activo: {par}") tech_lines.append(f"- Probabilidad bayesiana P({par} ↑) = {p_mean*100:.1f}% (IC95%: {p_low*100:.1f}%–{p_high*100:.1f}%). Convicción: {conviction}/100.") if calibration_applied: tech_lines.append("- Nota técnica: la predicción ha sido calibrada contra el sesgo medio del histórico para mejorar neutralidad.") if using_demo: tech_lines.append("- Advertencia: se están usando datos demo para calibración. Suba su CSV histórico para calibración real y backtests.") # Macro if fed_s > 0.4: tech_lines.append("- Macro: FedWatch con sesgo hawkish → favorece USD fuerte en el corto plazo.") elif fed_s < -0.4: tech_lines.append("- Macro: FedWatch dovish → presión a la baja para USD.") else: tech_lines.append("- Macro: FedWatch neutral/moderado → macro no es el driver dominante ahora.") # COT if abs(cot_s) < 0.15: tech_lines.append("- Posicionamiento (COT): cercano a neutral — riesgo de sobreacumulación limitado.") elif cot_s >= 0.15: tech_lines.append("- Posicionamiento (COT): sesgo long entre no-commercials — soporte estructural pero atención a toma de ganancias.") else: tech_lines.append("- Posicionamiento (COT): sesgo short — riesgo de short-squeeze si macro gira hawkish.") # Retail if retail_s > 0.15: tech_lines.append("- Retail: minoristas inclinados a comprar — posible señal contraria a corto plazo.") elif retail_s < -0.15: tech_lines.append("- Retail: minoristas inclinados a vender — puede reforzar momentum bajista si lo institucional confirma.") else: tech_lines.append("- Retail: posicionamiento minorista balanceado — no prevalece contrarian ahora.") # Contribuciones tech_lines.append("- Descomposición de contribuciones (valores y % firmadas):") for name, pct, val in zip(feature_cols, contrib_pct, x_live): sign = "+" if pct >= 0 else "-" tech_lines.append(f" • {name}: {sign}{abs(pct):.1f}% (valor {val:.3f})") # Razonamiento if decision_flag == "buy": tech_lines.append("- Recomendación táctica: acumulación gradual LONG. Ejecución: escala en 3 tramos; tamaño inicial 0.5–1% notional; stop 1.0–1.5×ATR; R:R ≥ 1:1.5.") elif decision_flag == "sell": tech_lines.append("- Recomendación táctica: considerar SHORT táctico o reducir largos. Ejecución: tamaño reducido; stops ajustados; vigilar noticias.") else: tech_lines.append("- Recomendación táctica: no operar. Esperar confirmación de flujos o ruptura técnica.") tech_lines.append("- Nota: COT es semanal (retardo); combine con order-flow intradía y skew de opciones para ejecución.") texto_tecnico = "\n".join(tech_lines) texto_simple = prob_text + "\n\n" + simple return texto_simple, texto_tecnico # Obtener textos simple y técnico texto_simple, texto_tecnico = narrative_tactical_simplificada( p_mean, p_low, p_high, conviction, decision_flag, x_live, contrib_pct, feature_cols, par, calibration_applied, using_demo ) # ------------------ Interfaz: resultado y narrativa ------------------ left, right = st.columns([2,3]) with left: st.markdown("
", unsafe_allow_html=True) st.markdown(f"
{decision}
", unsafe_allow_html=True) st.markdown( f"
Probabilidad (media): {p_mean*100:.1f}%    95% IC: {p_low*100:.1f}%–{p_high*100:.1f}%
", unsafe_allow_html=True ) st.markdown(f"
Convicción: {conviction}/100
", unsafe_allow_html=True) if calibration_applied: st.markdown("
Se aplicó calibración contra el sesgo histórico del modelo para favorecer neutralidad.
", unsafe_allow_html=True) if using_demo: st.markdown("
ATENCIÓN: usando datos demo. Suba CSV histórico para calibración real.
", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) st.markdown("### Plan táctico (resumen)", unsafe_allow_html=True) if decision_flag == "buy": st.markdown("- Escala en 3 tramos. Tramo inicial 0.5–1% notional.", unsafe_allow_html=True) st.markdown("- Stop: 1.0–1.5×ATR o por debajo de soporte técnico.", unsafe_allow_html=True) st.markdown("- Target: R:R ≥ 1:1.5.", unsafe_allow_html=True) elif decision_flag == "sell": st.markdown("- Reducir exposiciones largas o iniciar cortos pequeños (0.5–1%).", unsafe_allow_html=True) st.markdown("- Stops estrictos; vigila comunicados macro.", unsafe_allow_html=True) else: st.markdown("- No operar. Esperar señal clara o confirmación de flujos.", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) with right: fig = go.Figure(go.Indicator( mode="gauge+number", value=p_mean*100, number={'suffix': '%'}, domain={'x': [0,1], 'y': [0,1]}, title={'text': f"P({par} ↑)"}, gauge={ 'axis': {'range': [0,100]}, 'bar': {'color': "#0f2742"}, 'steps': [ {'range':[0,40], 'color':'#d9534f'}, {'range':[40,60], 'color':'#f0ad4e'}, {'range':[60,100], 'color':'#2a9d8f'} ], } )) st.plotly_chart(fig, use_container_width=True) st.markdown("### Resumen contextual — versión simple", unsafe_allow_html=True) st.markdown(f"
{texto_simple.replace(chr(10), '
')}
", unsafe_allow_html=True) with st.expander("Ver explicación técnica (detalle para analistas)"): st.markdown(f"
{texto_tecnico.replace(chr(10), '
')}
", unsafe_allow_html=True) # ------------------ Tabla de componentes y coeficientes ------------------ st.markdown("### Componentes (valores normalizados) y coeficientes MAP") comp_df = pd.DataFrame({ "Componente": feature_cols, "Valor (normalizado)": [f"{v:.4f}" for v in x_live], "beta_MAP": [f"{float(b):.4f}" for b in beta_map], "Contribución firmada (%)": [f"{float(c):.1f}%" for c in contrib_pct] }) st.table(comp_df) # ------------------ Exportar resultado ------------------ out_df = pd.DataFrame([{ "par": par, "cot_long": cot_long, "cot_short": cot_short, "net": net, "fed_prob": fed_prob, "retail_pct": retail_pct, "p_mean": p_mean, "p_2.5": p_low, "p_97.5": p_high, "conviction": conviction, "decision": decision, "intraday_signal": intraday_signal }]) try: excel_bytes = to_excel_bytes(out_df) st.download_button("Exportar resultado (Excel)", data=excel_bytes, file_name=f"kennis_{par.replace('/','')}_signal.xlsx") except Exception as e: st.error("Error exportando Excel: " + str(e)) st.markdown("---") st.markdown("
Implementación: Logistic bayesiano táctico (MAP + Laplace). Suba CSV con 'target_up' para calibración real y backtests. Uso profesional: combine con price feed y motor de ejecución.
", unsafe_allow_html=True) st.markdown("© Kennis FX Markets — Institutional Bayesian Intelligence Engine", unsafe_allow_html=True)