Repository: freqtrade/freqtrade

Issue:
Record profits in absolute value
This is a proposal to create a new column **trades.close_profit_abs** that records profit in absolute value. I think it can be used in different ways, such as easily aggregating total profits in stake_currency value, (would simplify _rpc_trade_statistics() for example), and it can be used in other references to calculate stake_currency based profit.

There's also lots of checks on the existing **Trade.close_profit** (percentage based) so we'd have to make sure we continue using it as a source of truth, and that we don't approve future PRs using the new column as a new source of truth, for consistency.

Let me know if you have any thoughts on this.


Discussion:
> There's also lots of checks on the existing Trade.close_profit (percentage based) so we'd have to make sure we continue using it as a source of truth, and that we don't approve future PRs using the new column as a new source of truth, for consistency.

why ... ? in theory (assuming stake-amount did not change) - the 2 should contain the same - or should at least be convertable between each other ... ? 

either way - i don't see adding that column as a problem ... the probably most difficult part is the migration ... 


"assuming stake-amount did not change" -- it already can be not same (last one is amended, plus Edge)...

In principle I agree with @yazeed , I think. This would simplify calculating PNL and be the bot better prepared for dynamic stake_amount for trades, which can be ...

Moreover, all loss functions in hyperopt should be calculated on abs profits instead of ratio values... For same reason -- stake_amounts can differ, and varying it can be a part of the trading strategy...

i think my point was lost above ... i'm not arguing adding this column - that's not much of a problem i think, i may add it sometime over the coming days.

I'm however not certain why we must make sure to not use this new column, it should be the "same truth", or rather,  it would depend on the usecase if it's the "correct" or wrong usage.

For backtesting / hyperopt / loss functions ... i'm not certain and will need to think this through (if it makes a difference or not) - but that's something to work on after this column is implemented.


For backtesting / hyperopt / loss functions -- I thinks there can be an option for a user to choose, if loss function is calculated on percentage data or on absolute results.


Resolving patch:
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -954,6 +954,7 @@ def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
 
             trade.close_rate = None
             trade.close_profit = None
+            trade.close_profit_abs = None
             trade.close_date = None
             trade.is_open = True
             trade.open_order_id = None
diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py
--- a/freqtrade/persistence.py
+++ b/freqtrade/persistence.py
@@ -86,7 +86,7 @@ def check_migrate(engine) -> None:
         logger.debug(f'trying {table_back_name}')
 
     # Check for latest column
-    if not has_column(cols, 'open_trade_price'):
+    if not has_column(cols, 'close_profit_abs'):
         logger.info(f'Running database migration - backup available as {table_back_name}')
 
         fee_open = get_column_def(cols, 'fee_open', 'fee')
@@ -106,6 +106,9 @@ def check_migrate(engine) -> None:
         ticker_interval = get_column_def(cols, 'ticker_interval', 'null')
         open_trade_price = get_column_def(cols, 'open_trade_price',
                                           f'amount * open_rate * (1 + {fee_open})')
+        close_profit_abs = get_column_def(
+            cols, 'close_profit_abs',
+            f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}")
 
         # Schema migration necessary
         engine.execute(f"alter table trades rename to {table_back_name}")
@@ -123,7 +126,7 @@ def check_migrate(engine) -> None:
                 stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
                 stoploss_order_id, stoploss_last_update,
                 max_rate, min_rate, sell_reason, strategy,
-                ticker_interval, open_trade_price
+                ticker_interval, open_trade_price, close_profit_abs
                 )
             select id, lower(exchange),
                 case
@@ -143,7 +146,7 @@ def check_migrate(engine) -> None:
                 {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
                 {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
                 {strategy} strategy, {ticker_interval} ticker_interval,
-                {open_trade_price} open_trade_price
+                {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
                 from {table_back_name}
              """)
 
@@ -190,6 +193,7 @@ class Trade(_DECL_BASE):
     close_rate = Column(Float)
     close_rate_requested = Column(Float)
     close_profit = Column(Float)
+    close_profit_abs = Column(Float)
     stake_amount = Column(Float, nullable=False)
     amount = Column(Float)
     open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
@@ -334,6 +338,7 @@ def close(self, rate: float) -> None:
         """
         self.close_rate = Decimal(rate)
         self.close_profit = self.calc_profit_ratio()
+        self.close_profit_abs = self.calc_profit()
         self.close_date = datetime.utcnow()
         self.is_open = False
         self.open_order_id = None
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -197,7 +197,7 @@ def _rpc_daily_profit(
                 Trade.close_date >= profitday,
                 Trade.close_date < (profitday + timedelta(days=1))
             ]).order_by(Trade.close_date).all()
-            curdayprofit = sum(trade.calc_profit() for trade in trades)
+            curdayprofit = sum(trade.close_profit_abs for trade in trades)
             profit_days[profitday] = {
                 'amount': f'{curdayprofit:.8f}',
                 'trades': len(trades)
@@ -246,8 +246,8 @@ def _rpc_trade_statistics(
                 durations.append((trade.close_date - trade.open_date).total_seconds())
 
             if not trade.is_open:
-                profit_ratio = trade.calc_profit_ratio()
-                profit_closed_coin.append(trade.calc_profit())
+                profit_ratio = trade.close_profit
+                profit_closed_coin.append(trade.close_profit_abs)
                 profit_closed_ratio.append(profit_ratio)
             else:
                 # Get current rate


Test patch:
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -476,12 +476,22 @@ def test_migrate_old(mocker, default_conf, fee):
                                      stake=default_conf.get("stake_amount"),
                                      amount=amount
                                      )
+    insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee,
+                          open_rate, close_rate, stake_amount, amount, open_date)
+                          VALUES ('BITTREX', 'BTC_ETC', 0, {fee},
+                          0.00258580, 0.00268580, {stake}, {amount},
+                          '2017-11-28 12:44:24.000000')
+                          """.format(fee=fee.return_value,
+                                     stake=default_conf.get("stake_amount"),
+                                     amount=amount
+                                     )
     engine = create_engine('sqlite://')
     mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
 
     # Create table using the old format
     engine.execute(create_table_old)
     engine.execute(insert_table_old)
+    engine.execute(insert_table_old2)
     # Run init to test migration
     init(default_conf['db_url'], default_conf['dry_run'])
 
@@ -500,6 +510,15 @@ def test_migrate_old(mocker, default_conf, fee):
     assert trade.stop_loss == 0.0
     assert trade.initial_stop_loss == 0.0
     assert trade.open_trade_price == trade._calc_open_trade_price()
+    assert trade.close_profit_abs is None
+
+    trade = Trade.query.filter(Trade.id == 2).first()
+    assert trade.close_rate is not None
+    assert trade.is_open == 0
+    assert trade.open_rate_requested is None
+    assert trade.close_rate_requested is None
+    assert trade.close_rate is not None
+    assert pytest.approx(trade.close_profit_abs) == trade.calc_profit()
 
 
 def test_migrate_new(mocker, default_conf, fee, caplog):
@@ -583,6 +602,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
     assert log_has("trying trades_bak2", caplog)
     assert log_has("Running database migration - backup available as trades_bak2", caplog)
     assert trade.open_trade_price == trade._calc_open_trade_price()
+    assert trade.close_profit_abs is None
 
 
 def test_migrate_mid_state(mocker, default_conf, fee, caplog):