Repository: Drakkar-Software/OctoBot-Trading (language: python)

Issue:
[Position] Add realised pnl calculation



Discussion / hints:

Fix patch:
diff --git a/octobot_trading/enums.py b/octobot_trading/enums.py
index b7235b930..5bf1ae0e2 100644
--- a/octobot_trading/enums.py
+++ b/octobot_trading/enums.py
@@ -101,6 +101,24 @@ class PositionStatus(enum.Enum):
     ADL = "auto_deleveraging"
 
 
+class BlockchainTransactionStatus(enum.Enum):
+    CREATED = "created"
+    CONFIRMING = "confirming"
+    REPLACED = "replaced"
+    FAIL = "fail"
+    SUCCESS = "success"
+
+
+class TransactionType(enum.Enum):
+    BLOCKCHAIN_DEPOSIT = "blockchain_deposit"
+    BLOCKCHAIN_WITHDRAWAL = "blockchain_withdrawal"
+    FUNDING_FEE = "funding_fee"
+    TRADING_FEE = "trading_fee"
+    REALISED_PNL = "realised_pnl"
+    CLOSE_REALISED_PNL = "close_realised_pnl"
+    TRANSFER = "transfer"
+
+
 class TraderOrderType(enum.Enum):
     BUY_MARKET = "buy_market"
     BUY_LIMIT = "buy_limit"
diff --git a/octobot_trading/errors.py b/octobot_trading/errors.py
index a19bdaafd..7e85316d6 100644
--- a/octobot_trading/errors.py
+++ b/octobot_trading/errors.py
@@ -96,3 +96,9 @@ class TooManyOpenPositionError(Exception):
     """
     Raised when changing future contract attributes without closing positions first
     """
+
+
+class DuplicateTransactionIdError(Exception):
+    """
+    Raised when trying to add a new transaction with a duplicate id
+    """
diff --git a/octobot_trading/personal_data/__init__.pxd b/octobot_trading/personal_data/__init__.pxd
index 7a77dc5cf..e4028ff80 100644
--- a/octobot_trading/personal_data/__init__.pxd
+++ b/octobot_trading/personal_data/__init__.pxd
@@ -92,7 +92,19 @@ from octobot_trading.personal_data.trades cimport (
     TradesUpdater,
     Trade,
 )
-
+from octobot_trading.personal_data cimport transactions
+from octobot_trading.personal_data.transactions cimport (
+    TransactionsManager,
+    Transaction,
+    BlockchainTransaction,
+    FeeTransaction,
+    RealisedPnlTransaction,
+    TransferTransaction,
+    create_blockchain_transaction,
+    create_realised_pnl_transaction,
+    create_fee_transaction,
+    create_transfer_transaction,
+)
 from octobot_trading.personal_data cimport exchange_personal_data
 from octobot_trading.personal_data.exchange_personal_data cimport (
     ExchangePersonalData,
@@ -171,4 +183,14 @@ __all__ = [
     "TradesUpdater",
     "Trade",
     "ExchangePersonalData",
+    "TransactionsManager",
+    "Transaction",
+    "BlockchainTransaction",
+    "FeeTransaction",
+    "RealisedPnlTransaction",
+    "TransferTransaction",
+    "create_blockchain_transaction",
+    "create_realised_pnl_transaction",
+    "create_fee_transaction",
+    "create_transfer_transaction",
 ]
diff --git a/octobot_trading/personal_data/__init__.py b/octobot_trading/personal_data/__init__.py
index 1d72733a6..8faa580b0 100644
--- a/octobot_trading/personal_data/__init__.py
+++ b/octobot_trading/personal_data/__init__.py
@@ -135,6 +135,19 @@
     TradesUpdater,
     Trade,
 )
+from octobot_trading.personal_data import transactions
+from octobot_trading.personal_data.transactions import (
+    TransactionsManager,
+    Transaction,
+    BlockchainTransaction,
+    FeeTransaction,
+    RealisedPnlTransaction,
+    TransferTransaction,
+    create_blockchain_transaction,
+    create_realised_pnl_transaction,
+    create_fee_transaction,
+    create_transfer_transaction,
+)
 from octobot_trading.personal_data import exchange_personal_data
 from octobot_trading.personal_data.exchange_personal_data import (
     ExchangePersonalData,
@@ -284,4 +297,14 @@
     "AUTHENTICATED_UPDATER_SIMULATOR_PRODUCERS",
     "TraderOrderTypeClasses",
     "TraderPositionTypeClasses",
+    "TransactionsManager",
+    "Transaction",
+    "BlockchainTransaction",
+    "FeeTransaction",
+    "RealisedPnlTransaction",
+    "TransferTransaction",
+    "create_blockchain_transaction",
+    "create_realised_pnl_transaction",
+    "create_fee_transaction",
+    "create_transfer_transaction",
 ]
diff --git a/octobot_trading/personal_data/exchange_personal_data.pxd b/octobot_trading/personal_data/exchange_personal_data.pxd
index 39d97a1ca..51ca7a9fc 100644
--- a/octobot_trading/personal_data/exchange_personal_data.pxd
+++ b/octobot_trading/personal_data/exchange_personal_data.pxd
@@ -19,6 +19,7 @@ cimport octobot_trading.personal_data.orders.order as order_class
 cimport octobot_trading.personal_data.portfolios.portfolio_manager as portfolio_manager
 cimport octobot_trading.personal_data.positions.positions_manager as positions_manager
 cimport octobot_trading.personal_data.trades.trades_manager as trades_manager
+cimport octobot_trading.personal_data.transactions.transactions_manager as transactions_manager
 cimport octobot_trading.util as util
 
 cdef class ExchangePersonalData(util.Initializable):
@@ -34,6 +35,7 @@ cdef class ExchangePersonalData(util.Initializable):
     cdef public trades_manager.TradesManager trades_manager
     cdef public orders_manager.OrdersManager orders_manager
     cdef public positions_manager.PositionsManager positions_manager
+    cdef public transactions_manager.TransactionsManager transactions_manager
 
     cpdef object get_order_portfolio(self, order_class.Order order)
     cpdef void clear(self)
diff --git a/octobot_trading/personal_data/exchange_personal_data.py b/octobot_trading/personal_data/exchange_personal_data.py
index 74d79f5d2..a24149187 100644
--- a/octobot_trading/personal_data/exchange_personal_data.py
+++ b/octobot_trading/personal_data/exchange_personal_data.py
@@ -23,6 +23,8 @@
 import octobot_trading.personal_data.positions.positions_manager as positions_manager
 import octobot_trading.personal_data.orders.orders_manager as orders_manager
 import octobot_trading.personal_data.trades.trades_manager as trades_manager
+import octobot_trading.personal_data.transactions.transactions_manager as transactions_manager
+import octobot_trading.personal_data.transactions.transaction_factory as transaction_factory
 import octobot_trading.util as util
 
 
@@ -36,10 +38,12 @@ def __init__(self, exchange_manager):
 
         self.trader = None
         self.exchange = None
+
         self.portfolio_manager = None
         self.trades_manager = None
         self.orders_manager = None
         self.positions_manager = None
