feat/skew-spread-config  ·  MR !71 open

Skew Profile:
Spread Config di-push ke semua OZ Settings Profile

Ringkasan perubahan di branch feat/skew-spread-config, dirangkum dari PRD “PRD – Skew Profile Surya OZ Hub” v2.0.0 dan diff aktual di GitLab. Satu edit spread di Risk Dashboard kini menjangkau seluruh profile di Surya Hub dalam satu push — tanpa replikasi manual Dealing.

11+4commit (onezero/sdk)
24file berubah (20+4)
3migrasi DB baru
6MR (4 sdk + 2 oz)
3status push

01 — Konteks

Masalah & Tujuan

Sebelumnya

Spread Config cuma kena Default Profile

Setiap perubahan Spread Configuration hanya diterapkan ke Default Profile. Dealing harus menyalin manual ke tiap profile lain di OZ Hub. Kalau lupa, skew di profile itu basi — spread yang dilihat client bisa beda antar profile, dan gak ada yang kasih tau.

Sesudahnya

Satu push, semua profile, satu sumber kebenaran

Satu perubahan Spread Config diterapkan ke semua Settings Profile yang ada dalam satu push. Skew identik di semua profile, gak peduli profile mana yang aktif. Match band dirombak biar gak ambigu, dan hasil push diklasifikasi + di-notify lewat Lark.

In scope

Yang masuk

  • Push volume band + markup (Buy/Sell Markup Bid/Ask) ke semua Settings Profile sekaligus.
  • Import/Export CSV Spread Config — perilaku tetap (cuma nyimpen konfigurasi, gak nulis ke Hub).
  • Queue History: granular per-job push, diklasifikasi Completed / Partially Failed / Failed.
  • Notifikasi Lark ke group General Notifications saat Partially Failed / Failed.
  • Pause OZ Hub queue → notifikasi ke Lark group VT Dealing Ops.
Out of scope

Yang gak masuk

  • Buat/rename/hapus Settings Profile dari Risk Dashboard — profile tetap dibuat di client oneZero.
  • Switching active profile / ganti maker pool yang live — tetap operasi sisi oneZero.
  • Hub VT dan AMB — hanya Surya Hub.
  • Retry otomatis — push yang gagal gak di-retry, cukup di-record + di-alert.

02 — End to end

Alur satu push skew

Trade trigger di Risk Dashboard → klasifikasi band → fan-out ke semua profile → satu Batch Query ke Hub → asynchronus response → roll-up Queue History → Lark kalau gagal.

01
Trade trigger

Trade masuk, Risk Dashboard cocokkan ukuran trade ke band Operator + Lot untuk Core Symbol & Taker Stream ID yang sesuai. Band cuma hidup di Risk Dashboard — Hub gak pernah ditanya soal band.

Operator: ≥ 0 · Lot: 1
02
Pilih markup pair

Arah trigger (buy/sell) menentukan pasangan markup mana yang jadi bid/ask spread points. Konfigurasi simpan tetap, tanpa save-and-push, tanpa popup.

Buy Markup Bid -2 / Ask 2
03
Resolve semua profile

Satu call membaca semua Settings Profile + rules-nya. Default profile gak ada di endpoint profiles — dibaca terpisah lalu disisipkan sebagai pseudo-profile.

GET …/price-channel-rule/profiles
04
Pre-flight match per profile

Rule dicari per profile memakai coreSymbol + priceChannelId (taker stream) — kunci match unik, gak ambigu. Ketemu → target tulis; gak ketemu → skip + catat.

applied_in[] / missing_in[]
05
Klasifikasi & enqueue

Satu baris feature_queues mewakili seluruh push, diberi push_group_id (UUID). Semua outcome per profile ditulis ke spread_queue_histories di awal.

completed | partially_failed | failed
06
Satu Batch Query

Semua profile yang match jadi sub-query PATCH dalam satu POST /api/rest/settings. Sekali tulis, berapa pun jumlah profile. Hub-side transaksional.

POST /api/rest/settings (array PATCH)
07
Worker proses response

Worker nunggu response async, pairing response[i] ↔ profile_map[i], update status per profile, lalu roll-up jadi satu status push.

one job → N profile history
08
History + Lark

Queue History nampilin satu baris per push dengan breakdown applied/missing. Lark di-fire kalau Partially Failed / Failed — Completed di-silent.

GET /api/spread-queue-history/{groupKey}

03 — Yang berubah

Fitur utama di branch ini

Req 1 · Push Mechanism

Push ke semua profile

