#!/usr/bin/env python3
"""Analyze user-supplied chronological OHLC CSV; no data is downloaded.
Usage: python3 candlestick-analysis.py prices.csv [--svg chart.svg]
Results are descriptive next-bar close changes, not a trading backtest.
"""
import argparse,csv,json,math,html
from pathlib import Path

def read_rows(path):
    with open(path,encoding="utf-8-sig",newline="") as f:
        reader=csv.DictReader(f)
        if not reader.fieldnames: raise ValueError("Missing header")
        reader.fieldnames=[x.strip().lower() for x in reader.fieldnames]
        if not {"date","open","high","low","close"}.issubset(reader.fieldnames): raise ValueError("Required: Date,Open,High,Low,Close")
        rows=[];last=""
        for line,r in enumerate(reader,2):
            date=r["date"].strip()
            if not date or date<=last: raise ValueError(f"Line {line}: dates must be unique and ascending ISO dates/timestamps")
            v={k:float(r[k]) for k in ["open","high","low","close"]}
            if not all(math.isfinite(x) and x>0 for x in v.values()): raise ValueError(f"Line {line}: prices must be finite and positive")
            if v["high"]<max(v.values()) or v["low"]>min(v.values()): raise ValueError(f"Line {line}: invalid high/low")
            rows.append({"date":date,**v});last=date
            if len(rows)>20000: raise ValueError("Maximum 20,000 rows")
    if len(rows)<2: raise ValueError("At least 2 rows required")
    return rows

def patterns(r,p=None):
    o,h,l,c=(r[x] for x in ["open","high","low","close"]);b=abs(c-o);span=h-l
    if span==0:return []
    out=[]
    if b<=span*.1:out.append("doji")
    if b>0 and min(o,c)-l>=2*b and h-max(o,c)<=b:out.append("lower_pin")
    if b>0 and h-max(o,c)>=2*b and min(o,c)-l<=b:out.append("upper_pin")
    if p:
        po,pc=p["open"],p["close"]
        if c>o and pc<po and o<=pc and c>=po:out.append("bullish_engulfing")
        if c<o and pc>po and o>=pc and c<=po:out.append("bearish_engulfing")
    return out

def analyze(rows):
    stats={k:[] for k in ["doji","lower_pin","upper_pin","bullish_engulfing","bearish_engulfing"]}
    for i,r in enumerate(rows[:-1]):
        for k in patterns(r,rows[i-1] if i else None):stats[k].append((rows[i+1]["close"]/r["close"]-1)*100)
    return {k:{"samples":len(a),"mean_next_close_change_pct":sum(a)/len(a) if a else None,"positive_next_close_pct":100*sum(x>0 for x in a)/len(a) if a else None} for k,a in stats.items()}

def chart_svg(rows):
    rows=rows[-120:];hi=max(r["high"] for r in rows);lo=min(r["low"] for r in rows);span=hi-lo or hi*.01
    width=1000;height=480;left=70;top=35;pw=900;ph=385;step=pw/len(rows);y=lambda v:top+(hi-v)/span*ph
    s=['<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="480" viewBox="0 0 1000 480"><rect width="1000" height="480" fill="white"/>']
    for i in range(5):
        val=lo+span*i/4;yy=y(val);s.append(f'<line x1="70" x2="970" y1="{yy}" y2="{yy}" stroke="#ddd"/><text x="4" y="{yy+4}" font-size="12">{val:.4g}</text>')
    for i,r in enumerate(rows):
        x=left+(i+.5)*step;col="#b83535" if r["close"]>=r["open"] else "#315c80";w=max(1,step*.6)
        s.append(f'<line x1="{x}" x2="{x}" y1="{y(r["high"])}" y2="{y(r["low"])}" stroke="{col}"/><rect x="{x-w/2}" y="{min(y(r["open"]),y(r["close"]))}" width="{w}" height="{max(1,abs(y(r["open"])-y(r["close"])))}" fill="{col}"/>')
    s.append('<text x="70" y="460" font-size="13">'+html.escape(rows[0]["date"])+" - "+html.escape(rows[-1]["date"])+"; supplied OHLC; last 120 rows; red up / blue down</text></svg>")
    return "".join(s)
if __name__=="__main__":
    p=argparse.ArgumentParser(description=__doc__);p.add_argument("csv");p.add_argument("--svg");a=p.parse_args()
    try:
        rows=read_rows(a.csv);print(json.dumps({"rows":len(rows),"first":rows[0]["date"],"last":rows[-1]["date"],"results":analyze(rows)},ensure_ascii=False,indent=2))
        if a.svg:Path(a.svg).write_text(chart_svg(rows),encoding="utf-8")
    except (ValueError,KeyError,OSError) as e:p.exit(1,str(e)+"\n")
