v2.6.2 web / 2.5.5 iOS / 2.5.2 macOS | August 2026
Everything happening in your world, on one screen.
The tools that fuse geodata, markets and news into one picture are built for governments and hedge funds, not a person checking what's happening near them or in their portfolio. Epiphany starts with a map, because a map is the one interface that puts unrelated feeds (earthquakes, flights, crime, prices, prediction markets) into a shared frame without forcing them into a single schema first. It can also trade a portfolio on a measured signal, so the same picture that shows what's happening can act on it. Palantir for regular people. Live at epiphany.heyitsmejosh.com, with companion apps for iOS, macOS, and watchOS.
This paper leads with the algorithms, because the trading and prediction pipeline is the one part of Epiphany making decisions on its own; everything after those sections is supporting detail.
The core bet is that a disciplined, sized, rules based strategy beats discretionary trading, because discretion is where fear and excitement override a plan. The pipeline runs signal to execution and is paper only by default, so the strategy proves itself on real prices before it risks real money; going live is a separate opt in step, never the default.
For each symbol the engine runs a Geometric Brownian Motion simulation: 500 price paths over a 30 day horizon. Drift and volatility come from the symbol's recent daily returns (log returns, annualized). Each path steps daily:
S(t+1) = S(t) * exp((mu - 0.5 * sigma^2) * dt + sigma * sqrt(dt) * Z)
where mu is drift, sigma is volatility, dt is one trading day, and Z is a
standard normal draw. The bull probability is the share of the 500 paths that close
above the current price, because a single point forecast hides how much the paths
disagree with each other; the spread across 500 runs is the actual signal. That
probability becomes the raw conviction score. This runs in the in app simulator at
60fps (src/utils/simBenchmark.js) across the full asset universe, and in the
weekday morning cron (server/api/broker/morning-run.js).
A trade only triggers when the technicals agree with the prediction, because a
Monte Carlo path is a statistical opinion about drift, not proof a trade is timed
right; the composite signal (src/utils/indicators.js) is what stops a good long
term simulation from buying into a short term downtrend. It combines:
- RSI (14): Wilder's smoothing. Above 55 reads as strength, below 45 as weakness.
- MACD (12/26/9): histogram above zero is bullish momentum, below zero bearish.
- Moving average trend: 50 period versus 200 period (or the longest available). Fast above slow is an uptrend.
Each component contributes plus or minus one to a score. Score of plus two or more is Buy, minus two or less is Sell, otherwise Hold. The same score drives the Buy/Hold/Sell badge shown on every stock.
Size comes from the fractional Kelly criterion. Full Kelly fraction is:
f* = (p * b - (1 - p)) / b
where p is the bull probability from step 1 and b is the reward to risk ratio
implied by the target and stop. Full Kelly is mathematically optimal but also
overbets in practice, since its inputs are estimates, not certainties, so
Epiphany uses a default 0.25 fraction of f* to cut variance, then caps any
single position at 10% of equity so one bad estimate cannot dominate the
account. A momentum strength and volatility gate blocks sizing into chop, since
Kelly's inputs mean little when price is not trending either way.
- Entry: moving average crossover confirmed by the composite signal.
- Exit: fixed stop and target, plus a trailing stop once in profit.
- Venue: paper by default. Live routing goes through SnapTrade after a paper proving period.
- Kill switch: removing the broker keys disables all order placement.
- Audit: a full per symbol trade log is written on every run.
The strategy is shared with a backtestable Pine Script port
(tradingview/monica-kelly-strategy.pine) so the same rules can be validated on
years of TradingView history instead of trusted on faith from a few weeks of
paper trading.
src/utils/broker.js defines one BrokerAdapter interface (connect, placeOrder,
getPositions, getBalance), so the strategy code never has to know which broker
it is talking to and a new broker is one adapter, not a rewrite. Adapters: Alpaca
(paper and live), SnapTrade (read only aggregator sync, HMAC signed REST, covers
Wealthsimple and Questrade in Canada plus US brokers under one connection, chosen
because Alpaca alone has no reach into Canadian brokers), cTrader (OAuth2),
TradingView (webhook), Wealthsimple (read only), IBKR (stub). Read only sync
(server/api/broker/sync.js) writes a holdings and cash snapshot to KV and, once
connected, becomes the portfolio value of record, since a synced real balance is
more trustworthy than a locally tracked one that can drift from reality.
| Layer | Source | Auth | Notes |
|---|---|---|---|
| Earthquakes | USGS | None | Global, real time |
| Flights | adsb.lol | None | Community ADS-B feed, replaced OpenSky |
| Incidents | OpenStreetMap Overpass | None | Police, fire, hospitals, cameras |
| Traffic | TomTom | API key | Free tier, key needed |
| Weather | NWS + Environment Canada | None | Alerts |
| Crime | Vancouver and Surrey open data, GDELT | None | Geo tagged |
| Local events and places | Wikipedia, OSM, Ticketmaster | Mixed | Wikipedia is the free fallback |
| Venue reviews | Yelp Fusion | API key | Ratings and review snippets on place markers |
| Wildfires | NASA EONET / FIRMS | Optional | Satellite detections |
| News | GDELT | None | Geo extraction |
| Predictions | Polymarket | None | Whale tracking, probability markets |
| Macro | FRED | API key | Fed funds, CPI, GDP, unemployment, treasuries |
All sources fetch in parallel every 120 seconds and pause when the tab is hidden
(useVisibilityPolling), because polling a backgrounded tab burns quota for a
picture nobody is looking at. Each source has its own error boundary, so a failure
in one feed (a rate limit, a dead endpoint) returns an empty array instead of
blocking the others: a map that goes fully blank because one of a dozen sources
hiccuped would be worse than one missing layer. Markers only render with real
coordinates. No synthetic scatter, no placeholder data, because a fake dot on a
live map is worse than no dot at all.
MapLibre GL JS on a CARTO dark basemap. DOM markers with per layer CSS pulse animations. The heatmap is computed client side from every geo tagged point. Geolocation chain is browser GPS, then cached position (30 minute TTL), then IP fallback, because a map with no starting location is a blank screen and a returning visitor shouldn't have to grant permission again just to see themselves on it. Position resolves before first render when cached, so the map does not jump on load, since the rule from the top of this paper is a steady map with no jumps or flashes.
Sessions use bcrypt with tokens in Upstash Redis (KV). Billing is Stripe Checkout, Free or $1 per week Premium. Free gets the map, ticker, and situation monitor, because the ambient picture of what's happening is the hook; Premium unlocks portfolio, ontology, and deep data, the parts that cost real money in API calls and compute per user.
| Platform | Framework | Notes |
|---|---|---|
| iOS | SwiftUI | 4 tabs, MapKit, parallel preload, auto refresh markets |
| macOS | SwiftUI | 5 section nav, MapKit |
| watchOS | SwiftUI | Glance complications |
Native apps are live on the App Store (id6779522175). Portfolio, ontology, and deep data are gated behind Pro on every platform, matching the web tiering, so a Premium subscription means the same thing everywhere instead of being a web only perk. | Widgets | SwiftUI | iOS and macOS extensions |
All native apps share the API backend over URLSession with cookie persistence, so the trading and prediction logic lives in one place instead of being reimplemented per platform.
Cold start about 2 seconds. Data refresh on a 120 second poll, paused when hidden. Bundle about 1MB gzipped, mostly MapLibre GL. Test suite runs across Vitest and Playwright.
MIT 2026, Joshua Trommel