import os, sqlite3, secrets, hmac
from pathlib import Path
from datetime import datetime,date
from functools import wraps
import pandas as pd, yfinance as yf
from flask import Flask,render_template,request,redirect,url_for,session,flash,jsonify
BASE=Path(__file__).resolve().parent
DATA=Path(os.environ.get("ALGO_DATA_DIR",str(BASE/"data"))).expanduser(); DATA.mkdir(parents=True,exist_ok=True)
DB=DATA/"algo.sqlite3"; app=Flask(__name__); app.secret_key=os.environ.get("ALGO_SESSION_SECRET") or secrets.token_hex(32)
app.config.update(SESSION_COOKIE_HTTPONLY=True,SESSION_COOKIE_SAMESITE="Lax",SESSION_COOKIE_SECURE=os.environ.get("COOKIE_SECURE","1")=="1",PERMANENT_SESSION_LIFETIME=1800)
PASSWORD=os.environ.get("ALGO_DASHBOARD_PASSWORD",""); CAPITAL=500000
DEFAULT=["RELIANCE.NS","TCS.NS","INFY.NS","HDFCBANK.NS","ICICIBANK.NS","SBIN.NS","LT.NS","ITC.NS","BHARTIARTL.NS","AXISBANK.NS"]
def db():
 c=sqlite3.connect(DB); c.row_factory=sqlite3.Row; return c
with db() as c:
 c.execute("CREATE TABLE IF NOT EXISTS watchlist(symbol TEXT PRIMARY KEY,added TEXT)")
 c.execute("""CREATE TABLE IF NOT EXISTS trades(id INTEGER PRIMARY KEY AUTOINCREMENT,symbol TEXT,qty INTEGER,entry REAL,entry_date TEXT,exit REAL,exit_date TEXT,status TEXT,costs REAL DEFAULT 0)""")
 c.execute("CREATE TABLE IF NOT EXISTS audit(event TEXT,detail TEXT,at TEXT)")
 if c.execute("SELECT COUNT(*) FROM watchlist").fetchone()[0]==0:
  c.executemany("INSERT OR IGNORE INTO watchlist VALUES(?,?)",[(s,date.today().isoformat()) for s in DEFAULT])
def audit(e,d=""):
 with db() as c:c.execute("INSERT INTO audit VALUES(?,?,?)",(e,d[:300],datetime.utcnow().isoformat()))
def auth(f):
 @wraps(f)
 def w(*a,**k):
  if not session.get("ok"):return redirect(url_for("login"))
  return f(*a,**k)
 return w
def symbols():
 with db() as c:return [r[0] for r in c.execute("SELECT symbol FROM watchlist ORDER BY symbol")]
def history(sym,period="2y"):
 d=yf.download(sym,period=period,interval="1d",auto_adjust=True,progress=False,threads=False)
 if d is None or d.empty:raise ValueError("No data returned; verify symbol, hosting internet access and provider availability.")
 if isinstance(d.columns,pd.MultiIndex):d.columns=d.columns.get_level_values(0)
 d=d.rename(columns=str.title)
 d=d[["Open","High","Low","Close","Volume"]].dropna()
 d.index=pd.to_datetime(d.index).tz_localize(None)
 return d
def indicators(d):
 d=d.copy(); d["SMA20"]=d.Close.rolling(20).mean(); d["SMA50"]=d.Close.rolling(50).mean()
 delta=d.Close.diff(); gain=delta.clip(lower=0).rolling(14).mean(); loss=(-delta.clip(upper=0)).rolling(14).mean()
 d["RSI"]=100-100/(1+gain/loss.replace(0,float("nan"))); d["H20"]=d.High.rolling(20).max()
 d["DD"]=(d.Close/d.H20-1)*100; return d
def scan(s):
 d=indicators(history(s,"1y")).dropna(subset=["SMA20","SMA50","RSI"])
 if d.empty:raise ValueError("Insufficient history.")
 r=d.iloc[-1]; trend="Uptrend" if r.Close>r.SMA50 and r.SMA20>r.SMA50 else ("Mixed" if r.Close>r.SMA50 else "Weak")
 return dict(symbol=s,day=d.index[-1].strftime("%Y-%m-%d"),close=round(float(r.Close),2),s20=round(float(r.SMA20),2),s50=round(float(r.SMA50),2),rsi=round(float(r.RSI),1),dd=round(float(r.DD),2),trend=trend,signal="REVIEW" if trend=="Uptrend" and 35<=r.RSI<=60 and r.DD<=-3 else "WATCH",bucket=next((f"-{b}%" for b in [5,8,10,12] if r.DD<=-b),"—"))
