FROTH + Triple Composite — Documentation

Strategy by @bitcoindata21  |  Monitor webapp reference
← Back to Monitor

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.

IndicatorRoleRangeKey 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

0
Deep value
50
Neutral
90
Sell trigger
95
Sell arms
100
Peak froth
ZoneRangeMeaningAction
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:

AnchorConditionFROTH 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

FROTHPrice vs SMA200Meaning
50−23% belowBuy zone threshold
76Exactly at SMA200Neutral (ratio = 1.0)
90+15% aboveSell trigger (after armed)
95+21% aboveSell arm threshold
Important: This is an approximation. The real FROTH value by @bitcoindata21 likely incorporates on-chain data or other proprietary inputs and will differ slightly.

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.

ScoreMeaningSignal
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:

ComponentConditionWhat 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

DatePriceRSIFROTH TCRuleResult
Aug 1, 2025$113,2983389.4 1 SELL (FROTH streak triggered)
No Rule A (TC=3) candidates between sell and Mar 6 buy ✓
Mar 6, 2026$68,11450.242.3 2 Rule B BUY (FROTH<50 ×35d + TC≥2)

Old TC component breakdown

Datep > SMA200p > SMA50RSI ∈ [30,65]Score
Aug 1, 2025 (33) 3
Mar 6, 2026 (50.2) 1

Current TC component breakdown (hybrid)

DateRSI ≥ 40RSI ≤ 65Dip ZoneScore
Aug 1, 2025 (33) (price > SMA200) 1
Mar 6, 2026 (50.2) (29% below SMA200) 2
Important: This is an approximation of the real Triple Composite by @bitcoindata21. The actual formula may incorporate additional factors.

Buy Rules

A buy signal enters the position at 100%. There are two buy rules — either triggers a buy.

RULE A — Trend Dip Buy
Triple Composite = 3  AND  FROTH < 75  →  Enter 100%
All three conditions active: RSI 40–65 (healthy momentum) + price in SMA200 dip zone (0–2.5% below 200-day average) + FROTH not overheated (<75). This catches the classic "buy the dip" at the long-term trend line. TC=3 requires price to be just below SMA200, so the FROTH<75 gate prevents buys when FROTH is elevated.
RULE B — Deep Value Buy
FROTH < 50 for 35+ consecutive days  AND  Triple Composite2  →  Enter 100%
Sustained deep value entry — FROTH must stay below 50 for 35+ consecutive days before a buy is valid. This filters out premature entries during bear market rallies where FROTH briefly dips below 50. The 35-day requirement ensures the market has truly cooled off. TC ≥ 2 confirms at least RSI momentum is healthy (both RSI conditions passing).
Position must be OUT for buy rules to be evaluated. If already in market, buy rules are ignored.

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.

SELL RULE — Stage 1: ARM
FROTH95 for 3 or more consecutive days  →  Sell is ARMED
The market must stay extremely overheated for at least 3 full days in a row. A single day above 95 is not enough. Once armed, the streak counter turns orange.
SELL RULE — Stage 2: TRIGGER
Sell is ARMED  AND  FROTH drops below 90  →  Exit to 0%
After the sell is armed, the actual exit happens when FROTH falls back below 90. This confirms that the froth peak has passed and the market is cooling. The gap between 90 and 95 is intentional — it filters out brief dips within an ongoing frothy period.

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.

Position must be IN for sell rules to be evaluated. If out of market, sell rules are ignored.

Signal State Machine

The monitor cycles through these states on every refresh:

1 Fetch latest BTC daily closes from Binance (400 days)
2 Calculate: 200d SMA, 50d SMA, RSI(14) → compute FROTH and TC (or use manual override values if set)
3 Update streak — runs once per calendar day only.
If FROTH ≥ 95: increment streak. If streak ≥ 3: arm sell.
If FROTH < 95: reset streak to 0 (sell stays armed until triggered).
4 Evaluate rules
If IN market: check sell condition (armed + FROTH < 90).
If OUT of market: check buy conditions (TC=3 or FROTH<50+TC≥2).
BUY signal
Set position = IN. Record entry price and date. Reset streak. Send Discord alert.
SELL signal
Set position = OUT. Disarm sell. Calculate PnL from entry. Send Discord alert.
No signal
Update UI. Save state. Wait for next refresh.
5 Save state to localStorage (webapp) or state.json (GitHub Actions). Signals de-duplicated: same signal not re-sent on same calendar day.

Dashboard Values

FieldDescriptionColor 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 stateMeaning
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.
Important — once-per-day update: The streak only increments once per calendar day. If you refresh the page multiple times in the same day, the streak count will not double-count. The date of the last update is stored in state.

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:

Channel mode
Bot posts into a server channel. Anyone with access to that channel sees it. Requires the bot to be invited to your server.
DM mode
Bot sends a private DM to a specific Discord user. Only that user sees it. Does NOT require a server.

Regardless of mode, setup begins the same way: create a Discord application and a bot.

Step 1 — Create a Discord Application & Bot

1 Go to discord.com/developers/applications and sign in with your Discord account.
2 Click New Application (top right). Give it a name — e.g. FROTH Monitor — and click Create.
3 In the left sidebar click Bot. Then click Add BotYes, do it!
4 Under the bot's username, click Reset Token → confirm → copy the token that appears.

Store this token securely. It is shown only once. If you lose it, reset it again — the old token becomes invalid immediately. Never paste it into a public file or commit it to a public repo.

Step 2 — Bot Permissions & Intents

Still on the Bot page in the developer portal:

SettingValueWhy
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).

Save Changes before leaving this page.

Step 3 — Invite the Bot to Your Server

This step is only required for Channel mode. For DM mode, skip to Step 4.

