Улучшение структуры кода

बनाया गया: 18 जनवरी 2025

का उपयोग करके उत्तर दिया गया OpenAI 01 द्वारा OpenAI01

प्रश्न

проанализируй код на языке qlua, дай рекомендации по улучшению кода, может быть нужно его построить с одной глобальной таблицей в которую нужно записывать сложное разветвлённое дерево и данные для функций брать от туда, в общем предложи лучшие варианты.
---@diagnostic disable: undefined-field
local sleep = _G.sleep
local message = _G.message
local getParamEx = _G.getParamEx
local getCandles = _G.getCandles
local getWorkingFolder = _G.getWorkingFolder
local getScriptPath = _G.getScriptPath
local getBuySellInfo = _G.getBuySellInfo
---@diagnostic enable: undefined-field
local os = _G.os
local tonumber = _G.tonumber
local tostring = _G.tostring
local table = _G.table
local string = _G.string
local pairs = _G.pairs
local ipairs = _G.ipairs
local path = getWorkingFolder()
package.path = path .. '\\Script\\Libs\\?.lua;' .. package.path
local log = require("log")
local TICKER_FILE = "TICKER_1.TXT"
local DATA_FILE = "DATA.TXT"
local TIME_FRAMES = { "M1" } -- Пример таймфреймов
local EMA_PERIODS = { 3, 5} -- Периоды EMA
local TP_PERCENT = 1.0 -- Take Profit 1%
local SL_PERCENT = 0.5 -- Stop Loss 0.5%
local MAX_BARS = 13 -- Максимальное количество баров для хранения
local MODE = "SIMULATION" -- "HISTORY", "SIMULATION", "REAL"
local TRADING_CAPITAL = 1000000 -- Торговый капитал
local LOT_SIZE = 10 -- Количество лотов
local TRADE_INTERVALS = {
{ start = "12:01", close_time = "20:39" }, -- Утренняя сессия
{ start = "21:06", close_time = "01:49" } -- Вечерняя сессия
}
local market_status_history = {} -- Хранение последних трех статусов рынка
local data = {} -- Данные по тикерам
local log_file = nil -- Лог-файл
local transaction_queue = {} -- Очередь транзакций
local isRun = true -- Флаг работы скрипта
local os_time = os.time() -- Текущее время в формате Unix
local function update_bars(ticker, time_frame, new_price, current_time)
if not data[ticker].bars[time_frame] then
log.debug("Ошибка: Нет данных для таймфрейма " .. time_frame .. " у тикера " .. ticker)
return
end
local bars = data[ticker].bars[time_frame]
local last_bar_time = data[ticker].last_bar_time[time_frame]
local tick_data = data[ticker].tick_data -- Используем tick_data из структуры data
local interval = 60 -- M1 (60 секунд)
if time_frame == "M1" then
interval = 20
elseif time_frame == "M3" then
interval = 20
elseif time_frame == "M10" then
interval = 600
elseif time_frame == "M15" then
interval = 900
elseif time_frame == "M20" then
interval = 1200
end
if last_bar_time == 0 then
last_bar_time = current_time - interval -- Устанавливаем время начала первого бара
data[ticker].last_bar_time[time_frame] = last_bar_time
end
tick_data.sum_NP = tick_data.sum_NP + new_price
tick_data.sum_N = tick_data.sum_N + 1
log.debug(string.format("Тик для %s (%s): время = %s, цена = %.2f, сумма = %.2f, количество = %d",ticker, time_frame, os.date("%Y-%m-%d %H:%M:%S", current_time), new_price, tick_data.sum_NP, tick_data.sum_N))
if current_time - last_bar_time >= interval then
local avg_close = tick_data.sum_NP / tick_data.sum_N
log.debug(string.format("Формирование бара для %s (%s): время = %s, средняя цена = %.2f (сумма = %.2f, количество = %d)",ticker, time_frame, os.date("%Y-%m-%d %H:%M:%S", current_time), avg_close, tick_data.sum_NP, tick_data.sum_N))
table.insert(bars, {
time = current_time,
close = avg_close -- Цена закрытия = средняя цена тиков
})
data[ticker].last_bar_time[time_frame] = current_time
log.debug(string.format("Новый бар для %s (%s): время = %s, цена закрытия = %.2f",ticker, time_frame, os.date("%Y-%m-%d %H:%M:%S", current_time), avg_close))
tick_data.sum_NP = 0
tick_data.sum_N = 0
while #bars > MAX_BARS do
local removed_bar = bars[1]
table.remove(bars, 1)
log.debug(string.format("Удален старый бар для %s (%s): время = %s, цена закрытия = %.2f",ticker, time_frame, os.date("%Y-%m-%d %H:%M:%S", removed_bar.time), removed_bar.close))
end
end
end
local function calculate_ema(ticker, tf, period)
local bars = data[ticker].bars[tf]
if #bars < period then
log.debug("Недостаточно баров для расчета EMA. Требуется: " .. period .. ", доступно: " .. #bars)
return
end
local recent_bars = {}
for i = #bars - period + 1, #bars do
table.insert(recent_bars, bars[i])
end
local k = 2 / (period + 1)
local ema = recent_bars[1].close -- Первое значение EMA равно цене закрытия первого бара
for i = 2, #recent_bars do
ema = recent_bars[i].close * k + ema * (1 - k)
end
data[ticker].emas[tf][period] = ema
log.debug(string.format("Рассчитано EMA для %s (%s, период %d): %.4f", ticker, tf, period, ema))
end
local function add_transaction(ticker, operation, price, quantity)
table.insert(transaction_queue, {
ticker = ticker,
operation = operation,
price = price,
quantity = quantity
})
end
local function get_best_prices(ticker)
local best_bid = tonumber(getParamEx("TQBR", ticker, "BID").param_value) or 0
local best_ask = tonumber(getParamEx("TQBR", ticker, "OFFER").param_value) or 0
log.debug("best_bid " .. best_bid)
log.debug("best_ask " .. best_ask)
return best_bid, best_ask
end
local function send_order(ticker, operation, quantity)
local best_bid, best_ask = get_best_prices(ticker)
local last_price = tonumber(getParamEx("TQBR", ticker, "LAST").param_value) or 0 -- Получаем LAST
local price = 0
if operation == "BUY" then
price = best_ask -- Покупаем по лучшей цене продажи
elseif operation == "SELL" then
price = best_bid -- Продаем по лучшей цене покупки
elseif operation == "CLOSE" then
local current_position = data[ticker].positions
if current_position then
if current_position.signal == "BUY" then
price = best_bid -- Закрываем позицию BUY по лучшей цене покупки
elseif current_position.signal == "SELL" then
price = best_ask -- Закрываем позицию SELL по лучшей цене продажи
end
else
log.debug("Ошибка: Нет активной позиции для закрытия.")
return
end
end
if not price or price <= 0 then
price = last_price
log.debug(string.format("Цена для заявки %s не определена. Используется LAST: %.2f", operation, price))
end
if not price or price <= 0 then
log.debug("Ошибка: Невозможно определить цену для заявки " .. operation)
return
end
add_transaction(ticker, operation, price, quantity)
log.debug(string.format(
"Заявка %s для %s: цена = %.2f, количество = %d",
operation, ticker, price, quantity
))
end
local function is_short_enabled(ticker)
local class_code = "TQBR"
local sec_code = ticker
local firm_id = "MC0003300000" -- Замените на ваш firm_id
local client_code = "1360DK" -- Замените на ваш client_code
local price = 0 -- Цена для расчета лимитов (0 для текущей цены)
local buy_sell_info = getBuySellInfo(firm_id, client_code, class_code, sec_code, price)
if not buy_sell_info then
log.debug(string.format("Ошибка: Не удалось получить данные для %s через getBuySellInfo", ticker))
return false
end
local can_sell = tonumber(buy_sell_info.can_sell) or 0
return can_sell > 0
end
local function check_ema_cross(ticker, time_frame)
local ema_short = data[ticker].emas[time_frame][EMA_PERIODS[1]]
local ema_long = data[ticker].emas[time_frame][EMA_PERIODS[2]]
if not ema_short or not ema_long then
return "NONE"
end
local current_dir = (ema_short > ema_long) and "BUY" or "SELL"
local last_dir = data[ticker].last_dir or "NONE"
local last_market_status = market_status_history[#market_status_history]
local new_status = (ema_short > ema_long) and "LONG" or "SHORT"
if not last_market_status then
table.insert(market_status_history, new_status)
log.debug(string.format("Первое определение статуса рынка для %s: %s", ticker, new_status))
return "NONE"
end
local is_trade_allowed = false
if last_market_status == "LONG" and current_dir == "SELL" then
is_trade_allowed = true
elseif last_market_status == "SHORT" and current_dir == "BUY" then
is_trade_allowed = true
end
if is_trade_allowed and current_dir ~= last_dir then
data[ticker].last_dir = current_dir
log.debug(string.format("Пересечение средних для %s: %s -> %s (EMA short: %.4f, EMA long: %.4f)",
ticker, last_dir, current_dir, ema_short, ema_long))
local current_position = data[ticker].positions
if not current_position then
if current_dir == "SELL" and not is_short_enabled(ticker) then
log.debug(string.format("Шорт недоступен для %s. Пропускаем сигнал на продажу.", ticker))
return "NONE"
end
send_order(ticker, current_dir, LOT_SIZE)
else
send_order(ticker, "CLOSE", current_position.quantity)
send_order(ticker, current_dir, LOT_SIZE)
end
if #market_status_history > 0 then
market_status_history[1] = new_status -- Обновляем последний статус
else
table.insert(market_status_history, new_status) -- Добавляем первый статус
end
log.debug(string.format("Статус рынка для %s обновлен: %s", ticker, new_status))
else
log.debug(string.format("Статус рынка %s для %s. Сигнал %s игнорируется.",
last_market_status, ticker, current_dir))
end
return "NONE"
end
local function check_pending_orders()
for ticker, tdata in pairs(data) do
if tdata.pending_orders then
for i, order in ipairs(tdata.pending_orders) do
if os.time() - order.time > 60 then -- Если заявка висит более 60 секунд
log.debug(string.format("Снимаем заявку для %s: %s, цена = %.2f, количество = %d",
ticker, order.operation, order.price, order.quantity))
table.remove(tdata.pending_orders, i)
end
end
end
end
end
local function get_lot_size(ticker)
local lot = tonumber(getParamEx("TQBR", ticker, "LOTSIZE").param_value)
log.debug("--lot: " .. tostring(lot))
if not lot or lot <= 0 then
log.debug("Ошибка: Не удалось получить реальный размер лота для " .. ticker)
lot = 1
end
return lot
end
local function manage_tp_sl(ticker)
local pos = data[ticker].positions
if pos then
local best_bid, best_ask = get_best_prices(ticker)
local current_price = pos.signal == "BUY" and best_bid or best_ask
local open_price = pos.price -- Цена открытия позиции
local profit_percent = (current_price - open_price) / open_price * 100
local loss_percent = (open_price - current_price) / open_price * 100
if profit_percent >= TP_PERCENT or loss_percent >= SL_PERCENT then
log.debug(string.format(
"%s сработал для %s: прибыль/убыток = %.2f%%",
profit_percent >= TP_PERCENT and "Take Profit" or "Stop Loss",
ticker, profit_percent >= TP_PERCENT and profit_percent or loss_percent
))
send_order(ticker, "CLOSE", pos.quantity) -- Закрываем позицию
local signal = check_ema_cross(ticker, TIME_FRAMES[1])
if signal ~= "NONE" then
if data[ticker].market_status == "LONG" and signal == "SELL" then
send_order(ticker, "SELL", LOT_SIZE)
elseif data[ticker].market_status == "SHORT" and signal == "BUY" then
send_order(ticker, "BUY", LOT_SIZE)
end
end
end
end
end
local function get_current_positions()
local positions = {}
for ticker, tdata in pairs(data) do
if tdata.positions then
positions[ticker] = tdata.positions.quantity
end
end
return positions
end
local function process_transactions()
while #transaction_queue > 0 do
local tr = table.remove(transaction_queue, 1)
local ticker = tr.ticker
local operation = tr.operation
local price = tr.price
local quantity = tr.quantity
if MODE == "REAL" then
log.debug("Реальный режим: " .. operation .. " " .. ticker .. ", лотов: " .. quantity)
elseif MODE == "SIMULATION" then
log.debug("Симуляция: " .. operation .. " " .. ticker .. ", лотов: " .. quantity)
message("Симуляция: " .. operation .. " " .. ticker .. ", лотов: " .. quantity)
else
log.debug("История: " .. operation .. " " .. ticker .. ", лотов: " .. quantity)
end
local real_lot_size = get_lot_size(ticker)
if operation == "CLOSE" then
local pos = data[ticker].positions
if pos then
table.insert(data[ticker].trades, {
direction = pos.signal,
open_price = pos.price,
close_price= price,
quantity = pos.quantity,
profit = (price - pos.price) * pos.quantity * real_lot_size
})
TRADING_CAPITAL = TRADING_CAPITAL + price * pos.quantity * real_lot_size
data[ticker].positions = nil
end
else
TRADING_CAPITAL = TRADING_CAPITAL - price * quantity * real_lot_size
data[ticker].positions = {
price = price,
signal = operation,
quantity= quantity
}
end
end
end
local function calculate_profit()
local initial_capital = TRADING_CAPITAL
local total_profit = 0
log.debug("--- Результаты торговли ---")
for ticker, tdata in pairs(data) do
local ticker_profit = 0
if ticker_profit ~= 0 then
log.debug("Тикер: " .. ticker)
end
for i, trade in ipairs(tdata.trades) do
local lot_size = get_lot_size(ticker)
local profit
if trade.direction == "BUY" then
profit = (trade.close_price - trade.open_price) * trade.quantity * lot_size
elseif trade.direction == "SELL" then
profit = (trade.open_price - trade.close_price) * trade.quantity * lot_size
else
profit = 0
end
profit = tonumber(string.format("%.2f", profit))
ticker_profit = ticker_profit + profit
if profit ~= 0 then
log.debug(" Сделка #" .. i)
log.debug(" Направление: " .. trade.direction)
log.debug(" Цена открытия: " .. trade.open_price)
log.debug(" Цена закрытия: " .. trade.close_price)
log.debug(" Кол-во лотов: " .. trade.quantity)
log.debug(" Прибыль: " .. profit)
message("Тикер: " .. ticker .. " Прибыль: " .. profit)
end
end
local ticker_percent = (ticker_profit / initial_capital) * 100
total_profit = total_profit + ticker_profit
if ticker_profit ~= 0 then
log.debug(" Итог по " .. ticker)
log.debug(" Прибыль: " .. ticker_profit)
log.debug(" % от депозита: " .. string.format("%.2f", ticker_percent) .. "%")
message(" Итог по " .. ticker .. " Прибыль: " .. ticker_profit)
end
end
local total_percent = (total_profit / initial_capital) * 100
log.debug("--- Итог ---")
log.debug("Общая прибыль: " .. string.format("%.2f", total_profit))
log.debug("Общий процент прибыли: " .. string.format("%.2f", total_percent) .. "%")
message("Общая прибыль: " .. string.format("%.2f", total_profit))
end
local function close_all_positions()
local positions = get_current_positions()
for ticker, quantity in pairs(positions) do
local price = tonumber(getParamEx("TQBR", ticker, "LAST").param_value) or 0
add_transaction(ticker, "CLOSE", price, quantity)
log.debug("Закрытие позиции: " .. ticker .. ", количество: " .. quantity)
end
process_transactions() -- Обрабатываем все транзакции
end
local function read_tickers()
for line in io.lines(TICKER_FILE) do
local ticker = line:gsub("%s+", "")
data[ticker] = {
bars = {},
emas = {},
positions = nil,
trades = {},
tick_data = { sum_NP = 0, sum_N = 0 },
last_bar_time = {},
last_dir = "NONE",
}
for _, tf in ipairs(TIME_FRAMES) do
data[ticker].bars[tf] = {}
data[ticker].emas[tf] = {}
data[ticker].last_bar_time[tf] = 0
end
end
end
local function read_historical_data()
if MODE == "HISTORY" then
local file = io.open(DATA_FILE, "r")
if file then
for line in file:lines() do
local ticker, time_str, price_str = line:match("(%S+),(%S+),(%S+)")
local time_val = tonumber(time_str)
local price = tonumber(price_str)
if data[ticker] and time_val and price then
for _, tf in ipairs(TIME_FRAMES) do
table.insert(data[ticker].bars[tf], {
time = time_val,
close = price
})
end
end
end
file:close()
else
message("Ошибка: Не удалось открыть файл " .. DATA_FILE, 1)
end
else
message("Режим " .. MODE .. ": данные загружаются онлайн.", 1)
end
end
local function is_evening_session_allowed(ticker)
local ev_sess_result = getParamEx("TQBR", ticker, "EV_SESS_ALLOWED")
if not ev_sess_result or not ev_sess_result.param_image then
log.debug(string.format("Ошибка: Не удалось получить параметр EV_SESS_ALLOWED для %s.", ticker))
return false -- Запрещаем торговлю, если данные не получены
end
local ev_sess_allowed = tostring(ev_sess_result.param_image)
if ev_sess_allowed == "Да" then
log.debug(string.format("Вечерняя сессия доступна для %s.", ticker))
return true
else
log.debug(string.format("Вечерняя сессия недоступна для %s.", ticker))
return false
end
end
local function can_trade(ticker)
local current_time = os.date("*t", os_time)
local current_minutes = current_time.hour * 60 + current_time.min
log.debug(string.format("Текущее время: %02d:%02d", current_time.hour, current_time.min))
for _, interval in ipairs(TRADE_INTERVALS) do
local start_hour, start_min = interval.start:match("(%d+):(%d+)")
local end_hour, end_min = interval.close_time:match("(%d+):(%d+)")
local start_minutes = tonumber(start_hour) * 60 + tonumber(start_min)
local end_minutes = tonumber(end_hour) * 60 + tonumber(end_min)
if interval.start == "21:06" and interval.close_time == "01:49" then
if not data[ticker] then
log.debug(string.format("Ошибка: Данные для тикера %s не инициализированы.", ticker))
return false -- Запрещаем торговлю
end
if data[ticker].evening_session_allowed == nil then
log.debug(string.format("Ошибка: Поле evening_session_allowed для %s не инициализировано.", ticker))
return false -- Запрещаем торговлю
end
if not data[ticker].evening_session_allowed then
log.debug(string.format("Вечерняя сессия недоступна для %s. Интервал %s - %s пропущен.",ticker, interval.start, interval.close_time))
return false
end
end
if end_minutes < start_minutes then
if current_minutes >= start_minutes or current_minutes < end_minutes then
log.debug(string.format("Торговля разрешена в интервале: %s - %s",interval.start, interval.close_time))
return true
end
else
if current_minutes >= start_minutes and current_minutes < end_minutes then
log.debug(string.format("Торговля разрешена в интервале: %s - %s",interval.start, interval.close_time))
return true
end
end
end
log.debug("Торговля запрещена: текущее время вне торговых интервалов.")
return false
end
local tasks = {
{
name = "idle_check", -- Проверка бездействия
interval = 60, -- Интервал: 60 секунд
last_execution = os.time(),
callback = function()
log.debug("Проверка бездействия...")
end
},
{
name = "main_loop", -- Основной цикл
interval = 0.1, -- Интервал: 100 миллисекунд
last_execution = os.time(),
callback = function()
log.debug("Выполнение основного цикла...")
if can_trade() then -- <-- Используем can_trade
for ticker, tdata in pairs(data) do
local raw = getParamEx("TQBR", ticker, "LAST")
if raw and raw.param_value then
local price = tonumber(raw.param_value)
if price then
local current_time = os.time()
for _, tf in ipairs(TIME_FRAMES) do
update_bars(ticker, tf, price, current_time)
end
local signal = check_ema_cross(ticker, TIME_FRAMES[1])
if signal ~= "NONE" then
if tdata.positions then
add_transaction(ticker, "CLOSE", price, tdata.positions.quantity)-- Закрываем текущую позицию
add_transaction(ticker, signal, price, LOT_SIZE) -- Открываем новую позицию
else
add_transaction(ticker, signal, price, LOT_SIZE) -- Открываем новую позицию
end
end
manage_tp_sl(ticker)
process_transactions()
end
end
end
end
end
},
{
name = "last_update", -- Обновление LAST и вызов calculate_ema
interval = 1, -- Интервал: 1 секунда
last_execution = os.time(),
callback = function()
for ticker, _ in pairs(data) do
local last_price = tonumber(getParamEx("TQBR", ticker, "LAST").param_value)
if last_price then
log.debug("Обновление LAST для " .. ticker .. ": " .. last_price)
end
for _, tf in ipairs(TIME_FRAMES) do
for _, period in ipairs(EMA_PERIODS) do
calculate_ema(ticker, tf, period) -- Вызов функции
end
end
end
end
},
{
name = "lotsize_update", -- Обновление LOTSIZE и SHORT_ENABLED
interval = nil, -- Однократное выполнение
last_execution = nil,
callback = function()
for ticker, tdata in pairs(data) do
local lot_size = tonumber(getParamEx("TQBR", ticker, "LOTSIZE").param_value)
if lot_size then
tdata.lot_size = lot_size
log.debug(string.format("Размер лота для %s: %d",ticker, lot_size))
else
log.debug(string.format("Ошибка: Не удалось получить размер лота для %s",ticker))
tdata.lot_size = 1 -- Значение по умолчанию
end

data[ticker].evening_session_allowed = is_evening_session_allowed(ticker)
log.debug(string.format("Вечерняя сессия для %s: %s",ticker, data[ticker].evening_session_allowed and "доступна" or "недоступна"))
tdata.short_enabled = is_short_enabled(ticker)
log.debug(string.format("Шорт для %s: %s",ticker, tdata.short_enabled and "доступен" or "недоступен"))
end
end
},
{
name = "check_pending_orders", -- Проверка и снятие неисполненных заявок
interval = 10, -- Интервал: 10 секунд
last_execution = os.time(),
callback = function()
check_pending_orders()
end
}
}
local function execute_tasks()
local current_time = os.time()
for _, task in ipairs(tasks) do
if not task.last_execution then
task.callback()
task.last_execution = current_time
end
if task.interval and (current_time - task.last_execution >= task.interval) then
task.callback()
task.last_execution = current_time
end
end
end
function OnInit()
log.openfile(getScriptPath() .. "\\777.log", "w")
log.level = 'debug'
read_tickers()
read_historical_data()
log.debug("Робот запущен, режим: " .. MODE)
log.debug("Доступные средства: " .. TRADING_CAPITAL)
end
function OnStop()
log.debug("Запуск процедуры остановки робота...")
close_all_positions()
calculate_profit()
if log_file then
log_file:close()
log_file = nil
end
log.debug("Робот остановлен")
isRun = false
end
function _G.main()
while isRun do
execute_tasks()
sleep(10) -- Спим 10 миллисекунд
end
end

विचार

Analyzing complexity

I'm working through the analysis of a QLua script, considering a single global table for managing data for a trading robot to improve code clarity and maintainability.

Streamlining code

I’m considering best practices like reducing repeated code, using a single configuration table for settings, and maintaining a global data structure to manage multiple tasks and tickers effectively.

Refining code structure

I’m noting the importance of localizing functions, considering an object-oriented approach, and consolidating tasks to streamline the code. Additionally, addressing repeated logic and code unification is crucial.

Refining strategies

I’m thinking through concurrency patterns, utilizing state machines or strategy patterns for specific code sections, and enhancing testability by isolating logic from QLua specifics.

Thought about code optimization strategies для 6 секунд

उत्तर

Ниже приведён разбор основных моментов в коде и рекомендации по его улучшению. Пункты разбиты по темам: архитектура, организация данных, обработка событий, стиль кода и т.д.


1. Общая архитектура и организация кода

1.1 Разделение на модули (файлы)

Код в QLua часто начинает «разбухать» и становится трудно поддерживать, когда всё находится в одном файле. Рекомендуется вынести некоторые блоки кода в отдельные модули:

  • Модуль конфигурации (например, config.lua)
    Хранит такие вещи, как TIME_FRAMES, EMA_PERIODS, MODE, торговые интервалы, размер лота по умолчанию и другие «магические» числа.

  • Модуль стратегии/логики (например, strategy.lua)

    • Логика по обработке сигналов (EMA-кроссы, TP/SL).
    • Функции, типа check_ema_cross, manage_tp_sl, которые относятся к принятиям решений о входе/выходе и т.д.
  • Модуль данных (например, data_storage.lua), где описаны структуры хранения (bars, ema, trades и т.д.). Там же можно определить функции для чтения/записи файлов, работы со data[ticker].

  • Модуль вспомогательных функций (например, utils.lua) для всяких проверок, преобразований времени, чтения-интерпретации настроек, проверок доступности сессий и т.п.

Таким образом, в основном скрипте (main.lua или robot.lua) у вас останутся только высокоуровневые вызовы:

function OnInit() config = require("config") strategy = require("strategy") data_storage = require("data_storage") utils = require("utils") ... -- Инициализация, чтение тикеров, исторических данных и т.п. end function main() while isRun do execute_tasks() sleep(10) end end

1.2 Единая глобальная таблица или несколько

В текущем коде у вас есть глобальная таблица data, в которой для каждого тикера находятся:

  • bars[tf]
  • emas[tf]
  • positions
  • trades
  • tick_data
  • last_bar_time
  • last_dir
    и т.д.

Подобная структура — это уже достаточно хороший подход, позволяющий по тикеру получать всё нужное. Если вы хотите ещё более «монолитно» хранить данные, можно сделать иерархию вида:

global_state = { market_status_history = {}, transaction_queue = {}, tickers = { [ticker1] = { bars = { M1 = { ... }, M5 = { ... } }, emas = { M1 = { [3] = <value>, [5] = <value> } }, position = { signal = "BUY", price = 123.45, quantity = 10 }, trades = { ... }, -- и т.д. }, [ticker2] = { ... } } }

Плюс, возможно, в эту же «глобальную» структуру (или в отдельный модуль) складывать и «константы» вроде TP_PERCENT, SL_PERCENT и пр.

Однако:

  • Иногда удобнее, когда часть настроек (константы) выносится в отдельный модуль config.
  • «Одна глобальная таблица» может получиться очень большой. И если она «на всё про всё», то бывает сложно держать её в голове.
  • Важно соблюдать единый стиль: если data — это таблица, куда всё складывается по тикеру, то старайтесь все вспомогательные глобальные переменные (например, isRun, transaction_queue) тоже хранить либо внутри data, либо внутри другого специального объекта. Иначе будет «разброс» глобальных переменных, что ухудшает читаемость.

1.3 Локальные переменные vs. глобальные

В QLua (и вообще в Lua) глобальные переменные потенциально опасны тем, что их легко переопределить, к ним может быть «тишком» доступ из любого кода.

  • Если возможно, делайте переменные локальными внутри модуля или функции.
  • Если нужно, чтобы переменные были доступны во многих модулях, действительно можно хранить их в одной глобальной таблице типа _G.STATE или global_state.

Пример:

local M = {} -- модуль с функциями local state = { transaction_queue = {}, tickers = {}, ... } -- Функции, которые меняют state function M.addTransaction(...) table.insert(state.transaction_queue, ...) end return M

Так вы ограничите «разброс» по глобальному пространству имен и будете обновлять всё через один объект.


2. Обработка событий и задач

2.1 Механизм tasks и расписания

У вас реализован список задач (tasks), каждая из которых запускается с определённой периодичностью. Это хороший подход, но обратите внимание:

  1. Интервалы запуска

    • interval = 0.1 (100 миллисекунд) в main_loop и при этом sleep(10) миллисекунд в главном цикле может приводить к тому, что реальная частота вызовов будет всегда чуть реже, чем 100 мс. Это нормально, но стоит держать в уме, что кратные интервалы могут «съезжать».
    • Если часть задач не критична по времени, можно увеличить интервал, дабы экономить ресурсы и уменьшать нагрузку на QUIK.
  2. Повторная vs. однократная

    • Для lotsize_update стоит прямо указать комментарий в коде, что это «однократная задача» в начале, чтобы потом не забыть.
    • Если вдруг понадобится выполнять её повторно (например, при смене сессии), можно добавить флаг или переопределить интервал.
  3. Ошибки и исключения

    • Если внутри task.callback() произойдет ошибка, скрипт может «упасть» или зависнуть. Хорошо бы завернуть вызов callback в pcall, чтобы робота не выбивало из-за мелких ошибок, а в лог записывать информацию.

3. Логика торговли и функции

3.1 Единообразие расчётов

У вас есть несколько мест, где рассчитывается цена заявки (для BUY, для SELL, для CLOSE). В коде это разбросано между send_order, add_transaction и check_ema_cross. Проверьте, чтобы:

  • Результирующая цена всегда получалась по единым правилам (либо best_bid/best_ask, либо LAST).
  • Вся логика выставления заявки концентрировалась в одной функции (к примеру, send_order).
    Вызывающая функция (например, check_ema_cross) лишь говорит: «хочу BUY», «хочу SELL», «хочу CLOSE» и сколько лотов. А дальше send_order сам определяет оптимальную цену, добавляет в очередь транзакций.

3.2 Проверка шортов

Вы вызываете функцию is_short_enabled(ticker), которая внутри делает getBuySellInfo. Это нормально. Можно кэшировать результат (например, сохранить в data[ticker].short_enabled) и обновлять его раз в какое-то время, ведь это редко меняется.

3.3 Take Profit / Stop Loss

  • Сейчас TP/SL проверяются в функции manage_tp_sl(ticker). Если вы хотите масштабировать эту логику (примеры: трейлинг-стоп, частичное закрытие), удобно вынести её в отдельный модуль/функцию, особенно если планируется поддержка разных «типов» TP/SL.
  • При закрытии сделки по TP/SL вы снова вызываете check_ema_cross. Нужно аккуратно, чтобы не возникали коллизии (например, слишком быстрое переворачивание позиции). Возможно, стоит добавить задержку после закрытия (cooldown) — в зависимости от желаемой логики.

4. Чтение и хранение данных

4.1 read_tickers и read_historical_data

  • Хорошо, что тикеры хранятся в отдельном TICKER_FILE. Аналогично для DATA_FILE.
  • Если вы будете ещё расширять формат файла (добавлять колонку «лотов», «шорт разрешён» и пр.), подумайте о переносе структуры в более гибкий формат (CSV с разделителями или JSON).
  • Не забывайте проверять ошибки чтения файла. Сейчас при ошибке чтения DATA.TXT выводится сообщение, но потом код может продолжать работать в неполном состоянии.

4.2 Структура баров и EMA

В обновлении баров (update_bars) у вас есть «счётчик тиков» через tick_data.sum_NP и tick_data.sum_N. Это позволяет строить «среднюю» цену в баре. Логика интересная, но она не универсальна (учитывает только среднюю цену закрытия).

  • Если вам понадобится ещё и HIGH, LOW, объём и т.д., следует расширить структуру.
  • Возможно, есть смысл хранить «среднюю цену» отдельно от «цены закрытия», чтобы потом не путаться, где именно средняя, а где Close.
  • В любом случае, если стратегия такова — ок, просто не забывайте, что сейчас формирование бара не совсем каноническое (конечно, вы можете хотеть именно «среднюю цену внутри бара»).

5. Стиль кода и детали реализации

  1. Унификация временных интервалов
    Сейчас у вас есть:

    if time_frame == "M1" then interval = 20 elseif time_frame == "M3" then interval = 20 elseif time_frame == "M10" then interval = 600 ...

    Можно вынести в таблицу:

    local INTERVAL_MAP = { M1 = 60, M3 = 180, M5 = 300, M10 = 600, ... }

    Тогда interval = INTERVAL_MAP[time_frame] or 60.

  2. Минимизация повторяющегося кода

    • Код для проверки торгового интервала повторяется много раз. Можно сделать одну функцию:
      local function is_time_in_interval(cur_hour, cur_min, start_str, end_str) -- возвращает true/false end
    • Аналогично для «настроек» (class_code, firm_id, client_code) лучше вынести куда-то в config.lua.
  3. Уровень логирования
    Если стратегию нужно будет отлаживать на боевом счёте, подумайте о том, чтобы иметь разные уровни логирования (info, debug, error) и возможность быстро отключать детализацию.

  4. Защита от «пустых» данных
    Много где:

    local price = tonumber(getParamEx("TQBR", ticker, "LAST").param_value) or 0 if not price or price <= 0 then ... end

    При работе на реальном счёте такое редко, но бывает (QUIK не сразу даёт данные). Добавляйте в лог: «getParamEx(...) вернул 0, пропускаем расчет» и т.д.
    Иначе можно получить деление на 0 или «вход в сделку по цене 0».

  5. Дополнительная проверка транзакций
    Вы кладёте транзакции в очередь, а потом их обрабатываете (process_transactions). Если в реальном режиме QUIK вернёт «нехватка ГО», «заявка отвергнута» и т.д., хорошо бы обрабатывать эти ответы тоже. Это делается через функции OnTransReply, OnOrder в QLua.
    Тогда у вас будет более полноценная система:

    • «Положили в очередь» -> «Отправили» -> «QUIK дал ответ» -> «Если успех, то зафиксировали позицию» / «Если отказ — удалили транзакцию или перевыставили» и т.д.

6. Резюме по улучшениям

  1. Рассмотрите перенос большинства настроек и глобальных переменных в одну (или несколько) структур (таблиц):

    local config = { TIME_FRAMES = {"M1", "M5", ...}, EMA_PERIODS = {3, 5}, TP_PERCENT = 1.0, SL_PERCENT = 0.5, ... }

    И затем пользоваться config.TIME_FRAMES и т.д.
    Или же в одну глобальную таблицу global_state, где и data, и config, и tasks.

  2. Разделите код на модули, если он дальше будет расти. Выделите:

    • Логику торговли (стратегию)
    • Хранение данных
    • Конфигурацию
    • Вспомогательные функции (время, даты, строки и т.д.)
  3. Сделайте единый механизм работы с заявками через одну функцию (send_order + process_transactions) и старайтесь не дублировать эту логику. Если логика расчёта цены отличается для BUY/SELL/CLOSE, делайте это внутри send_order.

  4. Упростите проверку торгового времени:

    • Вынесите интервалы в одну таблицу.
    • Сделайте функцию, которая на вход принимает текущее время и список интервалов, а возвращает true/false.
  5. Локализуйте функции и переменные, где это возможно, чтобы сократить риск случайных переопределений. Используйте local function myfunc(...) и возвращайте эти функции из модуля.

  6. Добавьте обработку ошибок и сценариев, когда QUIK возвращает «null» значения (например, на старте или при потере связи). Убедитесь, что код не падает при отсутствующих данных.

  7. Подумайте о хранении/отображении результатов:

    • Вывод в лог (777.log) — это хорошо.
    • Если вам нужно потом анализировать торговлю, удобнее сохранять результаты сделок в CSV/JSON, чтобы смотреть на них графически.
  8. Настройки вечерней сессии:

    • Логика is_evening_session_allowed и проверка data[ticker].evening_session_allowed — хороший подход. Но если таких ограничений станет больше (например, торговля только в те дни, когда нет «длинных» праздников), лучше собрать все подобные проверки в одном месте.

Заключение

Ваш текущий код уже довольно структурирован и использует «централизованную» таблицу data[ticker] для хранения информации по инструментам. Основные улучшения сводятся к следующему:

  1. Сократить разброс глобальных переменных и чётче разделить ответственность функций (разделить модули, если код будет расти).
  2. Упростить/объединить повторяющиеся фрагменты кода (логика торговых интервалов, определение цены заявки, работа с EMA, чтение данных и т.д.).
  3. Чётко разделить (хотя бы логически, если не по файлам) уровни: «Что делать?» (стратегия) и «Как делать?» (через какие функции QUIK).
  4. Проверить и доработать логику обработки ошибок (отсутствие данных, отклонение заявок, кратковременное отсутствие связи и т.д.).

Если все эти моменты учесть, код станет легче поддерживать, расширять и отлаживать. Удачи в развитии вашей QLua-стратегии!

इस प्रश्नोत्तर को साझा करें