mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
PERF - cache crypto market ticks server-side (#42)
This commit is contained in:
@@ -6,7 +6,10 @@ local exchange_lock = false
|
|||||||
local markets = {}
|
local markets = {}
|
||||||
local market_order = {}
|
local market_order = {}
|
||||||
local market_dynamics = {}
|
local market_dynamics = {}
|
||||||
|
local market_state = {}
|
||||||
|
local market_history = {}
|
||||||
local market_cursor = 1
|
local market_cursor = 1
|
||||||
|
local market_persistence_interval = 5 * 60 * 1000
|
||||||
local global_market_trend = 0
|
local global_market_trend = 0
|
||||||
local global_market_cycle = {
|
local global_market_cycle = {
|
||||||
direction = 0,
|
direction = 0,
|
||||||
@@ -370,6 +373,100 @@ local function initialize_markets()
|
|||||||
market_cursor = math.min(market_cursor, math.max(#market_order, 1))
|
market_cursor = math.min(market_cursor, math.max(#market_order, 1))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function load_market_cache()
|
||||||
|
local rows = Bridge.Database.Query([[
|
||||||
|
SELECT `id`,`price`,`version`,`status`, UNIX_TIMESTAMP(`updated_at`) AS `updated_at`
|
||||||
|
FROM `sky_phone_crypto_markets`
|
||||||
|
]], {})
|
||||||
|
local next_market_state = {}
|
||||||
|
local next_market_history = {}
|
||||||
|
for _, row in ipairs(rows) do
|
||||||
|
if markets[row.id] then
|
||||||
|
next_market_state[row.id] = {
|
||||||
|
price = tonumber(row.price) or markets[row.id].InitialPrice,
|
||||||
|
version = tonumber(row.version) or 1,
|
||||||
|
status = row.status,
|
||||||
|
updated_at = tonumber(row.updated_at) or os.time(),
|
||||||
|
dirty = false,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, market_id in ipairs(market_order) do
|
||||||
|
local state = next_market_state[market_id]
|
||||||
|
if not state then
|
||||||
|
error(("[sky_phone] Crypto market state is missing after initialization: %s"):format(market_id))
|
||||||
|
end
|
||||||
|
local rows_for_market = Bridge.Database.Query([[
|
||||||
|
SELECT `price`,`version`, UNIX_TIMESTAMP(`created_at`) AS `created_at`
|
||||||
|
FROM `sky_phone_crypto_market_ticks`
|
||||||
|
WHERE `market_id` = ? ORDER BY `id` DESC LIMIT ?
|
||||||
|
]], { market_id, Config.Crypto.HistoryRetentionTicks })
|
||||||
|
local history = {}
|
||||||
|
for index = #rows_for_market, 1, -1 do
|
||||||
|
local tick = rows_for_market[index]
|
||||||
|
history[#history + 1] = {
|
||||||
|
price = tonumber(tick.price) or state.price,
|
||||||
|
version = tonumber(tick.version) or state.version,
|
||||||
|
created_at = tonumber(tick.created_at) or state.updated_at,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if #history == 0 then
|
||||||
|
history[1] = {
|
||||||
|
price = state.price,
|
||||||
|
version = state.version,
|
||||||
|
created_at = state.updated_at,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
next_market_history[market_id] = history
|
||||||
|
end
|
||||||
|
market_state = next_market_state
|
||||||
|
market_history = next_market_history
|
||||||
|
end
|
||||||
|
|
||||||
|
local function persist_market_cache()
|
||||||
|
local queries = {}
|
||||||
|
local persisted_markets = {}
|
||||||
|
for _, market_id in ipairs(market_order) do
|
||||||
|
local state = market_state[market_id]
|
||||||
|
if state and state.dirty then
|
||||||
|
queries[#queries + 1] = {
|
||||||
|
query = [[
|
||||||
|
UPDATE `sky_phone_crypto_markets`
|
||||||
|
SET `price` = ?, `version` = ?, `status` = ?, `updated_at` = FROM_UNIXTIME(?)
|
||||||
|
WHERE `id` = ?
|
||||||
|
]],
|
||||||
|
params = { state.price, state.version, state.status, state.updated_at, market_id },
|
||||||
|
}
|
||||||
|
queries[#queries + 1] = {
|
||||||
|
query = [[
|
||||||
|
INSERT INTO `sky_phone_crypto_market_ticks`
|
||||||
|
(`market_id`,`version`,`price`,`created_at`) VALUES (?, ?, ?, FROM_UNIXTIME(?))
|
||||||
|
]],
|
||||||
|
params = { market_id, state.version, state.price, state.updated_at },
|
||||||
|
}
|
||||||
|
persisted_markets[#persisted_markets + 1] = market_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #queries == 0 then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
queries[#queries + 1] = {
|
||||||
|
query = [[
|
||||||
|
DELETE FROM `sky_phone_crypto_market_ticks`
|
||||||
|
WHERE `created_at` < DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR)
|
||||||
|
]],
|
||||||
|
params = {},
|
||||||
|
}
|
||||||
|
if not Bridge.Database.Transaction(queries) then
|
||||||
|
print("[sky_phone] Failed to persist the crypto market cache.")
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for _, market_id in ipairs(persisted_markets) do
|
||||||
|
market_state[market_id].dirty = false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
local function require_phone(source)
|
local function require_phone(source)
|
||||||
local phone_session, error_response = SkyPhone.RequireSession(source)
|
local phone_session, error_response = SkyPhone.RequireSession(source)
|
||||||
if not phone_session then
|
if not phone_session then
|
||||||
@@ -451,20 +548,7 @@ local function balance(account, asset)
|
|||||||
row and (tonumber(row.version) or 0) or 0
|
row and (tonumber(row.version) or 0) or 0
|
||||||
end
|
end
|
||||||
|
|
||||||
local function market_rows()
|
|
||||||
local rows = Bridge.Database.Query([[
|
|
||||||
SELECT `id`,`price`,`version`,`status`, UNIX_TIMESTAMP(`updated_at`) AS `updated_at`
|
|
||||||
FROM `sky_phone_crypto_markets`
|
|
||||||
]], {})
|
|
||||||
local indexed = {}
|
|
||||||
for _, row in ipairs(rows) do
|
|
||||||
indexed[row.id] = row
|
|
||||||
end
|
|
||||||
return indexed
|
|
||||||
end
|
|
||||||
|
|
||||||
local function market_dtos(selected_market_ids)
|
local function market_dtos(selected_market_ids)
|
||||||
local current = market_rows()
|
|
||||||
local selected = nil
|
local selected = nil
|
||||||
if selected_market_ids then
|
if selected_market_ids then
|
||||||
selected = {}
|
selected = {}
|
||||||
@@ -472,28 +556,16 @@ local function market_dtos(selected_market_ids)
|
|||||||
selected[market_id] = true
|
selected[market_id] = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local daily_rows = Bridge.Database.Query([[
|
|
||||||
SELECT `market_id`, MIN(`price`) AS `low`, MAX(`price`) AS `high`
|
|
||||||
FROM `sky_phone_crypto_market_ticks`
|
|
||||||
WHERE `created_at` >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 24 HOUR)
|
|
||||||
GROUP BY `market_id`
|
|
||||||
]], {})
|
|
||||||
local daily = {}
|
|
||||||
for _, daily_row in ipairs(daily_rows) do
|
|
||||||
daily[daily_row.market_id] = daily_row
|
|
||||||
end
|
|
||||||
local result = {}
|
local result = {}
|
||||||
for _, market_id in ipairs(market_order) do
|
for _, market_id in ipairs(market_order) do
|
||||||
if not selected or selected[market_id] then
|
if not selected or selected[market_id] then
|
||||||
local config = markets[market_id]
|
local config = markets[market_id]
|
||||||
local row = current[market_id]
|
local row = market_state[market_id]
|
||||||
local ticks = Bridge.Database.Query([[
|
local history = market_history[market_id]
|
||||||
SELECT `price` FROM `sky_phone_crypto_market_ticks`
|
|
||||||
WHERE `market_id` = ? ORDER BY `id` DESC LIMIT ?
|
|
||||||
]], { market_id, Config.Crypto.SparklinePoints })
|
|
||||||
local prices = {}
|
local prices = {}
|
||||||
for index = #ticks, 1, -1 do
|
local first_history_index = math.max(1, #history - Config.Crypto.SparklinePoints + 1)
|
||||||
prices[#prices + 1] = tonumber(ticks[index].price) or tonumber(row.price)
|
for index = first_history_index, #history do
|
||||||
|
prices[#prices + 1] = history[index].price
|
||||||
end
|
end
|
||||||
if #prices == 0 then
|
if #prices == 0 then
|
||||||
prices[1] = tonumber(row.price)
|
prices[1] = tonumber(row.price)
|
||||||
@@ -509,7 +581,17 @@ local function market_dtos(selected_market_ids)
|
|||||||
end
|
end
|
||||||
local first = prices[1]
|
local first = prices[1]
|
||||||
local price = tonumber(row.price) or config.InitialPrice
|
local price = tonumber(row.price) or config.InitialPrice
|
||||||
local daily_range = daily[market_id]
|
local daily_low = price
|
||||||
|
local daily_high = price
|
||||||
|
local daily_cutoff = os.time() - 24 * 60 * 60
|
||||||
|
for index = #history, 1, -1 do
|
||||||
|
local tick = history[index]
|
||||||
|
if tick.created_at < daily_cutoff then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
daily_low = math.min(daily_low, tick.price)
|
||||||
|
daily_high = math.max(daily_high, tick.price)
|
||||||
|
end
|
||||||
result[#result + 1] = {
|
result[#result + 1] = {
|
||||||
id = market_id,
|
id = market_id,
|
||||||
symbol = config.Symbol,
|
symbol = config.Symbol,
|
||||||
@@ -519,8 +601,8 @@ local function market_dtos(selected_market_ids)
|
|||||||
price = decimal_string(price, Config.Crypto.PriceScale),
|
price = decimal_string(price, Config.Crypto.PriceScale),
|
||||||
changePercent = first > 0 and ((price - first) / first) * 100 or 0,
|
changePercent = first > 0 and ((price - first) / first) * 100 or 0,
|
||||||
enabled = row.status == "active",
|
enabled = row.status == "active",
|
||||||
high24h = decimal_string(daily_range and daily_range.high or maximum, Config.Crypto.PriceScale),
|
high24h = decimal_string(daily_high, Config.Crypto.PriceScale),
|
||||||
low24h = decimal_string(daily_range and daily_range.low or minimum, Config.Crypto.PriceScale),
|
low24h = decimal_string(daily_low, Config.Crypto.PriceScale),
|
||||||
issuedSupply = decimal_string(config.IssuedSupply * Config.Crypto.AssetScale, Config.Crypto.AssetScale),
|
issuedSupply = decimal_string(config.IssuedSupply * Config.Crypto.AssetScale, Config.Crypto.AssetScale),
|
||||||
treasuryAvailable = decimal_string(balance("treasury", market_id), Config.Crypto.AssetScale),
|
treasuryAvailable = decimal_string(balance("treasury", market_id), Config.Crypto.AssetScale),
|
||||||
priceHistory = price_history,
|
priceHistory = price_history,
|
||||||
@@ -567,13 +649,12 @@ end
|
|||||||
|
|
||||||
local function bootstrap(profile)
|
local function bootstrap(profile)
|
||||||
local cash = balance(account_id(profile.id), "CASH")
|
local cash = balance(account_id(profile.id), "CASH")
|
||||||
local current_markets = market_rows()
|
|
||||||
local holdings = {}
|
local holdings = {}
|
||||||
local portfolio = cash
|
local portfolio = cash
|
||||||
for _, market_id in ipairs(market_order) do
|
for _, market_id in ipairs(market_order) do
|
||||||
local available = balance(account_id(profile.id), market_id)
|
local available = balance(account_id(profile.id), market_id)
|
||||||
if available > 0 then
|
if available > 0 then
|
||||||
local price = tonumber(current_markets[market_id].price) or 0
|
local price = tonumber(market_state[market_id].price) or 0
|
||||||
local value = math.floor(available * price / Config.Crypto.AssetScale)
|
local value = math.floor(available * price / Config.Crypto.AssetScale)
|
||||||
local fill = Bridge.Database.Query([[
|
local fill = Bridge.Database.Query([[
|
||||||
SELECT FLOOR(SUM(fill.`gross`) * ? / NULLIF(SUM(fill.`quantity`), 0)) AS `price`
|
SELECT FLOOR(SUM(fill.`gross`) * ? / NULLIF(SUM(fill.`quantity`), 0)) AS `price`
|
||||||
@@ -1044,10 +1125,7 @@ Bridge.Callbacks.Register("sky_phone:crypto:quote", function(source, data)
|
|||||||
if not config or not side or not quantity then
|
if not config or not side or not quantity then
|
||||||
return { success = false, error = "invalid_quantity" }
|
return { success = false, error = "invalid_quantity" }
|
||||||
end
|
end
|
||||||
local market = Bridge.Database.Query(
|
local market = market_state[config.Id]
|
||||||
"SELECT `price`,`version`,`status` FROM `sky_phone_crypto_markets` WHERE `id` = ? LIMIT 1",
|
|
||||||
{ config.Id }
|
|
||||||
)[1]
|
|
||||||
if not market or market.status ~= "active" then
|
if not market or market.status ~= "active" then
|
||||||
return { success = false, error = "market_unavailable" }
|
return { success = false, error = "market_unavailable" }
|
||||||
end
|
end
|
||||||
@@ -1127,9 +1205,8 @@ local function execute_trade(profile, data)
|
|||||||
or { success = false, error = "duplicate_request" }
|
or { success = false, error = "duplicate_request" }
|
||||||
end
|
end
|
||||||
local quote = Bridge.Database.Query([[
|
local quote = Bridge.Database.Query([[
|
||||||
SELECT quote.*, market.`status` AS `market_status`, market.`version` AS `current_version`
|
SELECT quote.*
|
||||||
FROM `sky_phone_crypto_quotes` quote
|
FROM `sky_phone_crypto_quotes` quote
|
||||||
JOIN `sky_phone_crypto_markets` market ON market.`id` = quote.`market_id`
|
|
||||||
WHERE quote.`id` = ? AND quote.`profile_id` = ? LIMIT 1
|
WHERE quote.`id` = ? AND quote.`profile_id` = ? LIMIT 1
|
||||||
]], { data.quoteId, profile.id })[1]
|
]], { data.quoteId, profile.id })[1]
|
||||||
if not quote or quote.consumed_operation_id then
|
if not quote or quote.consumed_operation_id then
|
||||||
@@ -1142,7 +1219,10 @@ local function execute_trade(profile, data)
|
|||||||
if not expiry or tonumber(expiry.expires_at) < os.time() then
|
if not expiry or tonumber(expiry.expires_at) < os.time() then
|
||||||
return { success = false, error = "quote_expired" }
|
return { success = false, error = "quote_expired" }
|
||||||
end
|
end
|
||||||
if quote.market_status ~= "active" or tonumber(quote.current_version) ~= tonumber(quote.market_version) then
|
local current_market = market_state[quote.market_id]
|
||||||
|
if not current_market or current_market.status ~= "active"
|
||||||
|
or current_market.version ~= tonumber(quote.market_version)
|
||||||
|
then
|
||||||
return { success = false, error = "quote_expired" }
|
return { success = false, error = "quote_expired" }
|
||||||
end
|
end
|
||||||
local quantity = tonumber(quote.quantity)
|
local quantity = tonumber(quote.quantity)
|
||||||
@@ -1367,6 +1447,7 @@ end)
|
|||||||
ensure_schema()
|
ensure_schema()
|
||||||
migrate_crypto_keys()
|
migrate_crypto_keys()
|
||||||
initialize_markets()
|
initialize_markets()
|
||||||
|
load_market_cache()
|
||||||
|
|
||||||
local function reconcile_settlements(include_recent)
|
local function reconcile_settlements(include_recent)
|
||||||
local age_clause = include_recent and "" or " AND settlement.`updated_at` < DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 5 MINUTE)"
|
local age_clause = include_recent and "" or " AND settlement.`updated_at` < DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 5 MINUTE)"
|
||||||
@@ -1507,6 +1588,16 @@ local function start_crypto_schedulers()
|
|||||||
local generation = scheduler_generation
|
local generation = scheduler_generation
|
||||||
reconcile_settlements(true)
|
reconcile_settlements(true)
|
||||||
|
|
||||||
|
CreateThread(function()
|
||||||
|
while scheduler_generation == generation and Config.Crypto.Enabled == true do
|
||||||
|
Wait(market_persistence_interval)
|
||||||
|
if scheduler_generation ~= generation or Config.Crypto.Enabled ~= true then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
with_exchange_lock(persist_market_cache)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
CreateThread(function()
|
CreateThread(function()
|
||||||
while scheduler_generation == generation and Config.Crypto.Enabled == true do
|
while scheduler_generation == generation and Config.Crypto.Enabled == true do
|
||||||
Wait(5 * 60 * 1000)
|
Wait(5 * 60 * 1000)
|
||||||
@@ -1542,10 +1633,7 @@ local function start_crypto_schedulers()
|
|||||||
local order_index = ((market_cursor + offset - 1) % #market_order) + 1
|
local order_index = ((market_cursor + offset - 1) % #market_order) + 1
|
||||||
local market_id = market_order[order_index]
|
local market_id = market_order[order_index]
|
||||||
local config = markets[market_id]
|
local config = markets[market_id]
|
||||||
local row = Bridge.Database.Query(
|
local row = market_state[market_id]
|
||||||
"SELECT `price`,`version`,`status` FROM `sky_phone_crypto_markets` WHERE `id` = ? LIMIT 1",
|
|
||||||
{ market_id }
|
|
||||||
)[1]
|
|
||||||
if row and row.status == "active" then
|
if row and row.status == "active" then
|
||||||
local price = tonumber(row.price) or config.InitialPrice
|
local price = tonumber(row.price) or config.InitialPrice
|
||||||
local impulse = crypto_random_int(
|
local impulse = crypto_random_int(
|
||||||
@@ -1614,22 +1702,22 @@ local function start_crypto_schedulers()
|
|||||||
next_price = price + (movement > 0 and 1 or -1)
|
next_price = price + (movement > 0 and 1 or -1)
|
||||||
end
|
end
|
||||||
next_price = math.max(config.MinimumPrice, math.min(config.MaximumPrice, next_price))
|
next_price = math.max(config.MinimumPrice, math.min(config.MaximumPrice, next_price))
|
||||||
local next_version = (tonumber(row.version) or 0) + 1
|
local next_version = row.version + 1
|
||||||
if Bridge.Database.Transaction({
|
local updated_at = os.time()
|
||||||
{ query = [[UPDATE `sky_phone_crypto_markets` SET `price` = ?, `version` = ? WHERE `id` = ? AND `version` = ?]], params = { next_price, next_version, market_id, row.version } },
|
row.price = next_price
|
||||||
{ query = [[INSERT INTO `sky_phone_crypto_market_ticks` (`market_id`,`version`,`price`) VALUES (?, ?, ?)]], params = { market_id, next_version, next_price } },
|
row.version = next_version
|
||||||
}) then
|
row.updated_at = updated_at
|
||||||
changed_markets[#changed_markets + 1] = market_id
|
row.dirty = true
|
||||||
Bridge.Database.Query([[
|
local history = market_history[market_id]
|
||||||
DELETE FROM `sky_phone_crypto_market_ticks`
|
history[#history + 1] = {
|
||||||
WHERE `market_id` = ? AND `id` NOT IN (
|
price = next_price,
|
||||||
SELECT `id` FROM (
|
version = next_version,
|
||||||
SELECT `id` FROM `sky_phone_crypto_market_ticks`
|
created_at = updated_at,
|
||||||
WHERE `market_id` = ? ORDER BY `id` DESC LIMIT ?
|
}
|
||||||
) retained
|
if #history > Config.Crypto.HistoryRetentionTicks then
|
||||||
)
|
table.remove(history, 1)
|
||||||
]], { market_id, market_id, Config.Crypto.HistoryRetentionTicks })
|
|
||||||
end
|
end
|
||||||
|
changed_markets[#changed_markets + 1] = market_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
market_cursor = ((market_cursor + market_count - 1) % #market_order) + 1
|
market_cursor = ((market_cursor + market_count - 1) % #market_order) + 1
|
||||||
@@ -1645,11 +1733,26 @@ local function start_crypto_schedulers()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function refresh_crypto_runtime()
|
local function refresh_crypto_runtime()
|
||||||
initialize_markets()
|
if exchange_lock then
|
||||||
start_crypto_schedulers()
|
print("[sky_phone] Crypto runtime refresh skipped because the exchange is busy.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
with_exchange_lock(function()
|
||||||
|
if not persist_market_cache() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
initialize_markets()
|
||||||
|
load_market_cache()
|
||||||
|
start_crypto_schedulers()
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
AddEventHandler("sky_phone:configurator:serverUpdated", refresh_crypto_runtime)
|
AddEventHandler("sky_phone:configurator:serverUpdated", refresh_crypto_runtime)
|
||||||
|
AddEventHandler("onResourceStop", function(resource_name)
|
||||||
|
if resource_name == GetCurrentResourceName() then
|
||||||
|
persist_market_cache()
|
||||||
|
end
|
||||||
|
end)
|
||||||
start_crypto_schedulers()
|
start_crypto_schedulers()
|
||||||
|
|
||||||
end)
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user