<?php
namespace ccxt;

// ----------------------------------------------------------------------------

// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code

// -----------------------------------------------------------------------------
use \ccxt\Precise;
include_once PATH_TO_CCXT . '/test/exchange/base/test_order.php';

// ----------------------------------------------------------------------------
function tco_debug($exchange, $symbol, $message) {
    // just for debugging purposes
    $debug_create_order = true;
    if ($debug_create_order) {
        // for c# fix, extra step to convert them to string
        var_dump(' >>>>> testCreateOrder [', ((string) ($exchange['id'])), ' : ', $symbol, '] ', $message);
    }
    return true;
}


// ----------------------------------------------------------------------------
function test_create_order($exchange, $skipped_properties, $symbol) {
    $log_prefix = log_template($exchange, 'createOrder', [$symbol]);
    assert($exchange->has['cancelOrder'] || $exchange->has['cancelOrders'] || $exchange->has['cancelAllOrders'], $log_prefix . ' does not have cancelOrder|cancelOrders|canelAllOrders method, which is needed to make tests for `createOrder` method. Skipping the test...');
    // pre-define some coefficients, which will be used down below
    $limit_price_safety_multiplier_from_median = 1.045; // todo: when this https://github.com/ccxt/ccxt/issues/22442 is implemented, we'll remove hardcoded value. atm 5% is enough
    $market = $exchange->market($symbol);
    $is_swap_future = $market['swap'] || $market['future'];
    assert($exchange->has['fetchBalance'], $log_prefix . ' does not have fetchBalance() method, which is needed to make tests for `createOrder` method. Skipping the test...');
    $balance = $exchange->fetch_balance();
    $initial_base_balance = $balance[$market['base']]['free'];
    $initial_quote_balance = $balance[$market['quote']]['free'];
    assert($initial_quote_balance !== null, $log_prefix . ' - testing account not have balance of' . $market['quote'] . ' in fetchBalance() which is required to test');
    tco_debug($exchange, $symbol, 'fetched balance for ' . $symbol . ' : ' . ((string) $initial_base_balance) . ' ' . $market['base'] . '/' . $initial_quote_balance . ' ' . $market['quote']);
    [$best_bid, $best_ask] = fetch_best_bid_ask($exchange, 'createOrder', $symbol);
    // **************** [Scenario 1 - START] **************** //
    tco_debug($exchange, $symbol, '### SCENARIO 1 ###');
    // create a "limit order" which IS GUARANTEED not to have a fill (i.e. being far from the real price)
    tco_create_unfillable_order($exchange, $market, $log_prefix, $skipped_properties, $best_bid, $best_ask, $limit_price_safety_multiplier_from_median, 'buy', null);
    if ($is_swap_future) {
        // for swap markets, we test sell orders too
        tco_create_unfillable_order($exchange, $market, $log_prefix, $skipped_properties, $best_bid, $best_ask, $limit_price_safety_multiplier_from_median, 'sell', null);
    }
    tco_debug($exchange, $symbol, '### SCENARIO 1 PASSED ###');
    // **************** [Scenario 2 - START] **************** //
    tco_debug($exchange, $symbol, '### SCENARIO 2 ###');
    // create an order which IS GUARANTEED to have a fill (full or partial)
    tco_create_fillable_order($exchange, $market, $log_prefix, $skipped_properties, $best_bid, $best_ask, $limit_price_safety_multiplier_from_median, 'buy', null);
    if ($is_swap_future) {
        // for swap markets, we test sell orders too
        tco_create_fillable_order($exchange, $market, $log_prefix, $skipped_properties, $best_bid, $best_ask, $limit_price_safety_multiplier_from_median, 'sell', null);
    }
    tco_debug($exchange, $symbol, '### SCENARIO 2 PASSED ###');
    // **************** [Scenario 3 - START] **************** //
    return true;
}


