diff --git a/kennis_streamlit.py b/kennis_streamlit.py
index 528e18e..160edf7 100644
--- a/kennis_streamlit.py
+++ b/kennis_streamlit.py
@@ -5,57 +5,58 @@ import pandas as pd
from io import BytesIO
import plotly.graph_objects as go
-# ----- Page config and minimal styling (institutional) -----
+# ------------------ 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 -----
+# ------------------ Header / Hero ------------------
col1, col2 = st.columns([4,1])
with col1:
st.markdown("
", unsafe_allow_html=True)
st.markdown("Institutional Bayesian Intelligence Engine
", unsafe_allow_html=True)
- st.markdown("Detection and strategic allocation guidance.
", 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 & upload -----
-st.sidebar.header("Live inputs (tactical / swing)")
-uploaded = st.sidebar.file_uploader("Upload historical CSV (optional). Required columns: cot_long,cot_short,fed_prob,retail_pct,target_up", type=["csv"])
-st.sidebar.markdown("Or use the live controls below:")
+# ------------------ 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", ["USD/JPY", "EUR/USD", "GBP/USD", "AUD/USD", "USD/CHF"], 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 (%) for current bin", min_value=0.0, max_value=100.0, value=96.0, step=0.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("Model & normalization")
-s_cot = st.sidebar.number_input("S_cot (scale for COT)", value=50000, step=1000)
-prior_var = st.sidebar.number_input("Prior variance (gauss)", value=0.5, step=0.1)
-retrain = st.sidebar.button("Retrain model (if CSV uploaded)")
+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)")
-# ----- Utilities -----
+# ------------------ 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):
- # MAP (Newton-Raphson) with Gaussian prior N(0, prior_var)
n, p = X.shape
beta = np.zeros(p)
prior_prec = np.eye(p) / prior_var
@@ -78,18 +79,23 @@ def fit_bayesian_logit(X, y, prior_var=0.5, maxiter=200, tol=1e-8):
cov = np.linalg.inv(H)
return beta, cov
-def to_excel(df):
+def to_excel_bytes(df):
out = BytesIO()
+ # pandas con openpyxl como engine — openpyxl debe estar en requirements.txt
with pd.ExcelWriter(out, engine="openpyxl") as writer:
df.to_excel(writer, index=False, sheet_name="signal")
return out.getvalue()
-# ----- Data ingestion and feature building -----
+# ------------------ Ingesta de datos ------------------
if uploaded is not None:
- df = pd.read_csv(uploaded)
- st.info(f"CSV loaded: {uploaded.name} — {len(df)} rows")
+ 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()
else:
- # synthetic demo history so app is usable without CSV
+ # demo sintético (multi-par no real) — solo para UI y pruebas
np.random.seed(42)
N = 420
cot_long_demo = np.random.normal(12000, 3000, size=N).astype(int)
@@ -102,17 +108,18 @@ else:
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("Running on demo historical data. Upload CSV for real-data calibration.")
+ 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)
- # rolling z using up to 252 rows (business-year style)
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)
@@ -124,37 +131,38 @@ def build_features(df_in, s_cot_local=50000):
df_feat = build_features(df, s_cot_local=s_cot)
-# features chosen for the tactical model
+# Features used in el modelo táctico
feature_cols = ["z_net", "z_delta_1w", "z_delta_4w", "signal_fed", "signal_retail"]
-# prepare training data (if uploaded and has target_up will be used)
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:
- # fallback (demo includes target_up)
train_df = df_feat.copy()
X_train = train_df[feature_cols].fillna(0).values
y_train = train_df["target_up"].astype(int).values
-# If user pressed retrain and uploaded provided CSV, we would re-fit (button here for UI clarity)
if retrain and uploaded is None:
- st.warning("No CSV uploaded — retrain requires historical CSV with 'target_up' labels.")
-# Fit model (MAP + Laplace)
-with st.spinner("Calibrating Bayesian tactical model (MAP + Laplace)..."):
- beta_map, cov_post = fit_bayesian_logit(X_train, y_train, prior_var=prior_var)
+ st.warning("Para reentrenar debe subir un CSV histórico con etiqueta 'target_up'.")
-# ----- Live row features (from sidebar) -----
+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))
+ # fallback: coeficientes neutros
+ 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)}
-# to compute z etc we build by appending the live row to the history head
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 = live_feat["net"]
-# ----- Posterior sampling and predictive distribution -----
-# defensive: ensure covariance is PSD-ish
+# Posterior sampling + predicción (defensiva)
try:
samples = np.random.multivariate_normal(beta_map, cov_post, size=4000)
sample_probs = sigmoid(samples.dot(x_live))
@@ -162,17 +170,17 @@ try:
p_low = float(np.percentile(sample_probs, 2.5))
p_high = float(np.percentile(sample_probs, 97.5))
except Exception:
- # fallback to point probability
eta = float(x_live.dot(beta_map))
p_mean = float(sigmoid(eta))
p_low, p_high = p_mean, p_mean
+ sample_probs = np.array([p_mean])
-# Conviction index: transforms p_mean and dispersion into 0-100
-dispersion = max(1e-6, float(np.std(sample_probs))) if 'sample_probs' in locals() else 0.0
+# Índice de convicción
+dispersion = max(1e-9, float(np.std(sample_probs)))
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)
-# Decision thresholds (tactical)
+# Decisión táctica
if p_mean >= 0.60:
decision = "COMPRAR (LONG)"
decision_flag = "buy"
@@ -183,127 +191,122 @@ else:
decision = "ESPERAR (NO OPERAR)"
decision_flag = "wait"
-# Compute contribution breakdown (feature * beta) and normalize for narrative
+# Descomposición de contribuciones
contrib_raw = np.array(beta_map) * np.array(x_live)
-# normalize to percent contribution with sign
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)
-# ----- Expert narrative generator (deterministic, audit-friendly) -----
-def narrative_tactical(p_mean, p_low, p_high, conviction, decision_flag, x_live, contrib_pct, feature_cols):
- # Build human expert style narrative using magnitudes and signed contributions
+# ------------------ Narrativa experta (en español) ------------------
+def narrative_tactical(p_mean, p_low, p_high, conviction, decision_flag, x_live, contrib_pct, feature_cols, par):
lines = []
- # regime sentence
- fed_s = x_live[3] # signal_fed
- cot_s = x_live[0] # z_net
+ lines.append(f"Resumen ejecutivo (táctico) — activo: {par}")
+ lines.append(f"- Probabilidad bayesiana P({par} ↑) = {p_mean*100:.1f}% (IC95%: {p_low*100:.1f}%–{p_high*100:.1f}%). Convicción: {conviction}/100.")
+ fed_s = x_live[3]
+ cot_s = x_live[0]
retail_s = x_live[4]
- lines.append("Executive summary (tactical):")
- lines.append(f"- Bayesian posterior P(USD/JPY ↑) = {p_mean*100:.1f}% (95% CI: {p_low*100:.1f}%–{p_high*100:.1f}%). Conviction: {conviction}/100.")
- # interpretation
if fed_s > 0.4:
- lines.append("- Macro: FedWatch is strongly hawkish — this structurally supports USD strength versus JPY over short horizon.")
+ lines.append("- Macro: FedWatch muestra sesgo hawkish — esto favorece fuerza del USD frente al JPY en el horizonte táctico.")
elif fed_s < -0.4:
- lines.append("- Macro: FedWatch is dovish — downside pressure on USD is likely.")
+ lines.append("- Macro: FedWatch muestra sesgo dovish — presión a la baja sobre USD esperable.")
else:
- lines.append("- Macro: FedWatch neutral/moderately balanced — macro is not the dominant driver.")
- # COT
+ lines.append("- Macro: FedWatch neutral/moderado — macro por sí sola no domina la señal táctica.")
if abs(cot_s) < 0.15:
- lines.append("- Positioning: COT non-commercials are currently near neutral (low extremeness); tail risk from crowded positioning is limited.")
+ lines.append("- Posicionamiento: COT de no-commercials cerca de neutral; riesgo de cola limitado por sobreposicionamiento.")
elif cot_s >= 0.15:
- lines.append("- Positioning: COT shows noticeable long bias among non-commercials — structural support exists but be wary of profit-taking risk.")
+ lines.append("- Posicionamiento: COT exhibe sesgo long entre no-commercials — soporte estructural, pero cuidado con toma de ganancias.")
else:
- lines.append("- Positioning: COT shows notable short bias — risk of short-squeeze exists if macro tilts hawkish.")
- # Retail
+ lines.append("- Posicionamiento: COT exhibe sesgo short — riesgo de short-squeeze si macro gira hawkish.")
if retail_s > 0.15:
- lines.append("- Retail: retail crowd leaning long — often a contrarian warning for short-term reversals; exercise caution.")
+ lines.append("- Retail: minoristas inclinados a comprar — aviso contrarian en plazos cortos; prudencia en entradas agresivas.")
elif retail_s < -0.15:
- lines.append("- Retail: retail lean short — can be supportive for medium momentum if institutional flow aligns.")
+ lines.append("- Retail: minoristas inclinados a vender — puede reforzar momentum si flujo institucional coincide.")
else:
- lines.append("- Retail: retail positioning near balanced — not a dominant contrarian signal now.")
- # Contributions
- lines.append("- Signal decomposition (signed % contribution):")
+ lines.append("- Retail: posicionamiento minorista balanceado — señal contraria no relevante ahora.")
+ lines.append("- Descomposición de señales (contribución firmada en %):")
for name, pct, val in zip(feature_cols, contrib_pct, x_live):
sign = "+" if pct >= 0 else "-"
- lines.append(f" • {name}: {sign}{abs(pct):.1f}% (value {val:.3f})")
- # Decision rationale
+ lines.append(f" • {name}: {sign}{abs(pct):.1f}% (valor {val:.3f})")
if decision_flag == "buy":
- lines.append("- Tactical recommendation: Gradual LONG accumulation. Rationale: hawkish Fed probabilities dominate neutral institutional positioning; market structure allows tactical upside.")
- lines.append("- Suggested execution (tactical): scale-in (3 tranches), initial exposure 0.5–1.5% notional, use volatility stop (1.0–1.5 × recent ATR) and target R:R ≥ 1:1.5.")
+ lines.append("- Recomendación táctica: Acumulación gradual LONG. Razón: P posterior y convicción respaldan sesgo alcista táctico.")
+ lines.append("- Ejecución sugerida: escala en 3 tramos; tamaño inicial 0.5–1% notional; stop técnico 1.0–1.5×ATR; objetivo R:R ≥ 1:1.5.")
elif decision_flag == "sell":
- lines.append("- Tactical recommendation: Consider SHORT exposure or reduce existing long exposure. Rationale: posterior favors downside with sufficient conviction.")
- lines.append("- Suggested execution: tight initial sizing, stop at 1.0–1.5 × ATR, target adapt to event risk.")
+ lines.append("- Recomendación táctica: Considerar SHORT táctico o reducción de posiciones largas. Razón: posterior favorece la baja con convicción.")
+ lines.append("- Ejecución sugerida: tamaño reducido; stops ajustados; vigilar noticias macro.")
else:
- lines.append("- Tactical recommendation: No trade. Rationale: posterior is near-neutral and uncertainty is significant; wait for clearer regime signal or confirmatory flows.")
- # Caveats
- lines.append("- Caveats: model uses weekly-positioning (COT) which is lagged; complement with intraday order flow and option skew for execution decisions.")
+ lines.append("- Recomendación táctica: No operar. Razón: incertidumbre relevante; esperar confirmación de flujo o ruptura macro.")
+ lines.append("- Nota: COT es semanal (retardo); combinar con order flow intradía y skew de opciones para ejecución.")
return "\n".join(lines)
-narrative = narrative_tactical(p_mean, p_low, p_high, conviction, decision_flag, x_live, contrib_pct, feature_cols)
+narrative = narrative_tactical(p_mean, p_low, p_high, conviction, decision_flag, x_live, contrib_pct, feature_cols, par)
-# ----- UI: top result and narrative -----
+# ------------------ 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"
Prob. (mean): {p_mean*100:.1f}% 95% CI: {p_low*100:.1f}%–{p_high*100:.1f}%
", unsafe_allow_html=True)
- st.markdown(f"
Conviction: {conviction}/100
", 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)
st.markdown("
", unsafe_allow_html=True)
- st.markdown("### Tactical trade plan (concise)", unsafe_allow_html=True)
+ st.markdown("### Plan táctico (resumen)", unsafe_allow_html=True)
if decision_flag == "buy":
- st.markdown("- Scale-in (3 tranches). Initial tranche: 0.5–1% notional.", unsafe_allow_html=True)
- st.markdown("- Stop: technical stop (1.0–1.5×ATR) or below nearest structure.", unsafe_allow_html=True)
- st.markdown("- Target: prefer R:R ≥ 1:1.5; manage increments.", unsafe_allow_html=True)
+ 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("- Consider reducing longs or initiating small short (0.5–1%).", unsafe_allow_html=True)
- st.markdown("- Tight stops; monitor macro headlines closely.", unsafe_allow_html=True)
+ 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 trade. Await clearer signal or follow confirmatory flow.", unsafe_allow_html=True)
+ 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:
- # gauge
fig = go.Figure(go.Indicator(
- mode="gauge+number+delta",
+ mode="gauge+number",
value=p_mean*100,
number={'suffix': '%'},
domain={'x': [0,1], 'y': [0,1]},
- title={'text': "P(USD/JPY ↑)"},
+ title={'text': f"P({par} ↑)"},
gauge={
'axis': {'range': [0,100]},
- 'bar': {'color': "#1f7a8c"},
+ 'bar': {'color': "#0f2742"},
'steps': [
- {'range':[0,40], 'color':'#a62b2b'},
- {'range':[40,60], 'color':'#bfae59'},
- {'range':[60,100], 'color':'#2a8f6b'}
+ {'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("### Contextual narrative — expert style", unsafe_allow_html=True)
+st.markdown("### Narrativa contextual — experto", unsafe_allow_html=True)
st.markdown(f"{narrative.replace(chr(10), '
')}
", unsafe_allow_html=True)
-# ----- Component table and coefficients -----
-st.markdown("### Components (normalized features) and MAP coefficients")
+# ------------------ Tabla de componentes y coeficientes ------------------
+st.markdown("### Componentes (valores normalizados) y coeficientes MAP")
comp_df = pd.DataFrame({
"Componente": feature_cols,
- "Valor (normalized)": [f"{v:.4f}" for v in x_live],
+ "Valor (normalizado)": [f"{v:.4f}" for v in x_live],
"beta_MAP": [float(b) for b in beta_map],
- "Signed % contrib": [f"{float(c):.1f}%" for c in contrib_pct]
+ "Contribución firmada (%)": [f"{float(c):.1f}%" for c in contrib_pct]
})
st.table(comp_df)
-# ----- Export & footer -----
+# ------------------ Exportar resultado ------------------
out_df = pd.DataFrame([{
- "cot_long": cot_long, "cot_short": cot_short, "net": net,
+ "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
}])
-st.download_button("Export result (Excel)", data=to_excel(out_df), file_name="kennis_usdjpy_signal.xlsx")
+
+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("Implementation: Tactical Bayesian logistic (MAP + Laplace). Replace demo with real historical CSV (with 'target_up' labels) for production-grade calibration. Use with execution policy and risk controls.
", unsafe_allow_html=True)
+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)