1 In the developer portal left sidebar, go to OAuth2 → URL Generator.
2 Under Scopes, tick: bot
3 Under Bot Permissions that appears below, tick:

PermissionWhy needed
Send MessagesPost alerts into a channel
Embed LinksRender the coloured embed cards
View ChannelSee the channel to post in it
4 Copy the generated URL at the bottom of the page. Open it in your browser. Select your server from the dropdown and click Authorise.
5 The bot now appears in your server's member list as offline — that is normal. Bots that only post via REST API do not maintain a gateway connection.

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:

Right-click the target channel → Edit ChannelPermissions+ Add members or roles → find your bot → tick View Channel and Send Messages → Save. Then remove those permissions from the @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.

1 Open Discord → User Settings (gear icon bottom left) → Advanced → toggle Developer Mode ON.

Copy a Channel ID

In the server sidebar, right-click the channel you want alerts posted to → Copy Channel ID. Paste it into the monitor's Channel ID field.

Copy a User ID (for DM mode)

Option A Right-click any message the target user has sent in a server → Copy User ID.
or
Option B Click on the user's avatar anywhere in Discord to open their profile → click the (three dots) menu → Copy User ID.
or
Option C — your own ID Click your own avatar (bottom left) → in the profile popup, click the menu → Copy User ID. Use this if you want the bot to DM yourself.
Username ≠ User ID. The display name like John#1234 or @john cannot be used directly. You must use the numeric User ID.

Step 5a — Channel Mode Setup

Use this mode to post alerts into a server text channel.

Config fieldValue
Bot TokenThe token copied in Step 1
Delivery ModeChannel
Channel IDThe numeric channel ID copied in Step 4

Verify it works

1 Open the monitor webapp → click CONFIG to expand the panel.
2 Fill in Bot Token, set mode to Channel, fill in Channel ID.
3 Click Test Discord. A blue embed should appear in your channel within a few seconds.
4 If no message appears, check the Alert Log at the bottom of the monitor page for the Discord error code. See the Troubleshooting section.

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.

DM requirement: The target user must share at least one server with the bot, OR the user must have Allow direct messages from server members enabled in their Privacy & Safety settings. If neither condition is met, the API returns a 403 error.

The easiest way: invite the bot to any private server you are both in, even a server you created just for this purpose.
Config fieldValue
Bot TokenThe token copied in Step 1
Delivery ModeDM to user
User IDThe 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:

1 Open DM channel: the bot calls 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.
2 Send message: the bot calls 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

1 Open the monitor webapp → CONFIG.
2 Fill in Bot Token, set mode to DM to user, fill in the User ID.
3 Click Test Discord. A DM from your bot should appear in your Discord inbox.

Alert Reference

All alerts are sent as Discord embeds with a colour-coded left border and footer.

BUY
Signals only + All
SELL
Signals only + All
FROTH ≥ 95
All level only
Sell armed
All level only
Alert typeEmbed colourFields 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.

VariableValueNotes
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.
Recommended: use 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

ErrorCauseFix
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.

FieldWhat to enterEffect
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.
Leave any override blank to revert to auto-calculation. Values are saved to localStorage and persist across page reloads.

Threshold Reference Table

ThresholdIndicatorUsed inMeaning
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.

EndpointUsed 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.

ScenarioPrice / SMA200This monitorNotes
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

BEFORE
FROTH = clamp( (log₂(price/SMA200) + 1) × 50, 0, 100 )
Anchored at: ratio=1.0 → FROTH=50, ratio=2.0 → FROTH=100. Problem: FROTH peaked at ~94.3 during the Jul 2025 bull run — never reaching the 95 threshold needed to arm the sell signal. The sell never triggered.
AFTER
FROTH = clamp( (ln(price/SMA200) + 0.7693) × 98.96, 0, 100 )
Calibrated from two anchor points: (1) Mar 2026 buy zone at FROTH ≈ 42–46, (2) Jul 2025 sell arm at FROTH ≈ 95. The ln function provides better dynamic range at the extremes, correctly triggering both sell arms and buy signals.

Triple Composite: three iterations of calibration

v1 — ORIGINAL
TC = (price > SMA200) + (price > SMA50) + (RSI ∈ [30, 65])
Problem: On Aug 1 (after sell), RSI=33 + price above both SMAs → TC=3 → instant re-buy.
v2 — RSI SPLIT
TC = (RSI ≥ 40) + (RSI ≤ 65) + (price > SMA50)
Fixed Aug 1 re-buy, but TC=3 still fired during the decline (Jan 2, Jan 18, etc.) causing unwanted buys.
v3 — HYBRID (CURRENT)
TC = (RSI ≥ 40) + (RSI ≤ 65) + SMA200 Dip Zone (price < SMA200 AND > SMA200 × 0.975)
Dip Zone replaces SMA50: only fires when price tests SMA200 from below within 2.5%. Combined with Rule A (TC=3 + FROTH<75) and Rule B (FROTH<50 ×35d + TC≥2), this produces zero false signals between Aug 1 sell and Mar 6 buy.

Key date verification

DateEventv1 resultv3 resultCorrect?
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.

SettingValue
Resource typeDockerfile
Repositoryhttps://github.com/soerenwa/btc-monitor
Branchmain
Base Directorybtc-monitor
DockerfileDockerfile
Ports Exposes80
Volume destination/data (persists state.json)

Environment variables

VariableValue
DISCORD_WEBHOOKYour webhook URL — mark as Secret
ALERT_LEVELsignals (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.

After deploy: the cron job fires immediately on container start, then every 4 hours. State is persisted in the /data volume and survives redeploys. Push to main and click Redeploy in Coolify to update.