Endpoint baru edit-spread-all-profiles. Behavior edit config tetap persis seperti hari ini — Hub cuma ditulis oleh trigger. Skew sebelumnya di-overwrite, gak ada yang dibuat di Hub.

  • Fire-and-forget: trigger gak pernah diblokir nunggu response.
  • Rule gak ada di 1 profile → tulis di-skip untuk profile itu, gak membatalkan yang lain.
  • Target tulis: coreSymbol + priceChannelId saja.
Req 3 · Concurrency

Stale push di-discard

Trigger kedua untuk band yang sama boleh jalan begitu push sebelumnya sudah terkirim. Kalau response push lama masih pending saat trigger baru datang, response lama dianggap basi dan dibuang — hanya outcome push terbaru yang dicatat.

  • Key debounce pindah ke dedupe_key = driver|coreSymbol|priceChannelId, karena payload batch berupa array dan semua batch nembak URL yang sama.
Req 4 · Queue History

Roll-up per push

History di-grup per push_group_id — satu trigger = satu baris, lengkap dengan hitungan profile total / applied / failed / pending. Baris legacy (tanpa group) tetap tampil sendiri.

  • Endpoint baru GET /api/spread-queue-history/{groupKey} buat expand detail per profile.
  • Filter: driver, core symbol, taker stream, direction, free-text search.
  • Partial success tidak pernah dilaporkan Completed.
Req 5 · Lark Notification

Alert ke General Notifications

Tiga format alert, dikirim lewat profile notification one_zero_spread_update_all_profile di rv3_system.notification_profiles. Alert dikirim setelah Hub menjawab (atau langsung kalau gak ada target sama sekali), biar satu push cuma bikin satu pesan.

  • Completed → tidak ada notifikasi.
  • Webhook tidak terkonfigurasi → log warning, push tetap jalan (non-blocking).
Req 6 · Pause Queue

Pause/resume → Lark VT Dealing Ops

Pause manual maupun jadwal otomatis kini kirim pesan ke group VT Dealing Ops berisi waktu dan siapa yang pause/resume. Email auto-pause NOTIF_AUTO_PAUSE_OZ_QUEUE lama sudah dihapus dari jalur schedule.

  • Nama actor di-resolve dari model user, fallback ke klaim JWT (name → preferred_username → email), terakhir Unknown.
Req 2 · Status

Enum & prioritas baru

Enum SkewPushStatus baru (completed / partially_failed / failed) plus nama queue baru priceChannelRuleSpreadAllProfiles dengan prioritas 1 — setara queue spread lama, di atas priceChannelRule (prioritas 2).

04 — Klasifikasi

Tiga status hasil push

Dihitung dari jumlah profile applied / failed / pending. Selama masih ada yang pending, statusnya processing — roll-up final baru keluar setelah semua row selesai.

StatusKapanDampak
COMPLETED Rule match di semua profile dan semua tulisan sukses. Satu baris history. Tanpa notifikasi Lark.
PARTIALLY FAILED Symbol hilang di sebagian profile (bukan semua). Profile yang punya rule tetap di-update. History + Lark alert “Applied in: … / Missing in: …”. Skew di profile yang missing beda dengan yang lain sampai rule dibuat di client oneZero.
FAILED Rule hilang di semua profile (Missing Profile → abort), atau gagal koneksi / timeout / tidak ada response. History + Lark alert URGENT. Trade tetap memakai skew lama yang sudah live. Tidak ada auto-retry.
PROCESSING Masih ada row profile berstatus unprocessed/processing. Tampil di list selama worker belum selesai; job lebih lama dari stuck_processing_secs (default 120s) otomatis di-reclaim.
Catatan implementasi: kalau tidak ada profile yang match, worker tidak perlu jalan — queue langsung ditutup FAILED dan alert dikirim di saat itu juga, karena memang tidak akan ada response yang bisa ditunggu.

05 — Notifikasi

Format pesan Lark

Field bersama tiap alert: Core Symbol, Taker Stream ID, Direction, Band (Operator + Lot), dan Skew sent (Bid/Ask). Penanda Pushes affected in this window: [count] berasal dari PRD.

Partially Failed · warn

Symbol missing di sebagian profile

// group: General Notifications · color: orange
⚠️ [Surya] Skew not applied - EUR/USD missing in Profile Roll 2

**Core Symbol:**     EUR/USD
**Taker Stream ID:** TAKER-EURUSD
**Direction:**       Buy
**Band:**            ≥ 0 1
**Skew sent:**       Bid -2, Ask 2