// ----------------------------------------------------------------------------
function tco_create_unfillable_order($exchange, $market, $log_prefix, $skipped_properties, $best_bid, $best_ask, $limit_price_safety_multiplier_from_median, $buy_or_sell, $predefined_amount = null) {
    try {
        $symbol = $market['symbol'];
        $minimun_prices = $exchange->safe_dict($market['limits'], 'price', array());
        $minimum_price = $minimun_prices['min'];
        $maximum_price = $minimun_prices['max'];
        // below we set limit price, where the order will not be completed.
        // We do not use the extreme "limits" values for that market, because, even though min purchase amount for BTC/USDT can be 0.01 BTC, it means with 10$ you can buy 1000 BTC, which leads to unrealistic outcome. So, we just use around 5%-10% far price from the current price.
        $limit_buy_price_non_fillable = $best_bid / $limit_price_safety_multiplier_from_median;
        if ($minimum_price !== null && $limit_buy_price_non_fillable < $minimum_price) {
            $limit_buy_price_non_fillable = $minimum_price;
        }
        $limit_sell_price_non_fillable = $best_ask * $limit_price_safety_multiplier_from_median;
        if ($maximum_price !== null && $limit_sell_price_non_fillable > $maximum_price) {
            $limit_sell_price_non_fillable = $maximum_price;
        }
        $created_order = null;
        if ($buy_or_sell === 'buy') {
            $order_amount = tco_get_minimum_amount_for_limit_price($exchange, $market, $limit_buy_price_non_fillable, $predefined_amount);
            $created_order = tco_create_order_safe($exchange, $symbol, 'limit', 'buy', $order_amount, $limit_buy_price_non_fillable, array(), $skipped_properties);
        } else {
            $order_amount = tco_get_minimum_amount_for_limit_price($exchange, $market, $limit_sell_price_non_fillable, $predefined_amount);
            $created_order = tco_create_order_safe($exchange, $symbol, 'limit', 'sell', $order_amount, $limit_sell_price_non_fillable, array(), $skipped_properties);
        }
        $fetched_order = fetch_order($exchange, $symbol, $created_order['id'], $skipped_properties);
        // test fetched order object
        if ($fetched_order !== null) {
            test_order($exchange, $skipped_properties, 'createOrder', $fetched_order, $symbol, $exchange->milliseconds());
        }
        // ensure that order is not filled
        assert_order_state($exchange, $skipped_properties, 'createdOrder', $created_order, 'open', false);
        assert_order_state($exchange, $skipped_properties, 'fetchedOrder', $fetched_order, 'open', true);
        // ensure that order side matches
        assert_in_array($exchange, $skipped_properties, 'createdOrder', $created_order, 'side', [null, $buy_or_sell]);
        assert_in_array($exchange, $skipped_properties, 'fetchedOrder', $fetched_order, 'side', [null, $buy_or_sell]);
        tco_cancel_order($exchange, $symbol, $created_order['id']);
    } catch(\Throwable $e) {
        throw new Error($log_prefix . ' failed for Scenario 1: ' . ((string) $e));
    }
    return true;
}


function tco_create_fillable_order($exchange, $market, $log_prefix, $skipped_properties, $best_bid, $best_ask, $limit_price_safety_multiplier_from_median, $buy_or_sell_string, $predefined_amount = null) {
    try {
        $is_swap_future = $market['swap'] || $market['future'];
        $is_buy = ($buy_or_sell_string === 'buy');
        $entry_side = $is_buy ? 'buy' : 'sell';
        $exit_side = $is_buy ? 'sell' : 'buy';
        $entryorder_price = $is_buy ? $best_ask * $limit_price_safety_multiplier_from_median : $best_bid / $limit_price_safety_multiplier_from_median;
        $exitorder_price = $is_buy ? $best_bid / $limit_price_safety_multiplier_from_median : $best_ask * $limit_price_safety_multiplier_from_median; // todo revise: (tcoMininumCost (exchange, market) / amountToClose) / limitPriceSafetyMultiplierFromMedian;
        //
        //
        $symbol = $market['symbol'];
        $entry_amount = tco_get_minimum_amount_for_limit_price($exchange, $market, $entryorder_price);
        $entryorder_filled = tco_create_order_safe($exchange, $symbol, 'limit', $entry_side, $entry_amount, $entryorder_price, array(), $skipped_properties);
        // just for case, cancel any possible unfilled amount (though it is not be expected because the order was fillable)
        tco_try_cancel_order($exchange, $symbol, $entryorder_filled, $skipped_properties);
        // now, as order is closed/canceled, we can reliably fetch the order information
        $entryorder_fetched = fetch_order($exchange, $symbol, $entryorder_filled['id'], $skipped_properties);
        tco_assert_filled_order($exchange, $market, $log_prefix, $skipped_properties, $entryorder_filled, $entryorder_fetched, $entry_side, $entry_amount);
        //
        // ### close the traded position ###
        //
        $amount_to_close = $exchange->parse_to_numeric($exchange->safe_string($entryorder_fetched, 'filled'));
        $params = array();
        // as we want to close position, we should use 'reduceOnly' to ensure we don't open a margined position accidentally, because some exchanges might have automatically enabled margin-mode (on spot) or hedge-mode (on contracts)
        if ($is_swap_future) {
            $params['reduceOnly'] = true;
        }
        $exitorder_filled = tco_create_order_safe($exchange, $symbol, 'market', $exit_side, $amount_to_close, ($market['spot'] ? null : $exitorder_price), $params, $skipped_properties);
        $exitorder_fetched = fetch_order($exchange, $symbol, $exitorder_filled['id'], $skipped_properties);
        tco_assert_filled_order($exchange, $market, $log_prefix, $skipped_properties, $exitorder_filled, $exitorder_fetched, $exit_side, $amount_to_close);
    } catch(\Throwable $e) {
        throw new Error('failed for Scenario 2: ' . ((string) $e));
    }
    return true;
}


