Пример #1
0
    def test_able_to_call_with_dot_notation(self):
        obj = {
            "cost": 2.03,
            "entryPrice": 2.03,
            "future": "some-future",
            "initialMarginRequirement": 2.03,
            "longOrderSize": 2.03,
            "maintenanceMarginRequirement": 2.03,
            "netSize": 2.03,
            "openSize": 2.03,
            "realizedPnl": 2.03,
            "shortOrderSize": 2.03,
            "side": "buy",
            "size": 2.03,
            "unrealizedPnl": 2.03,
        }
        tp: TradingPosition = parse_to_dataclass(obj)

        assert tp.cost == 2.03
        assert tp.entryPrice == 2.03
        assert tp.future == "some-future"
        assert tp.initialMarginRequirement == 2.03
        assert tp.longOrderSize == 2.03
        assert tp.maintenanceMarginRequirement == 2.03
        assert tp.netSize == 2.03
        assert tp.openSize == 2.03
        assert tp.realizedPnl == 2.03
        assert tp.shortOrderSize == 2.03
        assert tp.side == "buy"
        assert tp.size == 2.03
        assert tp.unrealizedPnl == 2.03
Пример #2
0
def get_zones() -> List[Zone]:
    global ZONE_FILE_PATH

    # TODO: fix duplication
    zone_df = pd.read_csv(
        ZONE_FILE_PATH,
        dtype={
            "zone_price": "float",
            "zone_amount": "float",
            "buy_id": "str",
            "buy_price": "float",
            "buy_unit": "float",
            "buy_fee_unit": "float",
            "buy_recive_unit": "float",
            "buy_open_time": "str",
            "buy_status": "str",
            "buy_close_time": "str",
            "sell_id": "str",
            "sell_price": "float",
            "sell_unit": "float",
            "sell_fee_amount": "float",
            "sell_recive_amount": "float",
            "sell_open_time": "str",
            "sell_close_time": "str",
            "sell_status": "str",
        },
    )

    zone_df = zone_df.fillna(np.nan).replace([np.nan], [None])
    zones = [parse_to_dataclass(row) for row in zone_df.to_dict('records')]

    return zones
Пример #3
0
def get_balance_coin_value(coin: str) -> float:
    global FTX

    try:
        balance: Balance = parse_to_dataclass(FTX.fetch_balance())
        coins = [coin.__dict__ for coin in balance.info.result]
        balance_df = pd.DataFrame(coins, columns=['coin', 'usdValue'])
        balance_df = balance_df.set_index('coin')
        usd_value = balance_df.loc[coin, 'usdValue']

        return usd_value
    except:
        return 0
Пример #4
0
def reblance():
    try:
        symbol = f'{COIN}/USD'

        # close all open orders
        open_orders: List[LimitOrderInfo] = parse_to_dataclass(
            FTX.fetch_open_orders(symbol))
        if len(open_orders) >= 1:
            txt = FTX.cancel_all_orders(symbol)
            LOGGER.info(txt)

        ticker: Ticker = parse_to_dataclass(FTX.fetch_ticker(symbol))

        sell_price = ticker.bid
        buy_price = ticker.ask

        coin_value = get_balance_coin_value(COIN)
        usd_value = get_balance_coin_value('USD')
        port_value = coin_value + usd_value
        coin_ratio = coin_value / port_value

        cols = ['symbol', 'id', 'price', 'side', 'amount', 'datetime']

        if coin_ratio > UPPER_LIMIT:
            sell_unit = (coin_ratio -
                         COIN_TARGET_PC / 100) * port_value / sell_price
            order_result: LimitOrder = parse_to_dataclass(
                FTX.create_order(symbol, "limit", "sell", sell_unit,
                                 sell_price))
            res = pd.DataFrame(order_result.__dict__, columns=cols, index=[0])
            res.to_csv(STATEMENT_FILE_PATH,
                       mode='a',
                       header=False,
                       index=False)

            txt = f'SELL {symbol}, ratio: {coin_ratio}, price: {sell_price}, unit: {sell_unit}'
            LOGGER.info(txt)

        elif coin_ratio < LOWER_LIMIT:
            buy_unit = (COIN_TARGET_PC / 100 -
                        coin_ratio) * port_value / buy_price
            order_result: LimitOrder = parse_to_dataclass(
                FTX.create_order(symbol, "limit", "buy", buy_unit, buy_price))

            res = pd.DataFrame(order_result.__dict__, columns=cols, index=[0])
            res.to_csv(STATEMENT_FILE_PATH,
                       mode='a',
                       header=False,
                       index=False)

            txt = f'BUY {symbol}, ratio: {coin_ratio}, price: {buy_price}, unit: {buy_unit}'
            LOGGER.info(txt)

        else:
            ratio = round(coin_ratio * 100, 1)
            value = round(port_value, 2)

            txt = f'port value($): {value}, coin: {COIN}, ratio: {ratio}%'
            LOGGER.debug(txt)

    except Exception as e:

        txt = f'Rebalance {COIN} Error : {e}'
        LOGGER.error(txt)
