Merge pull request #770 from c9s/fix/backtest-stop-limit

This commit is contained in:
Yo-An Lin 2022-06-28 04:08:28 +08:00 committed by GitHub
commit f2a642b165
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 349 additions and 94 deletions

View File

@ -1,15 +1,19 @@
import {Checkbox, Group, Table} from "@mantine/core";
import {Button, Checkbox, Group, Table} from "@mantine/core";
import React, {useState} from "react";
import {Order} from "../types";
import moment from "moment";
interface OrderListTableProps {
orders: Order[];
onClick?: (order: Order) => void;
limit?: number;
}
const OrderListTable = (props: OrderListTableProps) => {
let orders = props.orders;
const [showCanceledOrders, setShowCanceledOrders] = useState(false);
const [limit, setLimit] = useState(props.limit || 100);
if (!showCanceledOrders) {
orders = orders.filter((order: Order) => {
@ -17,6 +21,10 @@ const OrderListTable = (props: OrderListTableProps) => {
})
}
if (orders.length > limit) {
orders = orders.slice(0, limit)
}
const rows = orders.map((order: Order) => (
<tr key={order.order_id} onClick={(e) => {
props.onClick ? props.onClick(order) : null;
@ -33,7 +41,8 @@ const OrderListTable = (props: OrderListTableProps) => {
<td>{order.price}</td>
<td>{order.quantity}</td>
<td>{order.status}</td>
<td>{order.creation_time.toString()}</td>
<td>{formatDate(order.creation_time)}</td>
<td>{order.tag}</td>
</tr>
));
@ -41,24 +50,32 @@ const OrderListTable = (props: OrderListTableProps) => {
<Group>
<Checkbox label="Show Canceled" checked={showCanceledOrders}
onChange={(event) => setShowCanceledOrders(event.currentTarget.checked)}/>
<Button onClick={() => {
setLimit(limit + 500)
}}>Load More</Button>
</Group>
<Table highlightOnHover striped>
<thead>
<tr>
<th>Order ID</th>
<th>Symbol</th>
<th>Side</th>
<th>Order Type</th>
<th>Price</th>
<th>Quantity</th>
<th>Status</th>
<th>Creation Time</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
<thead>
<tr>
<th>Order ID</th>
<th>Symbol</th>
<th>Side</th>
<th>Order Type</th>
<th>Price</th>
<th>Quantity</th>
<th>Status</th>
<th>Creation Time</th>
<th>Tag</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
</div>
}
const formatDate = (d : Date) : string => {
return moment(d).format("MMM Do YY hh:mm:ss A Z");
}
export default OrderListTable;

View File

@ -10,7 +10,7 @@ $react-time-range--track--disabled: repeating-linear-gradient( -45deg, transpare
.react_time_range__time_range_container {
padding: 30px 52px 0 52px;
height: 70px;
height: 90px;
// width: 90%;
box-sizing: border-box;
}

View File

@ -181,6 +181,11 @@ const ordersToMarkers = (interval: string, orders: Array<Order> | void): Array<M
}
}
let text = '' + order.price
if (order.tag) {
text += " #" + order.tag;
}
switch (order.side) {
case "BUY":
markers.push({
@ -188,8 +193,7 @@ const ordersToMarkers = (interval: string, orders: Array<Order> | void): Array<M
position: 'belowBar',
color: '#239D10',
shape: 'arrowUp',
text: '' + order.price
//text: 'B',
text: text,
});
break;
case "SELL":
@ -198,8 +202,7 @@ const ordersToMarkers = (interval: string, orders: Array<Order> | void): Array<M
position: 'aboveBar',
color: '#e91e63',
shape: 'arrowDown',
text: '' + order.price
//text: 'S',
text: text,
});
break;
}
@ -573,6 +576,7 @@ const TradingViewChart = (props: TradingViewChartProps) => {
<div>
<Group>
<SegmentedControl
value={currentInterval}
data={intervals.map((interval) => {
return {label: interval, value: interval}
})}
@ -657,6 +661,10 @@ const createLegendUpdater = (legend: HTMLDivElement, prefix: string) => {
}
}
const formatDate = (d : Date) : string => {
return moment(d).format("MMM Do YY hh:mm:ss A Z");
}
const createOHLCLegendUpdater = (legend: HTMLDivElement, prefix: string) => {
return (param: any, time : any) => {
if (param) {
@ -664,7 +672,7 @@ const createOHLCLegendUpdater = (legend: HTMLDivElement, prefix: string) => {
const changePercentage = Math.round((param.close - param.open) / param.close * 10000.0) / 100.0;
const ampl = Math.round((param.high - param.low) / param.low * 10000.0) / 100.0;
const t = new Date(time * 1000);
const dateStr = moment(t).format("MMM Do YY hh:mm:ss A Z");
const dateStr = formatDate(t);
legend.innerHTML = prefix + ` O: ${param.open} H: ${param.high} L: ${param.low} C: ${param.close} CHG: ${change} (${changePercentage}%) AMP: ${ampl}% T: ${dateStr}`;
} else {
legend.innerHTML = prefix + ' O: - H: - L: - C: - T: -';

View File

@ -11,4 +11,5 @@ export interface Order {
update_time: Date;
creation_time: Date;
time?: Date;
tag?: string;
}

View File

@ -124,7 +124,8 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
return o, nil
}
func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *types.Order, trades *types.Trade, err error) {
// PlaceOrder returns the created order object, executed trade (if any) and error
func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *types.Trade, error) {
// price for checking account balance, default price
price := o.Price
@ -135,7 +136,7 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ
}
price = m.LastPrice
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
case types.OrderTypeLimit, types.OrderTypeStopLimit, types.OrderTypeLimitMaker:
price = o.Price
}
@ -301,11 +302,57 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool
}
}
// BuyToPrice means price go up and the limit sell should be triggered
func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
klineMatchingLogger.Debugf("kline buy to price %s", price.String())
var askOrders []types.Order
var bidOrders []types.Order
for _, o := range m.bidOrders {
switch o.Type {
case types.OrderTypeStopMarket:
// should we trigger the order
if o.StopPrice.Compare(price) <= 0 {
// not triggering it, put it back
bidOrders = append(bidOrders, o)
break
}
o.Type = types.OrderTypeMarket
o.ExecutedQuantity = o.Quantity
o.Price = price
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
case types.OrderTypeStopLimit:
// should we trigger the order?
if price.Compare(o.StopPrice) <= 0 {
bidOrders = append(bidOrders, o)
break
}
// convert this order to limit order
// we use value object here, so it's a copy
o.Type = types.OrderTypeLimit
// is it a taker order?
// higher than the current price, then it's a taker order
if o.Price.Compare(price) >= 0 {
// taker order, move it to the closed order
o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
} else {
// keep it as a maker order
bidOrders = append(bidOrders, o)
}
default:
bidOrders = append(bidOrders, o)
}
}
m.bidOrders = bidOrders
var askOrders []types.Order
for _, o := range m.askOrders {
switch o.Type {
@ -333,10 +380,13 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
o.Type = types.OrderTypeLimit
// is it a taker order?
if price.Compare(o.Price) >= 0 {
// higher than the current price, then it's a taker order
if o.Price.Compare(price) >= 0 {
// price protection, added by @zenix
if o.Price.Compare(m.LastKLine.Low) < 0 {
o.Price = m.LastKLine.Low
}
o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
@ -386,13 +436,57 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
klineMatchingLogger.Debugf("kline sell to price %s", price.String())
var sellPrice = price
var askOrders []types.Order
for _, o := range m.askOrders {
switch o.Type {
case types.OrderTypeStopMarket:
// should we trigger the order
if o.StopPrice.Compare(sellPrice) >= 0 {
o.Type = types.OrderTypeMarket
o.ExecutedQuantity = o.Quantity
o.Price = sellPrice
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
} else {
askOrders = append(askOrders, o)
}
case types.OrderTypeStopLimit:
// if the price is lower than the stop price
// we should trigger the stop sell order
if sellPrice.Compare(o.StopPrice) > 0 {
askOrders = append(askOrders, o)
break
}
o.Type = types.OrderTypeLimit
// if the order price is lower than the current price
// it's a taker order
if o.Price.Compare(sellPrice) <= 0 {
o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
} else {
askOrders = append(askOrders, o)
}
default:
askOrders = append(askOrders, o)
}
}
m.askOrders = askOrders
var bidOrders []types.Order
for _, o := range m.bidOrders {
switch o.Type {
case types.OrderTypeStopMarket:
// should we trigger the order
if sellPrice.Compare(o.StopPrice) <= 0 {
if o.StopPrice.Compare(sellPrice) >= 0 {
o.Type = types.OrderTypeMarket
o.ExecutedQuantity = o.Quantity
o.Price = sellPrice
o.Status = types.OrderStatusFilled
@ -402,11 +496,12 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
}
case types.OrderTypeStopLimit:
// should we trigger the order
// if the price is lower than the stop price
// we should trigger the stop order
if sellPrice.Compare(o.StopPrice) <= 0 {
o.Type = types.OrderTypeLimit
if sellPrice.Compare(o.Price) <= 0 {
if o.Price.Compare(sellPrice) <= 0 {
if o.Price.Compare(m.LastKLine.High) > 0 {
o.Price = m.LastKLine.High
}

View File

@ -151,17 +151,7 @@ func newKLine(symbol string, interval types.Interval, startTime time.Time, o, h,
}
}
func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
account := &types.Account{
MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
}
account.UpdateBalances(types.BalanceMap{
"USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(1000000.0)},
"BTC": {Currency: "BTC", Available: fixedpoint.NewFromFloat(100.0)},
})
func getTestMarket() types.Market {
market := types.Market{
Symbol: "BTCUSDT",
PricePrecision: 8,
@ -172,7 +162,135 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
MinAmount: fixedpoint.MustNewFromString("10.0"),
MinQuantity: fixedpoint.MustNewFromString("0.001"),
}
return market
}
func getTestAccount() *types.Account {
account := &types.Account{
MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
}
account.UpdateBalances(types.BalanceMap{
"USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(1000000.0)},
"BTC": {Currency: "BTC", Available: fixedpoint.NewFromFloat(100.0)},
})
return account
}
func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
Market: market,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(19000.0),
}
stopOrder := types.SubmitOrder{
Symbol: market.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeStopLimit,
Quantity: fixedpoint.NewFromFloat(0.1),
Price: fixedpoint.NewFromFloat(22000.0),
StopPrice: fixedpoint.NewFromFloat(21000.0),
TimeInForce: types.TimeInForceGTC,
}
createdOrder, trade, err := engine.PlaceOrder(stopOrder)
assert.NoError(t, err)
assert.Nil(t, trade, "place stop order should not trigger the stop buy")
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop buy")
closedOrders, trades := engine.BuyToPrice(fixedpoint.NewFromFloat(20000.0))
assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy")
assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy")
closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(21001.0))
assert.Len(t, closedOrders, 1, "should trigger the stop buy order")
assert.Len(t, trades, 1, "should have stop order trade executed")
assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status)
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type)
assert.Equal(t, stopOrder.Price, trades[0].Price)
}
func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
Market: market,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(22000.0),
}
stopOrder := types.SubmitOrder{
Symbol: market.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeStopLimit,
Quantity: fixedpoint.NewFromFloat(0.1),
Price: fixedpoint.NewFromFloat(20000.0),
StopPrice: fixedpoint.NewFromFloat(21000.0),
TimeInForce: types.TimeInForceGTC,
}
createdOrder, trade, err := engine.PlaceOrder(stopOrder)
assert.NoError(t, err)
assert.Nil(t, trade, "place stop order should not trigger the stop sell")
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell")
closedOrders, trades := engine.SellToPrice(fixedpoint.NewFromFloat(21500.0))
assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy")
assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy")
closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(20990.0))
assert.Len(t, closedOrders, 1, "should trigger the stop sell order")
assert.Len(t, trades, 1, "should have stop order trade executed")
assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status)
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type)
assert.Equal(t, stopOrder.Price, trades[0].Price)
}
func TestSimplePriceMatching_StopMarketOrderSell(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
Market: market,
closedOrders: make(map[uint64]types.Order),
LastPrice: fixedpoint.NewFromFloat(22000.0),
}
stopOrder := types.SubmitOrder{
Symbol: market.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeStopMarket,
Quantity: fixedpoint.NewFromFloat(0.1),
Price: fixedpoint.NewFromFloat(20000.0),
StopPrice: fixedpoint.NewFromFloat(21000.0),
TimeInForce: types.TimeInForceGTC,
}
createdOrder, trade, err := engine.PlaceOrder(stopOrder)
assert.NoError(t, err)
assert.Nil(t, trade, "place stop order should not trigger the stop sell")
assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell")
closedOrders, trades := engine.SellToPrice(fixedpoint.NewFromFloat(21500.0))
assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy")
assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy")
closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(20990.0))
assert.Len(t, closedOrders, 1, "should trigger the stop sell order")
assert.Len(t, trades, 1, "should have stop order trade executed")
assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status)
assert.Equal(t, types.OrderTypeMarket, closedOrders[0].Type)
assert.Equal(t, fixedpoint.NewFromFloat(20990.0), trades[0].Price)
}
func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{
Account: account,
Market: market,

View File

@ -2,6 +2,7 @@ package bbgo
import (
"context"
"strings"
log "github.com/sirupsen/logrus"
@ -117,12 +118,14 @@ func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error {
return nil
}
func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error {
submitOrder := e.position.NewMarketCloseOrder(percentage)
if submitOrder == nil {
return nil
}
submitOrder.Tag = strings.Join(tags, ",")
_, err := e.SubmitOrders(ctx, *submitOrder)
return err
}

View File

@ -3,10 +3,10 @@ package pivotshort
import "github.com/c9s/bbgo/pkg/bbgo"
type ExitMethod struct {
RoiStopLoss *RoiStopLoss `json:"roiStopLoss"`
ProtectionStopLoss *ProtectionStopLoss `json:"protectionStopLoss"`
RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"`
LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"`
RoiStopLoss *RoiStopLoss `json:"roiStopLoss"`
ProtectionStopLoss *ProtectionStopLoss `json:"protectionStopLoss"`
RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"`
LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"`
CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"`
}

View File

@ -58,6 +58,7 @@ func (s *ProtectionStopLoss) placeStopOrder(ctx context.Context, position *types
Price: s.stopLossPrice.Mul(one.Add(fixedpoint.NewFromFloat(0.005))), // +0.5% from the trigger price, slippage protection
StopPrice: s.stopLossPrice,
Market: position.Market,
Tag: "protectionStopLoss",
})
if len(createdOrders) > 0 {
@ -174,7 +175,7 @@ func (s *ProtectionStopLoss) checkStopPrice(closePrice fixedpoint.Value, positio
if s.shouldStop(closePrice) {
log.Infof("[ProtectionStopLoss] protection stop order is triggered at price %f, position = %+v", closePrice.Float64(), position)
if err := s.orderExecutor.ClosePosition(context.Background(), one); err != nil {
if err := s.orderExecutor.ClosePosition(context.Background(), one, "protectionStopLoss"); err != nil {
log.WithError(err).Errorf("failed to close position")
}
}

View File

@ -48,7 +48,7 @@ func (s *RoiStopLoss) checkStopPrice(closePrice fixedpoint.Value, position *type
if roi.Compare(s.Percentage.Neg()) < 0 {
// stop loss
bbgo.Notify("[RoiStopLoss] %s stop loss triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Neg().Percentage(), closePrice.Float64())
_ = s.orderExecutor.ClosePosition(context.Background(), fixedpoint.One)
_ = s.orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "roiStopLoss")
return
}
}

View File

@ -34,7 +34,7 @@ func (s *RoiTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.
if roi.Compare(s.Percentage) > 0 {
// stop loss
bbgo.Notify("[RoiTakeProfit] %s take profit is triggered by ROI %s/%s, price: %f", position.Symbol, roi.Percentage(), s.Percentage.Percentage(), kline.Close.Float64())
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One)
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "roiTakeProfit")
return
}
})

View File

@ -68,12 +68,6 @@ type Entry struct {
MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"`
}
type CumulatedVolume struct {
Enabled bool `json:"enabled"`
MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"`
Window int `json:"window"`
}
type Strategy struct {
*bbgo.Graceful
@ -226,16 +220,38 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.orderExecutor.Bind()
store, _ := session.MarketDataStore(s.Symbol)
standardIndicator, _ := session.StandardIndicatorSet(s.Symbol)
s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
s.pivot.Bind(store)
if kLinesP, ok := store.KLinesOfInterval(s.IntervalWindow.Interval); ok {
s.pivot.Update(*kLinesP)
}
// update pivot low data
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
return
}
lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow())
if lastLow.IsZero() {
return
}
if lastLow.Compare(s.lastLow) != 0 {
log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time())
}
s.lastLow = lastLow
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
})
if s.BounceShort != nil && s.BounceShort.Enabled {
s.resistancePivot = &indicator.Pivot{IntervalWindow: s.BounceShort.IntervalWindow}
s.resistancePivot.Bind(store)
}
standardIndicator, _ := session.StandardIndicatorSet(s.Symbol)
if s.BreakLow.StopEMA != nil {
s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA)
}
@ -288,8 +304,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
return
}
isPositionOpened := !s.Position.IsClosed() && !s.Position.IsDust(kline.Close)
if isPositionOpened && s.Position.IsShort() {
if !s.Position.IsClosed() && !s.Position.IsDust(kline.Close) {
return
}
@ -305,6 +320,18 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.pivotLowPrices = s.pivotLowPrices[len(s.pivotLowPrices)-10:]
}
ratio := fixedpoint.One.Add(s.BreakLow.Ratio)
breakPrice := previousLow.Mul(ratio)
closePrice := kline.Close
// if previous low is not break, skip
if closePrice.Compare(breakPrice) >= 0 {
return
}
log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64())
// stop EMA protection
if s.stopEWMA != nil && !s.BreakLow.StopEMARange.IsZero() {
ema := fixedpoint.NewFromFloat(s.stopEWMA.Last())
if ema.IsZero() {
@ -312,24 +339,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.BreakLow.StopEMARange))
if kline.Close.Compare(emaStopShortPrice) < 0 {
if closePrice.Compare(emaStopShortPrice) < 0 {
log.Infof("stopEMA protection: close price %f < EMA(%v) = %f", closePrice.Float64(), s.BreakLow.StopEMA, ema.Float64())
return
}
}
ratio := fixedpoint.One.Add(s.BreakLow.Ratio)
breakPrice := previousLow.Mul(ratio)
// if previous low is not break, skip
if kline.Close.Compare(breakPrice) >= 0 {
return
}
if !s.Position.IsClosed() && !s.Position.IsDust(kline.Close) {
// s.Notify("skip opening %s position, which is not closed", s.Symbol, s.Position)
return
}
_ = s.orderExecutor.GracefulCancel(ctx)
quantity := s.useQuantityOrBaseBalance(s.BreakLow.Quantity)
@ -338,6 +353,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.placeMarketSell(ctx, quantity)
} else {
sellPrice := kline.Close.Mul(fixedpoint.One.Add(s.BreakLow.BounceRatio))
bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", s.Symbol, kline.Close.Float64(), previousLow.Float64(), s.BreakLow.Ratio.Float64(), sellPrice.Float64())
s.placeLimitSell(ctx, sellPrice, quantity)
}
})
@ -376,26 +393,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
})
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
// StrategyController
if s.Status != types.StrategyStatusRunning {
return
}
if !bbgo.IsBackTesting {
// use market trade to submit short order
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
return
}
if s.pivot.LastLow() > 0.0 {
lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow())
if lastLow.Compare(s.lastLow) != 0 {
log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time())
}
s.lastLow = lastLow
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
}
})
})
}
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())