function tco_assert_filled_order($exchange, $market, $log_prefix, $skipped_properties, $created_order, $fetched_order, $requested_side, $requested_amount) {
    // test filled amount
    $precision_amount = $exchange->safe_string($market['precision'], 'amount');
    $entryorder_amount_string = $exchange->number_to_string($requested_amount);
    $filled_string = $exchange->safe_string($fetched_order, 'filled');
    assert($filled_string !== null, $log_prefix . ' order should be filled, but it is not. ' . $exchange->json($fetched_order));
    // filled amount should be whithin the expected range i.e. if you buy 100 DOGECOIN and amount-precision is 1,
    // and also considering possible roundings in implementation, then filled amount should be between 99 and 101
    $max_expected_filled_amount = Precise::string_add($entryorder_amount_string, $precision_amount);
    $min_expected_filled_amount = Precise::string_sub($entryorder_amount_string, $precision_amount);
    assert(Precise::string_le($filled_string, $max_expected_filled_amount), $log_prefix . ' filled amount is more than expected, possibly some implementation issue. ' . $exchange->json($fetched_order));
    assert(Precise::string_ge($filled_string, $min_expected_filled_amount), $log_prefix . ' filled amount is less than expected, possibly some implementation issue. ' . $exchange->json($fetched_order));
    // order state should be "closed"
    assert_order_state($exchange, $skipped_properties, 'createdOrder', $created_order, 'closed', false);
    assert_order_state($exchange, $skipped_properties, 'fetchedOrder', $fetched_order, 'closed', true);
    // ensure that order side matches
    assert_in_array($exchange, $skipped_properties, 'createdOrder', $created_order, 'side', [null, $requested_side]);
    assert_in_array($exchange, $skipped_properties, 'fetchedOrder', $fetched_order, 'side', [null, $requested_side]);
    return true;
}


// ----------------------------------------------------------------------------
function tco_cancel_order($exchange, $symbol, $order_id = null) {
    $log_prefix = log_template($exchange, 'createOrder', [$symbol]);
    $used_method = '';
    $cancel_result = null;
    if ($exchange->has['cancelOrder'] && $order_id !== null) {
        $used_method = 'cancelOrder';
        $cancel_result = $exchange->cancel_order($order_id, $symbol);
    } elseif ($exchange->has['cancelAllOrders']) {
        $used_method = 'cancelAllOrders';
        $cancel_result = $exchange->cancel_all_orders($symbol);
    } elseif ($exchange->has['cancelOrders']) {
        throw new Error($log_prefix . ' cancelOrders method is not unified yet, coming soon...');
    }
    tco_debug($exchange, $symbol, 'canceled order using ' . $used_method . ':' . $cancel_result['id']);
    // todo:
    // testSharedMethods.assertOrderState (exchange, skippedProperties, 'cancelOrder', cancelResult, 'canceled', false);
    // testSharedMethods.assertOrderState (exchange, skippedProperties, 'cancelOrder', cancelResult, 'closed', true);
    return true;
}


// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
function tco_create_order_safe($exchange, $symbol, $order_type, $side, $amount, $price = null, $params = array(), $skipped_properties = array()) {
    tco_debug($exchange, $symbol, 'Executing createOrder ' . $order_type . ' ' . $side . ' ' . $amount . ' ' . $price . ' ' . $exchange->json($params));
    $order = $exchange->create_order($symbol, $order_type, $side, $amount, $price, $params);
    try {
        test_order($exchange, $skipped_properties, 'createOrder', $order, $symbol, round(microtime(true) * 1000));
    } catch(\Throwable $e) {
        if ($order_type !== 'market') {
            // if it was limit order, try to cancel it before exiting the script
            tco_try_cancel_order($exchange, $symbol, $order, $skipped_properties);
        }
        throw $e;
    }
    return $order;
}


function tco_mininum_amount($exchange, $market) {
    $amount_values = $exchange->safe_dict($market['limits'], 'amount', array());
    $amount_min = $exchange->safe_number($amount_values, 'min');
    assert($amount_min !== null, $exchange->id . ' ' . $market['symbol'] . ' can not determine minimum amount for order');
    return $amount_min;
}


function tco_mininum_cost($exchange, $market) {
    $cost_values = $exchange->safe_dict($market['limits'], 'cost', array());
    $cost_min = $exchange->safe_number($cost_values, 'min');
    assert($cost_min !== null, $exchange->id . ' ' . $market['symbol'] . ' can not determine minimum cost for order');
    return $cost_min;
}


function tco_get_minimum_amount_for_limit_price($exchange, $market, $price, $predefined_amount = null) {
    // this method calculates the minimum realistic order amount:
    // at first it checks the "minimum hardcap limit" (i.e. 7 DOGE), however, if exchange also has "minimum cost" limits,
    // then we need to calculate the amount using cost, because of price is volatile, today's 7 DOGE cost could be 1$
    // but "minimum cost" requirement could be 5$ (which translates to 35 DOGE amount)
    $minimum_amount = tco_mininum_amount($exchange, $market);
    $minimum_cost = tco_mininum_cost($exchange, $market);
    $final_amount = $minimum_amount;
    if ($minimum_cost !== null) {
        if ($final_amount * $price < $minimum_cost) {
            $final_amount = $minimum_cost / $price;
        }
    }
    if ($predefined_amount !== null) {
        $final_amount = max($final_amount, $predefined_amount);
    }
    // because it's possible that calculated value might get truncated down in "createOrder" (i.e. 0.129 -> 0.12), we should ensure that final amount * price would bypass minimum cost requirements, by adding the "minimum precision"
    $amount_precision = $exchange->safe_number($market['precision'], 'amount');
    $is_tick_size_precision = $exchange->precisionMode === 4;
    if ($amount_precision === null) {
        $amount_precision = 1e-15; // todo: revise this for better way in future
    } else {
        // todo: remove after \ccxt\TICK_SIZE unification
        if (!$is_tick_size_precision) {
            $amount_precision = 1 / pow(10, $amount_precision); // this converts DECIMAL_PRECISION into \ccxt\TICK_SIZE
        }
    }
    $final_amount = $final_amount + $amount_precision;
    $final_amount = $final_amount * 1.1; // add around 10% to ensure "cost" is enough
    $final_amount = floatval($exchange->decimal_to_precision($final_amount, 2, $market['precision']['amount'], $exchange->precisionMode)); // 2 stands for ROUND_UP constant, 0 stands for TRUNCATE
    return $final_amount;
}


function tco_try_cancel_order($exchange, $symbol, $order, $skipped_properties) {
    $order_fetched = fetch_order($exchange, $symbol, $order['id'], $skipped_properties);
    $needs_cancel = $exchange->in_array($order_fetched['status'], ['open', 'pending', null]);
    // if it was not reported as closed/filled, then try to cancel it
    if ($needs_cancel) {
        tco_debug($exchange, $symbol, 'trying to cancel the remaining amount of partially filled order...');
        try {
            tco_cancel_order($exchange, $symbol, $order['id']);
        } catch(\Throwable $e) {
            // order might have been closed/filled already, before 'cancelOrder' call reaches server, so it is tolerable, we don't throw exception
            tco_debug($exchange, $symbol, ' a moment ago order was reported as pending, but could not be cancelled at this moment. Exception message: ' . ((string) $e));
        }
    } else {
        tco_debug($exchange, $symbol, 'order is already closed/filled, no need to cancel it');
    }
    return true;
}
