mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
Merge pull request #770 from c9s/fix/backtest-stop-limit
This commit is contained in:
commit
f2a642b165
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: -';
|
||||
|
|
|
@ -11,4 +11,5 @@ export interface Order {
|
|||
update_time: Date;
|
||||
creation_time: Date;
|
||||
time?: Date;
|
||||
tag?: string;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user