92 lines
4.3 KiB
Python
"""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()
|