/
CountZero
/
quik-cscalp-lua
Обзор
Документация
Войти
/
CountZero
/
quik-cscalp-lua
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
qsbridge.lua
1 037 строк
32 KB
CountZero
Update
05 авг 2026, 20:59
05 авг 2026, 20:59
500bdaf
Код
Авторство
О чём код?
-- Central state machine for the CScalp <-> QUIK bridge. local Config = require("qsconfig") local Cache = require("qscache") local AccountAdapter = require("qsaccount_adapter") local MarketData = require("qsmarketdata") local FilterCleanup = require("qsfilter_cleanup") local Bridge = {} Bridge.__index = Bridge local M = {} local singleton = nil local REFERENCE_TTL_MSEC = 5 * 60 * 1000 local MAX_REFERENCE_ENTRIES = 12000 local BROKER_POLL_MSEC = 500 local SESSION_POLL_MSEC = 5000 local READINESS_CONFIRMATIONS = 2 local function trim(value) if value == nil then return "" end return (tostring(value):gsub("^%s+", ""):gsub("%s+$", "")) end local function normalize_class(value) return string.upper(trim(value)) end local function default_now() if type(_G.timemsec) == "function" then return math.floor(tonumber(_G.timemsec()) or 0) end return math.floor(os.clock() * 1000) end local function default_delay(msec) if type(_G.delay) == "function" then _G.delay(msec) end end local function default_log(message_text, level) if type(_G.log) == "function" then _G.log(message_text, level) end end local function default_local_connected() return rawget(_G, "is_connected") == true end local function default_emit(command, data) if type(_G.sendCallback) ~= "function" then return nil, "sendCallback is unavailable" end return _G.sendCallback({ cmd = command, t = default_now(), data = data, }) end local function safe_global(name, ...) local fn = rawget(_G, name) if type(fn) ~= "function" then return nil, name .. " is unavailable" end local ok, first, second = pcall(fn, ...) if not ok then return nil, tostring(first) end return first, second end local function join_path(base, name) local text = tostring(base or "") if text == "" then return name end local last = text:sub(-1) if last == "\\" or last == "/" then return text .. name end return text .. "\\" .. name end local function shallow_copy(value) if type(value) ~= "table" then return value end local result = {} for key, item in pairs(value) do if type(item) == "table" then local nested = {} for nested_key, nested_item in pairs(item) do nested[nested_key] = nested_item end result[key] = nested else result[key] = item end end return result end local function parse_csv(value) local result = {} if type(value) ~= "string" then return result end for item in value:gmatch("([^,]+)") do local normalized = trim(item) if normalized ~= "" then result[#result + 1] = normalized end end return result end function Bridge.new(options) options = options or {} local self = setmetatable({ now = options.now or default_now, delay = options.delay or default_delay, log = options.log or default_log, emit_raw = options.emit or default_emit, local_connected_probe = options.local_connected or default_local_connected, injected_config = options.config, config_path = options.config_path, api = options.api or {}, initialized = false, state = "STARTING", state_since_msec = 0, state_reason = "created", local_connected = false, quik_connected = false, reference_ready = false, market_api_ready = false, ever_ready = false, stale_since_msec = nil, last_critical_error = nil, last_broker_poll_msec = 0, last_reference_poll_msec = 0, last_session_poll_msec = 0, readiness_confirmations = 0, ready_generation = 0, ready_notified_generation = 0, session_marker = nil, warmup = nil, reconciliation = nil, recovery_market_done = false, recovery_reconciliation_done = false, }, Bridge) self.state_since_msec = self.now() return self end function Bridge:_transition(new_state, reason) if self.state == new_state then local updated_reason = tostring(reason or self.state_reason or "") if self.state_reason ~= updated_reason then self.state_reason = updated_reason self.log("Bridge state " .. tostring(new_state) .. " reason updated: " .. updated_reason, "info") end return false end local old_state = self.state self.state = new_state self.state_reason = tostring(reason or "") self.state_since_msec = self.now() self.log("Bridge state " .. tostring(old_state) .. " -> " .. tostring(new_state) .. "; reason=" .. self.state_reason, "info") return true end function Bridge:emit(command, data) if not self.local_connected_probe() then return nil, "CScalp is disconnected" end local ok, result, emit_error = pcall(self.emit_raw, command, data) if not ok then self.last_critical_error = "callback emit failed: " .. tostring(result) self.log(self.last_critical_error, "error") return nil, result end if not result then return nil, emit_error end return true end function Bridge:_emit_status_error(text) self.last_critical_error = tostring(text) self:emit("lua_error", "Lua error: " .. tostring(text)) end function Bridge:_working_folder() local value, folder_error = safe_global("getWorkingFolder") if trim(value) ~= "" then return value end return nil, folder_error or "getWorkingFolder returned an empty path" end function Bridge:_run_filter_cleanup() if not self.config.clear_managed_all_trades_filters_on_startup then self.log("INFO.INI cleanup disabled; no global QUIK settings changed", "info") return true end local working_folder, folder_error = self:_working_folder() if not working_folder then self.log("Managed INFO.INI cleanup skipped: " .. tostring(folder_error), "warn") return nil end local path = join_path(working_folder, "INFO.INI") local ok, report = FilterCleanup.apply(path, self.config.classes) if not ok then self.log("Managed INFO.INI cleanup failed; bridge continues without " .. "assuming the optimization is active: " .. tostring(report and report.error), "warn") return nil end self.log("Managed INFO.INI cleanup complete; changed=" .. tostring(report.changed) .. "; path=" .. tostring(report.path) .. (report.backup_path and ("; backup=" .. tostring(report.backup_path)) or ""), "info") return true end function Bridge:initialize() if self.initialized then return self.config ~= nil end self.initialized = true self:_transition("LOADING_CONFIG", "initialization") local config, config_error if self.injected_config then config = self.injected_config else config, config_error = Config.load(self.config_path) end if not config then self.last_critical_error = tostring(config_error) self:_transition("CONFIG_ERROR", tostring(config_error)) self.log("Bridge configuration error; trading is fail-closed: " .. tostring(config_error), "error") return nil, config_error end self.config = config self.cache = Cache.new({ now = self.now, max_entries = MAX_REFERENCE_ENTRIES, }) self.adapter = AccountAdapter.new(config, { now = self.now, log = self.log, max_correlation_entries = 8192, correlation_ttl_msec = 8 * 60 * 60 * 1000, }) self.market = MarketData.new(config, { now = self.now, delay = self.delay, log = self.log, emit = function(command, data) return self:emit(command, data) end, is_ready = function() return self.market_api_ready end, api = self.api.market, }) self:_run_filter_cleanup() self:_transition("WAITING_QUIK", "configuration valid") self:housekeeping(true) return true end function Bridge:_probe_broker_connection() local value, connection_error if type(self.api.is_connected) == "function" then local ok ok, value = pcall(self.api.is_connected) if not ok then connection_error = value value = nil end else value, connection_error = safe_global("isConnected") end if value == nil then return false, tostring(connection_error) end return value == true or tonumber(value) == 1, nil end function Bridge:_class_info(class_code) if type(self.api.get_class_info) == "function" then local ok, value = pcall(self.api.get_class_info, class_code) return ok and value or nil, ok and nil or value end return safe_global("getClassInfo", class_code) end function Bridge:_class_securities(class_code) if type(self.api.get_class_securities) == "function" then local ok, value = pcall(self.api.get_class_securities, class_code) return ok and value or nil, ok and nil or value end return safe_global("getClassSecurities", class_code) end function Bridge:_security_info(class_code, sec_code) if type(self.api.get_security_info) == "function" then local ok, value = pcall( self.api.get_security_info, class_code, sec_code) return ok and value or nil, ok and nil or value end return safe_global("getSecurityInfo", class_code, sec_code) end function Bridge:_get_info_param(name) if type(self.api.get_info_param) == "function" then local ok, value = pcall(self.api.get_info_param, name) return ok and value or nil end return safe_global("getInfoParam", name) end function Bridge:_references_are_ready() for i = 1, #self.config.classes do local class_code = self.config.classes[i] local info, info_error = self:_class_info(class_code) if type(info) ~= "table" then return false, class_code .. " class info unavailable: " .. tostring(info_error or info) end local securities, securities_error = self:_class_securities(class_code) if type(securities) ~= "string" or trim(securities) == "" then return false, class_code .. " securities unavailable: " .. tostring(securities_error or securities) end end return true end function Bridge:_clear_reference_cache(reason) if self.cache then self.cache:clear() end self.warmup = nil self.log("Reference cache cleared: " .. tostring(reason), "info") end function Bridge:_handle_broker_disconnect(reason) if not self.config then return false end if not self.quik_connected and not self.reference_ready and self.state == "BROKER_DISCONNECTED" then return false end local was_operational = self.quik_connected or self.reference_ready or self.ever_ready self.quik_connected = false self.reference_ready = false self.market_api_ready = false self.readiness_confirmations = 0 self.stale_since_msec = self.stale_since_msec or self.now() self:_clear_reference_cache("broker disconnected") local uncertain = self.adapter and self.adapter:mark_broker_disconnect() or 0 if self.market then self.market:mark_broker_disconnected() end self:_transition(was_operational and "BROKER_DISCONNECTED" or "WAITING_QUIK", reason) if was_operational then self:_emit_status_error("QUIK lost connection to the broker; market " .. "data may be stale and new transactions are blocked; " .. "uncertain_transactions=" .. tostring(uncertain)) end return true end function Bridge:on_quik_disconnected(reason) if not self.initialized then self:initialize() end if not self.config then return nil, self.last_critical_error end return self:_handle_broker_disconnect( reason or "QLua OnDisconnected") end function Bridge:on_quik_connected(reason) if not self.initialized then self:initialize() end if not self.config then return nil, self.last_critical_error end if self.state == "READY" and self.quik_connected then return end self.last_broker_poll_msec = self.now() - BROKER_POLL_MSEC self:housekeeping(false) return true end function Bridge:_start_reconciliation() self.reconciliation = { tables = { "orders", "stop_orders", "trades", "depo_limits", "money_limits", "futures_client_limits", "FUTURES_CLIENT_HOLDING", }, table_i = 1, index = 0, count = nil, scanned = 0, skipped = 0, } self.recovery_reconciliation_done = false end function Bridge:_table_count(name) if type(self.api.get_number_of) == "function" then local ok, value = pcall(self.api.get_number_of, name) return ok and tonumber(value) or nil end local value = safe_global("getNumberOf", name) return tonumber(value) end function Bridge:_table_item(name, index) if type(self.api.get_item) == "function" then local ok, value = pcall(self.api.get_item, name, index) return ok and value or nil end return safe_global("getItem", name, index) end function Bridge:_reconciliation_step(limit) if not self.reconciliation then return true end local work = self.reconciliation local budget = tonumber(limit) or 100 local processed = 0 while processed < budget and work.table_i <= #work.tables do local table_name = work.tables[work.table_i] if work.count == nil then work.count = self:_table_count(table_name) work.index = 0 if work.count == nil then self.log("Reconciliation skipped unavailable table: " .. table_name, "warn") work.skipped = work.skipped + 1 work.table_i = work.table_i + 1 work.count = nil end elseif work.index >= work.count then work.table_i = work.table_i + 1 work.count = nil else local row = self:_table_item(table_name, work.index) work.index = work.index + 1 processed = processed + 1 work.scanned = work.scanned + 1 if type(row) == "table" then local route = self.adapter:resolve_route(row) if route then self.adapter.correlation:observe(row, route) end end end end if work.table_i > #work.tables then self.log("Transaction reconciliation complete; scanned=" .. tostring(work.scanned) .. "; skipped_tables=" .. tostring(work.skipped), work.skipped > 0 and "warn" or "info") self.reconciliation = nil self.recovery_reconciliation_done = true return true end return false end function Bridge:_start_recovery(reason) self.market_api_ready = true self.recovery_market_done = false self.market:begin_recovery() self:_start_reconciliation() self:_transition("RECOVERING", reason) end function Bridge:_start_warmup() self.warmup = { active = true, class_i = 1, security_i = 1, securities = nil, cached = 0, started_msec = self.now(), } end function Bridge:_warmup_step() local warmup = self.warmup if not warmup or not warmup.active or self.state ~= "READY" then return end local started = self.now() local processed = 0 while processed < 20 and self.now() - started < 20 do local class_code = self.config.classes[warmup.class_i] if not class_code then warmup.active = false self.log("Reference cache warmup complete; cached=" .. tostring(warmup.cached) .. "; elapsed_msec=" .. tostring(self.now() - warmup.started_msec), "info") return end if not warmup.securities then local csv = self:get_class_securities(class_code) warmup.securities = parse_csv(csv) warmup.security_i = 1 end local sec_code = warmup.securities[warmup.security_i] if not sec_code then warmup.class_i = warmup.class_i + 1 warmup.security_i = 1 warmup.securities = nil else local info = self:get_security_info(class_code, sec_code) if info then warmup.cached = warmup.cached + 1 end warmup.security_i = warmup.security_i + 1 processed = processed + 1 end end end function Bridge:_notify_ready_if_needed() if not self.local_connected_probe() then return end if self.ready_notified_generation == self.ready_generation then return end local ok = self:emit("OnConnected", "") if ok then self.ready_notified_generation = self.ready_generation self.log("CScalp full refresh requested for ready generation " .. tostring(self.ready_generation), "info") end end function Bridge:_finish_recovery() self.reference_ready = true self.market_api_ready = true self.ever_ready = true self.stale_since_msec = nil self.ready_generation = self.ready_generation + 1 self:_transition("READY", "references, subscriptions and reconciliation ready") self:_start_warmup() self:_notify_ready_if_needed() end function Bridge:_poll_session() if not self.quik_connected then return end local marker = trim(self:_get_info_param("TRADEDATE")) if marker == "" then return end if self.session_marker and self.session_marker ~= marker then self:_clear_reference_cache("trading date changed from " .. self.session_marker .. " to " .. marker) self.adapter:reset_session() if self.state == "READY" then self:_start_warmup() end end self.session_marker = marker end function Bridge:_poll_broker(force) local now = self.now() if not force and now - self.last_broker_poll_msec < BROKER_POLL_MSEC then return end self.last_broker_poll_msec = now local connected, connection_error = self:_probe_broker_connection() if not connected then if self.quik_connected or self.reference_ready or self.state == "RECOVERING" or self.state == "READY" then self:_handle_broker_disconnect( connection_error or "broker connection probe returned false") elseif self.state ~= "CONFIG_ERROR" and self.state ~= "BROKER_DISCONNECTED" then self:_transition("WAITING_QUIK", connection_error or "QUIK is not connected") end return end if not self.quik_connected then self.quik_connected = true self.reference_ready = false self.market_api_ready = false self.readiness_confirmations = 0 self:_clear_reference_cache("broker transport connected") self:_transition(self.ever_ready and "RECOVERING" or "WAITING_REFERENCE", "broker transport connected") end if not force and now - self.last_reference_poll_msec < BROKER_POLL_MSEC then return end self.last_reference_poll_msec = now local ready, readiness_error = self:_references_are_ready() if not ready then self.reference_ready = false self.market_api_ready = false self.readiness_confirmations = 0 self:_transition(self.ever_ready and "RECOVERING" or "WAITING_REFERENCE", readiness_error) return end self.readiness_confirmations = self.readiness_confirmations + 1 if not self.reference_ready and self.readiness_confirmations >= READINESS_CONFIRMATIONS then self.reference_ready = true self:_start_recovery("reference data confirmed") end end function Bridge:housekeeping(force) if not self.initialized then return end if not self.config then return end self:_poll_broker(force == true) local now = self.now() if force or now - self.last_session_poll_msec >= SESSION_POLL_MSEC then self.last_session_poll_msec = now self:_poll_session() end if self.state == "RECOVERING" and self.reference_ready then self.recovery_market_done = self.market:recovery_step(2) or self.recovery_market_done self.recovery_reconciliation_done = self:_reconciliation_step(100) or self.recovery_reconciliation_done if self.recovery_market_done and self.recovery_reconciliation_done then self:_finish_recovery() end end self.market:housekeeping() self.adapter:housekeeping() self.cache:housekeeping(64) self:_warmup_step() local warnings = self.adapter.correlation:pending_warnings(15000) for i = 1, #warnings do local item = warnings[i] self.log("Transaction has no final reply; no automatic retry: " .. "TRANS_ID=" .. tostring(item.trans_id) .. "; action=" .. tostring(item.action) .. "; class=" .. tostring(item.class_code) .. "; state=" .. tostring(item.state) .. "; age_msec=" .. tostring(item.age_msec), "warn") end end function Bridge:on_local_connected() self.local_connected = true self.ready_notified_generation = 0 if not self.config then self:_emit_status_error("bridge configuration is invalid; trading is blocked") elseif self.state == "READY" then self:_notify_ready_if_needed() else self:_emit_status_error("bridge is not ready; state=" .. self.state .. "; new transactions are blocked") end end function Bridge:on_local_disconnected(reason) self.local_connected = false self.ready_notified_generation = 0 if self.market then self.market:cleanup(reason or "CScalp disconnected") end end function Bridge:stop(reason) if self.market then self.market:cleanup(reason or "bridge stopped") end self.market_api_ready = false self:_transition("STOPPING", reason or "stop") end function Bridge:require_ready(operation) if not self.config then return nil, tostring(self.last_critical_error or "bridge configuration is invalid") end if self.state ~= "READY" then return nil, tostring(operation or "operation") .. " blocked: bridge state=" .. tostring(self.state) .. "; reason=" .. tostring(self.state_reason) end return true end function Bridge:send_transaction(transaction) local ready, ready_error = self:require_ready("sendTransaction") if not ready then self.log("Transaction blocked before routing; state=" .. tostring(self.state) .. "; reason=" .. tostring(self.state_reason), "warn") return nil, ready_error end local routed, meta, route_error = self.adapter:route_transaction(transaction) if not routed then self.log("Transaction rejected before QUIK API; error=" .. tostring(route_error), "warn") return nil, route_error end self.log("Routing transaction TRANS_ID=" .. tostring(meta.trans_id) .. "; action=" .. tostring(meta.action) .. "; class=" .. tostring(meta.class_code) .. "; virtual_account=" .. tostring(self.config.visible_account) .. "; real_account=" .. tostring(meta.route.trade_account), "info") local send_fn = self.api.send_transaction or rawget(_G, "sendTransaction") if type(send_fn) ~= "function" then self.adapter:record_immediate_error(meta.trans_id) self.log("Transaction blocked after routing; TRANS_ID=" .. tostring(meta.trans_id) .. "; error=QUIK sendTransaction is unavailable", "error") return nil, "QUIK sendTransaction is unavailable" end local ok, result = pcall(send_fn, routed) if not ok then self.adapter:record_immediate_error(meta.trans_id) self.log("Transaction send raised an error; TRANS_ID=" .. tostring(meta.trans_id) .. "; error=" .. tostring(result), "error") return nil, "QUIK sendTransaction raised an error: " .. tostring(result) end if result ~= nil and tostring(result) ~= "" then self.adapter:record_immediate_error(meta.trans_id) self.log("Transaction rejected immediately; TRANS_ID=" .. tostring(meta.trans_id) .. "; error=" .. tostring(result), "warn") return nil, tostring(result) end self.log("Transaction accepted by sendTransaction; awaiting " .. "OnTransReply; TRANS_ID=" .. tostring(meta.trans_id), "info") return true, meta end function Bridge:forward_record(command, row) if not self.adapter or type(row) ~= "table" then return false end local route = self.adapter:resolve_route(row) if not route then return false end if (command == "OnMoneyLimit" or command == "OnMoneyLimitDelete") and route.class_code ~= self.config.cash_class then return false end local virtual = self.adapter:virtualize_record_for_route( row, route, command) if command == "OnTransReply" then local fields = self.adapter:extract_fields(row) local status = row.status or row.STATUS self.log("Transaction reply; TRANS_ID=" .. tostring(fields.trans_id) .. "; status=" .. tostring(status) .. "; order_num=" .. tostring(fields.order_num) .. "; class=" .. tostring(route.class_code), "info") end self:emit(command, virtual) return true end function Bridge:get_classes_list() local ready, ready_error = self:require_ready("getClassesList") if not ready then return nil, ready_error end return self.config.classes_csv end function Bridge:get_class_info(class_code) local ready, ready_error = self:require_ready("getClassInfo") if not ready then return nil, ready_error end local class = normalize_class(class_code) if not self.config.class_set[class] then return nil, "unsupported class: " .. class end local key = "class_info|" .. class local cached, hit = self.cache:get(key) if hit then return cached end local value, value_error = self:_class_info(class) if type(value) ~= "table" then return nil, tostring(value_error or "getClassInfo returned no table") end self.cache:put(key, value, REFERENCE_TTL_MSEC) return shallow_copy(value) end function Bridge:get_class_securities(class_code) local ready, ready_error = self:require_ready("getClassSecurities") if not ready then return nil, ready_error end local class = normalize_class(class_code) if not self.config.class_set[class] then return nil, "unsupported class: " .. class end local key = "class_securities|" .. class local cached, hit = self.cache:get(key) if hit then return cached end local value, value_error = self:_class_securities(class) if type(value) ~= "string" or trim(value) == "" then return nil, tostring(value_error or "getClassSecurities returned an empty value") end self.cache:put(key, value, REFERENCE_TTL_MSEC) return value end function Bridge:get_security_info(class_code, sec_code) local ready, ready_error = self:require_ready("getSecurityInfo") if not ready then return nil, ready_error end local class = normalize_class(class_code) local security = trim(sec_code) if not self.config.class_set[class] then return nil, "unsupported class: " .. class end if security == "" then return nil, "security code is required" end local key = "security_info|" .. class .. "|" .. security local cached, hit = self.cache:get(key) if hit then return cached end local value, value_error = self:_security_info(class, security) if type(value) ~= "table" then return nil, tostring(value_error or "getSecurityInfo returned no table") end self.cache:put(key, value, REFERENCE_TTL_MSEC) return shallow_copy(value) end local function contains_class(classes, class_code) for i = 1, #classes do if classes[i] == class_code then return true end end return false end function Bridge:get_security_class(sec_code, class_filter) local ready, ready_error = self:require_ready("getSecurityClass") if not ready then return nil, ready_error end local security = trim(sec_code) if security == "" then return nil, "security code is required" end local requested_classes = {} local filter = parse_csv(class_filter) if #filter > 0 then for i = 1, #filter do local class = normalize_class(filter[i]) if self.config.class_set[class] and not contains_class(requested_classes, class) then requested_classes[#requested_classes + 1] = class end end else for i = 1, #self.config.classes do requested_classes[i] = self.config.classes[i] end end local cache_key = "security_class|" .. table.concat(requested_classes, ",") .. "|" .. security local cached, cache_hit = self.cache:get(cache_key) if cache_hit then if cached == false then return nil, "security " .. security .. " is ambiguous across requested configured classes" end return cached end local matches = {} for i = 1, #requested_classes do local class = requested_classes[i] local securities, securities_error = self:get_class_securities(class) if securities == nil then return nil, securities_error end for candidate in securities:gmatch("([^,]+)") do if trim(candidate) == security then matches[#matches + 1] = class break end end end if #matches == 1 then self.cache:put(cache_key, matches[1], REFERENCE_TTL_MSEC) return matches[1] elseif #matches > 1 then self.cache:put(cache_key, false, REFERENCE_TTL_MSEC) return nil, "security " .. security .. " is ambiguous across configured classes: " .. table.concat(matches, ",") end self.cache:put(cache_key, "", REFERENCE_TTL_MSEC) return "" end function Bridge:clear_reference_cache(reason) self:_clear_reference_cache(reason or "explicit request") if self.state == "READY" then self:_start_warmup() end return true end function Bridge:health() local now = self.now() local health = { state = self.state, state_reason = self.state_reason, state_age_msec = now - self.state_since_msec, cscalp_connected = self.local_connected_probe(), quik_connected = self.quik_connected, reference_ready = self.reference_ready, ready_generation = self.ready_generation, stale_for_msec = self.stale_since_msec and (now - self.stale_since_msec) or 0, session_marker = self.session_marker, last_critical_error = self.last_critical_error, config_path = self.config and self.config.source_path or self.config_path, supported_classes = self.config and self.config.classes_csv or "", cash_class = self.config and self.config.cash_class or "", last_market_event_msec = self.market and self.market.last_market_event_msec or 0, } if self.market then health.market = self.market:health() end if self.adapter then health.correlation = self.adapter:stats() end if self.cache then health.reference_cache = self.cache:stats() end if self.warmup then health.reference_warmup = { active = self.warmup.active, class_i = self.warmup.class_i, security_i = self.warmup.security_i, cached = self.warmup.cached, } end return health end function M.new(options) return Bridge.new(options) end function M.instance() if not singleton then singleton = Bridge.new() end return singleton end function M._reset_for_tests(options) singleton = Bridge.new(options or {}) return singleton end return M