Repository navigation
Expand file tree
/
Copy pathSC_Range_Bounce_Auto.cpp
More file actions
481 lines (404 loc) · 17.6 KB
/
Copy pathSC_Range_Bounce_Auto.cpp
File metadata and controls
481 lines (404 loc) · 17.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
// SC Range Bounce Strategy - EXACT MATCH TO PYTHON BACKTEST
// ==========================================================
//
// FOR 1-SECOND CHARTS ONLY!
//
// This study replicates the Python backtest logic EXACTLY:
// 1. Range PERSISTS until broken (not recalculated every bar)
// 2. Range STAYS ACTIVE after target hit (can re-enter)
// 3. Range INVALIDATED after stop hit (cooldown starts)
// 4. Entry only when bars_in_range >= min_bars_in_range
// 5. Entry at EXACT boundary (limit order)
// 6. ATR filter for MEDIUM volatility only
// 7. Session hours filter (European session)
// 8. 1.5R max loss cap
#include "sierrachart.h"
SCDLLName("SC Range Bounce Auto")
/*==========================================================================*/
SCSFExport scsf_RangeBounceAuto(SCStudyInterfaceRef sc)
{
// Subgraphs
SCSubgraphRef Subgraph_RangeHigh = sc.Subgraph[0];
SCSubgraphRef Subgraph_RangeLow = sc.Subgraph[1];
SCSubgraphRef Subgraph_ATR = sc.Subgraph[2];
// Inputs
SCInputRef Input_Enabled = sc.Input[0];
SCInputRef Input_LookbackBars = sc.Input[1];
SCInputRef Input_RangePct = sc.Input[2];
SCInputRef Input_MinRangeSize = sc.Input[3];
SCInputRef Input_MinBarsInRange = sc.Input[4];
SCInputRef Input_TargetBuffer = sc.Input[5];
SCInputRef Input_StopBeyond = sc.Input[6];
SCInputRef Input_CooldownBars = sc.Input[7];
SCInputRef Input_ATRPeriod = sc.Input[8];
SCInputRef Input_ATRMin = sc.Input[9];
SCInputRef Input_ATRMax = sc.Input[10];
SCInputRef Input_SessionStartHour = sc.Input[11];
SCInputRef Input_SessionEndHour = sc.Input[12];
SCInputRef Input_MaxLossR = sc.Input[13];
SCInputRef Input_ATRStudyID = sc.Input[14];
// Persistent state - EXACTLY like Python backtest
float& RangeHigh = sc.GetPersistentFloat(0);
float& RangeLow = sc.GetPersistentFloat(1);
int& BarsInRange = sc.GetPersistentInt(0);
int& Cooldown = sc.GetPersistentInt(1);
int& InPosition = sc.GetPersistentInt(2); // 0=flat, 1=long, -1=short
float& PositionEntry = sc.GetPersistentFloat(2);
float& PositionRangeHigh = sc.GetPersistentFloat(3);
float& PositionRangeLow = sc.GetPersistentFloat(4);
int& SessionStartIdx = sc.GetPersistentInt(3); // Track session start bar index (-1 = not set)
int& LastSessionDate = sc.GetPersistentInt(4); // Track date to detect new session
int& SessionStartInitialized = sc.GetPersistentInt(5); // Flag to track if SessionStartIdx is valid
if (sc.SetDefaults)
{
sc.GraphName = "Range Bounce Auto (1s)";
sc.AutoLoop = 1;
sc.GraphRegion = 0;
Subgraph_RangeHigh.Name = "Range High";
Subgraph_RangeHigh.DrawStyle = DRAWSTYLE_LINE;
Subgraph_RangeHigh.PrimaryColor = RGB(0, 200, 0);
Subgraph_RangeHigh.LineWidth = 2;
Subgraph_RangeHigh.DrawZeros = false;
Subgraph_RangeLow.Name = "Range Low";
Subgraph_RangeLow.DrawStyle = DRAWSTYLE_LINE;
Subgraph_RangeLow.PrimaryColor = RGB(200, 0, 0);
Subgraph_RangeLow.LineWidth = 2;
Subgraph_RangeLow.DrawZeros = false;
Subgraph_ATR.Name = "ATR";
Subgraph_ATR.DrawStyle = DRAWSTYLE_IGNORE;
Input_Enabled.Name = "Enabled";
Input_Enabled.SetYesNo(true);
// PARAMETERS FOR 1-SECOND CHART (Python 10s * 10)
Input_LookbackBars.Name = "Lookback Bars";
Input_LookbackBars.SetInt(500); // 50 * 10 = 500 (8.3 minutes)
Input_RangePct.Name = "Max Range % of Price";
Input_RangePct.SetFloat(0.30f);
Input_MinRangeSize.Name = "Min Range Size (pts)";
Input_MinRangeSize.SetFloat(10.0f);
Input_MinBarsInRange.Name = "Min Bars In Range";
Input_MinBarsInRange.SetInt(150); // 15 * 10 = 150 (2.5 minutes)
Input_TargetBuffer.Name = "Target Inside (pts)";
Input_TargetBuffer.SetFloat(6.0f);
Input_StopBeyond.Name = "Stop Beyond (pts)";
Input_StopBeyond.SetFloat(10.0f);
Input_CooldownBars.Name = "Cooldown Bars";
Input_CooldownBars.SetInt(200); // 20 * 10 = 200 (3.3 minutes)
// ATR FILTER - matches Python MEDIUM volatility
Input_ATRPeriod.Name = "ATR Period";
Input_ATRPeriod.SetInt(60); // 60 bars on 1s = 1 minute
Input_ATRMin.Name = "ATR Min (MEDIUM low)";
Input_ATRMin.SetFloat(0.82f);
Input_ATRMax.Name = "ATR Max (MEDIUM high)";
Input_ATRMax.SetFloat(7.23f);
// SESSION FILTER - European session in UTC
Input_SessionStartHour.Name = "Session Start Hour (UTC)";
Input_SessionStartHour.SetInt(6);
Input_SessionEndHour.Name = "Session End Hour (UTC)";
Input_SessionEndHour.SetInt(13);
// MAX LOSS CAP
Input_MaxLossR.Name = "Max Loss (R multiple)";
Input_MaxLossR.SetFloat(1.5f);
// EXTERNAL ATR STUDY
Input_ATRStudyID.Name = "ATR Study ID";
Input_ATRStudyID.SetInt(1); // Set to your ATR study's ID number
// Trading settings
sc.AllowMultipleEntriesInSameDirection = 0;
sc.AllowOppositeEntryWithOpposingPositionOrOrders = 1;
sc.CancelAllOrdersOnEntriesAndReversals = 0;
sc.AllowOnlyOneTradePerBar = 0;
sc.SupportReversals = 0;
sc.AllowEntryWithWorkingOrders = 0;
sc.SupportAttachedOrdersForTrading = 0;
sc.UseGUIAttachedOrderSetting = 1;
sc.MaximumPositionAllowed = 100000000;
sc.FreeDLL = 1;
return;
}
// Skip during historical data download
if (sc.ChartIsDownloadingHistoricalData(sc.ChartNumber))
return;
// === DEBUG: Log data structure on first real bar ===
int& debugLogged = sc.GetPersistentInt(10); // Use persistent var instead of static
if (debugLogged == 0 && sc.Index == 500) {
debugLogged = 1;
// Count bars with volume vs no volume in first 1000 bars
int barsWithVolume = 0;
int barsNoVolume = 0;
int totalBars = sc.ArraySize;
for (int i = 0; i < 1000 && i < totalBars; i++) {
if (sc.Volume[i] > 0) {
barsWithVolume++;
} else {
barsNoVolume++;
}
}
SCString msg;
msg.Format("DEBUG: Total bars in chart: %d", totalBars);
sc.AddMessageToLog(msg, 0);
msg.Format("DEBUG: First 1000 bars - With Volume: %d, No Volume: %d", barsWithVolume, barsNoVolume);
sc.AddMessageToLog(msg, 0);
// Log first 10 bars details
sc.AddMessageToLog("DEBUG: First 10 bars:", 0);
for (int i = 0; i < 10 && i < totalBars; i++) {
SCDateTime barTime = sc.BaseDateTimeIn[i];
msg.Format(" Bar %d: %02d:%02d:%02d O=%.2f H=%.2f L=%.2f C=%.2f V=%d",
i, barTime.GetHour(), barTime.GetMinute(), barTime.GetSecond(),
sc.Open[i], sc.High[i], sc.Low[i], sc.Close[i], (int)sc.Volume[i]);
sc.AddMessageToLog(msg, 0);
}
// Count bars in first 10 minutes (should be ~179 like Python, or ~600 if filling gaps)
int barsFirst10Min = 0;
int firstHour = -1, firstMin = -1;
for (int i = 0; i < totalBars; i++) {
SCDateTime barTime = sc.BaseDateTimeIn[i];
if (firstHour < 0) {
firstHour = barTime.GetHour();
firstMin = barTime.GetMinute();
}
int elapsedMin = (barTime.GetHour() - firstHour) * 60 + (barTime.GetMinute() - firstMin);
if (elapsedMin < 10) {
barsFirst10Min++;
} else {
break;
}
}
msg.Format("DEBUG: Bars in first 10 minutes: %d (Python has 179)", barsFirst10Min);
sc.AddMessageToLog(msg, 0);
}
sc.SendOrdersToTradeService = !sc.GlobalTradeSimulationIsOn;
int idx = sc.Index;
float High = sc.High[idx];
float Low = sc.Low[idx];
float Close = sc.Close[idx];
// Get parameters
int lookback = Input_LookbackBars.GetInt();
float rangePct = Input_RangePct.GetFloat();
float minRangeSize = Input_MinRangeSize.GetFloat();
int minBarsInRange = Input_MinBarsInRange.GetInt();
float targetBuffer = Input_TargetBuffer.GetFloat();
float stopBeyond = Input_StopBeyond.GetFloat();
int cooldownBars = Input_CooldownBars.GetInt();
int atrPeriod = Input_ATRPeriod.GetInt();
float atrMin = Input_ATRMin.GetFloat();
float atrMax = Input_ATRMax.GetFloat();
int sessionStart = Input_SessionStartHour.GetInt();
int sessionEnd = Input_SessionEndHour.GetInt();
float maxLossR = Input_MaxLossR.GetFloat();
// === GET ATR FROM EXTERNAL STUDY ===
SCFloatArray ATRArray;
int atrStudyID = Input_ATRStudyID.GetInt();
sc.GetStudyArrayUsingID(atrStudyID, 0, ATRArray); // 0 = first subgraph (ATR value)
float atr = 0;
if (ATRArray.GetArraySize() > 0 && idx < ATRArray.GetArraySize()) {
atr = ATRArray[idx];
}
Subgraph_ATR[idx] = atr;
// === SESSION FILTER ===
// Get bar time in UTC (SC uses chart timezone, need to check settings)
SCDateTime barTime = sc.BaseDateTimeIn[idx];
int barHour = barTime.GetHour();
int barDate = barTime.GetDate(); // YYYYMMDD format
bool inSession = (barHour >= sessionStart && barHour < sessionEnd);
// === TRACK SESSION START ===
// Reset at the beginning of each new session (new day or re-entering session)
if (inSession) {
if (LastSessionDate != barDate) {
// New day - reset session tracking
SessionStartIdx = idx;
SessionStartInitialized = 1; // Mark as valid
LastSessionDate = barDate;
// Reset range state for new session (like Python starting fresh each day)
RangeHigh = 0;
RangeLow = 0;
BarsInRange = 0;
Cooldown = 0;
InPosition = 0;
SCString msg;
msg.Format("NEW SESSION: Date %d, starting at bar %d", barDate, idx);
sc.AddMessageToLog(msg, 0);
}
}
// Calculate bars elapsed in current session
int barsInSession = (SessionStartInitialized == 1) ? (idx - SessionStartIdx) : 0;
// Debug: Log when we first have enough bars for range detection
if (barsInSession == lookback) {
SCString msg;
msg.Format("DEBUG: Bar %d - now have %d bars in session, range detection possible. ATR=%.2f (valid: %s)",
idx, barsInSession, atr, (atr >= atrMin && atr <= atrMax) ? "YES" : "NO");
sc.AddMessageToLog(msg, 0);
}
// === ATR FILTER ===
bool atrValid = (atr >= atrMin && atr <= atrMax);
// Determine if we should trade
bool doTrades = (sc.UpdateStartIndex > 0) &&
(sc.GetBarHasClosedStatus(idx) == BHCS_BAR_HAS_CLOSED) &&
Input_Enabled.BooleanValue &&
inSession &&
atrValid;
// === COOLDOWN ===
if (Cooldown > 0) {
Cooldown--;
Subgraph_RangeHigh[idx] = 0;
Subgraph_RangeLow[idx] = 0;
return;
}
// === SYNC POSITION STATE WITH SC ===
s_SCPositionData PositionData;
sc.GetTradePosition(PositionData);
// If SC says we're flat but we think we're in position, we exited
if (PositionData.PositionQuantity == 0 && InPosition != 0) {
// Position closed - was it a stop (breakout)?
float rangeSize = PositionRangeHigh - PositionRangeLow;
if (InPosition == 1 && Close < PositionRangeLow - stopBeyond) {
// Long stopped out - range invalidated
RangeHigh = 0;
RangeLow = 0;
Cooldown = cooldownBars;
SCString msg;
msg.Format("STOP LONG: Range invalidated, cooldown %d bars", cooldownBars);
sc.AddMessageToLog(msg, 1);
} else if (InPosition == -1 && Close > PositionRangeHigh + stopBeyond) {
// Short stopped out - range invalidated
RangeHigh = 0;
RangeLow = 0;
Cooldown = cooldownBars;
SCString msg;
msg.Format("STOP SHORT: Range invalidated, cooldown %d bars", cooldownBars);
sc.AddMessageToLog(msg, 1);
} else {
// Target hit - range stays active
SCString msg;
msg.Format("TARGET HIT: Range stays active %.2f - %.2f", RangeLow, RangeHigh);
sc.AddMessageToLog(msg, 0);
}
InPosition = 0;
}
// === RANGE DETECTION (only when no active range) ===
// CRITICAL: Only look back within current session, not into previous day!
// This matches Python which starts fresh each day.
// Debug: Log why we can't detect range (every 500 bars to avoid spam)
if (RangeHigh == 0 && barsInSession >= lookback && (barsInSession % 500 == 0)) {
if (!inSession) {
SCString msg;
msg.Format("DEBUG: No range - outside session (hour=%d, need %d-%d)", barHour, sessionStart, sessionEnd);
sc.AddMessageToLog(msg, 0);
} else if (!atrValid) {
SCString msg;
msg.Format("DEBUG: No range - ATR invalid (ATR=%.2f, need %.2f-%.2f)", atr, atrMin, atrMax);
sc.AddMessageToLog(msg, 0);
}
}
if (RangeHigh == 0 && barsInSession >= lookback && inSession && atrValid) {
// Clamp lookback start to session start
int lookbackStart = idx - lookback + 1;
if (lookbackStart < SessionStartIdx) {
lookbackStart = SessionStartIdx;
}
float maxHigh = sc.High[lookbackStart];
float minLow = sc.Low[lookbackStart];
for (int i = lookbackStart + 1; i <= idx; i++) {
if (sc.High[i] > maxHigh) maxHigh = sc.High[i];
if (sc.Low[i] < minLow) minLow = sc.Low[i];
}
float rangeSize = maxHigh - minLow;
float pct = (rangeSize / Close) * 100.0f;
if (pct <= rangePct && rangeSize >= minRangeSize) {
RangeHigh = maxHigh;
RangeLow = minLow;
BarsInRange = 0;
SCString msg;
msg.Format("RANGE DETECTED: %.2f - %.2f (%.1f pts, %.3f%%, ATR=%.2f, session bar %d)",
RangeLow, RangeHigh, rangeSize, pct, atr, barsInSession);
sc.AddMessageToLog(msg, 0);
} else if (barsInSession % 500 == 0) {
// Debug: Log why range wasn't valid
SCString msg;
msg.Format("DEBUG: Range %.2f-%.2f rejected (size=%.1f pts %s, pct=%.3f%% %s)",
minLow, maxHigh, rangeSize,
(rangeSize >= minRangeSize) ? "OK" : "TOO SMALL",
pct,
(pct <= rangePct) ? "OK" : "TOO WIDE");
sc.AddMessageToLog(msg, 0);
}
}
// === CHECK FOR BREAKOUT (when have range but no position) ===
if (RangeHigh > 0 && InPosition == 0) {
if (Close > RangeHigh + stopBeyond || Close < RangeLow - stopBeyond) {
SCString msg;
msg.Format("RANGE BROKEN: Close %.2f outside range %.2f-%.2f", Close, RangeLow, RangeHigh);
sc.AddMessageToLog(msg, 1);
RangeHigh = 0;
RangeLow = 0;
BarsInRange = 0;
Cooldown = cooldownBars;
Subgraph_RangeHigh[idx] = 0;
Subgraph_RangeLow[idx] = 0;
return;
}
BarsInRange++;
}
// Draw range
if (RangeHigh > 0) {
Subgraph_RangeHigh[idx] = RangeHigh;
Subgraph_RangeLow[idx] = RangeLow;
} else {
Subgraph_RangeHigh[idx] = 0;
Subgraph_RangeLow[idx] = 0;
}
// === ENTRY LOGIC ===
if (!doTrades) return;
if (RangeHigh == 0) return;
if (BarsInRange < minBarsInRange) return;
if (InPosition != 0) return;
if (PositionData.PositionQuantity != 0) return;
float rangeSize = RangeHigh - RangeLow;
float maxLossPts = rangeSize * maxLossR;
float longEntry = RangeLow;
float longTarget = RangeHigh - targetBuffer;
float longStop = RangeLow - min(stopBeyond, maxLossPts);
float shortEntry = RangeHigh;
float shortTarget = RangeLow + targetBuffer;
float shortStop = RangeHigh + min(stopBeyond, maxLossPts);
// LONG: bar low reached entry level
if (Low <= longEntry) {
s_SCNewOrder order;
order.OrderQuantity = 1;
order.OrderType = SCT_ORDERTYPE_LIMIT;
order.Price1 = longEntry;
order.Target1Price = longTarget;
order.AttachedOrderTarget1Type = SCT_ORDERTYPE_LIMIT;
order.StopAllPrice = longStop;
order.AttachedOrderStopAllType = SCT_ORDERTYPE_STOP;
int result = static_cast<int>(sc.BuyOrder(order));
if (result > 0) {
InPosition = 1;
PositionEntry = longEntry;
PositionRangeHigh = RangeHigh;
PositionRangeLow = RangeLow;
SCString msg;
msg.Format("LONG @ %.2f | Target: %.2f | Stop: %.2f | ATR: %.2f", longEntry, longTarget, longStop, atr);
sc.AddMessageToLog(msg, 0);
}
}
// SHORT: bar high reached entry level
else if (High >= shortEntry) {
s_SCNewOrder order;
order.OrderQuantity = 1;
order.OrderType = SCT_ORDERTYPE_LIMIT;
order.Price1 = shortEntry;
order.Target1Price = shortTarget;
order.AttachedOrderTarget1Type = SCT_ORDERTYPE_LIMIT;
order.StopAllPrice = shortStop;
order.AttachedOrderStopAllType = SCT_ORDERTYPE_STOP;
int result = static_cast<int>(sc.SellOrder(order));
if (result > 0) {
InPosition = -1;
PositionEntry = shortEntry;
PositionRangeHigh = RangeHigh;
PositionRangeLow = RangeLow;
SCString msg;
msg.Format("SHORT @ %.2f | Target: %.2f | Stop: %.2f | ATR: %.2f", shortEntry, shortTarget, shortStop, atr);
sc.AddMessageToLog(msg, 0);
}
}
}