**Applied in:** Default, Profile Roll
**Missing in:** Profile Roll 2

The skew in the profiles listed as missing is out of line
with the rest until the rule is created in oneZero client.
Failed (missing semua) · urgent

Symbol gak ada di profile manapun

// color: red
⚠️ [Surya] URGENT - skew not applied at all,
   EUR/USD missing in all profiles

**Core Symbol:**     EUR/USD
**Taker Stream ID:** TAKER-EURUSD
**Direction:**       Sell
**Band:**            ≤ -75
**Skew sent:**       Bid -2, Ask 2

**Applied in:** none
**Missing in:** Default, Profile Roll, Profile Roll 2

The trade priced on whatever skew was already live in the Hub.
Push failure · urgent

Koneksi / timeout / batch ditolak

⚠️ [Surya] Skew push failed - EUR/USD

**Core Symbol:**     EUR/USD
**Taker Stream ID:** TAKER-EURUSD
**Direction:**       Buy
**Band:**            ≥ 100
**Skew sent:**       Bid -4, Ask 4

**Failed profiles:** Default, Profile Roll
**Error:**           no response received

Batch bersifat all-or-nothing: kalau satu sub-query gagal, seluruh batch di-rollback dan semua target profile dilaporkan gagal.

Queue status · VT Dealing Ops

Pause / Resume Hub Queue

// color: red (pause) / green (resume)
Surya Hub Queue Paused
**Paused at:** 2026-10-03 14:05:00
**Paused by:** Budi Santiko

Surya Hub Queue Resumed
// profile: one_zero_queue_status_vt_dealing_ops

Dikirim dari toggle pause manual (PATCH /api/queue/status) dan dari pause jadwal otomatis di worker — bukan cuma dari UI.

06 — Kontrak

API & perubahan skema

Endpoint

MethodPathStatusKeterangan
PATCH/{driver}/price-channel-rule-jobs/edit-spread-all-profiles BARU Push bid/ask spread ke rule yang match di semua Settings Profile. Balasan berisi pre-flight classification + push_group_id + applied_in/missing_in.
GET/api/spread-queue-history/{groupKey} BARU Breakdown per profile untuk satu push group (expand dari baris list). Legacy row pakai id-nya sendiri.
GET/api/spread-queue-history UBAH Dari list mentah per baris → di-roll-up jadi satu baris per push + agregat + status terklasifikasi.
PATCH/api/queue/status UBAH Toggle pause/resume sekarang juga menembak notifikasi Lark VT Dealing Ops.
PATCH/{driver}/price-channel-rule-jobs/edit-spread UBAH Tetap ada — jalur lama untuk profile Default saja.

Request edit-spread-all-profiles

{
  "requestId": "req-uuid",
  "coreSymbol": "EUR/USD",
  "priceChannelId": "TAKER-EURUSD",   // taker stream
  "direction": "buy",                 // buy | sell
  "bidSpreadPoints": -2,
  "askSpreadPoints": 2,
  // konteks band, direkam ke queue history untuk rekonsiliasi
  "config_operator": ">= 0",
  "config_lot": 1,
  "config_buy_markup_bid": -2, "config_buy_markup_ask": 2,
  "config_sell_markup_bid": -2, "config_sell_markup_ask": 2
}

Migrasi DB (connection ozhub)

FileKolomFungsi
2026_08_04_000000_add_profile_to_spread_queue_histories_table profile_id, profile_name,
push_group_id, direction
Granularitas per profile untuk satu push, plus pengelompok push dan arah trigger.
2026_08_04_000100_add_profile_id_to_feature_queue_details_table profile_id Mengaitkan tiap detail queue dengan history profile-nya.
2026_09_26_000000_add_batch_to_feature_queue_details_table profile_map (json),
dedupe_key (191, idx)
profile_map: daftar berurutan {profile_id, profile_name, rule_id} supaya response[i] bisa dipasangkan dengan target ke-i, plus daftar profile yang missing. dedupe_key: identitas stabil push target buat aturan konkurensi.

07 — Di dalam kode

Perubahan per file

Repo utama sam/risk/backend/onezero (11 commit, head 37742e7d, 02 Okt 2026). Lapisan SDK-nya di sam/risk/backend/sdk/risk-v3 ada di sub-bagian bawah — 4 file hasil 4 MR.

