Strategy Summary
The FROTH + Triple Composite strategy is a Bitcoin timing system developed by @bitcoindata21. It uses two custom indicators to decide whether to be fully in (100%) or fully out (0%) of Bitcoin. There is no partial sizing — it is binary.
The strategy backtested +965% from Dec 2022 to Mar 2026, versus +322% for buy-and-hold over the same period, with only a −19% max drawdown (vs −50% buy-and-hold) while being in market just 51% of the time.
| Indicator | Role | Range | Key levels |
|---|---|---|---|
| FROTH | Measures market overheating / frothiness | 0 – 100 | <50 oversold | >95 extreme froth |
| Triple Composite (TC) | Counts active bullish conditions | 0 – 3 | ≥2 with low FROTH = buy | 3 = strong buy |
FROTH Indicator
FROTH is a 0–100 oscillator that measures how "frothy" or overheated the Bitcoin market is relative to its long-term trend. High values signal that price has run far above its historical baseline. Low values signal that price is depressed or undervalued relative to trend.
Scale
| Zone | Range | Meaning | Action |
|---|---|---|---|
| DEEP VALUE | 0 – 30 | Price is far below its long-term trend. Extreme undervaluation historically rare. | Strong buy zone when combined with TC ≥ 2 |
| BUY ZONE | 30 – 50 | Price is below neutral. Market has cooled off. Favourable entry conditions. | Buy signal when FROTH < 50 AND TC ≥ 2 |
| NEUTRAL | 50 – 80 | Price is near or modestly above trend. No extreme reading in either direction. | Hold if already in. No new entry signal. |
| ELEVATED | 80 – 95 | Price has moved significantly above trend. Market is heating up. Watch closely. | Hold position. Monitor for FROTH ≥ 95. |
| FROTHY | 95 – 100 | Extreme overheating. Price is far above trend. Historically precedes corrections. | Streak counter starts. Sell arm activates after 3+ consecutive days. |
How FROTH is approximated in this monitor
The original FROTH formula by @bitcoindata21 is not publicly disclosed. This monitor approximates it using the ratio of Bitcoin's current price to its 200-day Simple Moving Average (SMA), transformed with a logarithm:
// ratio = 1.0 → price exactly at 200d SMA → FROTH ≈ 76.1 // ratio > 1.0 → price above SMA → FROTH rises toward 100 // ratio < 1.0 → price below SMA → FROTH falls toward 0 FROTH = clamp( (ln(price / SMA200) + 0.7693) × 98.96, min=0, max=100 )
Calibration anchor points
The formula was calibrated using two anchor points derived from @bitcoindata21's published chart:
| Anchor | Condition | FROTH target |
|---|---|---|
| Buy zone (Mar 2026) | BTC ~$70k, SMA200 ~$96k → ratio ≈ 0.71 | ≈ 42–46 (below 50, buy fires) |
| Sell arm zone (Jul 2025) | Price ~21% above SMA200 → ratio ≈ 1.21 | ≈ 95 (sell arm threshold) |
Key FROTH reference values
| FROTH | Price vs SMA200 | Meaning |
|---|---|---|
| 50 | −23% below | Buy zone threshold |
| 76 | Exactly at SMA200 | Neutral (ratio = 1.0) |
| 90 | +15% above | Sell trigger (after armed) |
| 95 | +21% above | Sell arm threshold |
Triple Composite (TC)
The Triple Composite is a score from 0 to 3 that counts how many bullish market conditions are currently active. A score of 3 means all three conditions are met — the strongest buy signal. A score of 0 means no bullish conditions are active.
| Score | Meaning | Signal |
|---|---|---|
| 0 | No bullish conditions active. Bearish or uncertain environment. | No action |
| 1 | One bullish condition active. Not enough for entry. | No action |
| 2 | Two conditions active. Valid buy if FROTH is also below 50. | BUY if FROTH < 50 |
| 3 | All three conditions active. Unconditional strong buy signal. | BUY regardless of FROTH |
How TC is approximated in this monitor
The original Triple Composite formula is not publicly disclosed. This monitor uses three independent technical signals, each contributing +1 to the score:
| Component | Condition | What it means |
|---|---|---|
| +1 RSI FLOOR | RSI(14, daily) ≥ 40 | Momentum has recovered from oversold territory. RSI above 40 filters out panic-driven bottoms where a reversal hasn't started. |
| +1 RSI CAP | RSI(14, daily) ≤ 65 | Not overbought. RSI below 65 ensures momentum is healthy and not already extended. |
| +1 DIP ZONE | Price < SMA200 AND Price > SMA200 × 0.975 | SMA200 Dip Zone — price is below the 200-day average but within 2.5%. Catches "buy the dip" at long-term trend support. Naturally fails when price is above SMA200 or far below it. |
Why RSI [40–65] instead of [30–65]?
The RSI floor was raised from 30 to 40 after backtesting revealed that RSI=30–39 often coincides with the immediate aftermath of a sell signal (e.g. Aug 1, 2025: RSI=33, FROTH=89). At that point, momentum is still weak and a re-entry would be premature. Raising the floor to 40 prevents false re-buys in the days following a sell, while still catching genuine recovery entries (e.g. Mar 6, 2026: RSI=50.2).
Why SMA200 Dip Zone instead of price > SMA50?
The SMA200 Dip Zone (price within 0–2.5% below SMA200) catches the moment when price tests the long-term trend line as support. This is the classic "buy the dip" at the 200-day moving average. The condition naturally fails when price is well above SMA200 (bull run, no dip) or far below it (crash, not yet recovered to support). Combined with RSI 40–65, it identifies the precise window when price touches the 200-day level with healthy momentum.
Hybrid TC — backtest results
| Date | Price | RSI | FROTH | TC | Rule | Result |
|---|---|---|---|---|---|---|
| Aug 1, 2025 | $113,298 | 33 | 89.4 | 1 | — | SELL (FROTH streak triggered) |
| No Rule A (TC=3) candidates between sell and Mar 6 buy ✓ | ||||||
| Mar 6, 2026 | $68,114 | 50.2 | 42.3 | 2 | Rule B | BUY (FROTH<50 ×35d + TC≥2) |
Old TC component breakdown
| Date | p > SMA200 | p > SMA50 | RSI ∈ [30,65] | Score |
|---|---|---|---|---|
| Aug 1, 2025 | ✓ | ✓ | ✓ (33) | 3 |
| Mar 6, 2026 | ✗ | ✗ | ✓ (50.2) | 1 |
Current TC component breakdown (hybrid)
| Date | RSI ≥ 40 | RSI ≤ 65 | Dip Zone | Score |
|---|---|---|---|---|
| Aug 1, 2025 | ✗ (33) | ✓ | ✗ (price > SMA200) | 1 |
| Mar 6, 2026 | ✓ (50.2) | ✓ | ✗ (29% below SMA200) | 2 |
Buy Rules
A buy signal enters the position at 100%. There are two buy rules — either triggers a buy.
Sell Rules
The sell rule is a two-stage process: the sell must first be armed, and then triggered. This prevents premature exits during short FROTH spikes.
Why the two-stage design?
A single threshold exit (e.g. "sell when FROTH > 95") would trigger on short-lived spikes. The two-stage approach ensures the market has been sustainably overheated before committing to an exit. The 90 trigger (not 95) gives the market room to cool slightly before the exit fires — reducing whipsaws.
Signal State Machine
The monitor cycles through these states on every refresh:
If FROTH ≥ 95: increment streak. If streak ≥ 3: arm sell.
If FROTH < 95: reset streak to 0 (sell stays armed until triggered).
If IN market: check sell condition (armed + FROTH < 90).
If OUT of market: check buy conditions (TC=3 or FROTH<50+TC≥2).
Set position = IN. Record entry price and date. Reset streak. Send Discord alert.
Set position = OUT. Disarm sell. Calculate PnL from entry. Send Discord alert.
Update UI. Save state. Wait for next refresh.
Dashboard Values
| Field | Description | Color meaning |
|---|---|---|
| BTC Price | Latest daily close price from Binance BTCUSDT. Updates on every refresh. | Orange (always) |
| 24h % | 24-hour price change percentage from Binance ticker. | Green = up, Red = down |
| FROTH |
Current FROTH value (0–100). Shows (approx) when auto-calculated,
(manual) when overridden.
|
Green <50 · White 50–80 · Orange 80–95 · Red ≥95 |
| Triple Composite |
Current TC score shown as N / 3. The sub-line shows which of the
three components (SMA200 ✓/✗, SMA50 ✓/✗, RSI value) are currently active.
|
Muted = 0–1 · Cyan = 2 · Green = 3 |
| Signal | Current state of the rule engine: MONITORING, WATCH (FROTH heating), SELL ARMED, ⚠ SELL (trigger active), or ⚡ BUY. | Orange = watch/armed · Red = sell · Green = buy · Muted = idle |
| 200d SMA | 200-day Simple Moving Average of BTC close price. The sub-line shows the percentage difference between current price and the SMA. | Muted (informational only) |
| Position Banner | Shows whether you are currently IN (100%) or OUT (0%) of the market, plus entry date and price if in. | Green = in market · Red = out of market |
Streak Counter
The streak counter tracks how many consecutive calendar days FROTH has been at or above 95. It is shown as a number and as a row of dots below the FROTH card.
| Dot state | Meaning |
|---|---|
| Empty | This streak day has not yet been reached. |
| Red | FROTH has been ≥ 95 for this many consecutive days. Sell not yet armed (streak < 3). |
| Orange | Sell is ARMED. FROTH was ≥ 95 for 3+ days. Waiting for drop below 90 to trigger exit. |
When FROTH drops below 95 (but is still ≥ 90), the streak resets to 0 but the sell stays armed — it is waiting for the drop below 90. When FROTH eventually drops below 90, the sell triggers and the arm is cleared.
Discord Setup — Overview
The monitor sends alerts through a Discord bot you create and own. The bot calls the Discord REST API to post an embed message. You choose where it lands:
Regardless of mode, setup begins the same way: create a Discord application and a bot.
Step 1 — Create a Discord Application & Bot
Step 2 — Bot Permissions & Intents
Still on the Bot page in the developer portal:
| Setting | Value | Why |
|---|---|---|
| Public Bot | OFF (recommended) | Prevents anyone else from adding your bot to their server. |
| Requires OAuth2 Code Grant | OFF | Not needed for a bot-only integration. |
| Presence Intent | OFF | Not used by this monitor. |
| Server Members Intent | OFF | Not used by this monitor. |
| Message Content Intent | OFF | Not used — the bot only sends messages, never reads them. |
Scroll down to Bot Permissions. You do not need to set anything here — permissions are granted at the channel level when you invite the bot (Step 3).
Step 3 — Invite the Bot to Your Server
This step is only required for Channel mode. For DM mode, skip to Step 4.
bot
| Permission | Why needed |
|---|---|
| Send Messages | Post alerts into a channel |
| Embed Links | Render the coloured embed cards |
| View Channel | See the channel to post in it |
Restrict the bot to one channel (recommended)
After inviting, you can limit the bot to only the alert channel so it cannot see other channels:
@everyone role if you want a private alert channel.
Step 4 — Enable Developer Mode & Copy IDs
Discord IDs are large numeric strings (e.g. 1234567890123456789).
To see them you must enable Developer Mode in Discord.
Copy a Channel ID
Copy a User ID (for DM mode)
Step 5a — Channel Mode Setup
Use this mode to post alerts into a server text channel.
| Config field | Value |
|---|---|
| Bot Token | The token copied in Step 1 |
| Delivery Mode | Channel |
| Channel ID | The numeric channel ID copied in Step 4 |
Verify it works
Step 5b — DM Mode Setup
Use this mode to send alerts as a private direct message to a specific Discord user. The bot does not need to be in any server for this to work.
The easiest way: invite the bot to any private server you are both in, even a server you created just for this purpose.
| Config field | Value |
|---|---|
| Bot Token | The token copied in Step 1 |
| Delivery Mode | DM to user |
| User ID | The numeric User ID of the person to DM (copied in Step 4) |
How DM mode works internally
Discord does not let bots post DMs directly to a user ID. Instead the API uses a two-step process:
POST /users/@me/channels
with the target User ID. Discord returns a DM channel object with a channel ID.
If a DM channel already exists between the bot and the user, Discord reuses it.
POST /channels/{dm_channel_id}/messages
— the same endpoint used for channel mode, just with the DM channel ID.
This means every DM alert makes two API calls. Both are lightweight and within Discord's rate limits.
Verify it works
Alert Reference
All alerts are sent as Discord embeds with a colour-coded left border and footer.
| Alert type | Embed colour | Fields included |
|---|---|---|
| 🟢 BUY SIGNAL | Green #00c853 | BTC price · FROTH · TC score · which rule triggered (A or B) |
| 🔴 SELL SIGNAL | Red #ff3b30 | BTC price · FROTH · TC score · streak days · PnL % from entry price |
| 🟠 FROTH warning | Orange #ff8c00 | FROTH value · streak day number · BTC price |
| 🔵 Sell armed | Cyan #00bcd4 | FROTH value · BTC price · note waiting for drop below 90 |
De-duplication
Each signal type (buy / sell) is sent at most once per calendar day, regardless of how many times the monitor refreshes. The date of the last sent signal is stored in state and checked before every send.
FROTH warnings and armed warnings are also capped to once per calendar day each.
Discord — Coolify Environment Variables
The scheduled cron job (check.js) reads Discord credentials from
Docker environment variables. Set these in Coolify under
Application → Environment Variables.
| Variable | Value | Notes |
|---|---|---|
DISCORD_WEBHOOK |
Full webhook URL | Simplest option. No bot setup needed. If set, takes priority over bot token. |
DISCORD_TOKEN |
Bot token | Only needed for bot mode (channel or DM). Mark as Secret. |
DISCORD_CHANNEL |
Channel ID (numeric) | Bot → channel mode. Leave blank for DM mode. |
DISCORD_USER_ID |
User ID (numeric) | Bot → DM mode. Leave blank for channel mode. |
ALERT_LEVEL |
signals or all |
Default: signals. Use all for streak warnings too. |
DISCORD_WEBHOOK only. Create a webhook in Discord
(Channel Settings → Integrations → Webhooks → New Webhook → Copy URL) and paste the URL.
No bot token, no channel ID, no permissions setup needed.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
401 Unauthorized |
Bot token is wrong or was reset after you copied it | Go to developer portal → Bot → Reset Token → copy the new token and update config |
403 Missing Permissions |
Bot does not have Send Messages in that channel, or the DM target has DMs disabled |
Channel: check bot permissions in the channel settings. DM: the user must share a server with the bot and have DMs from server members allowed (User Settings → Privacy & Safety) |
404 Unknown Channel |
Channel ID is wrong or the bot cannot see that channel | Re-copy the Channel ID with Developer Mode on. Verify the bot is in the same server. |
400 Cannot send messages to this user |
DM mode: target user has direct messages disabled | Ask the user to enable Allow direct messages from server members in Discord → Settings → Privacy & Safety. Or use channel mode instead. |
429 Too Many Requests |
Rate limit hit (unlikely at normal refresh intervals) | Increase the refresh interval to 10+ minutes. Discord allows 5 requests/5s per channel. |
| No error but no message | Token or ID saved incorrectly; config not saved before testing | Click inside any Config field and trigger a save (the fields save on every keystroke). Check the Alert Log for any logged errors. |
| Bot is offline in server | Normal — bots using REST only do not show as online | Not an issue. The bot can still send messages while showing as offline. |
Manual Overrides
Because FROTH and Triple Composite are approximated, the Config panel lets you enter the real values to get accurate rule evaluation and alerts.
| Field | What to enter | Effect |
|---|---|---|
| Manual FROTH | A number from 0 to 100 (decimals allowed, e.g. 45.8) |
Replaces the auto-calculated FROTH for rule evaluation and display.
The badge changes from (approx) to (manual).
|
| Manual TC | An integer: 0, 1, 2, or 3 | Replaces the auto-calculated Triple Composite for rule evaluation and display. |
| Current Position | IN or OUT | Sets whether you are currently holding (100%) or not (0%). Critical for correct rule evaluation — buy rules only fire when OUT, sell rules only fire when IN. |
| Entry Price | The price you entered at (e.g. 68114) | Used to calculate PnL % in sell alert Discord messages. Does not affect rule evaluation. |
Threshold Reference Table
| Threshold | Indicator | Used in | Meaning |
|---|---|---|---|
FROTH < 50 |
FROTH | Buy Rule B | Market has cooled to below neutral. Combined with TC ≥ 2 = buy. |
FROTH ≥ 95 |
FROTH | Sell (stage 1: arm) | Extreme froth threshold. Each consecutive day above this increments the streak. |
FROTH < 90 |
FROTH | Sell (stage 2: trigger) | Confirms that the froth peak has passed. Fires exit if sell was armed. |
TC = 3 |
Triple Composite | Buy Rule A | All three bullish conditions active. Unconditional buy. |
TC ≥ 2 |
Triple Composite | Buy Rule B | At least two bullish conditions active. Buy only if FROTH < 50. |
Streak ≥ 3 |
FROTH streak | Sell (arm) | FROTH has been ≥ 95 for 3 or more consecutive calendar days. |
RSI 30–65 |
RSI(14, daily) | TC component (+1) | Momentum is in a healthy range: not oversold (<30) and not overbought (>65). |
Price Data Source
All BTC price data is fetched from the Binance public API — no account or API key required.
| Endpoint | Used for |
|---|---|
GET /api/v3/klines?symbol=BTCUSDT&interval=1d&limit=400 |
400 daily candles → SMA200, SMA50, RSI14, FROTH, TC calculation |
GET /api/v3/ticker/24hr?symbol=BTCUSDT |
24h price change % shown in the dashboard header |
Details
- Base URL:
https://api.binance.com - No API key needed — these are public endpoints
- Rate limit: 1200 requests/minute (well within limits at 4h intervals)
- Price used: daily close price (field index 4 of each kline)
- History: 400 days of daily candles — enough for SMA200 + buffer
- Refreshed: every time the page loads, on manual Check Now, and every 4h via the cron job
Fallback
If Binance is unreachable the check fails with an error logged to the alert log
(browser) or /var/log/btc-monitor.log (server). No alert is sent and
state is not updated. The next scheduled check will retry automatically.
Approximations & Limitations
FROTH approximation accuracy
The ln-based FROTH formula was calibrated against @bitcoindata21's published chart. It is directionally correct and closely matches key thresholds (sell arm at 95, buy zone below 50), but the actual FROTH may incorporate on-chain data or other proprietary inputs.
| Scenario | Price / SMA200 | This monitor | Notes |
|---|---|---|---|
| Mar 2026 (BTC ~$68k) | 0.71 | ≈ 42 | Buy zone — FROTH < 50 fires correctly |
| BTC at 200d SMA | 1.00 | 76.1 | Neutral — matches chart baseline |
| Jul 2025 sell arm | ≈ 1.21 | ≈ 95 | Sell arm threshold — calibrated anchor |
| BTC at 2× SMA200 | 2.00 | 100 (capped) | Maximum froth |
TC approximation accuracy
The TC formula uses RSI(14) ∈ [40, 65] and price > SMA50 as its three components. The RSI floor of 40 (vs the original 30) was chosen to prevent instant re-buys after sell signals when RSI is still depressed. The SMA200 condition was replaced by splitting RSI into two separate conditions (floor and cap) for finer momentum discrimination.
For the real FROTH and TC values, check @bitcoindata21 on X/Twitter.
RSI calculation note
The RSI used for TC is a simplified version (averages the last 14 absolute gains/losses). A full Wilder's RSI uses exponential smoothing and requires a warm-up period of at least 28 candles. The simplified version may differ by 1–5 RSI points in practice.
Browser alerts require the page to be open
The browser webapp only checks rules when the page is open and refreshing. For 24/7 automated alerts, use the Coolify / Docker deployment — the cron job runs every 4 hours regardless of whether the browser is open.
Calibration Log
History of formula changes and the reasoning behind each recalibration.
FROTH: log₂ → ln recalibration
FROTH = clamp( (log₂(price/SMA200) + 1) × 50, 0, 100 )
FROTH = clamp( (ln(price/SMA200) + 0.7693) × 98.96, 0, 100 )
Triple Composite: three iterations of calibration
TC = (price > SMA200) + (price > SMA50) + (RSI ∈ [30, 65])
TC = (RSI ≥ 40) + (RSI ≤ 65) + (price > SMA50)
TC = (RSI ≥ 40) + (RSI ≤ 65) + SMA200 Dip Zone (price < SMA200 AND > SMA200 × 0.975)
Key date verification
| Date | Event | v1 result | v3 result | Correct? |
|---|---|---|---|---|
| Aug 1, 2025 | Sell signal fires | TC=3 → instant re-buy | TC=1 → stays out | ✓ |
| Aug–Feb | Decline (should stay out) | Multiple TC=3 false buys | No TC=3 candidates | ✓ |
| Mar 6, 2026 | $68,114 buy via Rule B | TC=1 → no buy | TC=2, FROTH<50 ×35d → BUY | ✓ |
Hosting — Coolify
The monitor runs as a single Docker container managed by Coolify.
nginx serves the web UI, and a cron job runs check.js every 4 hours automatically.
| Setting | Value |
|---|---|
| Resource type | Dockerfile |
| Repository | https://github.com/soerenwa/btc-monitor |
| Branch | main |
| Base Directory | btc-monitor |
| Dockerfile | Dockerfile |
| Ports Exposes | 80 |
| Volume destination | /data (persists state.json) |
Environment variables
| Variable | Value |
|---|---|
DISCORD_WEBHOOK | Your webhook URL — mark as Secret |
ALERT_LEVEL | signals (default) or all |
DNS
Add an A record pointing your subdomain to the server IP. Coolify (Traefik) handles HTTPS automatically via Let's Encrypt once the DNS resolves.
/data volume and survives redeploys.
Push to main and click Redeploy in Coolify to update.