def backtest(s,period):
 d=indicators(history(s,period)); d["sig"]=(d.Close>d.SMA50)&(d.SMA20>d.SMA50)&d.RSI.between(35,60)&(d.DD<=-3)
 d=d.dropna(subset=["SMA20","SMA50","RSI"])
 if len(d)<80:raise ValueError("Insufficient history.")
 cash=CAPITAL; qty=0; entry=0; curve=[]; trades=[]; fee=.003
 rows=list(d.iterrows())
 for i,(dt,r) in enumerate(rows[:-1]):
  nd,n=rows[i+1]; want=bool(r.sig)
  if qty==0 and want:
   px=float(n.Open)*1.001; q=int(min(cash,CAPITAL*.20)//(px*1.002))
   if q>0:cash-=q*px*(1+fee);qty=q;entry=px;trades.append([nd.strftime("%Y-%m-%d"),round(px,2),q,"",None,None])
  elif qty and not want:
   px=float(n.Open)*.999; pnl=(px-entry)*qty-(qty*entry+qty*px)*fee;cash+=qty*px*(1-fee);trades[-1][3]=nd.strftime("%Y-%m-%d");trades[-1][4]=round(px,2);trades[-1][5]=round(pnl,2);qty=0;entry=0
  curve.append((dt,cash+qty*float(r.Close)))
 eq=pd.Series([v for _,v in curve],index=[dt for dt,_ in curve]); dd=(eq/eq.cummax()-1).min()*100; closed=[t for t in trades if t[5] is not None]
 return dict(symbol=s,period=period,final=round(float(eq.iloc[-1]),2),ret=round((float(eq.iloc[-1])/CAPITAL-1)*100,2),dd=round(float(dd),2),n=len(closed),win=round(100*sum(t[5]>0 for t in closed)/len(closed),1) if closed else 0,trades=trades[-20:])
@app.after_request
def headers(r):
 r.headers["X-Content-Type-Options"]="nosniff";r.headers["X-Frame-Options"]="DENY";r.headers["Referrer-Policy"]="same-origin";r.headers["Cache-Control"]="no-store";r.headers["Content-Security-Policy"]="default-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'";return r
@app.route("/health")
def health():return jsonify(status="ok",mode="research-paper-only")
@app.route("/login",methods=["GET","POST"])
def login():
 if request.method=="POST":
  if PASSWORD and hmac.compare_digest(request.form.get("password",""),PASSWORD):
   session.clear();session["ok"]=True;session.permanent=True;audit("login");return redirect(url_for("home"))
  audit("login_failed");flash("Login failed. Confirm dashboard password is configured.")
 return render_template("login.html")
@app.post("/logout")
def logout():session.clear();return redirect(url_for("login"))
@app.route("/")
@auth
def home():
 with db() as c:tr=c.execute("SELECT * FROM trades ORDER BY id DESC LIMIT 10").fetchall();n=c.execute("SELECT COUNT(*) FROM trades WHERE status='OPEN'").fetchone()[0]
 return render_template("home.html",symbols=symbols(),trades=tr,n=n,capital=CAPITAL)
@app.route("/watchlist",methods=["GET","POST"])
@auth
def watchlist():
 if request.method=="POST":
  s=request.form.get("symbol","").strip().upper()
  if not s.endswith(".NS") or not s.replace(".","").isalnum():flash("Use an NSE Yahoo Finance ticker such as INFY.NS")
  else:
   with db() as c:c.execute("INSERT OR IGNORE INTO watchlist VALUES(?,?)",(s,date.today().isoformat()))
   flash("Symbol added.")
  return redirect(url_for("watchlist"))
 with db() as c:rows=c.execute("SELECT * FROM watchlist ORDER BY symbol").fetchall()
 return render_template("watchlist.html",rows=rows)
@app.post("/watchlist/delete")
@auth
def delete():
 with db() as c:c.execute("DELETE FROM watchlist WHERE symbol=?",(request.form.get("symbol",""),))
 return redirect(url_for("watchlist"))
@app.route("/scan")
@auth
def scanner():
 out=[];errs=[]
 for s in symbols():
  try:out.append(scan(s))
  except Exception as e:errs.append(s+": "+str(e)[:120])
 out.sort(key=lambda x:(x["signal"]!="REVIEW",x["dd"]));audit("scan",str(len(out)))
 return render_template("scan.html",out=out,errs=errs)
@app.route("/backtest",methods=["GET","POST"])
@auth
def bt():
 res=err=None;s=request.form.get("symbol","RELIANCE.NS").upper();p=request.form.get("period","5y")
 if request.method=="POST":
  if s not in symbols():err="Symbol must be in the watchlist."
  elif p not in ["1y","2y","5y","10y"]:err="Invalid period."
  else:
   try:res=backtest(s,p);audit("backtest",s+" "+p)
   except Exception as e:err=str(e)[:200]
 return render_template("backtest.html",res=res,err=err,s=s,p=p,symbols=symbols())
@app.route("/paper",methods=["GET","POST"])
@auth
def paper():
 if request.method=="POST":
  try:
   s=request.form["symbol"];q=int(request.form["qty"]);px=float(request.form["price"])
   if s not in symbols() or q<1 or px<=0:raise ValueError("Choose a watchlist symbol and positive quantity/price.")
   with db() as c:
    used=c.execute("SELECT COALESCE(SUM(qty*entry),0) FROM trades WHERE status='OPEN'").fetchone()[0]
    if used+q*px>CAPITAL:raise ValueError("Open paper positions exceed ₹5,00,000.")
    c.execute("INSERT INTO trades(symbol,qty,entry,entry_date,status) VALUES(?,?,?,?,?)",(s,q,px,date.today().isoformat(),"OPEN"))
   audit("paper_buy",f"{s} {q} {px}");flash("Paper entry saved; no broker order was sent.")
  except Exception as e:flash(str(e))
  return redirect(url_for("paper"))
 with db() as c:rows=c.execute("SELECT * FROM trades ORDER BY id DESC").fetchall()
 return render_template("paper.html",rows=rows,symbols=symbols())
@app.post("/paper/close")
@auth
def close():
 try:
  i=int(request.form["id"]);px=float(request.form["exit"])
  if px<=0:raise ValueError("Exit price must be positive.")
  with db() as c:
   t=c.execute("SELECT * FROM trades WHERE id=? AND status='OPEN'",(i,)).fetchone()
   if not t:raise ValueError("Open paper position not found.")
   cost=(t["qty"]*t["entry"]+t["qty"]*px)*.003
   c.execute("UPDATE trades SET exit=?,exit_date=?,status='CLOSED',costs=? WHERE id=?",(px,date.today().isoformat(),cost,i))
  flash("Paper position closed; estimated costs are not a contract-note calculation.")
 except Exception as e:flash(str(e))
 return redirect(url_for("paper"))
if __name__=="__main__":app.run("127.0.0.1",8000)
