[object Object]

← back to Ken

Ken: switch AI analyst from paid Gemini to local Ollama (hermes3:8b) + local boot on Mac3

a3dd1710b379c97f02e24a3bceab762809f721ce · 2026-07-10 11:03:57 -0700 · Steve

- geminiAnalyze() now calls local Ollama /api/generate (format:json), env-overridable
  via KEN_LOCAL_MODEL/OLLAMA_URL; removes GEMINI_API_KEY dependency
- raise per-day AI cap 200 -> 100000 (local = $0, no throttle)
- persona/label strings reflect local model
- add start.sh (sources .env) + reconstructed schema.sql (18 ken_* tables) so Ken
  boots + persists on Mac3 against local Postgres 'ken' + existing bertha_betting

Files touched

Diff

commit a3dd1710b379c97f02e24a3bceab762809f721ce
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jul 10 11:03:57 2026 -0700

    Ken: switch AI analyst from paid Gemini to local Ollama (hermes3:8b) + local boot on Mac3
    
    - geminiAnalyze() now calls local Ollama /api/generate (format:json), env-overridable
      via KEN_LOCAL_MODEL/OLLAMA_URL; removes GEMINI_API_KEY dependency
    - raise per-day AI cap 200 -> 100000 (local = $0, no throttle)
    - persona/label strings reflect local model
    - add start.sh (sources .env) + reconstructed schema.sql (18 ken_* tables) so Ken
      boots + persists on Mac3 against local Postgres 'ken' + existing bertha_betting
---
 kalshi-dash/.gitignore |   1 +
 kalshi-dash/schema.sql | 139 +++++++++++++++++++++++++++++++++++++++++++++++++
 kalshi-dash/server.js  |  52 +++++++++---------
 kalshi-dash/start.sh   |   7 +++
 4 files changed, 175 insertions(+), 24 deletions(-)

