[pshaiBot] Oscillator Scalper (SPOT)
stableDescription
Oscillator Scalper for SPOT markets
A scalper with a bunch of customization; one out of 3 implemented oscillator and method for how you would like trades to trigger.
The scalper is a multi-position bot and will monitor all positions individually!
For gains and minimizing losses, this scalper utilizes fixed TP, fixed SL and TASL to secure some of the achieved profits.
This scalper can also be set to monitor a so-called "Master Market", for example BTC/USD(T). This master market is monitored for any possible dumps within set minutes. You are able to define the size and lookback of the dump. For example, if BTC/USD(T) drops 2% within 5 minutes; dump all open positions.
Required CC's:
[pshaiCmd] Input Converted Trade Amount
[pshaiCmd] Trailing Arm Stop-Loss (TASL)
HaasScript
-- [pshaiBot] Oscillator Scalper
-- Author: pshai
EnableHighSpeedUpdates(true)
HideOrderSettings()
HideTradeAmountSettings()
local oscillators = {
rsi = 'RSI',
mfi = 'MFI',
cci = 'CCI'
}
local signalTypes = {
crossOver = 'Cross-over Level',
crossUnder = 'Cross-under Level',
isAbove = 'Is Above Level',
isBelow = 'Is Below Level',
}
local tradingDirections = {
long = 'Buy/Long',
short = 'Sell/Short'
}
InputGroupHeader('User Controls')
local forceExit = Input('Force ALL positions to close', false, 'If true, bot will force all positions to exit. This also prevents bot from opening new positions.')
local stopNoPos = Input('Deactivate on NoPosition', false, 'If true, bot will deactivate itself when it has no more open positions. This also prevents bot from opening new positions.')
local tradeAmount = CC_InputConvertedTradeAmount('Per Position')
InputGroupHeader('Oscillator Settings')
local oscillator = InputOptions('1. Oscillator', oscillators.rsi, oscillators)
local osc_len = Input('2. Oscillator Length', 14)
local osc_buy = Input('3. Buy Level', 30)
local osc_sell = Input('4. Sell Level', 70)
InputGroupHeader('Signal Settings')
local buySignalType = InputOptions('1. Buy Signal Type (Buy Level)', signalTypes.isBelow, signalTypes)
local sellSignalType = InputOptions('2. Sell Signal Type (Sell Level)', signalTypes.isAbove, signalTypes)
InputGroupHeader('Trading Settings')
local tradingDirection = InputOptions('1. Trading Direction', tradingDirections.long, tradingDirections)
local maxPositions = Input('2. Max Positions', 3)
local orderCooldown = Input('3. Order Cooldown (minutes)', 60)
local orderTimeout = Input('4. Order Timeout (seconds)', 360)
local entryOrderType = InputOrderType('5. Entry Order Type', MarketOrderType)
InputGroupHeader('Safety Settings')
local takeProfit = Input('1. Take-Profit %', 1)
local stopLoss = Input('2. Stop-Loss %', 1)
local armLength = Input('3. Trailing Activation %', 0.5)
local trailDist = Input('4. Trailing Distance %', 1)
local stopCooldown = Input('5. Stop Cooldown (minutes)', 60, 'If position is closed by Stop-Loss, trading is then put on cooldown.')
local exitOrderType = InputOrderType('6. Exit Order Type', MarketOrderType)
local masterMarket = InputPriceSourceMarket('7. Master', 'BINANCE_BTC_USDT_')
local masterDumpSize = Input('8. Master Market Dump %', 5)
local masterDumpLb = Input('9. Master Market Lookback', 2, 'Lookback value in minutes')
local masterCooldown = Input('10. Master Market Cooldown (minutes)', 60, 'If positions are closed by master market dumpage, trading is then put on cooldown.')
-- ===============================================================
-- Handy functions
-- deep clone an object
function clone(original)
local copy = {}
for k, v in pairs(original) do
if GetType(v) == ArrayDataType then
v = clone(v)
end
copy[k] = v
end
return copy
end
-- check and initialize a module
function IsModuleInitialized(module)
if module._init then
LogError('Module "' .. module.name .. '" already initialized!')
return true
end
module._init = true
return false
end
function arrayAdd(array, value)
if GetHaasScriptVersion() == 1 then
array[#array +1] = value
return array
else
return ArrayAdd(array, value)
end
end
function arrayUnshift(array, value)
if GetHaasScriptVersion() == 1 then
local newArr = {}
for i = 1, #array do
newArr[i +1] = array[i]
end
newArr[1] = value
return newArr
else
return ArrayUnshift(array, value)
end
end
function arrayRemove(array, index)
if GetHaasScriptVersion() == 1 then
local newArr = {}
for i = 1, #array do
if i < index then
newArr[i] = array[i]
elseif i > index then
newArr[i-1] = array[i]
end
end
return newArr
else
return ArrayRemove(array, index)
end
end
-- ===============================================================
-- Modules
-- wrapper for script settings
local Config = {}
-- position object
local Position = {}
-- position manager
local PosMan = {}
-- master dumpage
local MasterDumpage = {}
-- ---------------------------------------------------------------
-- Config
function Config:init()
if IsModuleInitialized(self) then
return
end
-- thought i'd need this, but turns out i dont.
end
--
function MasterDumpage:init()
if IsModuleInitialized(self) then
return
end
self.PriceList = Load('md:pl', {})
self.Timestamps = Load('md:ts', {})
self.DumpAll = false
self.Direction = Sign(masterDumpSize)
end
function MasterDumpage:save()
Save('md:pl', self.PriceList)
Save('md:ts', self.Timestamps)
end
function MasterDumpage:getDumpAll()
return self.DumpAll, self.Direction
end
function MasterDumpage:update()
local mkt = masterMarket
local cp = CurrentPrice(mkt)
local remove = 0
self.Timestamps = arrayUnshift(self.Timestamps, Time())
self.PriceList = arrayUnshift(self.PriceList, cp.close)
-- remove old values by timestamp
for i = 1, #self.Timestamps do
local ts = self.Timestamps[i]
if ts < Time() - masterDumpLb * 60 then
remove = i
break
end
end
if remove > 0 then
self.PriceList = Grab(self.PriceList, 0, remove-1) --arrayRemove(self.PriceList, remove[i] -i)
self.Timestamps = Grab(self.Timestamps, 0, remove-1) --arrayRemove(self.Timestamps, remove[i] -i)
end
-- we want to anyway update prices even though the system is not in use...
-- so we return here, after we recorded the prices.
if masterDumpSize <= 0 then
return
end
--local sign = self.Direction
local oldPrice = self.PriceList[ #self.PriceList ]
local curPrice = self.PriceList[ 1 ]
--local priceDiff = sign > 0
-- and oldPrice / curPrice * 100 - 100
-- or curPrice / oldPrice * 100 - 100
local priceDiff = oldPrice / curPrice * 100 - 100
local timeNow = self.Timestamps[1]
local timeThen = self.Timestamps[#self.Timestamps]
local timeDiff = ((timeNow - timeThen) / 60)
Log('[MasterDumpage] Current time difference: ' .. Round(timeDiff, 2) .. ' minutes', SkyBlue)
Log('[MasterDumpage] Current price difference: ' .. Round(priceDiff, 2) .. ' % / ' .. masterDumpSize .. ' %', SkyBlue)
if priceDiff > masterDumpSize
then
self.DumpAll = true
end
local logDumpAll = Load('md:lda', true)
if self.DumpAll and logDumpAll then
LogWarning('Master Market is dumping: DUMP ALL!')
logDumpAll = false
elseif not self.DumpAll and not logDumpAll then
LogWarning('Looks like Master Market dump is over...')
logDumpAll = true
end
Save('md:lda', logDumpAll)
end
-- ---------------------------------------------------------------
-- Position object
function Position:load(id, isBuy)
local pos = clone(Position)
pos.Pid = id
pos.IsBuy = Load(id..':ib', isBuy)
pos.Bid = Load(id..':bid', '')
pos.Sid = Load(id..':sid', '')
pos.Completed = Load(id..':completed', false)
return pos
end
function Position:save()
Save(self.Pid .. ':ib', self.IsBuy)
Save(self.Pid .. ':bid', self.Bid)
Save(self.Pid .. ':sid', self.Sid)
Save(self.Pid .. ':completed', self.Completed)
end
-- ---------------------------------------------------------------
-- Position Manager
function PosMan:init()
if IsModuleInitialized(self) then
return
end
self.Pids = Load('posman:pids', {})
self.Positions = {}
self.Timer = Load('posman:t', 0)
self.MasterTimer = Load('posman:mt', 0)
for i = 1, #self.Pids do
local pid = self.Pids[i] -- get ID from array
local pos = Position:load(pid) -- load position per ID
-- add position to array
self.Positions = arrayAdd(self.Positions, pos)
end
end
function PosMan:save()
Save('posman:pids', self.Pids)
Save('posman:t', self.Timer)
Save('posman:mt', self.MasterTimer)
end
function PosMan:getPositionCount()
return #self.Pids
end
function PosMan:newPosition(isBuy, price, amount, orderType, orderTimeout)
local timeLeft = self.Timer + stopCooldown * 60 - Time()
if timeLeft > 0 then
LogWarning('Blocking new position; system in Stop Cooldown: ' .. Round(timeLeft/60.0, 2) .. ' minutes left')
return
end
local masterTimeLeft = self.MasterTimer + masterCooldown * 60 - Time()
if masterTimeLeft > 0 then
LogWarning('Blocking new position; system in Master Market Dumpage Cooldown: ' .. Round(masterTimeLeft/60.0, 2) .. ' minutes left')
return
end
local newId = NewGuid()
local sideStr = isBuy and 'Buy' or 'Sell'
Log('Creating new '..sideStr..' position, ID: ' .. SubString(newId, 0, 4) .. '...', DarkGray)
-- add id to mem
self.Pids = arrayAdd(self.Pids, newId)
-- load new position
local pos = Position:load(newId, isBuy)
-- add position to array
self.Positions = arrayAdd(self.Positions, pos)
if pos.IsBuy then
-- set buy order ID
pos.Bid = PlaceBuyOrder(price, amount, {type = orderType, positionId = newId, timeout = orderTimeout, note = 'Buy Entry'})
else
-- set sell order ID
pos.Sid = PlaceSellOrder(price, amount, {type = orderType, positionId = newId, timeout = orderTimeout, note = 'Sell Entry'})
end
end
function PosMan:update(pos)
local pc = PositionContainer(pos.Pid)
local cp = CurrentPrice(pc.market)
if pc.positionId == '' and pos.Bid == '' then
Log('Trying to update a nil position?', DarkGray)
return
end
if pos.Bid != '' then
local order = OrderContainer(pos.Bid)
if order.isOpen then
-- do smth
else
if not pos.IsBuy and order.isFilled then
pos.Completed = true
end
pos.Bid = ''
end
end
if pos.Sid != '' then
local order = OrderContainer(pos.Sid)
if order.isOpen then
-- do smth
else
if pos.IsBuy and order.isFilled then
pos.Completed = true
end
pos.Sid = ''
end
end
if pc.amount > 0 and ((pos.IsBuy and pc.isLong) or (not pos.IsBuy and pc.isShort)) then
if IsTradeAmountEnough(PriceMarket(), cp.close, pc.amount, false) then
-- TODO: place sell order
-- TODO: stop-loss, take-profit, trailing-arm stop-loss
local tp = TakeProfit(takeProfit, pos.Pid)
local sl = StopLoss(stopLoss, pos.Pid)
local tsl = CC_TrailingArmStopLoss(trailDist, armLength, pos.Pid)
if tp then
self:exit(pos, 'Take-Profit')
elseif sl then
self:exit(pos, 'Stop-Loss')
self.Timer = Time() -- put on cooldown...
elseif tsl then
self:exit(pos, 'Trailing-Stop')
--self.Timer = Time() -- put on cooldown...
end
else
Log('Position amount too small; not able to sell (will be removed)', Cyan)
pos.Completed = true
end
end
end
function PosMan:exit(pos, note)
local pc = PositionContainer(pos.Pid)
local cp = CurrentPrice()
if pos.IsBuy and pos.Sid == '' then
-- set sell order ID
pos.Sid = PlaceSellOrder(cp.ask, pc.amount, {type = exitOrderType, positionId = pos.Pid, timeout = orderTimeout, note = note})
elseif not pos.IsBuy and pos.Bid == '' then
-- set buy order ID
pos.Bid = PlaceBuyOrder(cp.bid, pc.amount, {type = exitOrderType, positionId = pos.Pid, timeout = orderTimeout, note = note})
end
end
function PosMan:updateAll()
-- update master market stuff
local dumpAll, direction = MasterDumpage:getDumpAll()
-- update positions
local positions = self.Positions
local i = 1
local cp = CurrentPrice()
local removed = {}
while i <= #positions do
local pos = positions[i]
local pc = PositionContainer(pos.Pid)
if not dumpAll and not forceExit then
self:update(pos)
pos:save()
else
local dumpage = dumpAll --and ((direction > 0 and pos.IsBuy) or (direction < 0 and not pos.IsBuy))
local reason = 'Master Market Dump'
if forceExit then
reason = 'Force Exit'
end
if dumpage or forceExit then
if pc.amount > MinimumTradeAmount() then
PlaceExitPositionOrder(pos.Pid, {type = MarketOrderType, note = reason})
end
pos.Completed = true
end
end
if pos.Completed then
if not IsPositionClosed(pos.Pid) then
if not dumpAll then
Log('Position is completed, but left out with dust; simulated cleaning', DarkGray)
CloseVPosition(cp.close, pos.Pid)
else
Log('Waiting for dumpage to be over...')
end
else
Log('Completed position cycle; removing position.', Gold)
removed = arrayAdd(removed, i)
end
elseif pc.positionId == '' and pos.Bid == '' then
removed = arrayAdd(removed, i)
Log('Found empty position ID; removing position.', Gold)
end
i = i + 1
end
if dumpAll and self.MasterTimer == 0 then
self.MasterTimer = Time()
elseif self.MasterTimer + masterCooldown * 60 - Time() <= 0 then
self.MasterTimer = 0
end
if #removed > 0 then
local newPids = {}
for i = 1, #self.Pids do
if not ArrayContains(removed, i) then
if IsAnyOrderOpen(self.Pids[i]) then
CancelAllOrders(self.Pids[i])
end
newPids = arrayAdd(newPids, self.Pids[i])
end
end
self.Pids = newPids
end
end
-- ===============================================================
-- Trading logic
function hasSignal(value, level, type)
if type == signalTypes.isBelow then
return value <= level
elseif type == signalTypes.isAbove then
return value >= level
elseif type == signalTypes.crossOver then
return CrossOver(value, level)
elseif type == signalTypes.crossUnder then
return CrossUnder(value, level)
end
LogWarning('Signal type "' .. type .. '" is not defined in hasSignal() function!')
return false
end
-- fire up position manager
PosMan:init()
if stopNoPos and PosMan:getPositionCount() == 0 then
DeactivateBot('Deactivation on NoPosition triggered.', true)
end
-- handle master market dumpage check
MasterDumpage:init()
MasterDumpage:update()
local orderTimer = Load('ot', 0)
local logNotOK = Load('lnok', true)
local o = OpenPrices()
local h = HighPrices()
local l = LowPrices()
local c = ClosePrices()
local v = GetVolume()
local cp = CurrentPrice()
local osc
if oscillator == oscillators.rsi then
osc = RSI(c, osc_len)
elseif oscillator == oscillators.mfi then
osc = MFI(h, l, c, v, osc_len)
elseif oscillator == oscillators.cci then
osc = CCI(h, l, c, osc_len)
end
Plot(2, 'Oscillator', osc, Purple)
PlotHorizontalLine(2, 'Buy', Green, osc_buy, Dashed)
PlotHorizontalLine(2, 'Sell', Red, osc_sell, Dashed)
local buySignal = hasSignal(osc, osc_buy, buySignalType)
local sellSignal = hasSignal(osc, osc_sell, sellSignalType)
local shouldBuy = tradingDirection == tradingDirections.long and buySignal
local shouldSell = tradingDirection == tradingDirections.short and sellSignal
local timerOK = orderTimer + orderCooldown * 60 <= Time() -- cooldown check
local positionCount = PosMan:getPositionCount()
local positionsOK = positionCount < maxPositions -- max positions check
local dumpAll = MasterDumpage:getDumpAll()
Log('Buy signal: ' .. (shouldBuy and 'Yes' or 'No'))
Log('Sell signal: ' .. (shouldSell and 'Yes' or 'No'))
Log('Timer OK: ' .. (timerOK and 'Yes' or 'No'))
Log('Position count OK: ' .. (positionsOK and 'Yes' or 'No'))
if timerOK and positionsOK and not dumpAll then
logNotOK = true
if not forceExit and not stopNoPos then
if shouldBuy then
PosMan:newPosition(true, cp.bid, tradeAmount, GetOrderType(), orderTimeout)
orderTimer = Time()
elseif shouldSell then
PosMan:newPosition(false, cp.ask, tradeAmount, GetOrderType(), orderTimeout)
orderTimer = Time()
end
end
else
if logNotOK then
if not timerOK then
Log('Blocking new entries; entry cooldown', DarkGray)
logNotOK = false
elseif not positionsOK then
Log('Blocking new entries; max positions open', DarkGray)
logNotOK = false
elseif dumpAll then
Log('Blocking new entries; master market is dumping', DarkGray)
logNotOK = false
end
end
end
Save('lnok', logNotOK)
Save('ot', orderTimer)
Plot(1, 'Positions', positionCount)
-- update all positions and save
PosMan:updateAll()
PosMan:save()
MasterDumpage:save()
3 Comments
Sign in to leave a comment.
36. 11 Nov 2021 16:14:41 ERROR: Log(): Invalid input count of 2 (only takes 1)
It throws this error at me, when I start it and or run it through debug
A Log error like this is usually because in the updated beta version you can use colours, if you don’t have this, remove the colour after the “ , “ in the Log. Eg. Log(‘foo’, Green) => Log(‘foo’)
Hmm, looks like you are running an older version of HTS. I can't remember the version, but a custom color input was introduced for Log command. You can remove the color inputs from the Log() command calls as well.