View File

@ -132,6 +132,8 @@ type SubmitOrder struct {
IsFutures bool `json:"is_futures" db:"is_futures"`
ReduceOnly bool `json:"reduceOnly" db:"reduce_only"`
ClosePosition bool `json:"closePosition" db:"close_position"`
Tag string `json:"tag" db:"-"`
}
func (o *SubmitOrder) String() string {
@ -229,6 +231,7 @@ func (o Order) CsvHeader() []string {
"quantity",
"creation_time",
"update_time",
"tag",
}
}
@ -244,6 +247,7 @@ func (o Order) CsvRecords() [][]string {
o.Quantity.String(),
o.CreationTime.Time().Format(time.RFC1123),
o.UpdateTime.Time().Format(time.RFC1123),
o.Tag,
},
}
}
@ -267,7 +271,7 @@ func (o Order) String() string {
orderID = strconv.FormatUint(o.OrderID, 10)
}
return fmt.Sprintf("ORDER %s | %s | %s | %s | %s %-4s | %s/%s @ %s | %s",
desc := fmt.Sprintf("ORDER %s | %s | %s | %s | %s %-4s | %s/%s @ %s",
o.Exchange.String(),
o.CreationTime.Time().Local().Format(time.RFC1123),
orderID,
@ -276,8 +280,13 @@ func (o Order) String() string {
o.Side,
o.ExecutedQuantity.String(),
o.Quantity.String(),
o.Price.String(),
o.Status)
o.Price.String())
if o.Type == OrderTypeStopLimit {
desc += " Stop @ " + o.StopPrice.String()
}
return desc + " | " + string(o.Status)
}
// PlainText is used for telegram-styled messages