[TOC]
Dengan munculnya pertukaran desentralisasi (DEX) di bidang perdagangan mata uang kripto, para pedagang kuantitatif mulai beralih ke platform-platform ini untuk melakukan perdagangan otomatis yang efisien. Sebagai salah satu platform perdagangan desentralisasi yang paling populer, dYdX menawarkan fungsi perdagangan yang kuat, mendukung perdagangan kontrak permanen berjangka, dan versi terbarunya v4 yang mengoptimalkan kinerja dan pengalaman pengguna, menjadi pilihan utama bagi banyak pedagang kuantitatif.
Artikel ini akan menjelaskan cara melakukan praktik perdagangan kuantifikasi di dYdX v4, termasuk cara menggunakan API untuk melakukan perdagangan, mendapatkan data pasar, dan mengelola akun.
DYdX Testing Web App Page
dengandYdX v3
Dengan cara yang sama, transaksi menghasilkan imbalan, imbalan.dYdX
Token tersebut.
Sebelumnya, bursa DEX yang menggunakan protokol dYdX v3 telah offline, dan saat ini alamat aplikasi dYdX v4 adalah:
Setelah membuka halaman aplikasi, di pojok kanan atas terdapat tombol untuk menghubungkan dompet, dan kode scan untuk menghubungkan dompet.
Jika Anda ingin lebih familiar dengan pengujian lingkungan test net, Anda dapat menggunakan test net:
Selain itu, klik tombol menghubungkan dompet di pojok kanan atas, scan kode menghubungkan dompet, verifikasi tanda tangan. Setelah koneksi dompet berhasil, alamat dydx v4 akan secara otomatis dihasilkan, alamat ini akan ditampilkan di pojok kanan atas halaman aplikasi, dan menu akan muncul setelah diklik. Ini memiliki operasi pengisian, tanda tangan, transfer, dll.
Alamat akun dYdX v4
Alamat akun dYdX v4 berasal dari alamat dompet, dan alamat akun dYdX v4 terlihat seperti ini:dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx
, adalah alamat di mana didx1 dimulai. Alamat ini dapat dicari di blockchain explorers.
Kata Bantuan Anda dapat mengekspor kata sandi dari akun alamat dYdX saat ini dengan mengklik tombol "Ekspor Kata Sandi" di menu kanan atas. Anda perlu mengkonfigurasi kata sandi ini saat menambahkan bursa di platform FMZ.
Penulis kata dapat dikonfigurasi langsung di platform FMZ, atau dapat disimpan secara lokal oleh administrator. Saat menggunakan objek bursa dydx v4, akan dibaca isi file yang mencatat penulis kata, yang akan ditunjukkan di bagian operasi nyata ini.
Lingkungan test net memiliki beberapa perbedaan dari lingkungan main net, berikut adalah beberapa hal yang dapat Anda lihat.
subAccountNumber >= 128
Jika sub akun ID tersebut tidak disimpan, maka aset akan dihapus secara otomatis ke sub akun dengan sub Akun Nomor 0.
Dalam tes, ditemukan bahwa jaring tes tidak memiliki mekanisme tersebut (atau kondisi pemicu berbeda, tidak ada pemicu di jaring tes).DYDX
Percobaan jaringanDv4TNT
Situs utama:
Indeks alamat:https://indexer.dydx.trade
ID rantai:dydx-mainnet-1
REST node:https://dydx-dao-api.polkachu.com:443
Situs web:
Indeks alamat:https://indexer.v4testnet.dydx.exchange
ID rantai:dydx-testnet-4
REST node:https://dydx-testnet-api.polkachu.com
Protokol dYdX v4 didasarkan pada pengembangan ekosistem cosmos.
Layanan indeks menyediakan protokol REST dan protokol Websocket.
Protokol REST Antarmuka protokol REST mendukung kueri informasi pasar, informasi akun, informasi saham, informasi pesanan, dan lain-lain, yang telah dibungkus sebagai antarmuka API yang seragam di platform FMZ.
Protokol WebSocket Di platform FMZ, Anda dapat menggunakan fungsi Dial untuk membuat koneksi Websocket, informasi tentang pasar langganan, dll.
Perlu diperhatikan bahwa indeks di dydx v4 memiliki masalah yang sama dengan transaksi terpusat, pembaruan data tidak tepat waktu, misalnya kadang-kadang segera setelah memesan, mungkin tidak menanyakan pesanan.Sleep(n)
Tunggu beberapa detik dan tanyakan lagi.
Berikut adalah contoh dari penggunaan fungsi Dial untuk membuat koneksi Websocket API dan berlangganan order data tipis:
function dYdXIndexerWSconnManager(streamingPoint) {
var self = {}
self.base = streamingPoint
self.wsThread = null
// 订阅
self.CreateWsThread = function (msgSubscribe) {
self.wsThread = threading.Thread(function (streamingPoint, msgSubscribe) {
// 订单薄
var orderBook = null
// 更新订单薄
var updateOrderbook = function(orderbook, update) {
// 更新 bids
if (update.bids) {
update.bids.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// 删除价格为 price 的买单
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
} else {
// 更新或新增买单
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
orderbook.bids.push({price: price, size: size})
// 按价格降序排序
orderbook.bids.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))
}
})
}
// 更新 asks
if (update.asks) {
update.asks.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// 删除价格为 price 的卖单
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
} else {
// 更新或新增卖单
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
orderbook.asks.push({price: price, size: size})
// 按价格升序排序
orderbook.asks.sort((a, b) => parseFloat(a.price) - parseFloat(b.price))
}
})
}
return orderbook
}
var conn = Dial(`${streamingPoint}|reconnect=true&payload=${JSON.stringify(msgSubscribe)}`)
if (!conn) {
Log("createWsThread failed.")
return
}
while (true) {
var data = conn.read()
if (data) {
var msg = null
try {
msg = JSON.parse(data)
if (msg["type"] == "subscribed") {
orderBook = msg["contents"]
threading.currentThread().postMessage(orderBook)
} else if (msg["type"] == "channel_data") {
orderBook = updateOrderbook(orderBook, msg["contents"])
threading.currentThread().postMessage(orderBook)
}
} catch (e) {
Log("e.name:", e.name, "e.stack:", e.stack, "e.message:", e.message)
}
}
}
}, streamingPoint, msgSubscribe)
}
// 监听
self.Peek = function () {
return self.wsThread.peekMessage()
}
return self
}
function main() {
// real : wss://indexer.dydx.trade/v4/ws
// simulate : wss://indexer.v4testnet.dydx.exchange/v4/ws
var symbol = "ETH-USD"
var manager = dYdXIndexerWSconnManager("wss://indexer.dydx.trade/v4/ws")
manager.CreateWsThread({"type": "subscribe", "channel": "v4_orderbook", "id": symbol})
var redCode = "#FF0000"
var greenCode = "#006400"
while (true) {
var depthTbl = {type: "table", title: symbol + " / depth", cols: ["level", "price", "amount"], rows: []}
var depth = manager.Peek()
if (depth) {
for (var i = 0; i < depth.asks.length; i++) {
if (i > 9) {
break
}
var ask = depth.asks[i]
depthTbl.rows.push(["asks " + (i + 1) + greenCode, ask.price + greenCode, ask.size + greenCode])
}
depthTbl.rows.reverse()
for (var i = 0; i < depth.bids.length; i++) {
if (i > 9) {
break
}
var bid = depth.bids[i]
depthTbl.rows.push(["bids " + (i + 1) + redCode, bid.price + redCode, bid.size + redCode])
}
}
LogStatus(_D(), "\n`" + JSON.stringify(depthTbl) + "`")
}
}
Pesan pesanan, pesan penarikan, dan pesan transfer merupakan pesan yang paling sering digunakan dalam transaksi.
Pesan Berita Ringkasan
{
"@type": "/dydxprotocol.clob.MsgPlaceOrder",
"order": {
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": xxx,
"orderFlags": 64,
"clobPairId": 1
},
"side": "SIDE_BUY",
"quantums": "2000000",
"subticks": "3500000000",
"goodTilBlockTime": 1742295981
}
}
Daftar Harga:
Fungsi orderFlags yang digunakan untuk order dengan harga terbatas di platform FMZ adalah:ORDER_FLAGS_LONG_TERM = 64 # 长期订单
, karena keterbatasan protokol dydx v4, pemesanan yang paling lama digunakan, yaitu 90 hari (segala jenis pesanan di dydx v4 memiliki masa validitas).
Daftar harga:
Fungsi ordering yang dikemas di platform FMZ, orderFlags yang digunakan untuk order harga pasar adalah:ORDER_FLAGS_SHORT_TERM = 0 # 短期订单
Di bawah rekomendasi dari Dydx v4 protokol:
// Rekomendasi ditetapkan untuk harga oracle - 5% atau lebih rendah untuk MENJUAL, harga oracle + 5% untuk BUY
Karena tidak benar-benar daftar harga pasar, maka harga mesin prediksi digunakan, ditambah penurunan harga 5% sebagai daftar harga pasar. Setelan validitas pesanan pendek juga berbeda dengan pesanan jangka panjang, pesanan pendek menggunakan periode validitas tinggi blok, yang tidak berlaku setelah ditetapkan sebagai blok saat ini + 10 blok tinggi sesuai dengan rekomendasi dydx v4.
ID pesanan: Karena operasi suborder dilakukan langsung di rantai, tidak ada ID order yang dihasilkan indeks setelah siaran pesan dan tidak dapat menggunakan order indeks sebagai nilai yang dikembalikan oleh fungsi suborder platform. Untuk memastikan keunikan ID order dan permintaan pesanan yang akurat, ID order yang dikembalikan terdiri dari informasi berikut:
Berita yang diambil
{
"@type": "/dydxprotocol.clob.MsgCancelOrder",
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": 2585872024,
"orderFlags": 64,
"clobPairId": 1
},
"goodTilBlockTime": 1742295981
}
Untuk mendapatkan ID pesanan, Anda harus masuk ke antarmuka pemesanan FMZ.
Berita Pengiriman
{
"@type": "/dydxprotocol.sending.MsgCreateTransfer",
"transfer": {
"sender": {
"owner": "xxx"
},
"recipient": {
"owner": "xxx",
"number": 128
},
"amount": "10000000"
}
}
Saat ini banyak sub akun yang dapat dibuat di bawah alamat dydx v4, di mana subAccountNumber 0 adalah sub akun pertama yang dibuat secara otomatis, subAccountNumber lebih tinggi dari ID sub akun yang sama dengan 128 untuk transaksi varietas per stok, yang membutuhkan minimal 20 aset USD. Sebagai contoh, Anda dapat menggunakan subAccountNumber 0 -> 128 atau subAccountNumber 128 -> 0. Pergeseran membutuhkan biaya Gas. Biaya Gas dapat digunakan untuk token USDC, Dydx.
Hal-hal di atas menjelaskan beberapa rincian kemasan secara sederhana, selanjutnya kita akan berlatih penggunaan spesifik, di sini menggunakan dYdX v4 untuk demonstrasi test net, test net yang hampir konsisten dengan jaringan utama, dan ada keran otomatis yang dapat mengambil aset uji, operator penyebaran host tidak lagi bercerita, untuk membuat uji coba langsung di FMZ.
Setelah berhasil terhubung ke aplikasi dYdX v4 dengan menggunakan dompet cryptocurrency (di sini saya menggunakan dompet imToken), mengambil aset uji, dan kemudian mengekspor kata kunci akun dYdX v4 saat ini (dihasilkan dari dompet).
Konfigurasi kata sandi di platform FMZ, di mana kami menggunakan cara konfigurasi file lokal (Anda juga dapat mengisi langsung, mengkonfigurasi ke platform, kata sandi adalah pada konfigurasi pasca-enkripsi, bukan teks terbuka).
助记词文件:mnemonic.txt
Anda dapat menempatkan folder ID disk pada direktori admin, dan tentu saja Anda juga dapat menempatkan di direktori lain (menulis jalur tertentu diperlukan ketika mengkonfigurasi).
Konfigurasi Bursa di FMZ
Untuk mengedit kata-kata, isi kotak:file:///mnemonic.txt
"Sebenarnya, saya tidak tahu apa yang akan terjadi.托管者所在目录/logs/storage/594291
。
function main() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// 读取账户信息测试
Log(exchange.GetAccount())
}
Untuk informasi lebih lanjut tentang akun Testnet:
{
"Info": {
"subaccounts": [{
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
"subaccountNumber": 0,
"equity": "300.386228",
"latestProcessedBlockHeight": "28193227",
"freeCollateral": "300.386228",
"openPerpetualPositions": {},
"assetPositions": {
"USDC": {
"subaccountNumber": 0,
"size": "300.386228",
"symbol": "USDC",
"side": "LONG",
"assetId": "0"
}
},
"marginEnabled": true,
"updatedAtHeight": "28063818"
}, {
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
"equity": "0",
"freeCollateral": "0",
"openPerpetualPositions": {},
"marginEnabled": true,
"subaccountNumber": 1,
"assetPositions": {},
"updatedAtHeight": "27770289",
"latestProcessedBlockHeight": "28193227"
}, {
"equity": "0",
"openPerpetualPositions": {},
"marginEnabled": true,
"updatedAtHeight": "28063818",
"latestProcessedBlockHeight": "28193227",
"subaccountNumber": 128,
"freeCollateral": "0",
"assetPositions": {},
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez"
}],
"totalTradingRewards": "0.021744179376211564"
},
"Stocks": 0,
"FrozenStocks": 0,
"Balance": 300.386228,
"FrozenBalance": 0,
"Equity": 300.386228,
"UPnL": 0
}
Tidak beralih ke jaringan uji, uji dengan jaringan utama
function main() {
var markets = exchange.GetMarkets()
if (!markets) {
throw "get markets error"
}
var tbl = {type: "table", title: "test markets", cols: ["key", "Symbol", "BaseAsset", "QuoteAsset", "TickSize", "AmountSize", "PricePrecision", "AmountPrecision", "MinQty", "MaxQty", "MinNotional", "MaxNotional", "CtVal"], rows: []}
for (var symbol in markets) {
var market = markets[symbol]
tbl.rows.push([symbol, market.Symbol, market.BaseAsset, market.QuoteAsset, market.TickSize, market.AmountSize, market.PricePrecision, market.AmountPrecision, market.MinQty, market.MaxQty, market.MinNotional, market.MaxNotional, market.CtVal])
}
LogStatus("`" + JSON.stringify(tbl) + "`")
}
function main() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// 限价单,挂单
var idSell = exchange.CreateOrder("ETH_USD.swap", "sell", 4000, 0.002)
var idBuy = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.003)
// 市价单
var idMarket = exchange.CreateOrder("ETH_USD.swap", "buy", -1, 0.01)
Log("idSell:", idSell)
Log("idBuy:", idBuy)
Log("idMarket:", idMarket)
}
DYdX v4 App halaman:
Pada saat yang sama, situs web ini juga menyediakan aplikasi untuk mengunduh video game online.
function main() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
var orders = exchange.GetOrders()
Log("orders:", orders)
for (var order of orders) {
exchange.CancelOrder(order.Id, order)
Sleep(2000)
}
var tbl = {type: "table", title: "test GetOrders", cols: ["Id", "Price", "Amount", "DealAmount", "AvgPrice", "Status", "Type", "Offset", "ContractType"], rows: []}
for (var order of orders) {
tbl.rows.push([order.Id, order.Price, order.Amount, order.DealAmount, order.AvgPrice, order.Status, order.Type, order.Offset, order.ContractType])
}
LogStatus("`" + JSON.stringify(tbl) + "`")
}
function main() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
var p1 = exchange.GetPositions("USD.swap")
var p2 = exchange.GetPositions("ETH_USD.swap")
var p3 = exchange.GetPositions()
var p4 = exchange.GetPositions("SOL_USD.swap")
var tbls = []
for (var positions of [p1, p2, p3, p4]) {
var tbl = {type: "table", title: "test GetPosition/GetPositions", cols: ["Symbol", "Amount", "Price", "FrozenAmount", "Type", "Profit", "Margin", "ContractType", "MarginLevel"], rows: []}
for (var p of positions) {
tbl.rows.push([p.Symbol, p.Amount, p.Price, p.FrozenAmount, p.Type, p.Profit, p.Margin, p.ContractType, p.MarginLevel])
}
tbls.push(tbl)
}
LogStatus("`" + JSON.stringify(tbls) + "`")
}
function main() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// subAccountNumber 0 -> 128 : 20 USDC , Gas Fee 为 adv4tnt 即 dydx token
var ret = exchange.IO("transferUSDCToSubaccount", 0, 128, "adv4tnt", 20)
Log("ret:", ret)
// 切换到子账号subAccountNumber 128 ,读取账户信息检查
exchange.IO("subAccountNumber", 128)
var account = exchange.GetAccount()
Log("account:", account)
}
Mengubah subAccountNumber menjadi subaccount dengan nomor 128, data yang dikembalikan oleh GetAccount:
{
"Info": {
"subaccounts": [{
"subaccountNumber": 0,
"assetPositions": {
"USDC": {
"size": "245.696892",
"symbol": "USDC",
"side": "LONG",
"assetId": "0",
"subaccountNumber": 0
}
},
"updatedAtHeight": "28194977",
"latestProcessedBlockHeight": "28195008",
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
"freeCollateral": "279.5022142346",
"openPerpetualPositions": {
"ETH-USD": {
"closedAt": null,
"size": "0.01",
"maxSize": "0.01",
"exitPrice": null,
"unrealizedPnl": "-0.17677323",
"subaccountNumber": 0,
"status": "OPEN",
"createdAt": "2024-12-26T03:36:09.264Z",
"createdAtHeight": "28194494",
"sumClose": "0",
"netFunding": "0",
"market": "ETH-USD",
"side": "LONG",
"entryPrice": "3467.2",
"realizedPnl": "0",
"sumOpen": "0.01"
}
},
"marginEnabled": true,
"equity": "280.19211877"
}, {
"openPerpetualPositions": {},
"assetPositions": {},
"marginEnabled": true,
"latestProcessedBlockHeight": "28195008",
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
"subaccountNumber": 1,
"equity": "0",
"freeCollateral": "0",
"updatedAtHeight": "27770289"
}, {
"openPerpetualPositions": {},
"updatedAtHeight": "28194977",
"latestProcessedBlockHeight": "28195008",
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
"subaccountNumber": 128,
"assetPositions": {
"USDC": {
"assetId": "0",
"subaccountNumber": 128,
"size": "20",
"symbol": "USDC",
"side": "LONG"
}
},
"marginEnabled": true,
"equity": "20",
"freeCollateral": "20"
}],
"totalTradingRewards": "0.021886899964446858"
},
"Stocks": 0,
"FrozenStocks": 0,
"Balance": 20,
"FrozenBalance": 0,
"Equity": 20,
"UPnL": 0
}
Anda dapat melihat subAccountNumber sebagai sub-akun 128, yang ditransfer ke 20 USD.
Mengambil TxHash berdasarkan pesanan, menguji metode IO untuk memanggil node REST
Bagaimana mendapatkan TxHash dari pesanan, objek bursa dydx akan menyimpan TxHash, yang dapat diinterogasi dengan ID pesanan. Namun setelah kebijakan dihentikan, pesanan yang disimpan akan kosong.
function main() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
var id1 = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.002)
var hash1 = exchange.IO("getTxHash", id1)
Log("id1:", id1, "hash1:", hash1)
var id2 = exchange.CreateOrder("ETH_USD.swap", "buy", 2900, 0.003)
var hash2 = exchange.IO("getTxHash", id2)
Log("id2:", id2, "hash2:", hash2)
// 清空映射表可以使用:exchange.IO("getTxHash", "")
var arr = [hash1, hash2]
Sleep(10000)
for (var txHash of arr) {
// GET https://docs.cosmos.network /cosmos/tx/v1beta1/txs/{hash}
var ret = exchange.IO("api", "GET", "/cosmos/tx/v1beta1/txs/" + txHash)
Log("ret:", ret)
}
}
Berita yang dipetik melalui TxHash:
var ret =exchange.IO("api",
GET , /cosmos/tx/v1beta1/txs/ + txHash)
Di sini kita akan membahas beberapa hal yang sangat menarik tentang blog ini.
{
"tx_response": {
"codespace": "",
"code": 0,
"logs": [],
"info": "",
"height": "28195603",
"data": "xxx",
"raw_log": "",
"gas_wanted": "-1",
"gas_used": "0",
"tx": {
"@type": "/cosmos.tx.v1beta1.Tx",
"body": {
"messages": [{
"@type": "/dydxprotocol.clob.MsgPlaceOrder",
"order": {
"good_til_block_time": 1742961542,
"condition_type": "CONDITION_TYPE_UNSPECIFIED",
"order_id": {
"clob_pair_id": 1,
"subaccount_id": {
"owner": "xxx",
"number": 0
},
"client_id": 2999181974,
"order_flags": 64
},
"side": "SIDE_BUY",
"quantums": "3000000",
"client_metadata": 0,
"conditional_order_trigger_subticks": "0",
"subticks": "2900000000",
"time_in_force": "TIME_IN_FORCE_UNSPECIFIED",
"reduce_only": false
}
}],
"memo": "FMZ",
"timeout_height": "0",
"extension_options": [],
"non_critical_extension_options": []
},
...
Tes di atas, berdasarkan host terbaru, memerlukan download host terbaru untuk mendukung dYdX v4 DEX
Terima kasih atas dukungannya dan terima kasih telah membaca.