FileStatusIsi perubahannya
app/Enums/SkewPushStatus.phpbaru Enum klasifikasi hasil push: completed / partially_failed / failed.
app/Enums/QueueName.phpubah Tambah PRICE_CHANNEL_RULE_SPREAD_ALL_PROFILES.
app/Http/Requests/…/EditSpreadAllProfilesRequest.phpbaru Validasi request push: coreSymbol, priceChannelId, direction (buy/sell), bid/ask points, plus konteks band config_*.
app/Http/Controllers/PriceChannelRuleJobsController.phpubah Action baru editSpreadAllProfiles() yang me-return klasifikasi pre-flight + daftar profile.
app/Http/Controllers/SpreadQueueHistoryController.phpubah Action baru detail($groupKey) untuk breakdown per profile.
app/Services/OneZeroHub/PriceChannelRuleJobs.phpubah Inti fitur. editPriceChannelRuleSpreadAllProfiles(): baca semua profile + Default, match per profile, klasifikasi, tulis queue + history, enqueue satu batch, alert kalau gak ada target.
app/Services/OneZeroHub/PriceChannelRule.phpubah Method baru profiles() dan profileSettings(); semua request pindah ke opsi HTTP terpusat.
app/Services/OneZeroHub/QueueCategories/QueuePriceChannelRule.phpubah Worker: proses payload batch, debounce stale per dedupe_key, parse hasil batch per profile, kirim alert, kembalikan row missing ke status failed.
app/Services/OneZeroHub/OneZeroQueue.phpubah Guard supaya job multi-profile tidak menimpa status history secara massal; reclaim job nyangkut di processing; pause jadwal kirim Lark (email auto-pause dihapus).
app/Services/SkewNotifier.phpbaru Semua template Lark: missingInSomeProfiles(), missingInAllProfiles(), pushFailed(), queuePauseResumed(); resolve webhook+secret dari notification_profiles.
app/Services/SpreadQueueHistoryService.phpubah Query list jadi agregat per push_group_id, roll-up status, hitung applied/missing, plus filter HAVING.
app/Services/QueueService.phpubah editStatus() memanggil notifier pause/resume dengan nama user.
app/Services/OneZeroHub/Config.phpubah httpOptions() terpusat: verify off, timeout, Connection: close, backoff retry.
app/Services/OneZeroHub/QueuePriorities/…ubah Queue baru dapat prioritas 1.
config/onezero.phpubah Blok http (connect_timeout 5s, timeout 30s, force_close, retry_backoff 500ms) + stuck_processing_secs 120.
routes/api.phpubah Dua route baru: edit-spread-all-profiles dan spread-queue-history/{groupKey}.
database/migrations/onezero/*.php (×3)baru Lihat bagian skema di atas.
docs/API.mdubah Dokumentasi endpoint baru + penjelasan perilaku batch (non-transactional per profile di sisi kita, transaksional di sisi Hub).

Lapisan SDK — sam/risk/backend/sdk/risk-v3

Branch sama, feat/skew-spread-config, target develop, head 21f0174c (21 Sep 2026). Empat MR, empat file — semua perubahan yang menyentuh jalur skew/pause di SDK:

Risk Dashboard→sdk/risk-v3→onezero (OZ Hub)
FileMRStatusIsi perubahannya
src/OneZero/OneZeroJobsService.php !99merge Method baru editSpreadAllProfiles(company, body, version?) — PATCH {host}/api/{company}/price-channel-rule-jobs/edit-spread-all-profiles, timeout 30s, throwIfServerError() + throwIfClientError() jadi error HTTP langsung meledak ke pemanggil. Ini satu-satunya pintu SDK ke endpoint push multi-profile — belum ada method lain yang dipakai buat push skew.
src/OneZero/OneZeroSpreadQueueHistoryService.php !100merge Method baru detail(groupKey) — GET /api/spread-queue-history/{groupKey}. groupKey = push_group_id, atau id baris sendiri untuk row legacy. Dipakai buat expand satu baris roll-up history jadi rincian per profile.
src/OneZero/OneZeroQueueService.php !101merge editStatus(): ambil auth('keycloak')->user(), tulis log queue_update_logs (pause/unpause), lalu kirim field user (name → email) ke OZ Hub. Komentarnya eksplisit: supaya alert Lark VT Dealing Ops bisa nyebut siapa yang pause/resume (PRD Req 6) — bukan cuma statusnya.
src/OneZero/OneZeroQueueService.php !106open Perbaikan dari MR !101: $user?->getAuthIdentifier() (user null gak bikin 500 pas logging) plus resolveActingUser() — urutan fallback model → klaim JWT name → preferred_username → email → Unknown. Token dibaca via decodeTokenClaims() (base64 payload tanpa re-verify, guard sudah validasi) karena guard keycloak resolve User lokal by email sehingga name sering kosong walau token bawa username.

Catatan: diff mentah master...feat/skew-spread-config di SDK kebanyakan noise — branch itu diturunkan dari develop, jadi ikut bawa commit Person resolver, Retry/export, mailer, dll. Yang di atas adalah 4 file hasil 4 MR yang memang terkait PRD ini (dibaca dari tiap MR, bukan dari diff mentah).

08 — Di balik layar

Resilience & perbaikan yang menyertainya

Connection

Stale socket dihindari sejak awal

Worker prosesnya panjang dan bicara ke stunnel lokal yang mendaur ulang tunnel idle. Tiap request kini pakai Connection: close + timeout eksplisit supaya socket mati langsung gagal, bukan menggantung. Retry pakai backoff yang bisa diatur via OZ_HTTP_RETRY_BACKOFF_MS.

Self-healing

Job nyangkut di processing di-reclaim

Runner cuma ambil baris unprocessed, jadi job yang mati di tengah akan memblokir antrean selamanya. Worker kini me-reset job yang stuck > stuck_processing_secs (default 120s) kembali ke unprocessed, termasuk detail yang statusnya processing — log warning sebagai penanda.

Data integrity

History per profile gak ditimpa massal

Job multi-profile punya status per profile (sebagian processed, sebagian failed). Guard ditambahkan di dua titik tulis — saat pick-up dan saat selesai — supaya update massal tidak mengembalikan semua row ke processing selamanya. Ini bug yang diperbaiki di commit terakhir 37742e7d.

Batching

Satu batch query, satu hit tulis

Sebelumnya satu detail queue per profile. Sekarang satu feature_queue_details membawa array sub-query PATCH — jumlah request ke Hub konstan berapa pun banyaknya profile. Karena payload berupa array, kunci debounce lama (payload + url) tidak bisa dipakai, makanya ada dedupe_key.

Alerting

Satu push, satu pesan

Alert tidak ditembak saat enqueue — ditunggu sampai Hub menjawab, supaya masalah “rule hilang” dan “push ditolak” menyatu dalam satu pesan. Pengecualian: kalau gak ada profile yang match, memang tidak akan ada response, jadi alert langsung dikirim saat itu juga.

Diketahui

Limitasi yang perlu diingat

  • Baca daftar profile memuat seluruh settings tree (~2,6 MB) — bagian paling lambat dalam push; kalau gagal, push langsung FAILED + alert.
  • Gak ada auto-retry: push gagal harus memicu trigger berikutnya.
  • Retry mengubah skew di semua profile sekaligus — rollback per profile tidak tersedia dari sisi kita.

09 — Verifikasi

Skenario dari PRD

#TipeKasusEkspektasi
P1positif Buy EUR/USD, Lot 1 → match Operator ≥ 0 (Bid -2 / Ask 2) Bid Spread -2, Ask Spread 2, Queue History = Completed
P2positif Sell EUR/USD, Lot 1 → match Operator ≥ 0 (Sell markup) Bid Spread -2, Ask Spread 2, Queue History = Completed
P4positif Export CSV → edit threshold offline → import file valid Spread Config tersimpan; Hub tidak tersentuh sampai trigger berikutnya
P5/6positif Pause / Resume Queue Hub Lark notification terkirim (VT Dealing Ops)
N1negatif EUR/USD band ≥ 200 hilang di Profile Roll 2 (ada di Default + Profile Roll) Bid -4 / Ask 4 diterapkan di 2 profile pertama, di-skip di Profile Roll 2 → status Partially Failed + alert “Applied in / Missing in”
N2negatif EUR/USD band ≤ -75 tidak ada di profile manapun Tidak ada yang diterapkan, trade memakai skew lama → status Failed (Missing Profile) + alert “Applied in: none”
N3negatif oneZero API timeout saat push band ≥ 100 Status Failed (no response received) + alert dengan pesan error, tanpa auto-retry
N4ingat Update band EUR/USD ≥ 200 lalu Save Perubahan tidak diterapkan sampai trigger berikutnya — tidak ada save-and-push
Belum ada QA result yang tertaut di PRD — bagian Positive/Negative Scenario di dokumen masih kosong isinya selain tabel di atas. Kalau ada run Playwright (sam/risk/qa/risk-management-playwright) yang nyentuh branch ini, tabel ini bisa dilengkapi dengan bukti eksekusi.