| """OHLC reference for SR_Research_Zones.mq5. Standard-library Python only.
|
| Input: MT5 chart-bar CSV (comma, semicolon or tab), not a raw tick export.
|
| The last input bar is treated as forming and excluded from calculations.
|
| """
|
| import argparse
|
| import csv
|
| import math
|
| from datetime import datetime
|
|
|
|
|
| def load_bars(path):
|
| with open(path, newline='', encoding='utf-8-sig') as f:
|
| sample = f.read(8192)
|
| f.seek(0)
|
| dialect = csv.Sniffer().sniff(sample, delimiters=',;\t')
|
| reader = csv.DictReader(f, dialect=dialect)
|
| bars = []
|
| for row in reader:
|
| row = {k.strip().strip('<>').lower(): v for k, v in row.items()}
|
| dt = datetime.fromisoformat(row['date'].replace('.', '-') + ' ' + row['time'])
|
| b = dict(time=dt.isoformat(' '), **{k: float(row[k]) for k in ('open','high','low','close')})
|
| if not all(math.isfinite(b[k]) for k in ('open','high','low','close')):
|
| raise ValueError('Nonfinite price')
|
| if min(b[k] for k in ('open','high','low','close')) <= 0 or not (
|
| b['low'] <= min(b['open'], b['close']) <= max(b['open'], b['close']) <= b['high']):
|
| raise ValueError('Invalid OHLC candle')
|
| if bars and b['time'] <= bars[-1]['time']:
|
| raise ValueError('Bars must have unique timestamps in ascending order')
|
| bars.append(b)
|
| return bars
|
|
|
|
|
| def detect(bars, point=.001, history=2000, wing=2, period=14,
|
| multiplier=.25, minimum_points=10, break_closes=2,
|
| invalidate=True, show_broken=False, max_zones=20):
|
| if not all(math.isfinite(x) for x in (point, multiplier, minimum_points)):
|
| raise ValueError('Parameters must be finite')
|
| if point <= 0 or history < 100 or not 1 <= wing <= 20 or period < 1:
|
| raise ValueError('Invalid point size, history, swing or ATR period')
|
| if multiplier <= 0 or minimum_points <= 0 or break_closes < 1 or max_zones < 1:
|
| raise ValueError('Invalid width, break count or zone count')
|
| r = bars[-history:]
|
| last = len(r)-2
|
| zones = []
|
| for i in range(max(wing, period-wing), last-wing+1):
|
| c = i+wing
|
| for side in (1, -1):
|
| field = 'low' if side > 0 else 'high'
|
| anchor = r[i][field]
|
| neighbors = [r[j][field] for j in range(i-wing, i+wing+1) if j != i]
|
| if not all(anchor < x if side > 0 else anchor > x for x in neighbors):
|
| continue
|
| tr = [max(r[j]['high']-r[j]['low'], abs(r[j]['high']-r[j-1]['close']),
|
| abs(r[j]['low']-r[j-1]['close'])) for j in range(c-period+1, c+1)]
|
| width = max(minimum_points*point, multiplier*sum(tr)/period)
|
| lower, upper = anchor-width, anchor+width
|
| broken, run = None, 0
|
| if invalidate:
|
| for j in range(c+1, last+1):
|
| outside = r[j]['close'] < lower if side > 0 else r[j]['close'] > upper
|
| run = run+1 if outside else 0
|
| if run >= break_closes:
|
| broken = r[j+1]['time']
|
| break
|
| if broken and not show_broken:
|
| continue
|
| zones.append(dict(side='SUPPORT' if side > 0 else 'RESISTANCE',
|
| pivot=r[i]['time'], available=r[c+1]['time'],
|
| lower=lower, upper=upper, broken=bool(broken), ended=broken or ''))
|
| return zones[-max_zones:]
|
|
|
|
|
| def main():
|
| p = argparse.ArgumentParser(description=__doc__)
|
| p.add_argument('bars_csv')
|
| p.add_argument('--point', type=float, required=True, help='MT5 SYMBOL_POINT for this symbol')
|
| p.add_argument('--out', default='zones.csv')
|
| p.add_argument('--swing', type=int, default=2)
|
| p.add_argument('--history', type=int, default=2000)
|
| p.add_argument('--show-broken', action='store_true')
|
| args = p.parse_args()
|
| zones = detect(load_bars(args.bars_csv), point=args.point, wing=args.swing,
|
| history=args.history, show_broken=args.show_broken)
|
| with open(args.out, 'w', newline='', encoding='utf-8') as f:
|
| w = csv.DictWriter(f, fieldnames=['side','pivot','available','lower','upper','broken','ended'])
|
| w.writeheader()
|
| w.writerows(zones)
|
| print(f'{len(zones)} zones written to {args.out}; final bar excluded as forming.')
|
|
|
|
|
| if __name__ == '__main__':
|
| main()
|