Skip to content
Merged

app #27

Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 146 additions & 77 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,47 @@

DB_PATH = "candles.db"
PUSH_TOKEN = os.environ.get("PUSH_TOKEN")
if not PUSH_TOKEN:
raise RuntimeError("PUSH_TOKEN not set")


def init_db():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("""
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS candles (
code TEXT,
timeframe TEXT,
t TEXT,
o REAL,
h REAL,
l REAL,
c REAL,
v REAL,
code TEXT NOT NULL,
timeframe TEXT NOT NULL,
t TEXT NOT NULL,
o REAL NOT NULL,
h REAL NOT NULL,
l REAL NOT NULL,
c REAL NOT NULL,
v REAL NOT NULL,
PRIMARY KEY (code, timeframe, t)
)
""")

cur.execute("""
CREATE TABLE IF NOT EXISTS subscriptions (
code TEXT PRIMARY KEY,
enabled INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL
)
""")

conn.commit()
conn.close()

init_db()



def _require_push_token():
token = request.headers.get("X-PUSH-TOKEN", "")
if token != PUSH_TOKEN:
return jsonify({"error": "Unauthorized"}), 403
return None


# ---------------------------------------------------------------------
# Common
# ---------------------------------------------------------------------
Expand Down Expand Up @@ -498,6 +515,98 @@ def api_stocks_search():

return jsonify({"items": items})

@app.post("/api/subscribe")
def api_subscribe():
payload = request.get_json(silent=True) or {}
code = (payload.get("code") or "").strip()

if not re.fullmatch(r"\d{6}", code):
return jsonify({"error": "code must be 6 digits"}), 400

conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
INSERT OR REPLACE INTO subscriptions (code, enabled, updated_at)
VALUES (?, 1, ?)
""", (code, datetime.now().isoformat(timespec="seconds")))
conn.commit()
conn.close()

return jsonify({"ok": True, "code": code})


@app.post("/api/unsubscribe")
def api_unsubscribe():
payload = request.get_json(silent=True) or {}
code = (payload.get("code") or "").strip()

if not re.fullmatch(r"\d{6}", code):
return jsonify({"error": "code must be 6 digits"}), 400

conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("DELETE FROM subscriptions WHERE code=?", (code,))
conn.commit()
conn.close()

return jsonify({"ok": True, "code": code})

@app.get("/api/internal/subscriptions")
def api_internal_subscriptions():
auth = _require_push_token()
if auth:
return auth

conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
SELECT code
FROM subscriptions
WHERE enabled=1
ORDER BY updated_at DESC
LIMIT 200
""")
rows = cur.fetchall()
conn.close()

return jsonify({"codes": [r[0] for r in rows]})


@app.post("/api/internal/push/candles")
def push_candles():
token = request.headers.get("X-PUSH-TOKEN", "")
if token != PUSH_TOKEN:
return jsonify({"error": "Unauthorized"}), 403

data = request.get_json(silent=True) or {}
code = (data.get("code") or "").strip()
candles = data.get("candles") or []

if not re.fullmatch(r"\d{6}", code):
return jsonify({"error": "code must be 6 digits"}), 400

if not isinstance(candles, list) or not candles:
return jsonify({"error": "candles must be a non-empty list"}), 400

conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()

for cndl in candles:
try:
t = cndl["t"]
o = float(cndl["o"]); h = float(cndl["h"]); l = float(cndl["l"]); c = float(cndl["c"]); v = float(cndl["v"])
except Exception:
continue

cur.execute("""
INSERT OR REPLACE INTO candles (code, timeframe, t, o, h, l, c, v)
VALUES (?, '1m', ?, ?, ?, ?, ?, ?)
""", (code, t, o, h, l, c, v))

conn.commit()
conn.close()
return jsonify({"status": "ok"})


@app.get("/api/stocks/candles")
def api_stocks_candles():
Expand All @@ -508,53 +617,47 @@ def api_stocks_candles():
if not re.fullmatch(r"\d{6}", code):
return jsonify({"error": "code must be 6 digits"}), 400

# 프론트 tf -> 네이버 tf 매핑
# 네이버 siseJson은 day/week/month는 잘 맞는데, 분봉/틱봉은 여기서 못 뽑음.
if tf == "1d":
n_tf = "day"
elif tf == "1w":
n_tf = "week"
elif tf == "1M":
n_tf = "month"
elif tf == "1m":
# ✅ 1m은 DB에서
if tf == "1m":
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute("""
cur = conn.cursor()
cur.execute("""
SELECT t, o, h, l, c, v
FROM candles
WHERE code=?
WHERE code=? AND timeframe='1m'
ORDER BY t ASC
""", (code,))
rows = c.fetchall()
rows = cur.fetchall()
conn.close()

candles = [
{
"time": row[0],
"open": row[1],
"high": row[2],
"low": row[3],
"close": row[4],
"volume": row[5],
}
for row in rows
]
return jsonify(candles)
candles = [{
"time": r[0],
"open": r[1],
"high": r[2],
"low": r[3],
"close": r[4],
"volume": r[5],
} for r in rows][-count:]

return jsonify({"code": code, "name": code, "tf": tf, "candles": candles})

# ✅ 1d/1w/1M은 네이버 그대로
if tf == "1d":
n_tf = "day"
elif tf == "1w":
n_tf = "week"
elif tf == "1M":
n_tf = "month"
else:
return jsonify({"error": f"unknown tf: {tf}"}), 400

try:
candles = fetch_naver_stock_candles(code, tf=n_tf, count=min(max(count, 30), 1200))
return jsonify({
"code": code,
"name": code, # TODO: 종목명 매핑 테이블 붙이면 여기서 채우면 됨
"tf": tf,
"candles": candles,
})
return jsonify({"code": code, "name": code, "tf": tf, "candles": candles})
except Exception as e:
return jsonify({"error": str(e)}), 500


# ---------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------
Expand Down Expand Up @@ -675,40 +778,6 @@ def api_calendar_delete(date: str, event_id: str):
_save_calendar(data)
return jsonify({"ok": True})

@app.route("/api/internal/push/candles", methods=["POST"])
def push_candles():
token = request.headers.get("X-PUSH-TOKEN")
if token != PUSH_TOKEN:
return {"error": "Unauthorized"}, 403

data = request.json
code = data.get("code")
candles = data.get("candles", [])

conn = sqlite3.connect(DB_PATH)
c = conn.cursor()

for candle in candles:
c.execute("""
INSERT OR REPLACE INTO candles
(code, timeframe, t, o, h, l, c, v)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
code,
"1m",
candle["t"],
candle["o"],
candle["h"],
candle["l"],
candle["c"],
candle["v"],
))

conn.commit()
conn.close()

return {"status": "ok"}


# ---------------------------------------------------------------------
if __name__ == "__main__":
Expand Down
Loading