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

Feat/track pnl #966

Merged
merged 5 commits into from
Jul 21, 2023
Merged

Feat/track pnl #966

merged 5 commits into from
Jul 21, 2023

Conversation

da-kami
Copy link
Contributor

@da-kami da-kami commented Jul 20, 2023

Especially the first commit will be interesting for everybody to see - we are finally associating the positions with the contracts - Thanks for the great pairing session @luckysori :)

note: This PR only handles the realized pnl, I will see to add tracking for the unrealized pnl in a follow up.

related ticket: #932
(I'll add the fixes to the follow up :)

@da-kami
Copy link
Contributor Author

da-kami commented Jul 20, 2023

Note: I should probably extend the e2e tests with asserts that the position is actually Closed and the realized_pnl set. Maybe @klochowicz you can help me with that tomorrow :)

@da-kami
Copy link
Contributor Author

da-kami commented Jul 20, 2023

Note: It would potentially be better to change the insert to be an upsert and remove the specific update methods. But that would require some more model changes. I decided to get the feature done without more refactoring. Let me know what you think.

ALTER TABLE
positions
ADD
COLUMN "temporary_contract_id" TEXT NOT NULL;
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 think this could actually be UNIQUE... CC @luckysori

-- In order to allow re-running this migration we thus have to make sure to only add the value if it does not exist yet.
ALTER TYPE "PositionState_Type"
ADD
VALUE IF NOT EXISTS 'Closed';
Copy link
Contributor

Choose a reason for hiding this comment

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

Feel good state ☺️

Comment on lines +28 to +34
let positions = positions
.into_iter()
.filter(|p| {
p.position_state == PositionState::Open
&& OffsetDateTime::now_utc().ge(&p.expiry_timestamp)
})
.collect::<Vec<Position>>();
Copy link
Contributor

@bonomat bonomat Jul 20, 2023

Choose a reason for hiding this comment

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

Nit:

  • positions should already contain only open positions.
  • it might be more efficient to include the expiry check in the sql query

Copy link
Contributor Author

@da-kami da-kami Jul 20, 2023

Choose a reason for hiding this comment

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

I completely agree, but I would clean this up in a follow up. in this PR I just moved the code because I found having this code inline in main quite horrible for readability :)

@@ -67,6 +67,26 @@ impl Position {
Ok(positions)
}

#[autometrics]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need autometrics for this call?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

copy paste... we have this on all the functions inside of this Node impl block at the moment. I'm happy to remove it (or add it to the function below as well where I did not add it 😅)

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.

I'm wondering how this will look like once we support position resizing 🤔

Copy link
Contributor

@holzeis holzeis left a comment

Choose a reason for hiding this comment

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

I think this is good to merge, I left a couple of non-blocking remarks that you might want to address.

But before releasing that to production I guess we will need a migration script for existing positions without a temporary_contract_id

coordinator/src/node.rs Show resolved Hide resolved
coordinator/src/node.rs Show resolved Hide resolved
coordinator/src/position/models.rs Outdated Show resolved Hide resolved
crates/ln-dlc-node/src/node/dlc_channel.rs Outdated Show resolved Hide resolved
ALTER TYPE "PositionState_Type"
ADD
VALUE IF NOT EXISTS 'Closed';
ALTER TABLE
Copy link
Contributor

Choose a reason for hiding this comment

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

🙃 Maybe not 100% related to your change, but it would be great if we'd also save the closing price on the position and not just the pnl.

Copy link
Contributor Author

@da-kami da-kami Jul 20, 2023

Choose a reason for hiding this comment

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

I will see to add the closing price in a follow up, see #967 (comment)

