The chart shows you structure visually. The data export pipeline gives you the raw data behind it — every swing, every cycle count, every hierarchy position — for analysis in spreadsheets, databases, Python, R, or any tool you prefer.
STP offers two complementary export methods: CSV export for historical backfill (63 columns of hierarchical swing data via TradingView's native export), and webhook alerts for live forward feed (structured JSON payloads delivered to your endpoint on every confirmed swing). Both deliver the same atomic data and write to the same database schema.
Unlike SSI PRO's single-timeframe export, STP's export captures the state of all 10 timeframes simultaneously, giving you a complete multi-timeframe dataset whether you're downloading history or receiving live events.
How to Export
- Open your chart with STP applied
- In the indicator settings, navigate to Group ⑨ (Data Export)
- Enable "CSV Export"
- Right-click anywhere on the chart
- Select "Export chart data"
- Choose your date range and save the CSV file
The export includes every bar in the selected range. On bars where no swing occurred, the event columns (Swing_Event, Swing_Type) contain NaN/empty values while the state columns retain their last-known values.
<!-- IMAGE: stp-csv-export-menu.png — TradingView right-click menu with "Export chart data" highlighted. Show STP applied to the chart in the background. -->
Right-click the chart and select "Export chart data" to download the CSV file containing all 63 STP columns.
The 63 Columns
The export is organized into two categories: per-timeframe columns (6 columns × 10 timeframes = 60) and global columns (3).
Per-Timeframe Columns
Each of the 10 timeframes exports 6 columns using the naming pattern {TF}_{Field}:
| Column | Type | What It Contains |
|---|---|---|
{TF}_Cycle | Integer | The hierarchical cycle count — how many swings at this timeframe since the last parent-timeframe swing reset the counter. Resets to 1 when a parent swing confirms. |
{TF}_Phase | Integer | 1 = last swing was a High (market turned down), 0 = last swing was a Low (market turned up). Combined with Cycle, tells you both position and direction within the parent cycle. |
{TF}_Price | Float | The price at the most recent swing extreme for this timeframe. Updates when a new swing is confirmed. |
{TF}_Time | Unix timestamp | When the most recent swing extreme occurred. This is the timestamp of the bar where the high or low actually happened, not where it was confirmed. |
{TF}_Dir | Integer | Direction + pattern encoding. The sign indicates post-swing direction: positive = trending up, negative = trending down. The magnitude indicates pattern type: 1/-1 = regular swing, 2/-2 = OB reversal, 3/-3 = OB continuation. See Dir Encoding below. |
{TF}_TDays | Integer | Trading days elapsed since the previous swing at this timeframe. Only populated for Daily and intraday timeframes. Shows NaN for Weekly and above (use Time to calculate duration for those). |
Timeframe Prefixes
The 10 timeframe prefixes, from highest to lowest:
| Prefix | Timeframe | Example Column |
|---|---|---|
Y | Yearly | Y_Cycle, Y_Phase, Y_Price, Y_Time, Y_Dir, Y_TDays |
Q | Quarterly | Q_Cycle, Q_Phase, Q_Price, Q_Time, Q_Dir, Q_TDays |
M | Monthly | M_Cycle, M_Phase, M_Price, M_Time, M_Dir, M_TDays |
W | Weekly | W_Cycle, W_Phase, W_Price, W_Time, W_Dir, W_TDays |
3D | 3-Day | 3D_Cycle, 3D_Phase, 3D_Price, 3D_Time, 3D_Dir, 3D_TDays |
2D | 2-Day | 2D_Cycle, 2D_Phase, 2D_Price, 2D_Time, 2D_Dir, 2D_TDays |
D | Daily | D_Cycle, D_Phase, D_Price, D_Time, D_Dir, D_TDays |
4H | 4-Hour | 4H_Cycle, 4H_Phase, 4H_Price, 4H_Time, 4H_Dir, 4H_TDays |
1H | 1-Hour | 1H_Cycle, 1H_Phase, 1H_Price, 1H_Time, 1H_Dir, 1H_TDays |
15m | 15-Minute | 15m_Cycle, 15m_Phase, 15m_Price, 15m_Time, 15m_Dir, 15m_TDays |
Global Columns
Three columns capture cross-timeframe information:
| Column | Type | What It Contains |
|---|---|---|
Full_Hierarchy | Integer | The odometer — a single number encoding all 10 cycle counts. Each digit position represents one timeframe's count (Y in billions, Q in hundred millions, down to 15m in ones). Decode by extracting digits. |
Swing_Event | Integer | Which timeframe fired a swing on this bar. Uses rank indices: 0 = 15m through 9 = Y. NaN on bars with no swing event. ⚠️ Known limitation: when multiple timeframes fire on the same bar, only the last one is recorded. Use per-TF Phase columns for reliable extraction (see Recommended Extraction Method below). |
Swing_Type | Integer | Direction of the swing that fired: 1 = High, -1 = Low. NaN on bars with no event. ⚠️ Same limitation as Swing_Event — overwritten when multiple TFs fire on the same bar. |
Understanding the Data
State Columns vs. Event Columns
This distinction is critical for working with the data correctly.
State columns (_Cycle, _Phase, _Price, _Time, _Dir, _TDays) persist their values across bars. Once a Weekly swing high confirms and sets W_Phase to 1, that value remains 1 on every subsequent bar until the next Weekly swing changes it. These columns represent the current state of each timeframe — what's true right now.
Event columns (Swing_Event, Swing_Type) are populated only on bars where a swing was confirmed. On all other bars, they contain NaN. These columns mark the moments when something happened.
Practical consequence: If you filter the CSV to rows where Swing_Event is not NaN, you get a table of all swing events with their full hierarchical context. If you read the data row-by-row, the state columns give you a snapshot of all 10 timeframes at every bar.
Reading the Odometer (Full_Hierarchy)
The Full_Hierarchy column encodes all 10 cycle counts into a single integer using positional notation:
Full_Hierarchy = Y×10⁹ + Q×10⁸ + M×10⁷ + W×10⁶ + 3D×10⁵ + 2D×10⁴ + D×10³ + 4H×10² + 1H×10¹ + 15m×10⁰
Example: Full_Hierarchy = 1203007014
| Position | Digit | Meaning |
|---|---|---|
| Billions | 1 | Y_Cycle = 1 (1st yearly swing) |
| Hundred millions | 2 | Q_Cycle = 2 (2nd quarterly swing) |
| Ten millions | 0 | M_Cycle = 0 (no monthly swing yet) |
| Millions | 3 | W_Cycle = 3 (3rd weekly swing) |
| Hundred thousands | 0 | 3D_Cycle = 0 (no 3D swing yet) |
| Ten thousands | 0 | 2D_Cycle = 0 (no 2D swing yet) |
| Thousands | 7 | D_Cycle = 7 (7th daily swing) |
| Hundreds | 0 | 4H_Cycle = 0 (no 4H swing yet) |
| Tens | 1 | 1H_Cycle = 1 (1st hourly swing) |
| Ones | 4 | 15m_Cycle = 4 (4th 15m swing) |
To decode in Python:
def decode_hierarchy(value):
s = str(int(value)).zfill(10)
labels = ['Y', 'Q', 'M', 'W', '3D', '2D', 'D', '4H', '1H', '15m']
return {labels[i]: int(s[i]) for i in range(10)}
# decode_hierarchy(1203007014)
# → {'Y': 1, 'Q': 2, 'M': 0, 'W': 3, '3D': 0, '2D': 0, 'D': 7, '4H': 0, '1H': 1, '15m': 4}Note: The odometer uses single digits per position, so cycle counts above 9 at any level will overflow into the adjacent position. This is rare in practice — a timeframe would need 10+ swings before its parent resets the counter — but be aware of it when decoding very long datasets.
Dir Encoding
The _Dir column encodes both the post-swing direction and the swing pattern in a single integer:
| Dir Value | Post-Swing Direction | Pattern | Meaning |
|---|---|---|---|
1 | Up | Regular | Standard reversal, trend now moving up |
-1 | Down | Regular | Standard reversal, trend now moving down |
2 | Up | OB Reversal | Outside Bar reversed the trend, now moving up |
-2 | Down | OB Reversal | Outside Bar reversed the trend, now moving down |
3 | Up | OB Continuation | Outside Bar continued the trend via zigzag, now moving up |
-3 | Down | OB Continuation | Outside Bar continued the trend via zigzag, now moving down |
Decoding:
- Direction = sign of the value (positive = up, negative = down)
- Pattern = absolute value (1 = regular, 2 = ob_reversal, 3 = ob_continuation)
Python:
dir_val = int(float(row["D_Dir"]))
direction = "up" if dir_val > 0 else "down"
pattern = {1: "regular", 2: "ob_reversal", 3: "ob_continuation"}.get(abs(dir_val), "regular")Excel:
Direction: =IF(D_Dir > 0, "up", "down")
Pattern: =SWITCH(ABS(D_Dir), 1, "regular", 2, "ob_reversal", 3, "ob_continuation")This encoding uses no additional CSV columns — pattern information piggybacks on the existing Dir column. Prior to v2.0.1, Dir only used values 1 and -1. Code that reads Dir as a simple direction indicator will still work (positive = up, negative = down) but will miss the pattern information.
Reading Phase + Dir Together
_Phase and _Dir are related but encode different information:
_Phasetells you the type of the last confirmed swing:1= the last swing was a High (Top),0= the last swing was a Low (Bottom)_Dirtells you the current trend direction AND the pattern type: the sign indicates direction (positive = up, negative = down), and the magnitude indicates the pattern (1 = regular, 2 = ob_reversal, 3 = ob_continuation)
For simple direction checks, the sign relationship still holds: when Phase = 1 (last swing was High), Dir is negative (now trending down). When Phase = 0 (last swing was Low), Dir is positive (now trending up).
The magnitude adds pattern context that wasn't previously available in the CSV export. A Dir value of 3 means the swing was produced by an OB continuation pattern and the trend is now moving up — useful for filtering analysis by pattern type.
CSV Data Only Mode
For deep historical exports, STP offers a mode that bypasses TradingView's 500-label drawing limit.
The problem: TradingView limits visual labels to 500 per indicator instance. On a daily chart with 50 years of data and 4 active timeframes, you'd hit this limit quickly — meaning the indicator would only show labels for recent history.
The solution: Enable "CSV Data Only (No Labels)" in Group ⑨ (Data Export). This runs all swing detection, classification, hierarchy tracking, and CSV state computation on every bar — but skips label and line drawing entirely. Without visual objects consuming the 500-label budget, the indicator processes the full chart history.
What you get: A complete CSV with all 63 columns covering the entire visible date range, including decades of data on instruments with long histories (like major stock indices or commodities).
What you lose: No labels or lines appear on the chart. The chart looks as if the indicator isn't applied (except for the settings panel being visible). This mode is purely for data extraction.
When to use it: Building historical databases, backtesting across decades, academic research on cycle behavior, or any workflow where you need complete data and don't need visual chart output.
Working with the Data
Recommended Extraction Method
The reliable way to extract swing events is per-TF phase change detection. For each timeframe, scan its {TF}_Phase column row by row. When the value changes (0→1 or 1→0), that row marks a confirmed swing:
- Phase changes from 0 to 1 → Swing High (Top)
- Phase changes from 1 to 0 → Swing Low (Bottom)
Python:
import pandas as pd
df = pd.read_csv('export.csv')
# Extract Weekly swings via phase change
prev_phase = None
weekly_swings = []
for idx, row in df.iterrows():
phase = row.get('W_Phase')
if pd.isna(phase):
continue
if prev_phase is not None and phase != prev_phase:
weekly_swings.append({
'date': row['time'],
'direction': 'HIGH' if phase == 1 else 'LOW',
'price': row['W_Price'],
'time': row['W_Time'],
'pattern': {1: 'regular', 2: 'ob_reversal', 3: 'ob_continuation'}.get(abs(int(row['W_Dir'])), 'regular'),
'cycle': row['W_Cycle']
})
prev_phase = phaseWhy not use Swing_Event? The Swing_Event and Swing_Type global columns have a known limitation: when multiple timeframes fire on the same bar, only the last one is recorded — the others are overwritten. Per-TF Phase columns are independent and never overwrite each other.
This method guarantees strict alternation (HIGH-LOW-HIGH-LOW) by construction and captures all swing events including OB continuation pairs.
Extracting All Timeframes
Repeat the phase-change scan for each timeframe you need:
tf_list = ['D', '2D', '3D', 'W', 'M', 'Q', 'Y'] # adjust for your chart timeframe
all_swings = []
for tf in tf_list:
phase_col = f'{tf}_Phase'
prev_phase = None
for idx, row in df.iterrows():
phase = row.get(phase_col)
if pd.isna(phase):
continue
if prev_phase is not None and phase != prev_phase:
all_swings.append({
'timeframe': tf,
'direction': 'HIGH' if phase == 1 else 'LOW',
'price': row[f'{tf}_Price'],
'swing_time': row[f'{tf}_Time'],
'pattern': {1: 'regular', 2: 'ob_reversal', 3: 'ob_continuation'}.get(
abs(int(float(row[f'{tf}_Dir']))), 'regular'),
'cycle': row[f'{tf}_Cycle']
})
prev_phase = phase
swings_df = pd.DataFrame(all_swings).sort_values('swing_time')Converting Timestamps
The _Time columns use Unix timestamps in milliseconds (milliseconds since January 1, 1970). Convert to readable dates:
Python:
from datetime import datetime, timezone
timestamp_ms = 1731801600000
date = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc)
# → 2024-11-17 00:00:00+00:00JavaScript:
const timestamp_ms = 1731801600000;
const date = new Date(timestamp_ms); // JS Date() accepts milliseconds directlyExcel:
=A1/86400 + DATE(1970,1,1)
Format the cell as Date/Time.
Note on dates: The _Time column uses session close timestamps, which match the trading date shown on the TradingView chart axis. For futures instruments with overnight sessions (e.g., ES1!, CL1!), this ensures a Monday swing shows as Monday — not Sunday evening when the session opened.
Building a Swing Timeline
Use the per-TF phase-change extraction to build a chronological timeline across all timeframes:
import pandas as pd
from datetime import datetime, timezone
df = pd.read_csv('export.csv')
tf_list = ['D', '2D', '3D', 'W', 'M', 'Q', 'Y']
pattern_map = {1: 'regular', 2: 'ob_reversal', 3: 'ob_continuation'}
timeline = []
for tf in tf_list:
prev_phase = None
for _, row in df.iterrows():
phase = row.get(f'{tf}_Phase')
if pd.isna(phase):
continue
if prev_phase is not None and phase != prev_phase:
ts = row[f'{tf}_Time']
timeline.append({
'date': datetime.fromtimestamp(ts / 1000, tz=timezone.utc).strftime('%Y-%m-%d'),
'timeframe': tf,
'direction': 'High' if phase == 1 else 'Low',
'price': row[f'{tf}_Price'],
'pattern': pattern_map.get(abs(int(float(row[f'{tf}_Dir']))), 'regular')
})
prev_phase = phase
timeline_df = pd.DataFrame(timeline).sort_values('date')This produces a clean chronological table of every swing across all timeframes, with correct dates and pattern types.
Detecting Cycle Resets
When a parent timeframe confirms a swing, all child counters reset. You can detect these resets by watching for drops in cycle counts:
# Detect weekly cycle resets (triggered by monthly swings)
df['W_Cycle_prev'] = df['W_Cycle'].shift(1)
resets = df[(df['W_Cycle'] == 1) & (df['W_Cycle_prev'] > 1)]Each reset row corresponds to a bar where a Monthly (or higher) swing confirmed and reset the Weekly counter to 1.
Measuring Cycle Maturity
The cycle count tells you where you are within a parent cycle. If historical Monthly cycles typically contain 8–12 Weekly swings, and the current W_Cycle is 11, the cycle is mature — a Monthly-level reversal may be approaching.
# Weekly cycle lengths (number of weekly swings per monthly cycle)
monthly_swings = df[df['Swing_Event'] == 7]
cycle_lengths = monthly_swings['W_Cycle'].tolist()
print(f"Average weekly swings per monthly cycle: {sum(cycle_lengths) / len(cycle_lengths):.1f}")
print(f"Range: {min(cycle_lengths)} to {max(cycle_lengths)}")STP + SSI PRO Dual Export
If you run both STP and SSI PRO on the same chart, both indicators contribute their columns to the CSV export. This gives you the best of both worlds:
- STP columns (63): Multi-timeframe hierarchy, cycle counts, cross-timeframe state
- SSI PRO columns: Single-timeframe detail — ExtremeTimestamp vs ConfirmTimestamp, pattern codes, bar index
The combined export lets you cross-reference single-timeframe swing detail (from SSI PRO) with multi-timeframe context (from STP) in the same dataset.
Workflow:
- Apply both indicators to the same chart
- Enable CSV Export in both indicators' settings
- Right-click → Export chart data
- The resulting CSV contains columns from both indicators plus TradingView's standard OHLCV
Running STP and SSI PRO together provides the most comprehensive swing data export — multi-timeframe hierarchy plus single-timeframe detail in one file.
Building a Swing Database
CSV export and webhook alerts are two halves of the same data pipeline. CSV gives you the historical backfill — every swing the indicator has ever detected across the full chart history. Webhook alerts give you the live forward feed — every new swing as it confirms in real time. Both deliver the same atomic data, so they write to the same database table.
Historical: CSV Export → Import Script → Database
Live: Webhook Alert → Receiver → Database
↓
User Queries
(Python / SQL / Excel)Database Schema
The recommended schema stores one row per swing event with atomic fields only. No derived metrics are stored — range, duration, ratios, and velocity are all computed at query time from the atomic values. This avoids redundancy, prevents conflicts across timeframes with different natural units, and gives you full flexibility to derive whatever you need.
CREATE TABLE swing_events (
id SERIAL PRIMARY KEY,
received_at TIMESTAMP DEFAULT NOW(),
instrument VARCHAR(20),
timeframe VARCHAR(5),
direction VARCHAR(4),
price DECIMAL(18, 8),
swing_time BIGINT,
cycle_count INTEGER,
phase VARCHAR(6),
pattern VARCHAR(20),
hierarchy_tag VARCHAR(100),
volume DECIMAL(18, 2),
c_15m INTEGER,
c_1H INTEGER,
c_4H INTEGER,
c_D INTEGER,
c_2D INTEGER,
c_3D INTEGER,
c_W INTEGER,
c_M INTEGER,
c_Q INTEGER,
c_Y INTEGER,
UNIQUE(instrument, swing_time, timeframe)
);
CREATE INDEX idx_instrument_tf ON swing_events(instrument, timeframe);
CREATE INDEX idx_swing_time ON swing_events(swing_time);Why atomic fields only: The swing_time column is a Unix millisecond timestamp — universal and unit-agnostic. To compute duration, subtract the previous swing's timestamp. To compute range, subtract the previous swing's price. This works identically whether the swing is a 15-minute swing (where duration is minutes) or a yearly swing (where duration is months). No unit assumptions are baked into the schema.
Why per-timeframe cycle columns: The 10 c_* columns store a snapshot of every timeframe's cycle count at the moment the swing confirmed. This enables direct nesting queries — "show me all Daily swings that occurred during the 7th Weekly cycle" is a simple WHERE clause, not a string-parsing exercise.
CSV Import Workflow
- Export historical data — Use CSV Data Only mode for full history
- Parse the CSV per timeframe — For each TF, scan
{TF}_Phaserow by row. When the phase value changes, that row is a swing event - Extract swing data — Pull
{TF}_Price,{TF}_Time, and decode pattern from{TF}_Dir(1=regular, 2=ob_reversal, 3=ob_continuation) - Sort chronologically — Sort all extracted swings by
swing_time, with higher TFs first for same-timestamp events (ensures correct parent-before-child hierarchy computation) - Handle same-time collisions — If two swings at the same TF share the same timestamp (OB zigzag pairs on 2D), offset the second by 1ms
- Insert rows — One row per swing event. If using a database trigger for hierarchy computation, insert oldest first
- Verify — Run alternation check (zero violations expected) and pattern distribution check
A production-grade Python importer (import_csv.py) is available in the STP Pipeline Manual that handles all these steps automatically, including validation and error reporting.
Webhook Alerts — Live Data Feed
Webhook alerts complement CSV export by delivering swing data in real time. Every confirmed swing fires a structured JSON payload to a URL you specify — the same atomic data that the CSV contains, arriving automatically as new swings are detected.
How It Works
- Enable "Webhook Alerts" in the indicator settings (Group ⑨ Data Export)
- Create a TradingView alert on the indicator (right-click → Add Alert → select STP)
- In the alert dialog, check "Webhook URL" and enter your endpoint
- Save the alert — it now runs on TradingView's servers
Every confirmed swing across all enabled timeframes fires a single JSON payload to your webhook URL. A Weekly swing high, a Daily swing low, a 15-minute reversal — each one becomes one HTTP POST to your endpoint.
<!-- IMAGE: stp-webhook-setup.png — TradingView "Create Alert" dialog with the webhook URL field highlighted. Show STP selected as the condition. -->
Create a TradingView alert on STP and enter your webhook URL — every confirmed swing is delivered automatically.
One Chart, One Alert, All Timeframes
Because STP detects all 10 timeframes simultaneously on a single chart, you only need one alert per instrument. Place the indicator on a 15-minute chart (to capture all timeframes), enable the timeframes you want, enable the webhook toggle, and create the alert. That single alert covers everything from 15-minute swings to yearly swings on that instrument.
Chart timeframe determines which timeframes are captured:
| Chart Timeframe | Captures |
|---|---|
| 15-Min or less | All 10 (15m through Y) |
| 1-Hour | 1H through Y |
| Daily | D, 2D, 3D, W, M, Q, Y |
For the broadest coverage, use a 15-minute chart.
Multiple instruments: Each instrument needs its own alert. Tracking 5 instruments requires 5 alerts — one per symbol, each on its own chart instance with STP configured and the webhook toggle enabled.
Alert Behavior
Webhook alerts only fire on confirmed bars — the final update of a closed bar. This prevents historical replay alerts from flooding your endpoint when the alert is first created. When TradingView creates an alert, it runs a brief warm-up replay of recent history; the confirmation gate ensures no phantom alerts are generated during this replay.
Once a TradingView alert is created, it runs independently on TradingView's servers. You can change the chart timeframe, switch to a different symbol, or even remove the indicator from the chart — the alert continues running with the exact settings it had when you created it.
Important: The alert is a frozen snapshot. If you later change indicator settings (enable a new timeframe, switch OB mode, etc.), the existing alert does not pick up those changes. You need to delete the old alert and create a new one.
TradingView alert limits: The number of active alerts depends on your TradingView subscription plan. Since each instrument requires a separate alert, users tracking many instruments may need a higher-tier TradingView plan.
Webhook Payload
Each swing event delivers a JSON payload with 12 atomic fields plus 10 per-timeframe cycle counts:
{
"instrument": "ES1!",
"timeframe": "W",
"direction": "HIGH",
"price": 5843.5,
"swing_time": 1740700800000,
"cycle_count": 7,
"phase": "T",
"pattern": "regular",
"hierarchy_tag": "Y1.Q3.M2.W7",
"volume": 24518430,
"cycles": {
"15m": 42, "1H": 12, "4H": 5, "D": 3,
"2D": 4, "3D": 5, "W": 7, "M": 2, "Q": 3, "Y": 1
}
}Payload Field Reference
| Field | Type | Description |
|---|---|---|
instrument | String | The ticker symbol (e.g., "ES1!", "EURUSD", "BTCUSD") |
timeframe | String | Which timeframe fired: "15m", "1H", "4H", "D", "2D", "3D", "W", "M", "Q", "Y" |
direction | String | "HIGH" (swing high) or "LOW" (swing low) |
price | Float | The exact price at the swing extreme |
swing_time | Integer | Unix timestamp in milliseconds — when the swing extreme occurred |
cycle_count | Integer | The hierarchical cycle count for the firing timeframe |
phase | String | "T" (Top — last swing was a high) or "B" (Bottom — last swing was a low) |
pattern | String | How the swing was detected: "regular", "ob_continuation", "ob_reversal" |
hierarchy_tag | String | Full odometer position (e.g., "Y1.Q3.M2.W7") |
volume | Float | Total volume accumulated during the swing |
cycles | Object | Snapshot of all 10 TF cycle counts at the moment of confirmation |
The cycles object contains keys for all 10 timeframes (15m, 1H, 4H, D, 2D, 3D, W, M, Q, Y), each with the current cycle count. This is the key to nesting queries — when a Daily swing fires, cycles.W tells you which Weekly cycle it occurred in.
Receiving Webhooks
TradingView sends an HTTP POST with the JSON payload to your webhook URL. You need a receiver that parses the JSON and inserts it into your database. This can be minimal — a Python Flask app with SQLite is roughly 30 lines:
from flask import Flask, request
import sqlite3, json
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
data = json.loads(request.data)
conn = sqlite3.connect('swings.db')
c = data.get('cycles', {})
conn.execute('''INSERT OR IGNORE INTO swing_events
(instrument, timeframe, direction, price, swing_time,
cycle_count, phase, pattern, hierarchy_tag, volume,
c_15m, c_1H, c_4H, c_D, c_2D, c_3D, c_W, c_M, c_Q, c_Y)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''',
(data['instrument'], data['timeframe'], data['direction'],
data['price'], data['swing_time'], data['cycle_count'],
data['phase'], data['pattern'], data['hierarchy_tag'],
data['volume'], c.get('15m',0), c.get('1H',0), c.get('4H',0),
c.get('D',0), c.get('2D',0), c.get('3D',0), c.get('W',0),
c.get('M',0), c.get('Q',0), c.get('Y',0)))
conn.commit()
return 'OK', 200
if __name__ == '__main__':
app.run(port=5000)For production use, consider cloud-hosted receivers: Supabase Edge Functions, AWS Lambda with API Gateway, Google Cloud Functions, or no-code platforms like Make.com or Zapier that can route webhooks directly to Google Sheets, Airtable, or Notion databases.
CSV + Webhook Together
The typical workflow combines both:
- Backfill: Export historical data via CSV, import into your database
- Go live: Enable webhook alerts, point them at your receiver
- Done: Historical data and live data flow into the same table automatically
The UNIQUE(instrument, swing_time, timeframe) constraint prevents duplicates if a CSV re-export overlaps with webhook data.
Querying Your Database
Once swing data is in the swing_events table — whether from CSV import, webhook, or both — all analysis happens through standard SQL queries. Every metric you'd compute in Python or Excel can be expressed as a query against the atomic fields.
Basic Queries
-- All weekly swing lows for ES in the last 6 months
SELECT * FROM swing_events
WHERE instrument = 'ES1!'
AND timeframe = 'W'
AND direction = 'LOW'
AND swing_time > EXTRACT(EPOCH FROM NOW() - INTERVAL '6 months') * 1000;
-- Most recent swing at every timeframe for a given instrument
SELECT DISTINCT ON (timeframe) *
FROM swing_events
WHERE instrument = 'EURUSD'
ORDER BY timeframe, swing_time DESC;Nesting Queries (Using Per-TF Cycle Columns)
The c_* columns make nesting queries direct — no string parsing or integer decoding needed.
-- All Daily swings that occurred during the 7th Weekly cycle
SELECT * FROM swing_events
WHERE instrument = 'ES1!'
AND timeframe = 'D'
AND c_W = 7;
-- How many Daily swings occur in each Weekly cycle? (cycle length distribution)
SELECT c_W, COUNT(*) as daily_swings
FROM swing_events
WHERE instrument = 'ES1!' AND timeframe = 'D'
GROUP BY c_W
ORDER BY c_W;Derived Metrics (Duration, Range, Ratios)
Duration and range are computed from sequential rows using window functions. No pre-stored derived fields needed.
-- Add duration and range to each swing using the previous swing at the same TF
SELECT *,
price - LAG(price) OVER w AS range,
(swing_time - LAG(swing_time) OVER w) / 86400000.0 AS duration_days,
(swing_time - LAG(swing_time) OVER w) / 3600000.0 AS duration_hours
FROM swing_events
WHERE instrument = 'ES1!' AND timeframe = 'W'
WINDOW w AS (PARTITION BY instrument, timeframe ORDER BY swing_time);The user chooses the duration unit that fits the timeframe — /86400000 for days on Weekly swings, /3600000 for hours on 4-Hour swings, /60000 for minutes on 15-minute swings. One timestamp field, any unit.
Confluence Detection
Find where multiple timeframes detected swings at the same point (within a time window):
-- Swings from different TFs within 3 days of each other
SELECT a.timeframe AS tf1, b.timeframe AS tf2,
a.direction, a.price, a.swing_time
FROM swing_events a
JOIN swing_events b
ON a.instrument = b.instrument
AND a.direction = b.direction
AND a.timeframe < b.timeframe
AND ABS(a.swing_time - b.swing_time) < 259200000 -- 3 days in ms
WHERE a.instrument = 'ES1!';Cycle Maturity Analysis
-- Average number of Weekly swings per Monthly cycle
SELECT AVG(c_W) AS avg_weekly_per_monthly,
MIN(c_W) AS min, MAX(c_W) AS max
FROM swing_events
WHERE instrument = 'ES1!'
AND timeframe = 'M';Complete Column Reference
All 63 columns in export order, from highest timeframe to lowest:
| # | Column | Type | Category |
|---|---|---|---|
| 1 | Y_Cycle | Integer | Yearly state |
| 2 | Y_Phase | Integer | Yearly state |
| 3 | Y_Price | Float | Yearly state |
| 4 | Y_Time | Unix timestamp | Yearly state |
| 5 | Y_Dir | Integer | Yearly state |
| 6 | Y_TDays | Integer | Yearly state |
| 7 | Q_Cycle | Integer | Quarterly state |
| 8 | Q_Phase | Integer | Quarterly state |
| 9 | Q_Price | Float | Quarterly state |
| 10 | Q_Time | Unix timestamp | Quarterly state |
| 11 | Q_Dir | Integer | Quarterly state |
| 12 | Q_TDays | Integer | Quarterly state |
| 13 | M_Cycle | Integer | Monthly state |
| 14 | M_Phase | Integer | Monthly state |
| 15 | M_Price | Float | Monthly state |
| 16 | M_Time | Unix timestamp | Monthly state |
| 17 | M_Dir | Integer | Monthly state |
| 18 | M_TDays | Integer | Monthly state |
| 19 | W_Cycle | Integer | Weekly state |
| 20 | W_Phase | Integer | Weekly state |
| 21 | W_Price | Float | Weekly state |
| 22 | W_Time | Unix timestamp | Weekly state |
| 23 | W_Dir | Integer | Weekly state |
| 24 | W_TDays | Integer | Weekly state |
| 25 | 3D_Cycle | Integer | 3-Day state |
| 26 | 3D_Phase | Integer | 3-Day state |
| 27 | 3D_Price | Float | 3-Day state |
| 28 | 3D_Time | Unix timestamp | 3-Day state |
| 29 | 3D_Dir | Integer | 3-Day state |
| 30 | 3D_TDays | Integer | 3-Day state |
| 31 | 2D_Cycle | Integer | 2-Day state |
| 32 | 2D_Phase | Integer | 2-Day state |
| 33 | 2D_Price | Float | 2-Day state |
| 34 | 2D_Time | Unix timestamp | 2-Day state |
| 35 | 2D_Dir | Integer | 2-Day state |
| 36 | 2D_TDays | Integer | 2-Day state |
| 37 | D_Cycle | Integer | Daily state |
| 38 | D_Phase | Integer | Daily state |
| 39 | D_Price | Float | Daily state |
| 40 | D_Time | Unix timestamp | Daily state |
| 41 | D_Dir | Integer | Daily state |
| 42 | D_TDays | Integer | Daily state |
| 43 | 4H_Cycle | Integer | 4-Hour state |
| 44 | 4H_Phase | Integer | 4-Hour state |
| 45 | 4H_Price | Float | 4-Hour state |
| 46 | 4H_Time | Unix timestamp | 4-Hour state |
| 47 | 4H_Dir | Integer | 4-Hour state |
| 48 | 4H_TDays | Integer | 4-Hour state |
| 49 | 1H_Cycle | Integer | 1-Hour state |
| 50 | 1H_Phase | Integer | 1-Hour state |
| 51 | 1H_Price | Float | 1-Hour state |
| 52 | 1H_Time | Unix timestamp | 1-Hour state |
| 53 | 1H_Dir | Integer | 1-Hour state |
| 54 | 1H_TDays | Integer | 1-Hour state |
| 55 | 15m_Cycle | Integer | 15-Min state |
| 56 | 15m_Phase | Integer | 15-Min state |
| 57 | 15m_Price | Float | 15-Min state |
| 58 | 15m_Time | Unix timestamp | 15-Min state |
| 59 | 15m_Dir | Integer | 15-Min state |
| 60 | 15m_TDays | Integer | 15-Min state |
| 61 | Full_Hierarchy | Integer | Global |
| 62 | Swing_Event | Integer | Global |
| 63 | Swing_Type | Integer | Global |
Next Steps
- Tooltips & Metrics — The same data shown visually in chart tooltips
- Settings — Data Export settings (Group ⑨) including CSV and Webhook configuration
- Overview — How the data pipeline fits into the broader STP system
- Glossary — Definitions for all terms used in this page