Пример #5
0
def main():
    global ZONES
    global FTX
    global FEE_TAKER
    global OPEN_BUY_STATUS
    global CLOSED_BUY_STATUS
    global CANCELED_BUY_STATUS
    global OPEN_SELL_STATUS
    global CLOSED_SELL_STATUS
    global CANCELED_SELL_STATUS

    # load zone info
    ZONES = get_zones()

    # loop for each zone
    for i in range(len(ZONES)):
        zone: Zone = ZONES[i]

        # get latest price and time
        ticker: Ticker = parse_to_dataclass(FTX.fetch_ticker(SYMBOL))
        last_price = ticker.last
        last_time = ticker.datetime

        # no buying then force buy
        if zone.buy_status is None:
            # Create a new order if there is no opening order
            try:
                # force
                if last_price < zone.zone_price:
                    # perform market-order
                    # assumption: right liquidity (no big gap of buy-sell order)
                    # so we buy at "ask" price
                    current_ticket: Ticker = parse_to_dataclass(FTX.fetch_ticker(SYMBOL))
                    buy_price = current_ticket.ask
                    txt_action = "Buy collective order"
                else:
                    buy_price = zone.zone_price
                    txt_action = "Open limit buy order"

                # open limit buy order
                buy_unit = zone.zone_amount / buy_price
                order_result: LimitOrder = parse_to_dataclass(FTX.create_order(
                    SYMBOL, "limit", "buy", buy_unit, buy_price
                ))

                # Update & save zone.csv
                # update and save
                buy_unit = order_result.amount
                buy_fee = buy_unit * FEE_TAKER

                ZONES[i].zone_price = zone.zone_price
                ZONES[i].zone_amount = zone.zone_amount
                ZONES[i].buy_id = order_result.id
                ZONES[i].buy_price = order_result.price
                ZONES[i].buy_unit = buy_unit
                ZONES[i].buy_fee_unit = buy_fee
                ZONES[i].buy_recive_unit = buy_unit - buy_fee
                ZONES[i].buy_open_time = order_result.info.createdAt
                ZONES[i].buy_status = order_result.status

                save_zones()

                # log
                amt = buy_unit * order_result.price
                msg = f"{SYMBOL} Zone {zone.zone_price}: {txt_action} amount($) : {amt}"
                LOGGER.info(msg)

            except Exception as e:
                msg = f"{SYMBOL} Error : {e}"
                LOGGER.error(msg)

                # TODO: implement parent-try-catch instead
                time.sleep(10)

        # still open (no match or partial match)
        elif zone.buy_status == OPEN_BUY_STATUS:
            try:
                # Update Order Status
                current_order: LimitOrder = parse_to_dataclass(FTX.fetch_order(zone.buy_id))
                updated_buy_status = current_order.status
                txt_action = f"Buy status: {OPEN_BUY_STATUS} => {updated_buy_status}"

                # may be the order has been updated
                if zone.buy_status != updated_buy_status:
                    # TODO other values might also be updated
                    # update
                    ZONES[i].buy_status = updated_buy_status
                    save_zones()

                msg = f"{SYMBOL} Zone {zone.zone_price} : {txt_action} "
                LOGGER.debug(msg)

            except Exception as e:
                # TODO: implement parent-try-catch instead
                msg = f"{SYMBOL} Error : {e}"
                LOGGER.error(msg)

                time.sleep(10)

        # buy-order is fulfilled
        elif zone.buy_status == CLOSED_BUY_STATUS:

            # this should not be check but just incase
            # TODO: recheck it
            if zone.buy_close_time is None:
                # TODO: if you need more accurate then use time from FTX response instead
                ZONES[i].buy_close_time = last_time
                save_zones()

            # open TP
            if zone.sell_status is None:
                # Open a new limit sell order
                try:
                    # Open Limit Sell Order

                    # TAKE_PROFIT_PC = 1.5, create TP at 1.5% from the buy-order
                    # need "max(zone.zone_price, zone.buy_price)"
                    # cause sometime the "ask" (buy_price) already over the zone_price
                    sell_price = (1 + TAKE_PROFIT_PC / 100) * max(zone.zone_price, zone.buy_price)

                    # to avoid incremental size problem, actually it should be buy_recive_unit
                    # TODO: why not buy_recive_unit
                    sell_unit = zone.buy_unit
                    # sell_unit = zone.buy_recive_unit

                    order_result: LimitOrder = parse_to_dataclass(FTX.create_order(
                        SYMBOL, "limit", "sell", sell_unit, sell_price
                    ))

                    # Update & save zone.csv
                    sell_amount = order_result.price * order_result.amount
                    sell_fee = sell_amount * FEE_TAKER
                    ZONES[i].sell_id = order_result.id
                    ZONES[i].sell_price = order_result.price
                    ZONES[i].sell_unit = order_result.amount
                    ZONES[i].sell_fee_amount = sell_fee
                    ZONES[i].sell_recive_amount = sell_amount - sell_fee
                    ZONES[i].sell_open_time = order_result.info.createdAt
                    ZONES[i].sell_status = order_result.status

                    save_zones()

                    txt_action = "Open limit sell order"
                    msg = f"{SYMBOL} Zone {zone.zone_price} : {txt_action}  amount($) : {sell_amount}"
                    LOGGER.info(msg)

                except Exception as e:
                    msg = f"{SYMBOL} Error : {e}"
                    LOGGER.error(msg)
                    time.sleep(10)

            elif zone.sell_status == OPEN_SELL_STATUS:
                try:
                    # Update Order Status
                    current_order: LimitOrder = parse_to_dataclass(FTX.fetch_order(zone.sell_id))
                    updated_sell_status = current_order.status
                    txt_action = "Buy status : Closed   Sell status : Open"

                    if zone.sell_status != updated_sell_status:
                        # Save if status has changed
                        ZONES[i].sell_status = updated_sell_status
                        save_zones()
                        txt_action = f"Save updated sell status ({updated_sell_status})"

                    msg = f"{SYMBOL} Zone {zone.zone_price} : {txt_action} "
                    LOGGER.debug(msg)

                except Exception as e:
                    msg = f"{SYMBOL} Error : {e}"
                    LOGGER.error(msg)
                    time.sleep(10)

            elif zone.sell_status == CLOSED_SELL_STATUS:
                # calculate net profit
                buy_fee_amount = zone.buy_price * zone.buy_fee_unit

                # TODO: zone.sell_recive_amount ??
                sell_fee_amount = (zone.sell_price * zone.sell_unit) - zone.sell_recive_amount
                total_fee = buy_fee_amount + sell_fee_amount

                # profit
                net_profit = ((zone.sell_unit * zone.sell_price) - (zone.buy_unit * zone.buy_price) - total_fee)

                # COMPOUND_PC=50
                # we take the 50% from profit then re-invest into this zone
                ZONES[i].zone_amount = zone.zone_amount + round(net_profit * COMPOUND_PC / 100, 2)
                ZONES[i].sell_close_time = last_time

                # Update statement
                statement = Statement(
                    symbol=SYMBOL,
                    net_profit=net_profit,

                    buy_fee_amount=buy_fee_amount,  # new

                    zone_price=zone.zone_price,
                    zone_amount=zone.zone_amount,

                    buy_id=zone.buy_id,
                    buy_price=zone.buy_price,
                    buy_unit=zone.buy_unit,
                    buy_fee_unit=zone.buy_fee_unit,
                    buy_recive_unit=zone.buy_recive_unit,
                    buy_open_time=zone.buy_open_time,
                    buy_status=CLOSED_BUY_STATUS,
                    buy_close_time=zone.buy_close_time,

                    sell_id=zone.sell_id,
                    sell_price=zone.sell_price,
                    sell_unit=zone.sell_unit,
                    sell_fee_amount=zone.sell_fee_amount,
                    sell_recive_amount=zone.sell_recive_amount,
                    sell_open_time=zone.sell_open_time,
                    sell_status=zone.sell_status,
                    sell_close_time=zone.sell_close_time
                )
                save_new_statement(statement)

                # reset zone
                new_zone = Zone(
                    zone_price=zone.zone_price,
                    zone_amount=zone.zone_amount,
                )
                ZONES[i] = new_zone
                save_zones()

                # log
                msg = f"{SYMBOL} Zone {zone.zone_price} : Profit($) : {net_profit}"
                LOGGER.info(msg)

            # TODO: manual cancel sell order
            elif zone.sell_status == CANCELED_SELL_STATUS:

                # Remove canceled sell order
                ZONES[i].sell_id = None
                ZONES[i].sell_price = None
                ZONES[i].sell_unit = None
                ZONES[i].sell_fee_amount = None
                ZONES[i].sell_recive_amount = None
                ZONES[i].sell_open_time = None
                ZONES[i].sell_status = None
                ZONES[i].sell_close_time = None

                # save
                save_zones()

                # log
                msg = f"{SYMBOL} Zone {zone.zone_price} : Remove canceled sell order"
                LOGGER.debug(msg)

            else:
                msg = f"something wrong zone.sell_status: {zone.sell_status}"
                LOGGER.error(msg)

        # it will happened when user manually cancelling all orders in FTX
        elif zone.buy_status == CANCELED_BUY_STATUS:
            # Remove canceled buy order
            # reset buy order
            ZONES[i].buy_id = None
            ZONES[i].buy_price = None
            ZONES[i].buy_unit = None
            ZONES[i].buy_fee_unit = None
            ZONES[i].buy_recive_unit = None
            ZONES[i].buy_open_time = None
            ZONES[i].buy_status = None
            ZONES[i].buy_close_time = None
            save_zones()
            msg = f"{SYMBOL} Zone {zone.zone_price}: Remove canceled buy order"
            LOGGER.debug(msg)

        else:
            msg = f"something wrong zone.buy_status: {zone.buy_status}"
            LOGGER.error(msg)

        time.sleep(0.15)  # To avoid reaching maximum API call per sec (30 times)
Пример #6
0
    "COMPOUND_PC": COMPOUND_PC,
}
pprint(obj)
is_valid = True
for o in obj:
    if obj[o] == "" or obj[o] == 0:
        is_valid = False
        print(f"{o} must be provided")
if not is_valid:
    sys.exit()

# setup
LOGGER = BotLogger(SUB_ACCOUNT, LINE_TOKEN)
FTX = ccxt.ftx({"apiKey": API_KEY, "secret": API_SECRET, "enableRateLimit": True})
FTX.headers = {"FTX-SUBACCOUNT": SUB_ACCOUNT} if len(SUB_ACCOUNT) > 0 else {}
fee: TradingFeesResponse = parse_to_dataclass(FTX.fetch_trading_fees())
FEE_TAKER = fee.taker  # limit order
ZONE_FILE_PATH = os.path.join(BOT_DIR, 'zone.csv')
ZONES: List[Zone] = []
STATEMENT_FILE_PATH = os.path.join(BOT_DIR, 'statement.csv')


def save_zones() -> None:
    global ZONES
    global ZONE_FILE_PATH

    # revert into simple dict
    zones = [row.__dict__ for row in ZONES]

    # save
    pd.DataFrame(zones).to_csv(ZONE_FILE_PATH, index=False)