Harga live otomatis via cron: Yahoo Finance tiap 6 menit saat jam bursa IDX

- saat.saham: field harga_terakhir, harga_update_at + method _cron_fetch_live_prices()
- _in_trading_window() pakai timezone Asia/Jakarta eksplisit (container UTC) 08:45-16:00 WIB
- Data ir.cron (6 menit): ambil harga saham aktif, update KALAU berubah,
  sinkronkan ke saat.portofolio.harga_sekarang utk hitung equity
- Terverifikasi: 8 saham ter-fetch & ter-update (07:01 UTC / 14:01 WIB)
This commit is contained in:
2026-09-02 14:02:01 +07:00
parent 3417e077c7
commit b53406791f
4 changed files with 85 additions and 0 deletions

View File

@@ -1,6 +1,13 @@
# -*- coding: utf-8 -*-
import logging
import urllib.request
import json
from datetime import datetime
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class SaatSaham(models.Model):
"""Master daftar saham / emiten yang dicatat."""
@@ -13,6 +20,11 @@ class SaatSaham(models.Model):
bursa = fields.Char(string="Bursa", default="IDX")
sector = fields.Char(string="Sektor")
active = fields.Boolean(string="Aktif", default=True)
# Harga pasar terakhir (hasil fetch live), hanya utk jalur cron
harga_terakhir = fields.Monetary(string="Harga Pasar Terakhir", currency_field='currency_id')
harga_update_at = fields.Datetime(string="Harga Diperbarui")
currency_id = fields.Many2one(
'res.currency', string="Mata Uang", default=lambda self: self.env.ref('base.IDR', raise_if_not_found=False) or self.env.company.currency_id)
# o2m convenience menampilkan semua transaksi saham ini
transaksi_ids = fields.One2many('saat.transaksi', 'saham_id', string="Transaksi")
@@ -20,3 +32,57 @@ class SaatSaham(models.Model):
_sql_constraints = [
('saham_uniq', 'unique(name)', "Kode saham sudah terdaftar."),
]
# ---- Live price fetch (dipakai cron) ----
@api.model
def _yahoo_price_idr(self, ticker):
"""Ambil harga pasar terakhir dari Yahoo Finance utk saham IDX (.JK)."""
url = f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}.JK?range=1d&interval=1d"
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64)'})
try:
d = json.load(urllib.request.urlopen(req, timeout=15))
meta = d['chart']['result'][0]['meta']
return meta.get('regularMarketPrice')
except Exception as e:
_logger.warning("Yahoo fetch gagal utk %s: %s", ticker, e)
return None
@api.model
def _in_trading_window(self):
"""Jam bursa IDX (Asia/Jakarta/WIB): sesi 1 09.00-12.00, sesi 2 13.30-15.50.
Dengan toleransi ambil 08.45 s/d 16.00 WIB. Gunakan timezone Asia/Jakarta eksplisit
(container biasanya UTC)."""
try:
from zoneinfo import ZoneInfo
now = datetime.now(ZoneInfo('Asia/Jakarta'))
except Exception:
import pytz
now = datetime.now(pytz.timezone('Asia/Jakarta'))
hm = now.hour * 60 + now.minute
return 8 * 60 + 45 <= hm <= 16 * 60 # 08:45 - 16:00 WIB
@api.model
def _cron_fetch_live_prices(self):
"""Cron: ambil harga live semua saham aktif, update bila berubah.
Hanya saat jam bursa. Idempoten & aman (tidak sentuh transaksi/saham)."""
if not self._in_trading_window():
_logger.info("Di luar jam bursa, lewati fetch harga.")
return False
sahams = self.search([('active', '=', True)])
now = fields.Datetime.now()
updated = 0
for s in sahams:
price = self._yahoo_price_idr(s.name.strip())
if price is None:
continue
changed = False
if abs((price or 0) - (s.harga_terakhir or 0)) > 1e-6:
changed = True
if changed:
s.write({'harga_terakhir': price, 'harga_update_at': now})
# sinkronkan ke portofolio terkait utk hitung equity
self.env['saat.portofolio'].search([('saham_id', '=', s.id)]).write({'harga_sekarang': price})
updated += 1
_logger.info("Fetch harga selesai: %d saham di-perbarui (dari %d)", updated, len(sahams))
return updated