291 lines
7.8 KiB
Python
291 lines
7.8 KiB
Python
# =========================================================
|
|
# SIPXAR LIVE — SINGLE FILE
|
|
# =========================================================
|
|
import os
|
|
import yfinance as yf
|
|
import numpy as np
|
|
import math
|
|
import sqlite3
|
|
import logging
|
|
import uuid
|
|
import datetime as dt
|
|
from datetime import datetime, date
|
|
from kiteconnect import KiteConnect
|
|
|
|
# =========================================================
|
|
# CONFIG
|
|
# =========================================================
|
|
|
|
MONTHLY_SIP = 100
|
|
|
|
ALLOW_AFTER_MARKET_TEST = True # ONLY for testing
|
|
|
|
NIFTY_YF = "NIFTYBEES.NS"
|
|
GOLD_YF = "GOLDBEES.NS"
|
|
|
|
NIFTY_KITE = "NIFTYBEES"
|
|
GOLD_KITE = "GOLDBEES"
|
|
|
|
SMA_MONTHS = 36
|
|
|
|
BASE_EQUITY = 0.60
|
|
TILT_MULT = 1.5
|
|
MAX_TILT = 0.25
|
|
MIN_EQUITY = 0.20
|
|
MAX_EQUITY = 0.90
|
|
|
|
STRATEGY_VERSION = "SIPXAR_v1.0"
|
|
|
|
KITE_API_KEY = "YOUR_API_KEY"
|
|
KITE_ACCESS_TOKEN = "YOUR_ACCESS_TOKEN"
|
|
|
|
KILL_SWITCH_FILE = "KILL_SWITCH"
|
|
DB_FILE = "sipxar_live.db"
|
|
LOG_FILE = f"sipxar_{datetime.now().strftime('%Y_%m')}.log"
|
|
|
|
# =========================================================
|
|
# LOGGER
|
|
# =========================================================
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s | %(levelname)s | %(message)s",
|
|
handlers=[
|
|
logging.FileHandler(LOG_FILE),
|
|
logging.StreamHandler()
|
|
]
|
|
)
|
|
log = logging.getLogger("SIPXAR")
|
|
|
|
# =========================================================
|
|
# LEDGER (SQLite)
|
|
# =========================================================
|
|
|
|
def db():
|
|
return sqlite3.connect(DB_FILE)
|
|
|
|
def init_db():
|
|
with db() as conn:
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS runs (
|
|
run_date TEXT PRIMARY KEY,
|
|
run_id TEXT
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS trades (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
run_id TEXT,
|
|
run_date TEXT,
|
|
symbol TEXT,
|
|
strategy_version TEXT,
|
|
equity_weight REAL,
|
|
allocated_amount REAL,
|
|
price_used REAL,
|
|
quantity INTEGER,
|
|
order_id TEXT,
|
|
order_status TEXT,
|
|
timestamp TEXT,
|
|
notes TEXT
|
|
)
|
|
""")
|
|
|
|
def already_ran_today():
|
|
today = date.today().isoformat()
|
|
with db() as conn:
|
|
cur = conn.execute("SELECT 1 FROM runs WHERE run_date=?", (today,))
|
|
return cur.fetchone() is not None
|
|
|
|
def record_run(run_id):
|
|
with db() as conn:
|
|
conn.execute(
|
|
"INSERT INTO runs (run_date, run_id) VALUES (?, ?)",
|
|
(date.today().isoformat(), run_id)
|
|
)
|
|
|
|
def record_trade(**r):
|
|
with db() as conn:
|
|
conn.execute("""
|
|
INSERT INTO trades (
|
|
run_id, run_date, symbol, strategy_version,
|
|
equity_weight, allocated_amount, price_used,
|
|
quantity, order_id, order_status, timestamp, notes
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
r["run_id"], r["run_date"], r["symbol"], r["strategy_version"],
|
|
r["equity_weight"], r["allocated_amount"], r["price_used"],
|
|
r["quantity"], r["order_id"], r["order_status"],
|
|
datetime.now().isoformat(), r.get("notes")
|
|
))
|
|
|
|
# =========================================================
|
|
# DATA (YFINANCE)
|
|
# =========================================================
|
|
|
|
def fetch_sma(ticker):
|
|
df = yf.download(
|
|
ticker,
|
|
period=f"{SMA_MONTHS + 2}mo",
|
|
interval="1mo",
|
|
auto_adjust=True,
|
|
progress=False
|
|
).dropna()
|
|
return float(df["Close"].tail(SMA_MONTHS).mean())
|
|
|
|
def fetch_price(ticker):
|
|
df = yf.download(
|
|
ticker,
|
|
period="5d",
|
|
interval="1d",
|
|
auto_adjust=True,
|
|
progress=False
|
|
)
|
|
return float(df["Close"].iloc[-1].item())
|
|
|
|
# =========================================================
|
|
# STRATEGY
|
|
# =========================================================
|
|
|
|
def kill_switch_active():
|
|
return os.path.exists(KILL_SWITCH_FILE)
|
|
|
|
def market_open():
|
|
now = dt.datetime.now().time()
|
|
return dt.time(9, 15) <= now <= dt.time(15, 30)
|
|
|
|
def sanity_check(n_qty, g_qty):
|
|
if n_qty <= 0 and g_qty <= 0:
|
|
raise RuntimeError("Sanity check failed: both quantities zero")
|
|
|
|
def compute_weights(n_price, g_price, n_sma, g_sma):
|
|
assert isinstance(n_price, float)
|
|
assert isinstance(g_price, float)
|
|
assert isinstance(n_sma, float)
|
|
assert isinstance(g_sma, float)
|
|
|
|
dev_n = (n_price / n_sma) - 1
|
|
dev_g = (g_price / g_sma) - 1
|
|
rel = dev_n - dev_g
|
|
|
|
tilt = np.clip(-rel * TILT_MULT, -MAX_TILT, MAX_TILT)
|
|
|
|
eq_w = BASE_EQUITY * (1 + tilt)
|
|
eq_w = min(max(eq_w, MIN_EQUITY), MAX_EQUITY)
|
|
|
|
return eq_w, 1 - eq_w
|
|
|
|
# =========================================================
|
|
# ZERODHA EXECUTION
|
|
# =========================================================
|
|
|
|
kite = KiteConnect(api_key=KITE_API_KEY)
|
|
kite.set_access_token(KITE_ACCESS_TOKEN)
|
|
|
|
def place_buy(symbol, qty):
|
|
if qty <= 0:
|
|
return None
|
|
return kite.place_order(
|
|
variety=kite.VARIETY_REGULAR,
|
|
exchange=kite.EXCHANGE_NSE,
|
|
tradingsymbol=symbol,
|
|
transaction_type=kite.TRANSACTION_TYPE_BUY,
|
|
quantity=qty,
|
|
order_type=kite.ORDER_TYPE_MARKET,
|
|
product=kite.PRODUCT_CNC
|
|
)
|
|
|
|
# =========================================================
|
|
# MAIN RUN
|
|
# =========================================================
|
|
|
|
def main():
|
|
init_db()
|
|
|
|
if kill_switch_active():
|
|
log.critical("KILL SWITCH ACTIVE — ABORTING EXECUTION")
|
|
return
|
|
|
|
if not market_open() and not ALLOW_AFTER_MARKET_TEST:
|
|
log.error("Market closed — aborting")
|
|
return
|
|
|
|
if not market_open() and ALLOW_AFTER_MARKET_TEST:
|
|
log.warning("Market closed — TEST MODE override enabled")
|
|
|
|
if already_ran_today():
|
|
log.error("ABORT: Strategy already executed today")
|
|
return
|
|
|
|
run_id = str(uuid.uuid4())
|
|
record_run(run_id)
|
|
|
|
log.info(f"Starting SIPXAR LIVE | run_id={run_id}")
|
|
|
|
n_price = fetch_price(NIFTY_YF)
|
|
g_price = fetch_price(GOLD_YF)
|
|
|
|
n_sma = fetch_sma(NIFTY_YF)
|
|
g_sma = fetch_sma(GOLD_YF)
|
|
|
|
eq_w, g_w = compute_weights(n_price, g_price, n_sma, g_sma)
|
|
|
|
n_amt = MONTHLY_SIP * eq_w
|
|
g_amt = MONTHLY_SIP * g_w
|
|
|
|
n_qty = math.floor(n_amt / n_price)
|
|
g_qty = math.floor(g_amt / g_price)
|
|
sanity_check(n_qty, g_qty)
|
|
log.info(f"Weights → Equity={eq_w:.2f}, Gold={g_w:.2f}")
|
|
log.info(f"Qty → NIFTY={n_qty}, GOLD={g_qty}")
|
|
|
|
for sym, qty, price, amt in [
|
|
(NIFTY_KITE, n_qty, n_price, n_amt),
|
|
(GOLD_KITE, g_qty, g_price, g_amt)
|
|
]:
|
|
if qty <= 0:
|
|
log.warning(f"Skipping {sym}, qty=0")
|
|
continue
|
|
|
|
try:
|
|
oid = place_buy(sym, qty)
|
|
log.info(f"Order placed {sym} | order_id={oid}")
|
|
|
|
record_trade(
|
|
run_id=run_id,
|
|
run_date=date.today().isoformat(),
|
|
symbol=sym,
|
|
strategy_version=STRATEGY_VERSION,
|
|
equity_weight=eq_w,
|
|
allocated_amount=amt,
|
|
price_used=price,
|
|
quantity=qty,
|
|
order_id=oid,
|
|
order_status="PLACED",
|
|
notes="LIVE order"
|
|
)
|
|
|
|
except Exception as e:
|
|
log.error(f"Order failed {sym}: {e}")
|
|
|
|
record_trade(
|
|
run_id=run_id,
|
|
run_date=date.today().isoformat(),
|
|
symbol=sym,
|
|
strategy_version=STRATEGY_VERSION,
|
|
equity_weight=eq_w,
|
|
allocated_amount=amt,
|
|
price_used=price,
|
|
quantity=qty,
|
|
order_id=None,
|
|
order_status="FAILED",
|
|
notes=str(e)
|
|
)
|
|
|
|
log.info("SIPXAR LIVE run completed")
|
|
|
|
# =========================================================
|
|
|
|
if __name__ == "__main__":
|
|
main()
|