PositionState::Open => crate::position::models::PositionState::Open,
PositionState::Closing => crate::position::models::PositionState::Closing,
PositionState::Closed => crate::position::models::PositionState::Closed {
pnl: realized_pnl.expect("realized pnl to be set when position is closed"),
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 Not sure if I like putting the pnl into the enum. Why not simply keep it as option on the position itself? I think that would also make the transformation from and to the database easier?

Copy link
Contributor Author

@da-kami da-kami Jul 20, 2023

Choose a reason for hiding this comment

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

For me this is more idiomatic in rust code. This makes it clear that this pnl is only there in Closed state. I think this is more accurate for the model in the business logic. The database cannot easily depict that so I flattened it out in the db. But that's an implementation detail for me :)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think this is exactly what you want from Rust.

let node = node.clone();
async move {
loop {
tokio::time::sleep(CLOSED_POSITION_SYNC_INTERVAL).await;
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ Whats the rule of when to put an interval setting into the config file and when static on top? 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

huh, I did not think about putting it into the config file. My rule of thumb would be: if we think we want to configure it we configure it. Is this relevant for the tests? If so then I'd add it to the config file so we can potentially use shorter intervals for the tests 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we need it in the config I'll address it in a follow up!

.context("Failed to load open and closing positions")?;

for position in open_and_closing_positions {
let contract = match node
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 do we need a similar check for open contracts? I think there are scenarios where we also move backwards. cc @luckysori

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If needed please ping and I'll address it in a follow up!

Copy link
Contributor

Choose a reason for hiding this comment

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

I suppose we could generalise this function to look at also consider the possibility that closing positions could go back to open. I don't think it's currently possible to go backwards in the protocol, but we did talk about it in p2pderivatives/rust-dlc#120.

Is this what you are referring to, @holzeis?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, that was what I was referring, thanks for adding the ticket 🙂

{
Ok(Some(closed_contract)) => closed_contract,
Ok(None) => {
tracing::trace!(position_id=%position.id, "Position not closed yet, skipping");
Copy link
Contributor

Choose a reason for hiding this comment

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

🔧 we might want to add a timeout here and set the position to failed if its still in closing. No need to repeat that sync if the position is in closing for days.

Copy link
Contributor Author

@da-kami da-kami Jul 21, 2023

Choose a reason for hiding this comment

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

I agree that this feature might be good to have, but I think this should be solved outside of this PR.
I captured #969 - feel free to add more information there!

@da-kami
Copy link
Contributor Author

da-kami commented Jul 20, 2023

I think this is good to merge, I left a couple of non-blocking remarks that you might want to address.

But before releasing that to production I guess we will need a migration script for existing positions without a temporary_contract_id

That would require an in-code migration - but I'm questioning if it's even possible to deterministically know which position belongs to which contract so far. Is it worth investing time into this?

I propose:

  1. Ignore not having a temporary contract id in the old ones; potentially add a dummy value that can be ignored when loading 🤔

  2. taken a snapshot of all positions before upgrade

  3. upgrade (we have a Closed state now)

  4. Manually run a query that sets all Closing positions to Closed.

This is simpler, and - at least in my opinion - this tiny amount of pnl we lost/won so far should not matter 😅

@da-kami
Copy link
Contributor Author

da-kami commented Jul 21, 2023

I'm wondering how this will look like once we support position resizing 🤔

TLDR; I don't think this model requires changes to introduce resizing :)


A few more thoughts - might be good food for thoughts for later :)

In my mind the position is always an aggregate over contracts. Orders define the execution that will lead to a contract (or resize of a contract or no contract anymore).
Eventually more than two orders will define that aggregate, but the position still only exists (is "open") or does not exist (is "closed"). The current position model already depicts an average_entry_price which would be updated when it gets resized (extended or reduced).
I think the closing scenario will be the same; eventually there will be a closing_price (execution price that reduces the position's quantity to 0, effectively wiping out the position) and, once the execution succeeded and the contract was "paid out", a payout and realized_pnl (= payout - collateral).

One thing that is still missing is associating orders with the positions. But I don't think this association changes the behavior of "closing" a position if we opt to stick to this model in the database.

Below are a few thoughts that discuss how the model could be change. I mostly talk about changing it in the database - the business logic model could be changed as well, but does not have to :)


I think we could model the position as a purely virtual aggregate in code (without depicting the position in the database); the position would then be an aggregate of it's orders, and the orders would store more information (e.g. execution price, ...). The position would then not exist in the database, but in order to decide if there is a position (i.e. the position quantity is not 0) we would aggregate all the successfully executed orders. This could be similar to the aggregates we had in ItchySats; the position would be purely virtual, and there is no "open" and "close" position, but just a position exists or not based on the executed order history.
I am not sure it's worth going for this though; the experience with aggregates in ItchySats was sometimes cumbersome. It was a (quite) concious decision to save the position into the database and manage it's state. Note that we could quite easily change it - the business logic could be pretty much the same, i.e. the way the positions::models::Position is constructed from the database would change - we could opt for only storing orders there that are aggregated into defining the values in positions::models::Position.

We could also do an in-between and delete the position in the database if it does not exist instead of setting it to Closed. This in-between solution would still require some form of aggregate over all orders though, so we can come up with the overall pnl over time. So, I don't think there is much value in this in between solution (might as well keep the historic positions as closed then...).

We could also opt to depict the aggregate differently in the database; at the moment we go for a per user per "cycle" model (where a cycle is basically a contract at the moment). This has the advantage that we can drive the position from the contract state easily.
However, we could depict the database position as a different aggregate. We could do a complete aggregate (spanning over all cycles) per user and set the quantity to 0 as well in the database instead of "closing" the position; or even do a complete combined position over all users with a combined pnl. Not sure if it's worth doing this in the database though.

@da-kami
Copy link
Contributor Author

da-kami commented Jul 21, 2023

bors r+

bors bot added a commit that referenced this pull request Jul 21, 2023
966: Feat/track pnl r=da-kami a=da-kami

Especially the first commit will be interesting for everybody to see - we are finally associating the `positions` with the contracts - Thanks for the great pairing session `@luckysori` :)

note: This PR only handles the realized pnl, I will see to add tracking for the unrealized pnl in a follow up.

related ticket: #932
(I'll add the *fixes* to the follow up :)

Co-authored-by: Daniel Karzel <daniel@comit.network>
@da-kami
Copy link
Contributor Author

da-kami commented Jul 21, 2023

bors r-

@bors
Copy link
Contributor

bors bot commented Jul 21, 2023

Canceled.

@da-kami
Copy link
Contributor Author

da-kami commented Jul 21, 2023

bors r+

@bors
Copy link
Contributor

bors bot commented Jul 21, 2023

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.
For more help, visit the forum.

If you want to switch to GitHub's built-in merge queue, visit their help page.

@bors bors bot merged commit 6b05827 into main Jul 21, 2023
7 checks passed
@bors bors bot deleted the feat/track-pnl branch July 21, 2023 03:19
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.

LGTM!

bors bot added a commit that referenced this pull request Jul 21, 2023
970: Fix outdated schema and correctly handle temporary contract id r=da-kami a=da-kami

Unfortunately #966 merged with an outdated `schema.rs` version in the coordinator. 
This PR fixes this!

Co-authored-by: Daniel Karzel <daniel@comit.network>
bors bot added a commit that referenced this pull request Jul 25, 2023
967: Periodically update unrealized pnl for position in database r=da-kami a=da-kami

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 

Co-authored-by: Daniel Karzel <daniel@comit.network>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants