-
Notifications
You must be signed in to change notification settings - Fork 23
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
Periodically update unrealized pnl for position in database #967
Conversation
7dbfaaa
to
c6bbf8a
Compare
ac754fc
to
f6030a8
Compare
c6bbf8a
to
7f7b17a
Compare
7f7b17a
to
0691230
Compare
Direction::Short => (1.0_f32, position.leverage), | ||
}; | ||
|
||
// the position in the database is the trader's position, our direction is opposite |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FML haha.
let (long_leverage, short_leverage) = match position.direction { | ||
Direction::Long => (position.leverage, 1.0_f32), | ||
Direction::Short => (1.0_f32, position.leverage), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit scary that this kind of logic is located in this module. It's probably hard to define where this should be located atm. Maybe we can leave a comment talking about how the position is the trader's position and the coordinator is always leverage 1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a refactoring where I moved the coordinator_pnl
onto the Position
struct and added tests.
This particular bit remains within that pnl calculation function. Eventually we might also want to extract this.
Instead of using 1.0
I opted for using the COORDINATOR_LEVERAGE
constant that I made public in the coordinator's node
module.
let current_price = match position.direction { | ||
trade::Direction::Long => quote.bid_price, | ||
trade::Direction::Short => quote.ask_price, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't reason about this. I assume the direction
corresponds to the trader's direction. Given that this is the coordinator's perspective, does this affect which price it is. I would welcome a comment here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it is the price that the trader will get, we have to use that price. We cannot argue from the side of the coordinator here, otherwise we would try to close the position with two different prices.
I'll see to extract a function that we can use throughout the code - it's not the first time this code triggers a lengthy thinking process :)
0691230
to
3ec9b25
Compare
Direction::Short => (1.0_f32, position.leverage), | ||
}; | ||
|
||
// the position in the database is the trader's position, our direction is opposite |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is related: #941
coordinator/migrations/2023-07-20-055142_unrealized_pnl_positions/up.sql
Outdated
Show resolved
Hide resolved
|
||
let coordinator_pnl = position.calculate_coordinator_pnl(quote).unwrap(); | ||
|
||
assert_eq!(coordinator_pnl, -9_090_909); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's hard for me to follow these amounts :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, it took me a while to understand what is happening and why this value is correct:
In my words expressed: In this example, the person who went long, bought $20,000 worth of BTC at the price of 20,000, i.e. 1
BTC
At the price of $22,000 he sells $20,000 worth of BTC, i.e. he sells it for 0.909090909
BTC.
The difference is his profit, i.e.
1 BTC
- 0.909090909 BTC
= 0.09090909 BTC
profit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with both your statements: It is hard to follow the amounts and it's hard to understand why it's correct.
Any idea how to make it better? 😬
I tried to come up with better numbers, but I feel the numbers are not the problem but more the model and the way we structure and express things. I could not come up with something better so far though. We already had a hard time with such kind of tests in ItchySats as far as I remember.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know... Maybe add the calculation from my comment and add asserts for that?
Eitherway... if we have to touch this again, we very likely will need to go through this thought process again 🙈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth adding a comment like what Philipp said above each test, because I agree that the thought process is not intuitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done here.
@@ -57,10 +58,7 @@ pub async fn close(node: Node) { | |||
}; | |||
|
|||
let closing_price = match BitmexClient::get_quote(&position.expiry_timestamp).await { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know it's not your code, but this price should not be from bitmex but from our orderbook/counterparty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see where you are going, but for me using a price from the orderbook only makes sense if there is also an order that is eventually executed. We don't do this at the moment.
The coordinator can still use an orderbook price and just simulate the match - but I find this a bit odd.
I opened #988 to capture this; feel free to change the description
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see where you are going, but for me using a price from the orderbook only makes sense if there is also an order that is eventually executed. We don't do this at the moment.
Totally agree. We should be doing this though.
trade::Direction::Long => quote.bid_price, | ||
trade::Direction::Short => quote.ask_price, | ||
}, | ||
Ok(quote) => closing_price(position.direction, quote), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The closing_price
is not wrong but for me it adds to the confusion in this whole story and I had to do some mental gymnastics to follow what is happening here 🙈
What about we do something like this:
// 1. get counter direction, i.e. he was long, now he needs to go short, or he was short, now needs to go long
let direction = position.direction.opposite();
// 2. TODO: get latest quote from the orderbook/counter party
let quote = BitmexClient::get_quote(&position.expiry_timestamp)
.await
.unwrap();
// 3. get closing price based on order direction
let closing_price = match direction {
Direction::Long => quote.ask_price,
Direction::Short => quote.bid_price,
};
crates/trade/src/cfd.rs
Outdated
/// is effectively going short to close a long order. | ||
/// If the trader closes a short position then the trader gets the best ask price because the trader | ||
/// is effectively going long to close a short order. | ||
pub fn closing_price(trader_direction: Direction, quote: Quote) -> Decimal { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think of implementing this on Quote
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would then not call it closing_price
but price_for_direction
or so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's hard to fit this into my mental model, but I can see how it could make the code easier :)
For me the Quote
does not care about any underlying financial instrument, it just depicts the best bid and ask price in the market (weighted by volume).
For me "closing" is always related to the position, not to the quote - the quote does not care about closing.
Given that the quote defines the closing price I can see how it could somehow make sense to add it there, but if I would just look at the quote out of context I would be confused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you can share your mental model - it might help to make all this code better :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me "closing" is always related to the position, not to the quote - the quote does not care about closing.
Exactly, that's why I want to get rid of the term closing
:D
I would add something like:
impl Quote {
fn get_price_for_direction(&self, direction: Direction) -> Decimal {
match direction {
Direction::Long => self.ask_price,
Direction::Short => self.bid_price,
}
}
}
and then from the outside, i.e. where you currently call closing_price
you do:
get_price_for_direction(position.direction.oposite())`
Then we know, we want the price for the opposite direction.
If we know the `closing_price` then it is used for updating the unrealized pnl. Note that the realized pnl is *only* set once we update it based on the closed contract. The realized pnl is not updated based on the execution price, because there is no execution guarantee.
be1206d
to
cf3f06f
Compare
We use the same logic in three different places already, better extract it into a function to make this clearer.
Additionally adds more tests to `trade::cfd` that reflect the pnl calculation on the trader side. This reflects that the traders losses are the coordinator's profits and vice versa.
cf3f06f
to
2479fcf
Compare
bors r+ |
Build succeeded! The publicly hosted instance of bors-ng is deprecated and will go away soon. If you want to self-host your own instance, instructions are here. If you want to switch to GitHub's built-in merge queue, visit their help page.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, I like it!
|
||
let coordinator_pnl = position.calculate_coordinator_pnl(quote).unwrap(); | ||
|
||
assert_eq!(coordinator_pnl, -9_090_909); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth adding a comment like what Philipp said above each test, because I agree that the thought process is not intuitive.
994: Some cleanup and alignment r=da-kami a=da-kami A `Quote` is basically a more detailed `Price`; the `Quote` uses the `Price`'s `get_price_for_direction` logic to get the price by direction. --- `@luckysori` comments from #967 should all be worked in Co-authored-by: Daniel Karzel <[email protected]>
stacked on top of #966
Note: I did not bother setting the
unrealized_pnl
to null when we close the position. Not sure that's actually necessary.fixes: #932