-- Simple Market Maker by pshai
EnableHighSpeedUpdates(true)
HideOrderSettings()
HideTradeAmountSettings()
-- inputs
local slotCount = Input('01. Slot Count', 5, 'How many orders are constantly kept open on both long and short side')
local slotSize = Input('02. Slot Size', 10, 'Trade amount per slot')
local slotSpread = Input('03. Slot Spread %', 0.1, 'Percentage based spread value between each slot')
local slotCancel = Input('04. Cancel Distance %', 0.1, 'How much price can move to the opposite direction before orders are cancelled and replaced')
local minSpread = Input('05. Minimum Spread %', 0.1, 'Minimum spread percentage between the first long and short entries. This setting only works when bot has no position.')
local maxSize = Input('06. Max. Open Contracts', 12000, 'Maximum open contracts at any given time. After exceeding this value, the bot will dump a portion of position at a loss')
local reduceSize = Input('07. Size Reduction %', 25, 'How big of a portion the bot will dump once Max. Open Contracts is exceeded')
local reduceOrderType = InputOrderType('08. Reduction Order Type', MarketOrderType, 'The order type for size reduction dump')
local takeProfit = Input('09. Take-Profit %', 0.2, 'Fixed take-profit value, based on price change')
local tpOrderType = InputOrderType('10. TP Order Type', MakerOrCancelOrderType, 'The order type for take-profit')
local hedgeMode = Input('11. Hedge Mode', false, 'Hedge Mode works on exchanges like OKEX and Binance Futures with hedge mode enabled (need to set that via Binance Futrues website!). Changing this setting while bot is running will cause unwanted behavior!!')
local hedgeRoi = Input('12. Hedge Minimum ROI %', 10, 'The ROI % the current open position needs to have before we enable the opposite side')
local allowLong = Input('13. Allow Long Positions', true, 'Set to false to disable Long Positions during a down trend.')
local allowShort = Input('13. Allow Short Positions', true, 'Set to false to disable Short Positions during an up trend.')
--
minSpread = minSpread / 2.0
-- regular single-position logic
if not hedgeMode then
-- price and data
local cp = CurrentPrice()
local aep = GetPositionEnterPrice()
local pamt = GetPositionAmount()
local proi = GetPositionROI()
Log('position ROI: '..Round(proi, 4)..'%')
-- not using spread if we have a position
if pamt > 0 then
minSpread = 0
end
-- slot function
local slot = function(isLong, index, amount, spread, cancelDist)
local prefix = isLong and 'L' or 'S'
local name = prefix .. index
local cmd = isLong and PlaceExitShortOrder or PlaceExitLongOrder -- a little hack...
local priceBase = isLong
and cp.bid
or cp.ask
local spr = minSpread + spread * index
-- if we have average entry price
if aep > 0 then
priceBase = isLong
and Min(aep, priceBase)
or Max(aep, priceBase)
end
-- get price
local price = isLong
and SubPerc(priceBase, spr)
or AddPerc(priceBase, spr)
local oid = Load(name..'oid', '') -- order id
if oid != '' then
local order = OrderContainer(oid)
if order.isOpen then
local delta = isLong
and Delta(AddPerc(order.price, spr), priceBase)
or Delta(priceBase, SubPerc(order.price, spr))
if delta >= cancelDist then
CancelOrder(oid)
oid = '' -- reset id immediately, otherwise need 2 updates to get new order
LogWarning('Delta cancelled '..name)
end
else
oid = ''
end
else
SetFee(Abs(MakersFee())*-1)
oid = cmd(price, amount, {type = MakerOrCancelOrderType, note = name, timeout = 3600})
end
Save(name..'oid', oid)
end
-- update take-profit
local updateTakeProfit = function(entryPrice, targetRoi, cancelDist)
local name = 'Take-Profit'
local oid = Load('tp_oid', '')
local isLong = GetPositionDirection() == PositionLong
local timer = Load('tp_timer', Time())
local tp_delta = isLong and Delta(entryPrice, cp.bid) or Delta(cp.ask, entryPrice)
if oid != '' then
local order = OrderContainer(oid)
if order.isOpen then
local delta = isLong
and Delta(order.price, cp.close)
or Delta(cp.close, order.price)
if delta >= cancelDist then
CancelOrder(oid)
LogWarning('Delta cancelled '..name)
end
else
oid = ''
end
else
if tp_delta >= targetRoi and Time() >= timer then
SetFee(tpOrderType == MarketOrderType and TakersFee() or Abs(MakersFee())*-1)
oid = PlaceExitPositionOrder({type = tpOrderType, note = name, timeout = 3600})
timer = Time() + 60 -- 1min
end
end
Save('tp_oid', oid)
Save('tp_timer', timer)
end
-- update position size
local updatePositionManagement = function(currentSize, sizeLimit, cancelDist)
local name = 'Size Reduction'
local oid = Load('pos_oid', '')
local isLong = GetPositionDirection() == PositionLong
local amount = SubPerc(currentSize, 100 - reduceSize) -- take X% of position
local price = isLong
and cp.ask
or cp.bid
local cmd = isLong
and PlaceExitLongOrder
or PlaceExitShortOrder
local timer = Load('pos_timer', Time())
if oid != '' then
local order = OrderContainer(oid)
if order.isOpen then
local delta = isLong
and Delta(order.price, cp.close)
or Delta(cp.close, order.price)
if delta >= cancelDist then
CancelOrder(oid)
LogWarning('Delta cancelled '..name)
end
else
oid = ''
end
else
if currentSize > sizeLimit and Time() >= timer then
SetFee(reduceOrderType == MarketOrderType and TakersFee() or Abs(MakersFee())*-1)
oid = cmd(price, amount, {type = reduceOrderType, note = name, timeout = 6000})
timer = Time() + 60 -- 1min
end
end
Save('pos_oid', oid)
Save('pos_timer', timer)
end
-- da logica
-- take profit
updateTakeProfit(aep, takeProfit, slotCancel)
-- risk management
updatePositionManagement(pamt, maxSize, slotCancel)
if allowLong then
-- update slots
for i = 1, slotCount do
slot(true, i, slotSize, slotSpread, slotCancel) -- long slot
end
end
if allowShort then
for i = 1, slotCount do
slot(false, i, slotSize, slotSpread, slotCancel) -- short slot
end
end
if aep > 0 then
local posId = PositionContainer().positionId
Plot(0, 'AvgEP', aep, {c=Purple, id=posId, w=2})
end
--------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------
-- hedge it!
else
-- price and data
local cp = CurrentPrice()
local c = ClosePrices()
local rsi = RSI(c, 9) -- RSI is used to determine which side to start off with. this is to eliminate ghost positions.
local signal = rsi > 52 and -1 or rsi < 48 and 1 or 0
-- positions
local hedge_longPosId = Load('hedge_longPosId', NewGuid())
local hedge_shortPosId = Load('hedge_shortPosId', NewGuid())
local dir_l = GetPositionDirection(hedge_longPosId)
local aep_l = GetPositionEnterPrice(hedge_longPosId)
local pamt_l = GetPositionAmount(hedge_longPosId)
local proi_l = GetPositionROI(hedge_longPosId)
local dir_s = GetPositionDirection(hedge_shortPosId)
local aep_s = GetPositionEnterPrice(hedge_shortPosId)
local pamt_s = GetPositionAmount(hedge_shortPosId)
local proi_s = GetPositionROI(hedge_shortPosId)
Log('LONG position ROI: '..Round(proi_l, 4)..'%')
Log('SHORT position ROI: '..Round(proi_s, 4)..'%')
-- not using spread if we have a position
if pamt_l > 0 or pamt_s > 0 then
minSpread = 0
end
-- manage position ids
if pamt_l == 0 and IsPositionClosed(hedge_longPosId) then
if IsAnyOrderOpen(hedge_longPosId) then
CancelAllOrders(hedge_longPosId)
else
hedge_longPosId = NewGuid()
dir_l = GetPositionDirection(hedge_longPosId)
aep_l = GetPositionEnterPrice(hedge_longPosId)
pamt_l = GetPositionAmount(hedge_longPosId)
proi_l = GetPositionROI(hedge_longPosId)
end
end
if pamt_s == 0 and IsPositionClosed(hedge_shortPosId) then
if IsAnyOrderOpen(hedge_shortPosId) then
CancelAllOrders(hedge_shortPosId)
else
hedge_shortPosId = NewGuid()
dir_s = GetPositionDirection(hedge_shortPosId)
aep_s = GetPositionEnterPrice(hedge_shortPosId)
pamt_s = GetPositionAmount(hedge_shortPosId)
proi_s = GetPositionROI(hedge_shortPosId)
end
end
-- get pos id
local getPositionId = function(isLong)
return isLong and hedge_longPosId or hedge_shortPosId
end
-- slot function
local slot = function(isLong, index, amount, spread, cancelDist, canPlace)
local prefix = isLong and 'L' or 'S'
local name = prefix .. index
local cmd = isLong and PlaceGoLongOrder or PlaceGoShortOrder
local priceBase = isLong
and cp.bid
or cp.ask
local spr = minSpread + spread * index
local posId = getPositionId(isLong)
local aep = isLong and aep_l or aep_s
-- if we have average entry price
if aep > 0 then
priceBase = isLong
and Min(aep, priceBase)
or Max(aep, priceBase)
end
-- get price
local price = isLong
and SubPerc(priceBase, spr)
or AddPerc(priceBase, spr)
local oid = Load(name..'oid', '') -- order id
if oid != '' then
local order = OrderContainer(oid)
if order.isOpen then
local delta = isLong
and Delta(AddPerc(order.price, spr), priceBase)
or Delta(priceBase, SubPerc(order.price, spr))
if delta >= cancelDist then
CancelOrder(oid)
LogWarning('Delta cancelled '..name)
elseif not canPlace then
CancelOrder(oid)
LogWarning('Not allowed right now '..name)
end
else
oid = ''
end
else
if canPlace then
SetFee(Abs(MakersFee())*-1)
oid = cmd(price, amount, {type = MakerOrCancelOrderType, note = name, timeout = 3600, positionId = posId})
end
end
Save(name..'oid', oid)
end
-- update take-profit
local updateTakeProfit = function(isLong, entryPrice, targetRoi, cancelDist)
local prefix = isLong and 'Long' or 'Short'
local name = prefix .. ' Take-Profit'
local oid = Load(prefix .. 'tp_oid', '')
local timer = Load(prefix .. 'tp_timer', 0)
local posId = getPositionId(isLong)
local tp_delta = isLong and Delta(entryPrice, cp.bid) or Delta(cp.ask, entryPrice)
if oid != '' then
local order = OrderContainer(oid)
if order.isOpen then
local delta = isLong
and Delta(order.price, cp.close)
or Delta(cp.close, order.price)
if delta >= cancelDist then
CancelOrder(oid)
LogWarning('Delta cancelled '..name)
end
else
if order.isCancelled then
timer = 0
end
oid = ''
end
else
if tp_delta >= targetRoi and Time() >= timer then
SetFee(tpOrderType == MarketOrderType and TakersFee() or Abs(MakersFee())*-1)
oid = PlaceExitPositionOrder({type = tpOrderType, note = name, timeout = 3600, positionId = posId})
timer = Time() + 60 -- 1min
end
end
Save(prefix .. 'tp_oid', oid)
Save(prefix .. 'tp_timer', timer)
end
-- update position size
local updatePositionManagement = function(isLong, currentSize, sizeLimit, cancelDist)
local prefix = isLong and 'Long' or 'Short'
local name = prefix .. ' Size Reduction'
local oid = Load(prefix .. 'pos_oid', '')
local posId = getPositionId(isLong)
local amount = SubPerc(currentSize, 100 - reduceSize) -- take X% of position
local price = isLong
and cp.ask
or cp.bid
local cmd = isLong
and PlaceExitLongOrder
or PlaceExitShortOrder
local timer = Load(prefix .. 'pos_timer', Time())
if oid != '' then
local order = OrderContainer(oid)
if order.isOpen then
local delta = isLong
and Delta(order.price, cp.close)
or Delta(cp.close, order.price)
if delta >= cancelDist then
CancelOrder(oid)
LogWarning('Delta cancelled '..name)
end
else
oid = ''
end
else
if currentSize > sizeLimit and Time() >= timer then
SetFee(reduceOrderType == MarketOrderType and TakersFee() or Abs(MakersFee())*-1)
oid = cmd(price, amount, {type = reduceOrderType, note = name, timeout = 6000, positionId = posId})
timer = Time() + 60 -- 1min
end
end
Save(prefix .. 'pos_oid', oid)
Save(prefix .. 'pos_timer', timer)
end
-- da logica
-- take profit
if allowLong then
updateTakeProfit(true, aep_l, takeProfit, slotCancel)
end
if allowShort then
updateTakeProfit(false, aep_s, takeProfit, slotCancel)
end
-- risk management
if allowLong then
updatePositionManagement(true, pamt_l, maxSize, slotCancel)
end
if allowShort then
updatePositionManagement(false, pamt_s, maxSize, slotCancel)
end
-- update slots
if allowLong then
for i = 1, slotCount do
slot(true, i, slotSize, slotSpread, slotCancel, (pamt_s == 0 and signal == 1) or proi_s >= hedgeRoi) -- long slot
end
end
if allowShort then
for i = 1, slotCount do
slot(false, i, slotSize, slotSpread, slotCancel, (pamt_l == 0 and signal == -1) or proi_l >= hedgeRoi) -- short slot
end
end
if aep_l > 0 then
local posId = getPositionId(true)
Plot(0, 'AvgEP Long', aep_l, {c=Teal, id=posId, w=2})
end
if aep_s > 0 then
local posId = getPositionId(false)
Plot(0, 'AvgEP Short', aep_s, {c=Purple, id=posId, w=2})
end
Save('hedge_longPosId', hedge_longPosId)
Save('hedge_shortPosId', hedge_shortPosId)
end