Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deal with expired position the right way #1246

Merged
merged 14 commits into from
Sep 14, 2023

Conversation

holzeis
Copy link
Contributor

@holzeis holzeis commented Sep 7, 2023

This PR got quite large and addresses several issues we had with closing expired positions.

  1. Expired positions are now automatically matched against the orderbook

Since we currently have only one maker running which is publishing short and long orders this means the order for the expired position will get matched with the maker again, basically reducing his position with the coordinator.

  1. Once the user comes online (connects to our websocket) we check for any pending matches (e.g. due to an expired position) and send a AsyncMatch message which the app is immediately executing.

For the following video, I reduced the position expiry to 30 seconds and the regular interval check for expired positions to 5 seconds. What happened in the background is that the orderbook found that the position expired found a match and saved that for later when the user is coming back online. Once online the match is executed with the user.

Simulator.Screen.Recording.-.iPhone.14.Pro.-.2023-09-07.at.15.27.55.mp4

resolves #1173
resolves #1071

Copy link
Contributor

@bonomat bonomat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall good work.
It's not a good idea to change migration files, this makes commit not atomic and we could run into weird problems.

I don't like the concept of a forced order yet. You seem to define every order as forced which was not manual. Maybe we can describe the concept differently?

Otherwise, I think this looks good.

crates/orderbook-commons/src/lib.rs Outdated Show resolved Hide resolved
coordinator/src/node/expired_orders.rs Outdated Show resolved Hide resolved
coordinator/src/orderbook/db/orders.rs Outdated Show resolved Hide resolved
@@ -130,6 +131,51 @@ impl FromSql<OrderStateType, Pg> for OrderState {
}
}

#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)]
#[diesel(sql_type = OrderReasonType)]
pub(crate) enum OrderReason {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrderReason sounds weird to me. Is it more an OrderType?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have an OrderType 🙈

Copy link
Contributor Author

@holzeis holzeis Sep 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about SubOrderType?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrderCreationType? OrderMethod?

coordinator/src/orderbook/trading.rs Outdated Show resolved Hide resolved
coordinator/src/orderbook/db/custom_types.rs Show resolved Hide resolved
mobile/lib/features/trade/force_order_change_notifier.dart Outdated Show resolved Hide resolved
mobile/native/src/db/models.rs Outdated Show resolved Hide resolved
@holzeis
Copy link
Contributor Author

holzeis commented Sep 8, 2023

It's not a good idea to change migration files, this makes commit not atomic and we could run into weird problems.

I've split up the migration scripts into dedicated folders per commit.

I don't like the concept of a forced order yet. You seem to define every order as forced which was not manual. Maybe we can describe the concept differently?

As discussed offline I renamed it to async order and removed types that aren't part of this PR.

An async order is an order that has not been created by the user on the app, but automatically by the orderbook. For now this is just the market order created by the coordinator because the position expired, but this concept can also be used for stop loss, take profit, liquidation and limit orders.

@holzeis holzeis force-pushed the feat/update-maker-on-expired-positions branch 3 times, most recently from 689b799 to 39d8abb Compare September 9, 2023 09:56
@holzeis holzeis requested a review from bonomat September 9, 2023 09:58
Copy link
Contributor

@bonomat bonomat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, but please wait for a second pair of eyes

@holzeis holzeis force-pushed the feat/update-maker-on-expired-positions branch 3 times, most recently from c8471c5 to 27b3cc3 Compare September 12, 2023 18:08
Copy link
Contributor

@luckysori luckysori left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this long and complicated task!

Whilst I do like the concept of letting the coordinator post an order on behalf of the trader, who would most likely choose this over settling their position on-chain, the implementation is quite complicated and I am not convinced we are modelling things correctly with the introduction of Matches and OrderReason.

At this stage I don't really want to block the merge since other teammates have already approved this. My main worry is that the orderbook is becoming harder to understand and maintain, and I think it was already in a bad spot before this PR which adds more features.

crates/orderbook-commons/src/lib.rs Show resolved Hide resolved
Comment on lines +198 to +229
if matches.len() == 1 {
return matches.first().expect("to be exactly one").execution_price;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙃 I think this can be solved nicely with subslice patterns. No need to address this, I just think you might be interested.

coordinator/src/orderbook/trading.rs Outdated Show resolved Hide resolved
}
let match_params = matched_orders.matches();
for match_param in match_params {
matches::insert(&mut conn, match_param)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 It's kind of weird that we need to save a match for each trader involved in the trade. Why not just one entry with all the information?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I want to track if a match got executed on per trader. The model would have gotten to complex otherwise.

Also considering that we could have multiple matches per order. By having a single match record we can easily reflect 1:n and m:n relations.

crates/orderbook-commons/src/lib.rs Show resolved Hide resolved
Comment on lines +87 to +99
let uncapped_pnl = match opening_price != Decimal::ZERO && closing_price != Decimal::ZERO {
true => {
let uncapped_pnl = (quantity / opening_price) - (quantity / closing_price);
let uncapped_pnl = uncapped_pnl.round_dp_with_strategy(
8,
rust_decimal::RoundingStrategy::MidpointAwayFromZero,
);
uncapped_pnl
.to_f64()
.context("Could not convert Decimal to f64")?
}
false => 0.0,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 Can't we just use checked_divs and .unwrap_or_defaults? Also matching on a boolean is weird. if-else is the way to go.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing applies above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only touched that code to prevent the divide by zero panic (unblocking me during testing). Don't want to touch it further as it is not related to this PR.

Also happy to drop that commit, if you think that should not happen.

crates/orderbook-commons/src/lib.rs Outdated Show resolved Hide resolved
@holzeis holzeis force-pushed the feat/update-maker-on-expired-positions branch from 27b3cc3 to 325e32f Compare September 14, 2023 08:11
Having the order matching logic in the route layer made it difficult to utilize existing functionality. By moving it to the trading module it's now cleaner to interact with it from a different component.
Extracting the average execution price calculation from the `FilledWith` model makes things easier to reuse.
Keeping an order state on the coordinator side (similar to how it is done on the app side) allows us to depict more complex states of the order needed for handling expired positions.
If a position expires the coordinator will automatically create a closing order and look for a match.

Since the user is not to be expected to be online the coordinator saves the match into the database.
`TradeValues` are either created from the quantity or from the margin. The new name now reflects that much better.
Apparently it can happen that we get a 0 bid or 0 ask price. That can lead to a panic: Aborting after panic in task info=panicked at 'Division by zero'
That makes the navigator keys accessible from anywhere.
@holzeis holzeis force-pushed the feat/update-maker-on-expired-positions branch from 325e32f to 08ce864 Compare September 14, 2023 08:13
This commit introduces the order reason and the force match. Basically the idea is that we have certain orders that will have to get created by the orderbook (on behalf of the user like expiry, liquidation, take profit and stop loss). Those orders are matched automatically without the users intervention. Once matched we inform the user about the match for execution. If the user is online the trade will be automatically executed if not, the execution is delayed until the user comes online. Note, this match can still expire.
The taken flag is not needed anymore as it is replaced by a more elaborate order state.
Spawning a new tokio task when processing a message prevents us from potentially blocking trading requests as the trading module might still be busy processing the last request.
@holzeis holzeis force-pushed the feat/update-maker-on-expired-positions branch from 08ce864 to f21358c Compare September 14, 2023 08:40
@holzeis holzeis added this pull request to the merge queue Sep 14, 2023
Merged via the queue into main with commit 8bd6afc Sep 14, 2023
7 checks passed
@holzeis holzeis deleted the feat/update-maker-on-expired-positions branch September 14, 2023 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants