init
This commit is contained in:
525
llmjob/1525.go
Normal file
525
llmjob/1525.go
Normal file
@@ -0,0 +1,525 @@
|
||||
// 修复后的banbot完整策略代码
|
||||
package llmjob
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/banbox/banbot/btime"
|
||||
"github.com/banbox/banbot/config"
|
||||
"github.com/banbox/banbot/core"
|
||||
"github.com/banbox/banbot/orm/ormo"
|
||||
"github.com/banbox/banbot/strat"
|
||||
"github.com/banbox/banexg/log"
|
||||
ta "github.com/banbox/banta"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"go.uber.org/zap"
|
||||
"math"
|
||||
)
|
||||
|
||||
/*
|
||||
根据AtrBase基于不同品种权重,限制范围后更好
|
||||
从4h根据行情自动调整加仓阈值和止盈阈值后:
|
||||
之前:夏普0.93,显示回撤35.8%,最大回撤95.4%,最大资金占用3.2W,总收益119.6%
|
||||
之后:夏普1.38,显示回撤5.66%,最大回撤36.2%,最大资金占用1.64W,总收益79.4%
|
||||
|
||||
尝试使用公式0.383*pow(1+target_qty, x)加仓,符合测试的数据,但效果更差
|
||||
*/
|
||||
|
||||
// State1525S 存储每个品种的策略状态
|
||||
type State1525S struct {
|
||||
avgPrice float64 // 平均持仓成本
|
||||
positionQty float64 // 当前持仓数量
|
||||
tpLimit float64
|
||||
score float64 // 下跌行情力度分数,值域[0,6]
|
||||
}
|
||||
|
||||
func J1525(pol *config.RunPolicyConfig) *strat.TradeStrat {
|
||||
// 定义可调超参数
|
||||
smaPeriod := pol.DefInt("sma_period", 18, core.PNorm(5, 50))
|
||||
targetPercRaw := pol.Def("target_perc", -0.14, core.PNorm(-0.3, -0.05))
|
||||
takeProfitRaw := pol.Def("take_profit", 0.09, core.PNorm(0.05, 0.3))
|
||||
targetQty := pol.Def("target_qty", 0.65, core.PNorm(0.2, 1.0))
|
||||
// 加仓次数,超过20后变差;低于19后有段时间不稳定
|
||||
maxAdd := pol.DefInt("max_add", 19, core.PNorm(1, 30))
|
||||
maxOpenPair := pol.DefInt("max_open_pair", 25, core.PNorm(1, 30)) // 最大同时交易品种
|
||||
|
||||
openPairs := make(map[*strat.StratJob]int)
|
||||
pairLock := deadlock.Mutex{}
|
||||
lenAtr := 20
|
||||
baseAtrLen := int(math.Round(float64(lenAtr) * 4.3))
|
||||
|
||||
targetPerc := targetPercRaw
|
||||
takeProfit := takeProfitRaw
|
||||
|
||||
printMS := int64(0)
|
||||
maxPairs := 0
|
||||
// 加仓倍率,基于targetQty: 1, 0.6, 1 1.7 3 4.7 7.7 12.8 21.1 35 57.5 95 157 256 385 464 767
|
||||
return &strat.TradeStrat{
|
||||
WarmupNum: max(smaPeriod, baseAtrLen),
|
||||
HedgeOff: true, // 单向持仓模式
|
||||
RunTimeFrames: []string{"5m"},
|
||||
BatchInfo: true,
|
||||
OrderOnRotation: "open",
|
||||
OnStartUp: func(s *strat.StratJob) {
|
||||
// 初始化策略状态
|
||||
s.More = &State1525S{}
|
||||
},
|
||||
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
|
||||
return []*strat.PairSub{
|
||||
{"_cur_", "4h", 20},
|
||||
}
|
||||
},
|
||||
OnBar: func(s *strat.StratJob) {
|
||||
e := s.Env
|
||||
state, _ := s.More.(*State1525S)
|
||||
closePrice := e.Close.Get(0)
|
||||
|
||||
// 不利行情下调整参数
|
||||
targetPercJob, takeProfitJob := targetPercRaw, takeProfitRaw
|
||||
if state.score >= 5 {
|
||||
targetPercJob = targetPercRaw * 2
|
||||
takeProfitJob = takeProfitRaw * 0.6
|
||||
} else if state.score >= 4 {
|
||||
targetPercJob = targetPercRaw * 1.7
|
||||
takeProfitJob = takeProfitRaw * 0.7
|
||||
} else if state.score >= 3 {
|
||||
targetPercJob = targetPercRaw * 1.3
|
||||
takeProfitJob = takeProfitRaw * 0.8
|
||||
}
|
||||
targetPercCur := (targetPercJob + targetPerc) / 2
|
||||
takeProfitCur := (takeProfitJob + takeProfit) / 2
|
||||
|
||||
// 计算SMA
|
||||
ma := ta.SMA(e.Close, smaPeriod)
|
||||
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
|
||||
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0)
|
||||
openRate := 0.006 * closePrice / atrBase
|
||||
if openRate < 1 {
|
||||
openRate = math.Sqrt(openRate)
|
||||
}
|
||||
openRate = max(0.7, min(1.5, openRate))
|
||||
|
||||
// 计算当前持仓盈亏比例
|
||||
var pnl float64
|
||||
if state.positionQty > 0 && state.avgPrice > 0 {
|
||||
pnl = (closePrice - state.avgPrice) / state.avgPrice
|
||||
}
|
||||
|
||||
// 计算止盈价格
|
||||
if state.positionQty > 0 && state.avgPrice > 0 {
|
||||
curTakeProfit := state.avgPrice * (1 + takeProfitCur)
|
||||
if curTakeProfit != state.tpLimit {
|
||||
state.tpLimit = curTakeProfit
|
||||
// 设置限价单止盈
|
||||
s.CloseOrders(&strat.ExitReq{
|
||||
Tag: "Take Profit",
|
||||
Limit: state.tpLimit,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 首次入场条件:价格上穿SMA且无持仓
|
||||
if e.Close.Cross(ma) == 1 && state.positionQty == 0 {
|
||||
pairLock.Lock()
|
||||
_, allow := openPairs[s]
|
||||
if !allow && len(openPairs) < maxOpenPair {
|
||||
openPairs[s] = 1
|
||||
maxPairs = max(maxPairs, len(openPairs))
|
||||
allow = true
|
||||
}
|
||||
pairLock.Unlock()
|
||||
if allow {
|
||||
s.OpenOrder(&strat.EnterReq{
|
||||
Tag: "Long",
|
||||
CostRate: openRate,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 加仓条件:亏损达到目标比例且有持仓
|
||||
if state.positionQty > 0 && pnl <= targetPercCur && len(s.LongOrders) < maxAdd {
|
||||
// 计算加仓数量,确保是整数
|
||||
odNum := len(s.LongOrders)
|
||||
addQty := math.Floor(state.positionQty * targetQty)
|
||||
pairLock.Lock()
|
||||
openPairs[s] = odNum
|
||||
pairLock.Unlock()
|
||||
s.OpenOrder(&strat.EnterReq{
|
||||
Tag: fmt.Sprintf("%v", odNum),
|
||||
Amount: addQty,
|
||||
})
|
||||
if odNum > 5 {
|
||||
printMS = e.TimeStop
|
||||
}
|
||||
}
|
||||
},
|
||||
OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) {
|
||||
score := float64(0)
|
||||
if ta.RSI(e.Close, 14).Get(0) < 30 {
|
||||
score += 1
|
||||
}
|
||||
roc := ta.ROC(e.Close, 20).Get(0)
|
||||
if roc < -20 {
|
||||
score += 2
|
||||
} else if roc < -10 {
|
||||
score += 1
|
||||
}
|
||||
plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14)
|
||||
adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0)
|
||||
if minusDi.Get(0) > plusDi.Get(0) {
|
||||
if adx > 40 {
|
||||
score += 2
|
||||
} else if adx > 25 {
|
||||
score += 1
|
||||
}
|
||||
}
|
||||
ma25 := ta.SMA(e.Close, 25).Get(0)
|
||||
ma99 := ta.SMA(e.Close, 99).Get(0)
|
||||
if ma99 > ma25 {
|
||||
score += 1
|
||||
}
|
||||
state, _ := s.More.(*State1525S)
|
||||
state.score = score
|
||||
},
|
||||
OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) {
|
||||
jobNum := len(jobs)
|
||||
if jobNum < 3 {
|
||||
return
|
||||
}
|
||||
downNum, downHeavyNum := float64(0), float64(0)
|
||||
timeStop := int64(0)
|
||||
var last *strat.JobEnv
|
||||
for _, j := range jobs {
|
||||
state, _ := j.Job.More.(*State1525S)
|
||||
if state.score >= 5 {
|
||||
downHeavyNum += 1
|
||||
}
|
||||
if state.score >= 3 {
|
||||
downNum += 1
|
||||
}
|
||||
timeStop = j.Env.TimeStop
|
||||
last = j
|
||||
}
|
||||
downRate := downNum / float64(len(jobs))
|
||||
downHeavyRate := downHeavyNum / float64(len(jobs))
|
||||
if printMS > 0 {
|
||||
printMS = 0
|
||||
dateStr := btime.ToDateStr(timeStop, core.DefaultDateFmt)
|
||||
fmt.Printf("%v downRate: %.3f downHeavyRate: %.3f\n", dateStr, downRate*100, downHeavyRate*100)
|
||||
}
|
||||
if downHeavyRate >= 0.8 {
|
||||
// 极端下跌
|
||||
targetPerc = targetPercRaw * 2
|
||||
takeProfit = takeProfitRaw * 0.5
|
||||
} else if downRate >= 0.85 && downHeavyRate >= 0.6 {
|
||||
// 严重下跌
|
||||
targetPerc = targetPercRaw * 1.8
|
||||
takeProfit = takeProfitRaw * 0.5
|
||||
} else if downRate >= 0.7 && downRate <= 0.85 {
|
||||
// 市场中度下跌
|
||||
targetPerc = targetPercRaw * 1.6
|
||||
takeProfit = takeProfitRaw * 0.65
|
||||
} else if downRate >= 0.5 && downRate <= 0.7 {
|
||||
// 轻度下跌
|
||||
targetPerc = targetPercRaw * 1.3
|
||||
takeProfit = takeProfitRaw * 0.8
|
||||
} else {
|
||||
// 轻度或未下跌
|
||||
targetPerc = targetPercRaw
|
||||
takeProfit = takeProfitRaw
|
||||
}
|
||||
pairLock.Lock()
|
||||
totNum := 0
|
||||
for _, num := range openPairs {
|
||||
totNum += num
|
||||
}
|
||||
pairLock.Unlock()
|
||||
if totNum > 0 {
|
||||
last.Job.Strat.WriteOutput(fmt.Sprintf("pairNum: %v", totNum), true)
|
||||
}
|
||||
},
|
||||
OnOrderChange: func(s *strat.StratJob, od *ormo.InOutOrder, chgType int) {
|
||||
state, _ := s.More.(*State1525S)
|
||||
|
||||
// 当订单入场时更新持仓状态
|
||||
if chgType == strat.OdChgEnterFill && od.EnterTag != "" {
|
||||
// 计算新的平均成本
|
||||
if state.positionQty == 0 {
|
||||
// 首次开仓
|
||||
state.avgPrice = od.InitPrice
|
||||
state.positionQty = od.HoldAmount()
|
||||
} else {
|
||||
// 加仓,更新平均成本
|
||||
oldQty := state.positionQty
|
||||
oldAvgPrice := state.avgPrice
|
||||
newQty := od.HoldAmount()
|
||||
newAvgPrice := od.InitPrice
|
||||
|
||||
// 计算新的平均成本
|
||||
totalQty := oldQty + newQty
|
||||
totalCost := oldQty*oldAvgPrice + newQty*newAvgPrice
|
||||
state.avgPrice = totalCost / totalQty
|
||||
state.positionQty = totalQty
|
||||
}
|
||||
}
|
||||
|
||||
// 当订单完全平仓时重置持仓状态
|
||||
if chgType == strat.OdChgExitFill && od.ExitTag != "" && od.Status == ormo.InOutStatusFullExit {
|
||||
state.avgPrice = 0
|
||||
state.positionQty = 0
|
||||
state.tpLimit = 0
|
||||
pairLock.Lock()
|
||||
delete(openPairs, s)
|
||||
pairLock.Unlock()
|
||||
}
|
||||
},
|
||||
OnShutDown: func(s *strat.StratJob) {
|
||||
if maxPairs == 0 {
|
||||
return
|
||||
}
|
||||
log.Info("max open pairs", zap.Int("num", maxPairs))
|
||||
maxPairs = 0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// State1525Sim 存储每个品种的策略状态
|
||||
type State1525Sim struct {
|
||||
totAmount float64
|
||||
totCost float64
|
||||
tpLimit float64 // 止盈出场价格
|
||||
odNum int
|
||||
upFlag float64 // 1: ma20 > ma99 && ma20 > ma20[2]
|
||||
score float64 // 下跌行情力度分数,值域[0,6]
|
||||
}
|
||||
|
||||
func J1525Sim(pol *config.RunPolicyConfig) *strat.TradeStrat {
|
||||
// 最多30个品种,单笔30,初始3W
|
||||
// 定义可调超参数
|
||||
smaPeriod := pol.DefInt("sma_period", 18, core.PNorm(5, 50))
|
||||
targetPercRaw := pol.Def("target_perc", -0.14, core.PNorm(-0.3, -0.05))
|
||||
takeProfitRaw := pol.Def("take_profit", 0.09, core.PNorm(0.05, 0.3))
|
||||
targetQty := pol.Def("target_qty", 0.6, core.PNorm(0.2, 1.0))
|
||||
// 加仓次数,超过20后变差;低于19后有段时间不稳定
|
||||
maxAdd := pol.DefInt("max_add", 19, core.PNorm(1, 30))
|
||||
startLvlRaw := pol.DefInt("start_lvl", 3, core.PNorm(1, 15))
|
||||
maxOpenPair := pol.DefInt("max_open_pair", 20, core.PNorm(1, 30)) // 最大同时交易品种
|
||||
|
||||
openPairs := make(map[*strat.StratJob]int)
|
||||
pairLock := deadlock.Mutex{}
|
||||
lenAtr := 20
|
||||
baseAtrLen := int(math.Round(float64(lenAtr) * 4.3))
|
||||
|
||||
targetPerc := targetPercRaw
|
||||
takeProfit := takeProfitRaw
|
||||
startLvl := startLvlRaw
|
||||
|
||||
// 加仓倍率,基于targetQty: 1, 0.6, 1 1.7 3 4.7 7.7 12.8 21.1 35 57.5 95 157 256 385 464 767
|
||||
return &strat.TradeStrat{
|
||||
WarmupNum: max(smaPeriod, baseAtrLen),
|
||||
HedgeOff: true, // 单向持仓模式
|
||||
RunTimeFrames: []string{"5m"},
|
||||
BatchInfo: true,
|
||||
StakeRate: 0.25,
|
||||
OrderOnRotation: "open", // 品种切换时允许开单
|
||||
OnStartUp: func(s *strat.StratJob) {
|
||||
s.More = &State1525Sim{}
|
||||
},
|
||||
OnPairInfos: func(s *strat.StratJob) []*strat.PairSub {
|
||||
return []*strat.PairSub{
|
||||
{"_cur_", "4h", 20},
|
||||
}
|
||||
},
|
||||
OnBar: func(s *strat.StratJob) {
|
||||
e := s.Env
|
||||
state, _ := s.More.(*State1525Sim)
|
||||
closePrice := e.Close.Get(0)
|
||||
|
||||
// 不利行情下调整参数
|
||||
targetPercJob, takeProfitJob := targetPercRaw, takeProfitRaw
|
||||
if state.score >= 5 {
|
||||
targetPercJob = targetPercRaw * 2
|
||||
takeProfitJob = takeProfitRaw * 0.6
|
||||
} else if state.score >= 4 {
|
||||
targetPercJob = targetPercRaw * 1.7
|
||||
takeProfitJob = takeProfitRaw * 0.7
|
||||
} else if state.score >= 3 {
|
||||
targetPercJob = targetPercRaw * 1.3
|
||||
takeProfitJob = takeProfitRaw * 0.8
|
||||
}
|
||||
targetPercCur := (targetPercJob + targetPerc) / 2
|
||||
takeProfitCur := (takeProfitJob + takeProfit) / 2
|
||||
|
||||
// 计算SMA
|
||||
ma := ta.SMA(e.Close, smaPeriod)
|
||||
atr := ta.ATR(e.High, e.Low, e.Close, lenAtr)
|
||||
atrBase := ta.Lowest(ta.Highest(atr, lenAtr), baseAtrLen).Get(0)
|
||||
openRate := 0.006 * closePrice / atrBase
|
||||
if openRate < 1 {
|
||||
openRate = math.Sqrt(openRate)
|
||||
}
|
||||
openRate = max(0.7, min(1.5, openRate))
|
||||
|
||||
// 检查止盈触发
|
||||
if state.tpLimit > 0 && state.tpLimit <= e.High.Get(0) {
|
||||
state.totAmount = 0
|
||||
state.totCost = 0
|
||||
state.tpLimit = 0
|
||||
state.odNum = 0
|
||||
pairLock.Lock()
|
||||
delete(openPairs, s)
|
||||
pairLock.Unlock()
|
||||
}
|
||||
|
||||
// 计算当前持仓盈亏比例、止盈
|
||||
var pnl, avgPrice float64
|
||||
if state.totAmount > 0 {
|
||||
avgPrice = state.totCost / state.totAmount
|
||||
pnl = closePrice/avgPrice - 1
|
||||
curTP := avgPrice * (1 + takeProfitCur)
|
||||
if curTP != state.tpLimit {
|
||||
state.tpLimit = curTP
|
||||
// 设置限价单止盈
|
||||
s.CloseOrders(&strat.ExitReq{
|
||||
Tag: "Take Profit",
|
||||
Limit: curTP,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 首次入场条件:价格上穿SMA且无持仓
|
||||
if e.Close.Cross(ma) == 1 && state.totAmount == 0 && openRate > 0 {
|
||||
state.odNum = 1
|
||||
state.totAmount = openRate
|
||||
state.totCost = closePrice * openRate
|
||||
}
|
||||
|
||||
// 加仓条件:亏损达到目标比例且有持仓
|
||||
if avgPrice > 0 && pnl <= targetPercCur && state.odNum < maxAdd {
|
||||
amt := state.totAmount * targetQty
|
||||
state.totAmount += amt
|
||||
state.totCost += amt * closePrice
|
||||
curStart := startLvl
|
||||
if state.upFlag < 0 {
|
||||
// 下降趋势上升3个阶梯,和全局阈值取平均
|
||||
curStart = (startLvl + startLvlRaw + 3) / 2
|
||||
}
|
||||
if state.odNum >= curStart {
|
||||
pairLock.Lock()
|
||||
_, allow := openPairs[s]
|
||||
if !allow && len(openPairs) < maxOpenPair {
|
||||
allow = true
|
||||
}
|
||||
if allow {
|
||||
openPairs[s] = len(s.LongOrders)
|
||||
}
|
||||
pairLock.Unlock()
|
||||
if allow {
|
||||
s.OpenOrder(&strat.EnterReq{
|
||||
Tag: fmt.Sprintf("%v", state.odNum),
|
||||
CostRate: amt,
|
||||
})
|
||||
}
|
||||
}
|
||||
state.odNum += 1
|
||||
}
|
||||
},
|
||||
OnInfoBar: func(s *strat.StratJob, e *ta.BarEnv, pair, tf string) {
|
||||
score := float64(0)
|
||||
if ta.RSI(e.Close, 14).Get(0) < 30 {
|
||||
score += 1
|
||||
}
|
||||
roc := ta.ROC(e.Close, 20).Get(0)
|
||||
if roc < -20 {
|
||||
score += 2
|
||||
} else if roc < -10 {
|
||||
score += 1
|
||||
}
|
||||
plusDi, minusDi := ta.PluMinDI(e.High, e.Low, e.Close, 14)
|
||||
adx := ta.ADX(e.High, e.Low, e.Close, 14).Get(0)
|
||||
if minusDi.Get(0) > plusDi.Get(0) {
|
||||
if adx > 40 {
|
||||
score += 2
|
||||
} else if adx > 25 {
|
||||
score += 1
|
||||
}
|
||||
}
|
||||
ma7 := ta.SMA(e.Close, 7).Get(0)
|
||||
ma20 := ta.SMA(e.Close, 20)
|
||||
ma20V := ma20.Get(0)
|
||||
ma99 := ta.SMA(e.Close, 99).Get(0)
|
||||
if ma99 > ma20V {
|
||||
score += 1
|
||||
}
|
||||
state, _ := s.More.(*State1525Sim)
|
||||
state.score = score
|
||||
state.upFlag = 0
|
||||
if ma20V > ma99 && ma20V > ma20.Get(2) {
|
||||
state.upFlag = 1
|
||||
} else if ma7 < min(ma20V, ma99) {
|
||||
state.upFlag = -1
|
||||
}
|
||||
},
|
||||
OnBatchInfos: func(tf string, jobs map[string]*strat.JobEnv) {
|
||||
jobNum := len(jobs)
|
||||
if jobNum < 3 {
|
||||
return
|
||||
}
|
||||
downNum, downHeavyNum, sumUp := float64(0), float64(0), float64(0)
|
||||
var last *strat.JobEnv
|
||||
for _, j := range jobs {
|
||||
state, _ := j.Job.More.(*State1525Sim)
|
||||
if state.score >= 5 {
|
||||
downHeavyNum += 1
|
||||
}
|
||||
if state.score >= 3 {
|
||||
downNum += 1
|
||||
}
|
||||
sumUp += max(0, state.upFlag)
|
||||
last = j
|
||||
}
|
||||
downRate := downNum / float64(len(jobs))
|
||||
downHeavyRate := downHeavyNum / float64(len(jobs))
|
||||
sumUp /= float64(len(jobs))
|
||||
if downHeavyRate >= 0.8 {
|
||||
// 极端下跌
|
||||
targetPerc = targetPercRaw * 2
|
||||
takeProfit = takeProfitRaw * 0.5
|
||||
} else if downRate >= 0.85 && downHeavyRate >= 0.6 {
|
||||
// 严重下跌
|
||||
targetPerc = targetPercRaw * 1.8
|
||||
takeProfit = takeProfitRaw * 0.5
|
||||
} else if downRate >= 0.7 && downRate <= 0.85 {
|
||||
// 市场中度下跌
|
||||
targetPerc = targetPercRaw * 1.6
|
||||
takeProfit = takeProfitRaw * 0.65
|
||||
} else if downRate >= 0.5 && downRate <= 0.7 {
|
||||
// 轻度下跌
|
||||
targetPerc = targetPercRaw * 1.3
|
||||
takeProfit = takeProfitRaw * 0.8
|
||||
} else {
|
||||
// 轻度或未下跌
|
||||
targetPerc = targetPercRaw
|
||||
takeProfit = takeProfitRaw
|
||||
}
|
||||
// 调整最小开仓级别
|
||||
if sumUp > 0.7 {
|
||||
startLvl = startLvlRaw
|
||||
} else if sumUp > 0.5 && downRate <= 0.5 {
|
||||
startLvl = startLvlRaw + 2
|
||||
} else if sumUp > 0.3 && downRate <= 0.7 {
|
||||
startLvl = startLvlRaw + 4
|
||||
} else {
|
||||
startLvl = startLvlRaw + 6
|
||||
}
|
||||
pairLock.Lock()
|
||||
totNum := 0
|
||||
for _, num := range openPairs {
|
||||
totNum += num
|
||||
}
|
||||
pairLock.Unlock()
|
||||
if totNum > 0 {
|
||||
last.Job.Strat.WriteOutput(fmt.Sprintf("pairNum: %v", totNum), true)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user