diff --git a/kalshi-dash/.gitignore b/kalshi-dash/.gitignore
new file mode 100644
index 0000000..4c49bd7
--- /dev/null
+++ b/kalshi-dash/.gitignore
@@ -0,0 +1 @@
+.env
diff --git a/kalshi-dash/schema.sql b/kalshi-dash/schema.sql
new file mode 100644
index 0000000..bb98e54
--- /dev/null
+++ b/kalshi-dash/schema.sql
@@ -0,0 +1,139 @@
+-- Ken (Kalshi dashboard) reconstructed schema for local boot on Mac3 (2026-07-10).
+-- Derived from the INSERT/UPDATE/SELECT column usage in server.js — the original
+-- ken_* migration lived on the prod host and was never in this repo.
+-- Columns are permissive (nullable) so no pipeline INSERT fails on a NOT NULL.
+-- Portfolio/tournament tables are created EMPTY (their rows need the original dump
+-- or an explicit reseed from PORTFOLIO_STRATEGIES).
+
+CREATE TABLE IF NOT EXISTS ken_config (
+  key TEXT PRIMARY KEY,
+  value JSONB NOT NULL,
+  updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_sentiment (
+  id BIGSERIAL PRIMARY KEY,
+  source TEXT, subreddit TEXT, keyword TEXT, post_title TEXT, post_url TEXT,
+  score BIGINT, num_comments BIGINT, sentiment_score DOUBLE PRECISION,
+  relevance_ticker TEXT, raw_text TEXT,
+  captured_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_event_sentiment (
+  id BIGSERIAL PRIMARY KEY,
+  article_id TEXT, event_tag TEXT, market_ticker TEXT,
+  sentiment DOUBLE PRECISION, confidence DOUBLE PRECISION, magnitude DOUBLE PRECISION,
+  headline TEXT, source TEXT, created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_probability_shifts (
+  id BIGSERIAL PRIMARY KEY,
+  market_id TEXT, prior_probability DOUBLE PRECISION, sentiment_impact DOUBLE PRECISION,
+  adjusted_probability DOUBLE PRECISION, market_probability DOUBLE PRECISION,
+  edge DOUBLE PRECISION, num_articles BIGINT, created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_strategy_signals (
+  id BIGSERIAL PRIMARY KEY,
+  market_id TEXT, direction TEXT, expected_edge DOUBLE PRECISION,
+  size_recommendation DOUBLE PRECISION, confidence DOUBLE PRECISION,
+  reasoning TEXT, sources JSONB, risk_approved BOOLEAN,
+  created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_market_mappings (
+  id BIGSERIAL PRIMARY KEY,
+  market_id TEXT, keywords JSONB, category TEXT, contract_type TEXT,
+  created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_market_snapshots (
+  id BIGSERIAL PRIMARY KEY,
+  ticker TEXT, event_ticker TEXT, title TEXT, subtitle TEXT, category TEXT,
+  yes_bid BIGINT, yes_ask BIGINT, no_bid BIGINT, no_ask BIGINT, last_price BIGINT,
+  volume BIGINT, volume_24h BIGINT, open_interest BIGINT, spread BIGINT,
+  created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_predictions (
+  id BIGSERIAL PRIMARY KEY,
+  ticker TEXT, event_ticker TEXT, title TEXT, signal_type TEXT, side TEXT,
+  confidence DOUBLE PRECISION, entry_price DOUBLE PRECISION, edge_bps DOUBLE PRECISION,
+  reasoning TEXT, sources JSONB,
+  outcome TEXT, exit_price DOUBLE PRECISION, pnl_cents BIGINT, resolved_at TIMESTAMPTZ,
+  created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_performance (
+  id BIGSERIAL PRIMARY KEY,
+  total_markets BIGINT, active_markets BIGINT, predictions_made BIGINT,
+  long_signals BIGINT, short_signals BIGINT, sentiment_items BIGINT,
+  scan_duration_ms BIGINT, slack_sent BOOLEAN DEFAULT FALSE,
+  created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_trades (
+  id BIGSERIAL PRIMARY KEY,
+  ticker TEXT, event_ticker TEXT, title TEXT, side TEXT, action TEXT,
+  price_cents BIGINT, count BIGINT, cost_cents BIGINT, order_id TEXT,
+  signal_type TEXT, confidence DOUBLE PRECISION, reasoning TEXT, status TEXT,
+  exit_price DOUBLE PRECISION, pnl_cents BIGINT, closed_at TIMESTAMPTZ,
+  created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_learning (
+  id BIGSERIAL PRIMARY KEY,
+  category TEXT, signal_type TEXT, win_rate DOUBLE PRECISION,
+  avg_pnl_cents DOUBLE PRECISION, total_trades BIGINT,
+  last_updated TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_reddit_tracking (
+  id BIGSERIAL PRIMARY KEY,
+  source TEXT, subreddit TEXT, keyword TEXT, enabled BOOLEAN DEFAULT TRUE,
+  created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_portfolios (
+  id BIGINT PRIMARY KEY,
+  name TEXT, strategy TEXT,
+  balance_cents BIGINT, daily_budget_cents BIGINT,
+  total_invested_cents BIGINT DEFAULT 0, starting_balance_cents BIGINT,
+  color TEXT, last_trade_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_portfolio_trades (
+  id BIGSERIAL PRIMARY KEY,
+  portfolio_id BIGINT, signal_id BIGINT, market_id TEXT, market_title TEXT,
+  direction TEXT, entry_price_cents BIGINT, contracts BIGINT, cost_cents BIGINT,
+  reasoning TEXT, confidence DOUBLE PRECISION, ai_enhanced BOOLEAN, mc_consistent BOOLEAN,
+  status TEXT, exit_price_cents BIGINT, pnl_cents BIGINT, resolved_at TIMESTAMPTZ,
+  created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_portfolio_snapshots (
+  id BIGSERIAL PRIMARY KEY,
+  portfolio_id BIGINT, nav_cents BIGINT, open_positions BIGINT,
+  created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_daily_pnl (
+  id BIGSERIAL PRIMARY KEY,
+  portfolio_id BIGINT, date DATE, closed_trades BIGINT, wins BIGINT, losses BIGINT,
+  daily_pnl_cents BIGINT, cumulative_pnl_cents BIGINT,
+  created_at TIMESTAMPTZ DEFAULT NOW(),
+  UNIQUE (portfolio_id, date)
+);
+
+CREATE TABLE IF NOT EXISTS ken_tournament_entries (
+  id BIGSERIAL PRIMARY KEY,
+  tournament_id BIGINT, portfolio_id BIGINT, start_balance_cents BIGINT,
+  end_balance_cents BIGINT, pnl_cents BIGINT, trades_count BIGINT,
+  status TEXT, elimination_reason TEXT, created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS ken_weekly_tournaments (
+  id BIGSERIAL PRIMARY KEY,
+  week_start DATE, week_end DATE, budget_per_portfolio_cents BIGINT,
+  notes TEXT, status TEXT DEFAULT 'active', created_at TIMESTAMPTZ DEFAULT NOW()
+);
diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index 980dc3e..b4d2733 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -133,42 +133,46 @@ function bayesianUpdate(prior, forecastProb, weight = 0.7) {
 }
 
 // ══════════════════════════════════════════════════════
-// GEMINI AI PREDICTION ENGINE — uses CPU + AI for better signals
+// LOCAL AI PREDICTION ENGINE — uses local Ollama (no paid API) for better signals
 // ══════════════════════════════════════════════════════
-const GEMINI_API_KEY = process.env.GEMINI_API_KEY || '';
-const GEMINI_MODEL = 'gemini-2.0-flash';
+// Switched Gemini 2.0 Flash → local Ollama per Steve's local-models rule (2026-07-10).
+// Model is env-overridable (KEN_LOCAL_MODEL); hermes3:8b returns valid JSON fastest.
+const OLLAMA_URL = process.env.OLLAMA_URL || 'http://127.0.0.1:11434';
+const KEN_LOCAL_MODEL = process.env.KEN_LOCAL_MODEL || 'hermes3:8b';
+const GEMINI_MODEL = KEN_LOCAL_MODEL; // kept name so the dashboard's model label still resolves
 
 async function geminiAnalyze(prompt, maxTokens = 1024) {
-  if (!GEMINI_API_KEY) return null;
-
   try {
     const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 25000);
-    const res = await fetch(
-      `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`,
-      {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        signal: controller.signal,
-        body: JSON.stringify({
-          contents: [{ parts: [{ text: prompt }] }],
-          generationConfig: { temperature: 0.3, maxOutputTokens: maxTokens },
-        }),
-      }
-    );
+    const timeout = setTimeout(() => controller.abort(), 45000);
+    const res = await fetch(`${OLLAMA_URL}/api/generate`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      signal: controller.signal,
+      body: JSON.stringify({
+        model: KEN_LOCAL_MODEL,
+        prompt,
+        stream: false,
+        format: 'json', // constrain output to valid JSON (callers JSON.parse the result)
+        options: { temperature: 0.3, num_predict: maxTokens },
+      }),
+    });
     clearTimeout(timeout);
     if (!res.ok) return null;
     const data = await res.json();
-    return data?.candidates?.[0]?.content?.parts?.[0]?.text || null;
+    let text = data?.response || null;
+    // thinking-style models (qwen etc.) may wrap output in <think>…</think>; strip it
+    if (text) text = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
+    return text || null;
   } catch (e) {
-    console.log(`[Ken/Gemini] Error: ${e.message}`);
+    console.log(`[Ken/Local] Error: ${e.message}`);
     return null;
   }
 }
 
-// AI-Enhanced Signal Scoring — Gemini analyzes top signals for smarter predictions
+// AI-Enhanced Signal Scoring — local Ollama analyzes top signals for smarter predictions
 let geminiCallCount = 0;
-const GEMINI_DAILY_LIMIT = 200; // budget: ~200 calls/day
+const GEMINI_DAILY_LIMIT = 100000; // local Ollama = $0, so effectively uncapped (was 200 for paid Gemini)
 const geminiCallReset = setInterval(() => { geminiCallCount = 0; }, 24 * 60 * 60 * 1000);
 
 async function aiEnhanceSignal(signal, articles, market) {
@@ -216,7 +220,7 @@ Respond with ONLY a JSON object (no markdown):
       aiEnhanced: true,
     };
   } catch (e) {
-    console.log(`[Ken/Gemini] Parse error: ${e.message}`);
+    console.log(`[Ken/Local] Parse error: ${e.message}`);
     return signal;
   }
 }
@@ -2690,7 +2694,7 @@ const routes = {
       {
         id: 'gemini', name: 'Nova', role: 'AI Analyst', emoji: '🧠',
         color: '#EC4899', status: geminiCallCount < GEMINI_DAILY_LIMIT ? 'active' : 'rate_limited',
-        desc: 'Gemini 2.0 Flash analyzes high-edge signals with natural language reasoning',
+        desc: `Local Ollama (${KEN_LOCAL_MODEL}) analyzes high-edge signals with natural language reasoning`,
         stats: { callsToday: geminiCallCount, dailyBudget: GEMINI_DAILY_LIMIT, model: GEMINI_MODEL },
         skills: ['Signal Analysis', 'News Interpretation', 'Risk Assessment', 'Market Reasoning'],
       },
diff --git a/kalshi-dash/start.sh b/kalshi-dash/start.sh
new file mode 100755
index 0000000..f94a3c9
--- /dev/null
+++ b/kalshi-dash/start.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+# Ken (Kalshi dashboard) launcher — sources .env then runs the server.
+cd "$(dirname "$0")"
+set -a
+source .env
+set +a
+exec node server.js

← c303efd Untrack .DS_Store + gitignore it  ·  back to Ken  ·  Ken: seed 99 paper-trading portfolios + add portfolio stat c 2fa8032 →