+        self.transactions_manager = None
 
     async def initialize_impl(self):
         self.trader = self.exchange_manager.trader
@@ -51,10 +55,12 @@ async def initialize_impl(self):
                 self.trades_manager = trades_manager.TradesManager(self.trader)
                 self.orders_manager = orders_manager.OrdersManager(self.trader)
                 self.positions_manager = positions_manager.PositionsManager(self.trader)
+                self.transactions_manager = transactions_manager.TransactionsManager()
                 await self.portfolio_manager.initialize()
                 await self.trades_manager.initialize()
                 await self.orders_manager.initialize()
                 await self.positions_manager.initialize()
+                await self.transactions_manager.initialize()
             except Exception as e:
                 self.logger.exception(e, True, f"Error when initializing : {e}. "
                                                f"{self.exchange.name} personal data disabled.")
@@ -109,6 +115,9 @@ async def handle_portfolio_update_from_funding(self, position, funding_rate,
         try:
             changed: bool = await self.portfolio_manager.handle_balance_update_from_funding(
                 position=position, funding_rate=funding_rate, require_exchange_update=require_exchange_update)
+            transaction_factory.create_fee_transaction(self.exchange_manager, position.get_currency(), position.symbol,
+                                                       quantity=position.value * funding_rate,
+                                                       funding_rate=funding_rate)
             if should_notify:
                 await self.handle_portfolio_update_notification(self.portfolio_manager.portfolio.portfolio)
             return changed
@@ -309,3 +318,5 @@ def clear(self):
             self.positions_manager.clear()
         if self.trades_manager is not None:
             self.trades_manager.clear()
+        if self.transactions_manager is not None:
+            self.transactions_manager.clear()
diff --git a/octobot_trading/personal_data/positions/position.pxd b/octobot_trading/personal_data/positions/position.pxd
index e4bcd4307..b58df7441 100644
--- a/octobot_trading/personal_data/positions/position.pxd
+++ b/octobot_trading/personal_data/positions/position.pxd
@@ -106,6 +106,7 @@ cdef class Position(util.Initializable):
     cpdef object get_two_way_taker_fee(self)
     cpdef object get_order_cost(self)
     cpdef object get_fee_to_open(self)
+    cpdef str get_currency(self)
     cpdef void update_fee_to_close(self)
     cpdef bint is_open(self)
     cpdef bint is_liquidated(self)
diff --git a/octobot_trading/personal_data/positions/position.py b/octobot_trading/personal_data/positions/position.py
index 768239701..3ba201e0a 100644
--- a/octobot_trading/personal_data/positions/position.py
+++ b/octobot_trading/personal_data/positions/position.py
@@ -18,6 +18,7 @@
 import octobot_trading.enums as enums
 import octobot_trading.personal_data.positions.position_util as position_util
 import octobot_trading.personal_data.positions.states as positions_states
+import octobot_trading.personal_data.transactions.transaction_factory as transaction_factory
 import octobot_trading.util as util
 import octobot_trading.constants as constants
 
@@ -345,6 +346,11 @@ def _update_realized_pnl_from_size_update(self, size_update, is_closing=False):
         """
         try:
             realised_pnl_update = -size_update / self.size * self.unrealised_pnl
+            transaction_factory.create_realised_pnl_transaction(self.exchange_manager,
+                                                                self.get_currency(),
+                                                                self.symbol,
+                                                                realised_pnl=realised_pnl_update,
+                                                                is_closed_pnl=is_closing)
         except (decimal.DivisionByZero, decimal.InvalidOperation):
             realised_pnl_update = constants.ZERO
         self.realised_pnl += realised_pnl_update
@@ -401,6 +407,12 @@ def get_initial_margin_rate(self):
         except (decimal.DivisionByZero, decimal.InvalidOperation):
             return constants.ZERO
 
+    def get_currency(self):
+        """
+        :return: position currency
+        """
+        return self.currency if self.symbol_contract.is_inverse_contract() else self.market
+
     def calculate_maintenance_margin(self):
         raise NotImplementedError("calculate_maintenance_margin not implemented")
 
@@ -602,7 +614,7 @@ def __str__(self):
         return self.to_string()
 
     def to_string(self):
-        currency = self.currency if self.symbol_contract.is_inverse_contract() else self.market
+        currency = self.get_currency()
         return (f"{self.symbol} | "
                 f"Size : {round(self.size, 10).normalize()} "
                 f"({round(self.value, 10).normalize()} {currency}) "
diff --git a/octobot_trading/personal_data/transactions/__init__.pxd b/octobot_trading/personal_data/transactions/__init__.pxd
new file mode 100644
index 000000000..0a3ade4eb
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/__init__.pxd
@@ -0,0 +1,51 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+from octobot_trading.personal_data.transactions cimport transaction
+from octobot_trading.personal_data.transactions.transaction cimport (
+    Transaction,
+)
+from octobot_trading.personal_data.transactions cimport transactions_manager
+from octobot_trading.personal_data.transactions.transactions_manager cimport (
+    TransactionsManager,
+)
+from octobot_trading.personal_data.transactions cimport transaction_factory
+from octobot_trading.personal_data.transactions.transaction_factory cimport (
+    create_blockchain_transaction,
+    create_realised_pnl_transaction,
+    create_fee_transaction,
+    create_transfer_transaction,
+)
+from octobot_trading.personal_data.transactions cimport types
+from octobot_trading.personal_data.transactions.types cimport (
+    BlockchainTransaction,
+    FeeTransaction,
+    RealisedPnlTransaction,
+    TransferTransaction,
+)
+
+__all__ = [
+    "TransactionsManager",
+    "Transaction",
+    "BlockchainTransaction",
+    "FeeTransaction",
+    "RealisedPnlTransaction",
+    "TransferTransaction",
+    "create_blockchain_transaction",
+    "create_realised_pnl_transaction",
+    "create_fee_transaction",
+    "create_transfer_transaction",
+]
diff --git a/octobot_trading/personal_data/transactions/__init__.py b/octobot_trading/personal_data/transactions/__init__.py
new file mode 100644
index 000000000..f28aaf592
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/__init__.py
@@ -0,0 +1,52 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+from octobot_trading.personal_data.transactions import transactions_manager
+from octobot_trading.personal_data.transactions import transaction_factory
+from octobot_trading.personal_data.transactions import transaction
+from octobot_trading.personal_data.transactions import types
+
+from octobot_trading.personal_data.transactions.transactions_manager import (
+    TransactionsManager,
+)
+from octobot_trading.personal_data.transactions.transaction_factory import (
+    create_blockchain_transaction,
+    create_realised_pnl_transaction,
+    create_fee_transaction,
+    create_transfer_transaction,
+)
+from octobot_trading.personal_data.transactions.transaction import (
+    Transaction,
+)
+from octobot_trading.personal_data.transactions.types import (
+    BlockchainTransaction,
+    FeeTransaction,
+    RealisedPnlTransaction,
+    TransferTransaction,
+)
+
+__all__ = [
+    "TransactionsManager",
+    "Transaction",
+    "BlockchainTransaction",
+    "FeeTransaction",
+    "RealisedPnlTransaction",
+    "TransferTransaction",
+    "create_blockchain_transaction",
+    "create_realised_pnl_transaction",
+    "create_fee_transaction",
+    "create_transfer_transaction",
+]
diff --git a/octobot_trading/personal_data/transactions/transaction.pxd b/octobot_trading/personal_data/transactions/transaction.pxd
new file mode 100644
index 000000000..9fa4927c1
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/transaction.pxd
@@ -0,0 +1,29 @@
+# cython: language_level=3
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+cdef class Transaction:
+    cdef readonly str transaction_id
+    cdef readonly str exchange_name
+    cdef readonly str symbol
+    cdef readonly str currency
+
+    cdef readonly double creation_time
+
+    cdef readonly object logger
+    cdef readonly object transaction_type  # enums.TransactionType
+
+    cpdef void set_transaction_id(self, str new_id)
diff --git a/octobot_trading/personal_data/transactions/transaction.py b/octobot_trading/personal_data/transactions/transaction.py
new file mode 100644
index 000000000..6b09205b3
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/transaction.py
@@ -0,0 +1,43 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+import uuid
+
+import octobot_commons.logging as logging
+
+
+class Transaction:
+
+    def __init__(self, exchange_name, creation_time, transaction_type, currency, symbol=None, transaction_id=None):
+        self.logger = logging.get_logger(self.__class__.__name__)
+        self.transaction_id = transaction_id
+        self.transaction_type = transaction_type
+        self.creation_time = creation_time
+
+        self.exchange_name = exchange_name
+        self.symbol = symbol
+        self.currency = currency
+
+        # generate default transaction id
+        if self.transaction_id is None:
+            self.transaction_id = str(uuid.uuid4())
+
+    def set_transaction_id(self, new_id):
+        """
+        Shouldn't be called outside of TransactionsManager.update_transaction_id
+        to maintain TransactionsManager.transactions integrity
+        :param new_id: the new transaction id
+        """
+        self.transaction_id = new_id
diff --git a/octobot_trading/personal_data/transactions/transaction_factory.pxd b/octobot_trading/personal_data/transactions/transaction_factory.pxd
new file mode 100644
index 000000000..903d92e91
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/transaction_factory.pxd
@@ -0,0 +1,45 @@
+# cython: language_level=3
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+cimport octobot_trading.personal_data.transactions.types as transaction_types
+
+cpdef transaction_types.BlockchainTransaction create_blockchain_transaction(object exchange_manager,
+                                                                            str currency,
+                                                                            object blockchain_type,
+                                                                            str blockchain_transaction_id,
+                                                                            object blockchain_transaction_status=*,
+                                                                            str source_address=*,
+                                                                            str destination_address=*,
+                                                                            object quantity=*,
+                                                                            object transaction_fee=*,
+                                                                            bint is_deposit=*)
+cpdef transaction_types.RealisedPnlTransaction create_realised_pnl_transaction(object exchange_manager,
+                                                                               str currency,
+                                                                               str symbol,
+                                                                               object realised_pnl=*,
+                                                                               bint is_closed_pnl=*)
+cpdef transaction_types.FeeTransaction create_fee_transaction(object exchange_manager,
+                                                              str currency,
+                                                              str symbol,
+                                                              object quantity=*,
+                                                              str order_id=*,
+                                                              object funding_rate=*)
+cpdef transaction_types.TransferTransaction create_transfer_transaction(object exchange_manager,
+                                                                        str currency,
+                                                                        str symbol)
+
+cdef void _upsert_transaction_instance(object exchange_manager, object transaction)
diff --git a/octobot_trading/personal_data/transactions/transaction_factory.py b/octobot_trading/personal_data/transactions/transaction_factory.py
new file mode 100644
index 000000000..10dcfa2f0
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/transaction_factory.py
@@ -0,0 +1,87 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+import octobot_trading.enums as enums
+import octobot_trading.constants as constants
+import octobot_trading.personal_data.transactions.types as transaction_types
+
+
+def create_blockchain_transaction(exchange_manager, currency,
+                                  blockchain_type, blockchain_transaction_id,
+                                  blockchain_transaction_status=enums.BlockchainTransactionStatus.CREATED,
+                                  source_address=None, destination_address=None,
+                                  quantity=constants.ZERO, transaction_fee=constants.ZERO, is_deposit=False):
+    blockchain_transaction = transaction_types.BlockchainTransaction(
+        exchange_name=exchange_manager.exchange_name,
+        creation_time=exchange_manager.exchange.get_exchange_current_time(),
+        transaction_type=enums.TransactionType.BLOCKCHAIN_DEPOSIT
+        if is_deposit else enums.TransactionType.BLOCKCHAIN_WITHDRAWAL,
+        currency=currency,
+        blockchain_type=blockchain_type,
+        blockchain_transaction_id=blockchain_transaction_id,
+        blockchain_transaction_status=blockchain_transaction_status,
+        source_address=source_address,
+        destination_address=destination_address,
+        quantity=quantity,
+        transaction_fee=transaction_fee
+    )
+    _upsert_transaction_instance(exchange_manager, blockchain_transaction)
+    return blockchain_transaction
+
+
+def create_realised_pnl_transaction(exchange_manager, currency, symbol,
+                                    realised_pnl=constants.ZERO,
+                                    is_closed_pnl=False):
+    realised_pnl_transaction = transaction_types.RealisedPnlTransaction(
+        exchange_name=exchange_manager.exchange_name,
+        creation_time=exchange_manager.exchange.get_exchange_current_time(),
+        transaction_type=enums.TransactionType.CLOSE_REALISED_PNL
+        if is_closed_pnl else enums.TransactionType.REALISED_PNL,
+        currency=currency,
+        symbol=symbol,
+        realised_pnl=realised_pnl)
+    _upsert_transaction_instance(exchange_manager, realised_pnl_transaction)
+    return realised_pnl_transaction
+
+
+def create_fee_transaction(exchange_manager, currency, symbol,
+                           quantity=constants.ZERO,
+                           order_id=None,
+                           funding_rate=constants.ZERO):
+    fee_transaction = transaction_types.FeeTransaction(
+        exchange_name=exchange_manager.exchange_name,
+        creation_time=exchange_manager.exchange.get_exchange_current_time(),
+        transaction_type=enums.TransactionType.TRADING_FEE if order_id else enums.TransactionType.FUNDING_FEE,
+        currency=currency,
+        symbol=symbol,
+        quantity=quantity,
+        order_id=order_id,
+        funding_rate=funding_rate)
+    _upsert_transaction_instance(exchange_manager, fee_transaction)
+    return fee_transaction
+
+
+def create_transfer_transaction(exchange_manager, currency, symbol):
+    transfer_transaction = transaction_types.TransferTransaction(
+        exchange_name=exchange_manager.exchange_name,
+        creation_time=exchange_manager.exchange.get_exchange_current_time(),
+        currency=currency,
+        symbol=symbol)
+    _upsert_transaction_instance(exchange_manager, transfer_transaction)
+    return transfer_transaction
+
+
+def _upsert_transaction_instance(exchange_manager, transaction):
+    exchange_manager.exchange_personal_data.transactions_manager.upsert_transaction_instance(transaction)
diff --git a/octobot_trading/personal_data/transactions/transactions_manager.pxd b/octobot_trading/personal_data/transactions/transactions_manager.pxd
new file mode 100644
index 000000000..47ae751cd
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/transactions_manager.pxd
@@ -0,0 +1,32 @@
+# cython: language_level=3
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+cimport octobot_trading.util as util
+
+
+cdef class TransactionsManager(util.Initializable):
+    cdef object logger
+
+    cdef public object transactions
+
+    cdef void _check_transactions_size(self)
+    cdef void _reset_transactions(self)
+    cdef void _remove_oldest_transactions(self, int nb_to_remove)
+
+    cpdef object get_transaction(self, str transaction_id)
+    cpdef object update_transaction_id(self, str transaction_id, str new_transaction_id, bint replace_if_exists=*)  # needs object to forward exceptions
+    cpdef object upsert_transaction_instance(self, object transaction, bint replace_if_exists=*)  # needs object to forward exceptions
+    cpdef void clear(self)
diff --git a/octobot_trading/personal_data/transactions/transactions_manager.py b/octobot_trading/personal_data/transactions/transactions_manager.py
new file mode 100644
index 000000000..8b899d6e0
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/transactions_manager.py
@@ -0,0 +1,90 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+import collections
+
+import octobot_commons.logging as logging
+
+import octobot_trading.errors as errors
+import octobot_trading.util as util
+
+
+class TransactionsManager(util.Initializable):
+    MAX_TRANSACTIONS_COUNT = 100000
+
+    def __init__(self):
+        super().__init__()
+        self.logger = logging.get_logger(self.__class__.__name__)
+        self.transactions = collections.OrderedDict()
+
+    async def initialize_impl(self):
+        self._reset_transactions()
+
+    def upsert_transaction_instance(self, transaction, replace_if_exists=False):
+        """
+        Add the transaction instance to self.transactions by its transaction_id
+        Can raise DuplicateTransactionIdError if a transaction already exists
+        with the same transaction_id (when not replacing it with :replace_if_exists:)
+        :param transaction:
+        :param replace_if_exists: When True, replaces the transaction if a transaction has the same transaction_id
+        """
+        if transaction.transaction_id not in self.transactions or replace_if_exists:
+            self.transactions[transaction.transaction_id] = transaction
+            self._check_transactions_size()
+        else:
+            raise errors.DuplicateTransactionIdError(
+                f"Transaction with id '{transaction.transaction_id}' already exists")
+
+    def get_transaction(self, transaction_id):
+        """
+        Return the transaction instance with :transaction_id: as transaction_id
+        Can raise KeyError when transaction_id doesn't exist
+        :param transaction_id: the transaction id
+        :return: the transaction with the id :transaction_id:
+        """
+        return self.transactions[transaction_id]
+
+    def update_transaction_id(self, transaction_id, new_transaction_id, replace_if_exists=False):
+        """
+        Update a transaction by id
+        Can raise KeyError when initial transaction doesn't exist
+        Can raise DuplicateTransactionIdError when the :new_transaction_id: is already used
+        :param transaction_id: the transaction id to update
+        :param new_transaction_id: the transaction id to set
+        :param replace_if_exists: when True, replace the transaction with :new_transaction_id: if exist
+        """
+        transaction = self.get_transaction(transaction_id)
+        transaction.set_transaction_id(new_transaction_id)
+        try:
+            self.upsert_transaction_instance(transaction, replace_if_exists=replace_if_exists)
+            self.transactions.pop(transaction_id)
+        except errors.DuplicateTransactionIdError as e:
+            transaction.set_transaction_id(transaction_id)
+            raise errors.DuplicateTransactionIdError from e
+
+    # private
+    def _check_transactions_size(self):
+        if len(self.transactions) > self.MAX_TRANSACTIONS_COUNT:
+            self._remove_oldest_transactions(int(self.MAX_TRANSACTIONS_COUNT / 10))
+
+    def _reset_transactions(self):
+        self.transactions = collections.OrderedDict()
+
+    def _remove_oldest_transactions(self, nb_to_remove):
+        for _ in range(nb_to_remove):
+            self.transactions.popitem(last=False)
+
+    def clear(self):
+        self._reset_transactions()
diff --git a/octobot_trading/personal_data/transactions/types/__init__.pxd b/octobot_trading/personal_data/transactions/types/__init__.pxd
new file mode 100644
index 000000000..7a5673a69
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/types/__init__.pxd
@@ -0,0 +1,40 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+from octobot_trading.personal_data.transactions.types cimport blockchain_transaction
+from octobot_trading.personal_data.transactions.types cimport fee_transaction
+from octobot_trading.personal_data.transactions.types cimport realised_pnl_transaction
+from octobot_trading.personal_data.transactions.types cimport transfer_transaction
+
+from octobot_trading.personal_data.transactions.types.blockchain_transaction cimport (
+    BlockchainTransaction,
+)
+from octobot_trading.personal_data.transactions.types.fee_transaction cimport (
+    FeeTransaction,
+)
+from octobot_trading.personal_data.transactions.types.realised_pnl_transaction cimport (
+    RealisedPnlTransaction,
+)
+from octobot_trading.personal_data.transactions.types.transfer_transaction cimport (
+    TransferTransaction,
+)
+
+__all__ = [
+    "BlockchainTransaction",
+    "FeeTransaction",
+    "RealisedPnlTransaction",
+    "TransferTransaction",
+]
diff --git a/octobot_trading/personal_data/transactions/types/__init__.py b/octobot_trading/personal_data/transactions/types/__init__.py
new file mode 100644
index 000000000..b854d471d
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/types/__init__.py
@@ -0,0 +1,40 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+from octobot_trading.personal_data.transactions.types import blockchain_transaction
+from octobot_trading.personal_data.transactions.types import fee_transaction
+from octobot_trading.personal_data.transactions.types import realised_pnl_transaction
+from octobot_trading.personal_data.transactions.types import transfer_transaction
+
+from octobot_trading.personal_data.transactions.types.blockchain_transaction import (
+    BlockchainTransaction,
+)
+from octobot_trading.personal_data.transactions.types.fee_transaction import (
+    FeeTransaction,
+)
+from octobot_trading.personal_data.transactions.types.realised_pnl_transaction import (
+    RealisedPnlTransaction,
+)
+from octobot_trading.personal_data.transactions.types.transfer_transaction import (
+    TransferTransaction,
+)
+
+__all__ = [
+    "BlockchainTransaction",
+    "FeeTransaction",
+    "RealisedPnlTransaction",
+    "TransferTransaction",
+]
diff --git a/octobot_trading/personal_data/transactions/types/blockchain_transaction.pxd b/octobot_trading/personal_data/transactions/types/blockchain_transaction.pxd
new file mode 100644
index 000000000..721751624
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/types/blockchain_transaction.pxd
@@ -0,0 +1,33 @@
+# cython: language_level=3
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+cimport octobot_trading.personal_data.transactions.transaction as transaction
+
+cdef class BlockchainTransaction(transaction.Transaction):
+    cdef readonly str source_address
+    cdef readonly str destination_address
+    cdef readonly str blockchain_transaction_id
+
+    cdef readonly object blockchain_type
+    cdef readonly object blockchain_transaction_status
+    cdef readonly object quantity
+    cdef readonly object transaction_fee
+
+    cpdef bint is_deposit(self)
+    cpdef bint is_withdrawal(self)
+    cpdef bint is_pending(self)
+    cpdef bint is_validated(self)
diff --git a/octobot_trading/personal_data/transactions/types/blockchain_transaction.py b/octobot_trading/personal_data/transactions/types/blockchain_transaction.py
new file mode 100644
index 000000000..11bdfec8a
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/types/blockchain_transaction.py
@@ -0,0 +1,51 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+import uuid
+
+import octobot_trading.constants as constants
+import octobot_trading.enums as enums
+import octobot_trading.personal_data.transactions.transaction as transaction
+
+
+class BlockchainTransaction(transaction.Transaction):
+    def __init__(self, exchange_name, creation_time, transaction_type, currency, blockchain_type,
+                 blockchain_transaction_id,
+                 blockchain_transaction_status=enums.BlockchainTransactionStatus.CREATED,
+                 source_address=None,
+                 destination_address=None,
+                 quantity=constants.ZERO,
+                 transaction_fee=constants.ZERO):
+        self.source_address = source_address
+        self.destination_address = destination_address
+        self.blockchain_transaction_id = blockchain_transaction_id
+        self.blockchain_type = blockchain_type
+        self.blockchain_transaction_status = blockchain_transaction_status
+        self.quantity = quantity
+        self.transaction_fee = transaction_fee
+        super().__init__(exchange_name, creation_time, transaction_type, currency=currency)
+        self.transaction_id = self.blockchain_transaction_id if self.blockchain_transaction_id else str(uuid.uuid4())
+
+    def is_deposit(self):
+        return self.transaction_type is enums.TransactionType.BLOCKCHAIN_DEPOSIT
+
+    def is_withdrawal(self):
+        return self.transaction_type is enums.TransactionType.BLOCKCHAIN_WITHDRAWAL
+
+    def is_pending(self):
+        return self.blockchain_transaction_status is enums.BlockchainTransactionStatus.CONFIRMING
+
+    def is_validated(self):
+        return self.blockchain_transaction_status is enums.BlockchainTransactionStatus.SUCCESS
diff --git a/octobot_trading/personal_data/transactions/types/fee_transaction.pxd b/octobot_trading/personal_data/transactions/types/fee_transaction.pxd
new file mode 100644
index 000000000..32d44a5ae
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/types/fee_transaction.pxd
@@ -0,0 +1,27 @@
+# cython: language_level=3
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+cimport octobot_trading.personal_data.transactions.transaction as transaction
+
+cdef class FeeTransaction(transaction.Transaction):
+    cdef readonly object quantity
+    cdef readonly object funding_rate
+
+    cdef readonly str order_id
+
+    cpdef bint is_funding_fee(self)
+    cpdef bint is_trading_fee(self)
diff --git a/octobot_trading/personal_data/transactions/types/fee_transaction.py b/octobot_trading/personal_data/transactions/types/fee_transaction.py
new file mode 100644
index 000000000..29de6ff24
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/types/fee_transaction.py
@@ -0,0 +1,40 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+import uuid
+
+import octobot_trading.constants as constants
+import octobot_trading.enums as enums
+import octobot_trading.personal_data.transactions.transaction as transaction
+
+
+class FeeTransaction(transaction.Transaction):
+    def __init__(self, exchange_name, creation_time, transaction_type, currency, symbol, quantity,
+                 order_id=None,
+                 funding_rate=constants.ZERO):
+        self.quantity = quantity
+        self.order_id = order_id
+        self.funding_rate = funding_rate
+        super().__init__(exchange_name, creation_time, transaction_type, currency, symbol=symbol)
+        self.transaction_id = f"{self.exchange_name}" \
+                              f"-{self.order_id if self.order_id else str(uuid.uuid4())[:4]}" \
+                              f"-{self.symbol}" \
+                              f"-{str(self.creation_time)}"
+
+    def is_funding_fee(self):
+        return self.transaction_type is enums.TransactionType.FUNDING_FEE
+
+    def is_trading_fee(self):
+        return self.transaction_type is enums.TransactionType.TRADING_FEE
diff --git a/octobot_trading/personal_data/transactions/types/realised_pnl_transaction.pxd b/octobot_trading/personal_data/transactions/types/realised_pnl_transaction.pxd
new file mode 100644
index 000000000..1b34521c5
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/types/realised_pnl_transaction.pxd
@@ -0,0 +1,23 @@
+# cython: language_level=3
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+cimport octobot_trading.personal_data.transactions.transaction as transaction
+
+cdef class RealisedPnlTransaction(transaction.Transaction):
+    cdef readonly object realised_pnl
+
+    cpdef bint is_closed_pnl(self)
diff --git a/octobot_trading/personal_data/transactions/types/realised_pnl_transaction.py b/octobot_trading/personal_data/transactions/types/realised_pnl_transaction.py
new file mode 100644
index 000000000..a4e77fbcd
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/types/realised_pnl_transaction.py
@@ -0,0 +1,33 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+import uuid
+
+import octobot_trading.enums as enums
+import octobot_trading.personal_data.transactions.transaction as transaction
+
+
+class RealisedPnlTransaction(transaction.Transaction):
+    def __init__(self, exchange_name, creation_time, transaction_type, currency, symbol, realised_pnl):
+        super().__init__(exchange_name, creation_time, transaction_type, currency, symbol=symbol)
+        self.realised_pnl = realised_pnl
+        self.transaction_id = f"{self.exchange_name}" \
+                              f"-{str(uuid.uuid4())[:4]}" \
+                              f"-{self.symbol}" \
+                              f"-{str(self.creation_time)}" \
+                              f"{'-closed' if self.is_closed_pnl() else ''}"
+
+    def is_closed_pnl(self):
+        return self.transaction_type is enums.TransactionType.CLOSE_REALISED_PNL
diff --git a/octobot_trading/personal_data/transactions/types/transfer_transaction.pxd b/octobot_trading/personal_data/transactions/types/transfer_transaction.pxd
new file mode 100644
index 000000000..217480b86
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/types/transfer_transaction.pxd
@@ -0,0 +1,21 @@
+# cython: language_level=3
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+cimport octobot_trading.personal_data.transactions.transaction as transaction
+
+cdef class TransferTransaction(transaction.Transaction):
+    pass
diff --git a/octobot_trading/personal_data/transactions/types/transfer_transaction.py b/octobot_trading/personal_data/transactions/types/transfer_transaction.py
new file mode 100644
index 000000000..287b4d482
--- /dev/null
+++ b/octobot_trading/personal_data/transactions/types/transfer_transaction.py
@@ -0,0 +1,26 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+
+import octobot_trading.enums as enums
+import octobot_trading.personal_data.transactions.transaction as transaction
+
+
+class TransferTransaction(transaction.Transaction):
+    def __init__(self, exchange_name, creation_time, currency, symbol):
+        super().__init__(exchange_name, creation_time,
+                         transaction_type=enums.TransactionType.TRANSFER,
+                         currency=currency,
+                         symbol=symbol)
diff --git a/setup.py b/setup.py
index 0de68cb16..7dd9c4ab5 100644
--- a/setup.py
+++ b/setup.py
@@ -146,6 +146,13 @@ def build_ext(*args, **kwargs):
     "octobot_trading.personal_data.positions.states.liquidate_position_state",
     "octobot_trading.personal_data.positions.states.open_position_state",
     "octobot_trading.personal_data.positions.states.position_state_factory",
+    "octobot_trading.personal_data.transactions.transaction",
+    "octobot_trading.personal_data.transactions.transaction_factory",
+    "octobot_trading.personal_data.transactions.transactions_manager",
+    "octobot_trading.personal_data.transactions.types.blockchain_transaction",
+    "octobot_trading.personal_data.transactions.types.fee_transaction",
+    "octobot_trading.personal_data.transactions.types.realised_pnl_transaction",
+    "octobot_trading.personal_data.transactions.types.transfer_transaction",
     "octobot_trading.modes.modes_factory",
     "octobot_trading.modes.channel.abstract_mode_producer",
     "octobot_trading.modes.channel.mode",


Test patch:
diff --git a/tests/personal_data/positions/test_position.py b/tests/personal_data/positions/test_position.py
index f115338d8..563c48ba1 100644
--- a/tests/personal_data/positions/test_position.py
+++ b/tests/personal_data/positions/test_position.py
@@ -1068,10 +1068,12 @@ async def test_is_order_increasing_size_hedge(
 async def test__update_realized_pnl_from_size_update_long(future_trader_simulator_with_default_linear):
     config, exchange_manager_inst, trader_inst, default_contract = future_trader_simulator_with_default_linear
 
+    tm = exchange_manager_inst.exchange_personal_data.transactions_manager
     symbol_contract = default_contract
     exchange_manager_inst.exchange.set_pair_future_contract(DEFAULT_FUTURE_SYMBOL, symbol_contract)
     symbol_contract.set_position_mode(is_one_way=True)
     position_inst = personal_data.LinearPosition(trader_inst, symbol_contract)
+    position_inst.update_from_raw({enums.ExchangeConstantsPositionColumns.SYMBOL.value: DEFAULT_FUTURE_SYMBOL})
     mark_price = constants.ONE
     position_size = decimal.Decimal(10)
     await position_inst.update(mark_price=mark_price, update_size=position_size)
@@ -1086,6 +1088,10 @@ async def test__update_realized_pnl_from_size_update_long(future_trader_simulato
     # = position_size / decimal.Decimal(2) * decimal.Decimal(0.2)
     assert position_inst.realised_pnl == decimal.Decimal('1.000000000000000055511151232')
 
+    assert len(tm.transactions) == 1
+    assert list(tm.transactions.values())[-1].symbol == DEFAULT_FUTURE_SYMBOL
+    assert list(tm.transactions.values())[-1].realised_pnl == decimal.Decimal('1.000000000000000055511151232')
+
     position_size = decimal.Decimal(8)
 
     # reduce size by 50% --> 0.5
@@ -1095,20 +1101,27 @@ async def test__update_realized_pnl_from_size_update_long(future_trader_simulato
     # = position_size / decimal.Decimal(4) * decimal.Decimal(0.5) + previous realised pnl
     assert position_inst.realised_pnl == decimal.Decimal('3.000000000000000055511151232')
 
+    assert len(tm.transactions) == 2
+    assert list(tm.transactions.values())[-1].realised_pnl == decimal.Decimal('3.000000000000000055511151232') - \
+           decimal.Decimal('1.000000000000000055511151232')
+
     position_size = decimal.Decimal(4)
 
     # Increase by 50% --> shouldn't increase realised pnl
     await position_inst.update(position_size * decimal.Decimal(0.5))
     assert position_inst.realised_pnl == decimal.Decimal('3.000000000000000055511151232')  # = previous realised pnl
+    assert len(tm.transactions) == 2
 
 
 async def test__update_realized_pnl_from_size_update_short(future_trader_simulator_with_default_linear):
     config, exchange_manager_inst, trader_inst, default_contract = future_trader_simulator_with_default_linear
 
+    tm = exchange_manager_inst.exchange_personal_data.transactions_manager
     symbol_contract = default_contract
     exchange_manager_inst.exchange.set_pair_future_contract(DEFAULT_FUTURE_SYMBOL, symbol_contract)
     symbol_contract.set_position_mode(is_one_way=True)
     position_inst = personal_data.LinearPosition(trader_inst, symbol_contract)
+    position_inst.update_from_raw({enums.ExchangeConstantsPositionColumns.SYMBOL.value: DEFAULT_FUTURE_SYMBOL})
     mark_price = constants.ONE
     position_size = decimal.Decimal(100)
     await position_inst.update(mark_price=mark_price, update_size=-position_size)
@@ -1121,6 +1134,10 @@ async def test__update_realized_pnl_from_size_update_short(future_trader_simulat
     assert position_inst.unrealised_pnl == decimal.Decimal('35.00000000000000055511151232')
     assert position_inst.realised_pnl == decimal.Decimal('14.99999999999999944488848768')
 
+    assert len(tm.transactions) == 1
+    assert list(tm.transactions.values())[-1].symbol == DEFAULT_FUTURE_SYMBOL
+    assert list(tm.transactions.values())[-1].realised_pnl == decimal.Decimal('14.99999999999999944488848768')
+
     position_size = decimal.Decimal(70)
 
     # reduce size by 40% --> 0.4
@@ -1128,8 +1145,13 @@ async def test__update_realized_pnl_from_size_update_short(future_trader_simulat
     assert position_inst.unrealised_pnl == decimal.Decimal('20.99999999999999977795539508')
     assert position_inst.realised_pnl == decimal.Decimal('29.00000000000000022204460492')
 
+    assert len(tm.transactions) == 2
+    assert list(tm.transactions.values())[-1].realised_pnl == decimal.Decimal('29.00000000000000022204460492') - \
+           decimal.Decimal('14.99999999999999944488848768')
+
     position_size = decimal.Decimal(42)
 
     # Increase by 50% --> shouldn't increase realised pnl
     await position_inst.update(-position_size * decimal.Decimal(0.5))
     assert position_inst.realised_pnl == decimal.Decimal('29.00000000000000022204460492')  # = previous realised pnl
+    assert len(tm.transactions) == 2
diff --git a/tests/personal_data/transactions/__init__.py b/tests/personal_data/transactions/__init__.py
new file mode 100644
index 000000000..adbc2745b
--- /dev/null
+++ b/tests/personal_data/transactions/__init__.py
@@ -0,0 +1,15 @@
+#  Drakkar-Software OctoBot-Trading
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
diff --git a/tests/personal_data/transactions/test_transaction_factory.py b/tests/personal_data/transactions/test_transaction_factory.py
new file mode 100644
index 000000000..88969f95a
--- /dev/null
+++ b/tests/personal_data/transactions/test_transaction_factory.py
@@ -0,0 +1,102 @@
+#  Drakkar-Software OctoBot
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+import decimal
+import uuid
+
+import time
+
+import pytest
+
+import octobot_trading.enums as enums
+import octobot_trading.constants as constants
+import octobot_trading.personal_data.transactions.transaction_factory as transaction_factory
+
+from tests.exchanges import backtesting_trader, backtesting_config, backtesting_exchange_manager, fake_backtesting
+from tests import event_loop
+
+# All test coroutines will be treated as marked.
+pytestmark = pytest.mark.asyncio
+
+TRANSACTION_SYMBOL = "BTC/USDT"
+TRANSACTION_CURRENCY = "BTC"
+
+
+async def test_create_blockchain_transaction(backtesting_trader):
+    _, exchange_manager, _ = backtesting_trader
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 0
+    t_id = str(uuid.uuid4())
+    transaction = transaction_factory.create_blockchain_transaction(exchange_manager,
+                                                                    is_deposit=True,
+                                                                    currency=TRANSACTION_CURRENCY,
+                                                                    blockchain_type="TEST",
+                                                                    blockchain_transaction_id=t_id,
+                                                                    blockchain_transaction_status=enums.BlockchainTransactionStatus.CONFIRMING,
+                                                                    source_address="0x123456789",
+                                                                    quantity=decimal.Decimal(50),
+                                                                    transaction_fee=decimal.Decimal(0.1))
+    assert transaction.currency == TRANSACTION_CURRENCY
+    assert transaction.creation_time <= time.time()
+    assert transaction.transaction_id == t_id
+    assert transaction.blockchain_transaction_id == t_id
+    assert transaction.blockchain_transaction_status is enums.BlockchainTransactionStatus.CONFIRMING
+    assert transaction.source_address == "0x123456789"
+    assert transaction.quantity == decimal.Decimal(50)
+    assert transaction.transaction_fee == decimal.Decimal(0.1)
+    assert transaction.is_deposit()
+    assert not transaction.is_withdrawal()
+    assert not transaction.is_validated()
+    assert transaction.is_pending()
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 1
+
+
+async def test_create_realised_pnl_transaction(backtesting_trader):
+    _, exchange_manager, _ = backtesting_trader
+    transaction = transaction_factory.create_realised_pnl_transaction(exchange_manager,
+                                                                      currency=TRANSACTION_CURRENCY,
+                                                                      symbol=TRANSACTION_SYMBOL,
+                                                                      realised_pnl=decimal.Decimal(0.1))
+    assert transaction.currency == TRANSACTION_CURRENCY
+    assert transaction.symbol == TRANSACTION_SYMBOL
+    assert transaction.realised_pnl == decimal.Decimal(0.1)
+    assert transaction.creation_time <= time.time()
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 1
+
+
+async def test_create_fee_transaction(backtesting_trader):
+    _, exchange_manager, _ = backtesting_trader
+    o_id = str(uuid.uuid4())
+    transaction = transaction_factory.create_fee_transaction(exchange_manager,
+                                                             currency=TRANSACTION_CURRENCY,
+                                                             symbol=TRANSACTION_SYMBOL,
+                                                             quantity=decimal.Decimal(10), order_id=o_id)
+    assert transaction.currency == TRANSACTION_CURRENCY
+    assert transaction.symbol == TRANSACTION_SYMBOL
+    assert transaction.creation_time <= time.time()
+    assert transaction.order_id == o_id
+    assert transaction.transaction_id
+    assert transaction.quantity == decimal.Decimal(10)
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 1
+
+
+async def test_create_transfer_transaction(backtesting_trader):
+    _, exchange_manager, _ = backtesting_trader
+    transaction = transaction_factory.create_transfer_transaction(exchange_manager,
+                                                                  currency=TRANSACTION_CURRENCY,
+                                                                  symbol=TRANSACTION_SYMBOL)
+    assert transaction.currency == TRANSACTION_CURRENCY
+    assert transaction.symbol == TRANSACTION_SYMBOL
+    assert transaction.creation_time <= time.time()
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 1
diff --git a/tests/personal_data/transactions/test_transactions_manager.py b/tests/personal_data/transactions/test_transactions_manager.py
new file mode 100644
index 000000000..b34d4e3ea
--- /dev/null
+++ b/tests/personal_data/transactions/test_transactions_manager.py
@@ -0,0 +1,204 @@
+#  Drakkar-Software OctoBot
+#  Copyright (c) Drakkar-Software, All rights reserved.
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 3.0 of the License, or (at your option) any later version.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library.
+import decimal
+import uuid
+
+import pytest
+
+import octobot_trading.enums as enums
+import octobot_trading.errors as errors
+import octobot_trading.constants as constants
+import octobot_trading.personal_data.transactions.types as transaction_types
+
+from tests.exchanges import backtesting_trader, backtesting_config, backtesting_exchange_manager, fake_backtesting
+from tests import event_loop
+
+# All test coroutines will be treated as marked.
+pytestmark = pytest.mark.asyncio
+
+TRANSACTION_SYMBOL = "BTC/USDT"
+TRANSACTION_CURRENCY = "BTC"
+
+
+async def test_initialized(backtesting_trader):
+    _, exchange_manager, _ = backtesting_trader
+    assert exchange_manager.exchange_personal_data.transactions_manager.is_initialized
+
+
+async def test_upsert_transaction_instance(backtesting_trader):
+    _, exchange_manager, _ = backtesting_trader
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 0
+    t_id = str(uuid.uuid4())
+    t_id_2 = str(uuid.uuid4())
+    transaction = transaction_types.RealisedPnlTransaction(
+        exchange_name=exchange_manager.exchange_name,
+        creation_time=exchange_manager.exchange.get_exchange_current_time(),
+        transaction_type=enums.TransactionType.REALISED_PNL,
+        currency=TRANSACTION_CURRENCY,
+        symbol=TRANSACTION_SYMBOL,
+        realised_pnl=constants.ZERO)
+    transaction.set_transaction_id(t_id)
+    transaction_2 = transaction_types.BlockchainTransaction(
+        exchange_name=exchange_manager.exchange_name,
+        creation_time=exchange_manager.exchange.get_exchange_current_time(),
+        currency=TRANSACTION_CURRENCY,
+        transaction_type=enums.TransactionType.BLOCKCHAIN_DEPOSIT,
+        blockchain_type="Test",
+        blockchain_transaction_id=t_id,
+        blockchain_transaction_status=enums.BlockchainTransactionStatus.CONFIRMING,
+        source_address="0x123456789",
+        destination_address=None,
+        quantity=decimal.Decimal(50),
+        transaction_fee=decimal.Decimal(0.1)
+    )
+    transaction_2.set_transaction_id(t_id)
+
+    # succeed to upsert unknown transaction
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 0
+    exchange_manager.exchange_personal_data.transactions_manager.upsert_transaction_instance(transaction)
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 1
+
+    # can't add transaction with the same transaction id without replacing it --> DuplicateTransactionIdError
+    with pytest.raises(errors.DuplicateTransactionIdError):
+        exchange_manager.exchange_personal_data.transactions_manager.upsert_transaction_instance(transaction_2)
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 1
+    assert list(exchange_manager.exchange_personal_data.transactions_manager.transactions.values())[-1] is transaction
+
+    # can replace a transaction with the same transaction id
+    exchange_manager.exchange_personal_data.transactions_manager.upsert_transaction_instance(transaction_2,
+                                                                                             replace_if_exists=True)
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 1
+
+    # succeed to upsert a transaction with a new id
+    transaction.set_transaction_id(t_id_2)
+    exchange_manager.exchange_personal_data.transactions_manager.upsert_transaction_instance(transaction)
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 2
+
+    # check transaction instances
+    assert list(exchange_manager.exchange_personal_data.transactions_manager.transactions.values())[0] is transaction_2
+    assert list(exchange_manager.exchange_personal_data.transactions_manager.transactions.values())[-1] is transaction
+
+
+async def test_get_transaction(backtesting_trader):
+    _, exchange_manager, _ = backtesting_trader
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 0
+    transaction = transaction_types.RealisedPnlTransaction(
+        exchange_name=exchange_manager.exchange_name,
+        creation_time=exchange_manager.exchange.get_exchange_current_time(),
+        transaction_type=enums.TransactionType.REALISED_PNL,
+        currency=TRANSACTION_CURRENCY,
+        symbol=TRANSACTION_SYMBOL,
+        realised_pnl=constants.ZERO)
+    transaction_2 = transaction_types.RealisedPnlTransaction(
+        exchange_name=exchange_manager.exchange_name,
+        creation_time=exchange_manager.exchange.get_exchange_current_time(),
+        transaction_type=enums.TransactionType.REALISED_PNL,
+        currency=TRANSACTION_CURRENCY,
+        symbol=TRANSACTION_SYMBOL,
+        realised_pnl=constants.ZERO)
+    exchange_manager.exchange_personal_data.transactions_manager.upsert_transaction_instance(transaction)
+
+    # succeed to return transaction instance
+    assert exchange_manager.exchange_personal_data.transactions_manager. \
+               get_transaction(transaction.transaction_id) is transaction
+
+    # can't get unknown transaction id
+    with pytest.raises(KeyError):
+        exchange_manager.exchange_personal_data.transactions_manager.get_transaction(str(uuid.uuid4()))
+
+    # can't get a transaction which has not been added
+    with pytest.raises(KeyError):
+        exchange_manager.exchange_personal_data.transactions_manager.get_transaction(transaction_2.transaction_id)
+
+    # succeed to return transaction instances
+    exchange_manager.exchange_personal_data.transactions_manager.upsert_transaction_instance(transaction_2)
+    assert exchange_manager.exchange_personal_data.transactions_manager. \
+               get_transaction(transaction.transaction_id) is transaction
+    assert exchange_manager.exchange_personal_data.transactions_manager. \
+               get_transaction(transaction_2.transaction_id) is transaction_2
+
+
+async def test_update_transaction_id(backtesting_trader):
+    _, exchange_manager, _ = backtesting_trader
+    t_id = str(uuid.uuid4())
+    t_id_2 = str(uuid.uuid4())
+    t_id_3 = str(uuid.uuid4())
+    t_id_4 = str(uuid.uuid4())
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 0
+    transaction = transaction_types.RealisedPnlTransaction(
+        exchange_name=exchange_manager.exchange_name,
+        creation_time=exchange_manager.exchange.get_exchange_current_time(),
+        transaction_type=enums.TransactionType.REALISED_PNL,
+        currency=TRANSACTION_CURRENCY,
+        symbol=TRANSACTION_SYMBOL,
+        realised_pnl=constants.ZERO)
+    transaction.set_transaction_id(t_id)
+    transaction_2 = transaction_types.RealisedPnlTransaction(
+        exchange_name=exchange_manager.exchange_name,
+        creation_time=exchange_manager.exchange.get_exchange_current_time(),
+        transaction_type=enums.TransactionType.REALISED_PNL,
+        currency=TRANSACTION_CURRENCY,
+        symbol=TRANSACTION_SYMBOL,
+        realised_pnl=constants.ZERO)
+    transaction_2.set_transaction_id(t_id_2)
+
+    # Add transaction instances to TransactionsManager.transactions
+    exchange_manager.exchange_personal_data.transactions_manager.upsert_transaction_instance(transaction)
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 1
+    exchange_manager.exchange_personal_data.transactions_manager.upsert_transaction_instance(transaction_2)
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 2
+
+    # Update the first transaction id and see if the previous entry was properly deleted
+    exchange_manager.exchange_personal_data.transactions_manager.update_transaction_id(t_id, t_id_3)
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 2
+    assert transaction.transaction_id == t_id_3
+    assert list(exchange_manager.exchange_personal_data.transactions_manager.transactions.values())[-1] is transaction
+    assert list(exchange_manager.exchange_personal_data.transactions_manager.transactions.values())[-1].transaction_id == t_id_3
+
+    # ensure that the first transaction_id doesn't exist anymore
+    with pytest.raises(KeyError):
+        exchange_manager.exchange_personal_data.transactions_manager.get_transaction(t_id)
+
+    # try to update with an already used transaction id without replacing it
+    with pytest.raises(errors.DuplicateTransactionIdError):
+        exchange_manager.exchange_personal_data.transactions_manager.update_transaction_id(t_id_2, t_id_3)
+
+    # check if the transaction keeps its old transaction id
+    assert transaction_2.transaction_id == t_id_2
+
+    # check if transaction_2 was not dropped from the list
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 2
+    exchange_manager.exchange_personal_data.transactions_manager.get_transaction(t_id_2)
+
+    # try to update with an already used transaction id with replacement
+    exchange_manager.exchange_personal_data.transactions_manager.update_transaction_id(t_id_2, t_id_3,
+                                                                                       replace_if_exists=True)
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 1  # transaction replaced by transaction_2
+    assert transaction_2.transaction_id == t_id_3
+    assert list(exchange_manager.exchange_personal_data.transactions_manager.transactions.values())[-1] is transaction_2
+    assert list(exchange_manager.exchange_personal_data.transactions_manager.transactions.values())[
+               -1].transaction_id == t_id_3
+
+    # try to update an unknown transaction
+    with pytest.raises(KeyError):
+        exchange_manager.exchange_personal_data.transactions_manager.update_transaction_id(str(uuid.uuid4()),
+                                                                                           str(uuid.uuid4()))
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 1
+
+    # restore transaction in list
+    transaction.set_transaction_id(t_id_4)
+    exchange_manager.exchange_personal_data.transactions_manager.upsert_transaction_instance(transaction)
+    assert len(exchange_manager.exchange_personal_data.transactions_manager.transactions) == 2