• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 6181253459

08 Sep 2023 06:04AM UTC coverage: 94.614% (+0.06%) from 94.556%
6181253459

push

github-actions

web-flow
Merge pull request #9159 from stash86/fix-adjust

remove old codes when we only can do partial entries

2 of 2 new or added lines in 1 file covered. (100.0%)

19114 of 20202 relevant lines covered (94.61%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.71
/freqtrade/persistence/migrations.py
1
import logging
1✔
2
from typing import List, Optional
1✔
3

4
from sqlalchemy import inspect, select, text, tuple_, update
1✔
5

6
from freqtrade.exceptions import OperationalException
1✔
7
from freqtrade.persistence.trade_model import Order, Trade
1✔
8

9

10
logger = logging.getLogger(__name__)
1✔
11

12

13
def get_table_names_for_table(inspector, tabletype) -> List[str]:
1✔
14
    return [t for t in inspector.get_table_names() if t.startswith(tabletype)]
1✔
15

16

17
def has_column(columns: List, searchname: str) -> bool:
1✔
18
    return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
1✔
19

20

21
def get_column_def(columns: List, column: str, default: str) -> str:
1✔
22
    return default if not has_column(columns, column) else column
1✔
23

24

25
def get_backup_name(tabs: List[str], backup_prefix: str):
1✔
26
    table_back_name = backup_prefix
1✔
27
    for i, table_back_name in enumerate(tabs):
1✔
28
        table_back_name = f'{backup_prefix}{i}'
1✔
29
        logger.debug(f'trying {table_back_name}')
1✔
30

31
    return table_back_name
1✔
32

33

34
def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str):
1✔
35
    order_id: Optional[int] = None
1✔
36
    trade_id: Optional[int] = None
1✔
37

38
    if engine.name == 'postgresql':
1✔
39
        with engine.begin() as connection:
1✔
40
            trade_id = connection.execute(text("select nextval('trades_id_seq')")).fetchone()[0]
1✔
41
            order_id = connection.execute(text("select nextval('orders_id_seq')")).fetchone()[0]
1✔
42
        with engine.begin() as connection:
1✔
43
            connection.execute(text(
1✔
44
                f"ALTER SEQUENCE orders_id_seq rename to {order_back_name}_id_seq_bak"))
45
            connection.execute(text(
1✔
46
                f"ALTER SEQUENCE trades_id_seq rename to {trade_back_name}_id_seq_bak"))
47
    return order_id, trade_id
1✔
48

49

50
def set_sequence_ids(engine, order_id, trade_id, pairlock_id=None):
1✔
51

52
    if engine.name == 'postgresql':
1✔
53
        with engine.begin() as connection:
1✔
54
            if order_id:
1✔
55
                connection.execute(text(f"ALTER SEQUENCE orders_id_seq RESTART WITH {order_id}"))
1✔
56
            if trade_id:
1✔
57
                connection.execute(text(f"ALTER SEQUENCE trades_id_seq RESTART WITH {trade_id}"))
1✔
58
            if pairlock_id:
1✔
59
                connection.execute(
1✔
60
                    text(f"ALTER SEQUENCE pairlocks_id_seq RESTART WITH {pairlock_id}"))
61

62

63
def drop_index_on_table(engine, inspector, table_bak_name):
1✔
64
    with engine.begin() as connection:
1✔
65
        # drop indexes on backup table in new session
66
        for index in inspector.get_indexes(table_bak_name):
1✔
67
            if engine.name == 'mysql':
1✔
68
                connection.execute(text(f"drop index {index['name']} on {table_bak_name}"))
×
69
            else:
70
                connection.execute(text(f"drop index {index['name']}"))
1✔
71

72

73
def migrate_trades_and_orders_table(
1✔
74
        decl_base, inspector, engine,
75
        trade_back_name: str, cols: List,
76
        order_back_name: str, cols_order: List):
77
    base_currency = get_column_def(cols, 'base_currency', 'null')
1✔
78
    stake_currency = get_column_def(cols, 'stake_currency', 'null')
1✔
79
    fee_open = get_column_def(cols, 'fee_open', 'fee')
1✔
80
    fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
1✔
81
    fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
1✔
82
    fee_close = get_column_def(cols, 'fee_close', 'fee')
1✔
83
    fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null')
1✔
84
    fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null')
1✔
85
    open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
1✔
86
    close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
1✔
87
    stop_loss = get_column_def(cols, 'stop_loss', '0.0')
1✔
88
    stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null')
1✔
89
    initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
1✔
90
    initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null')
1✔
91
    is_stop_loss_trailing = get_column_def(
1✔
92
        cols, 'is_stop_loss_trailing',
93
        f'coalesce({stop_loss_pct}, 0.0) <> coalesce({initial_stop_loss_pct}, 0.0)')
94
    stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
1✔
95
    stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
1✔
96
    max_rate = get_column_def(cols, 'max_rate', '0.0')
1✔
97
    min_rate = get_column_def(cols, 'min_rate', 'null')
1✔
98
    exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null'))
1✔
99
    strategy = get_column_def(cols, 'strategy', 'null')
1✔
100
    enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null'))
1✔
101
    realized_profit = get_column_def(cols, 'realized_profit', '0.0')
1✔
102

103
    trading_mode = get_column_def(cols, 'trading_mode', 'null')
1✔
104

105
    # Leverage Properties
106
    leverage = get_column_def(cols, 'leverage', '1.0')
1✔
107
    liquidation_price = get_column_def(cols, 'liquidation_price',
1✔
108
                                       get_column_def(cols, 'isolated_liq', 'null'))
109
    # sqlite does not support literals for booleans
110
    if engine.name == 'postgresql':
1✔
111
        is_short = get_column_def(cols, 'is_short', 'false')
×
112
    else:
113
        is_short = get_column_def(cols, 'is_short', '0')
1✔
114

115
    # Futures Properties
116
    interest_rate = get_column_def(cols, 'interest_rate', '0.0')
1✔
117
    funding_fees = get_column_def(cols, 'funding_fees', '0.0')
1✔
118
    max_stake_amount = get_column_def(cols, 'max_stake_amount', 'stake_amount')
1✔
119

120
    # If ticker-interval existed use that, else null.
121
    if has_column(cols, 'ticker_interval'):
1✔
122
        timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
1✔
123
    else:
124
        timeframe = get_column_def(cols, 'timeframe', 'null')
1✔
125

126
    open_trade_value = get_column_def(cols, 'open_trade_value',
1✔
127
                                      f'amount * open_rate * (1 + {fee_open})')
128
    close_profit_abs = get_column_def(
1✔
129
        cols, 'close_profit_abs',
130
        f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}")
131
    exit_order_status = get_column_def(cols, 'exit_order_status',
1✔
132
                                       get_column_def(cols, 'sell_order_status', 'null'))
133
    amount_requested = get_column_def(cols, 'amount_requested', 'amount')
1✔
134

135
    amount_precision = get_column_def(cols, 'amount_precision', 'null')
1✔
136
    price_precision = get_column_def(cols, 'price_precision', 'null')
1✔
137
    precision_mode = get_column_def(cols, 'precision_mode', 'null')
1✔
138
    contract_size = get_column_def(cols, 'contract_size', 'null')
1✔
139

140
    # Schema migration necessary
141
    with engine.begin() as connection:
1✔
142
        connection.execute(text(f"alter table trades rename to {trade_back_name}"))
1✔
143

144
    drop_index_on_table(engine, inspector, trade_back_name)
1✔
145

146
    order_id, trade_id = get_last_sequence_ids(engine, trade_back_name, order_back_name)
1✔
147

148
    drop_orders_table(engine, order_back_name)
1✔
149

150
    # let SQLAlchemy create the schema as required
151
    decl_base.metadata.create_all(engine)
1✔
152

153
    # Copy data back - following the correct schema
154
    with engine.begin() as connection:
1✔
155
        connection.execute(text(f"""insert into trades
1✔
156
            (id, exchange, pair, base_currency, stake_currency, is_open,
157
            fee_open, fee_open_cost, fee_open_currency,
158
            fee_close, fee_close_cost, fee_close_currency, open_rate,
159
            open_rate_requested, close_rate, close_rate_requested, close_profit,
160
            stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
161
            stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
162
            is_stop_loss_trailing, stoploss_order_id, stoploss_last_update,
163
            max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag,
164
            timeframe, open_trade_value, close_profit_abs,
165
            trading_mode, leverage, liquidation_price, is_short,
166
            interest_rate, funding_fees, realized_profit,
167
            amount_precision, price_precision, precision_mode, contract_size,
168
            max_stake_amount
169
            )
170
        select id, lower(exchange), pair, {base_currency} base_currency,
171
            {stake_currency} stake_currency,
172
            is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
173
            {fee_open_currency} fee_open_currency, {fee_close} fee_close,
174
            {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
175
            open_rate, {open_rate_requested} open_rate_requested, close_rate,
176
            {close_rate_requested} close_rate_requested, close_profit,
177
            stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id,
178
            {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct,
179
            {initial_stop_loss} initial_stop_loss,
180
            {initial_stop_loss_pct} initial_stop_loss_pct,
181
            {is_stop_loss_trailing} is_stop_loss_trailing,
182
            {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
183
            {max_rate} max_rate, {min_rate} min_rate,
184
            case when {exit_reason} = 'sell_signal' then 'exit_signal'
185
                 when {exit_reason} = 'custom_sell' then 'custom_exit'
186
                 when {exit_reason} = 'force_sell' then 'force_exit'
187
                 when {exit_reason} = 'emergency_sell' then 'emergency_exit'
188
                 else {exit_reason}
189
            end exit_reason,
190
            {exit_order_status} exit_order_status,
191
            {strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe,
192
            {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
193
            {trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price,
194
            {is_short} is_short, {interest_rate} interest_rate,
195
            {funding_fees} funding_fees, {realized_profit} realized_profit,
196
            {amount_precision} amount_precision, {price_precision} price_precision,
197
            {precision_mode} precision_mode, {contract_size} contract_size,
198
            {max_stake_amount} max_stake_amount
199
            from {trade_back_name}
200
            """))
201

202
    migrate_orders_table(engine, order_back_name, cols_order)
1✔
203
    set_sequence_ids(engine, order_id, trade_id)
1✔
204

205

206
def drop_orders_table(engine, table_back_name: str):
1✔
207
    # Drop and recreate orders table as backup
208
    # This drops foreign keys, too.
209

210
    with engine.begin() as connection:
1✔
211
        connection.execute(text(f"create table {table_back_name} as select * from orders"))
1✔
212
        connection.execute(text("drop table orders"))
1✔
213

214

215
def migrate_orders_table(engine, table_back_name: str, cols_order: List):
1✔
216

217
    ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
1✔
218
    average = get_column_def(cols_order, 'average', 'null')
1✔
219
    stop_price = get_column_def(cols_order, 'stop_price', 'null')
1✔
220
    funding_fee = get_column_def(cols_order, 'funding_fee', '0.0')
1✔
221
    ft_amount = get_column_def(cols_order, 'ft_amount', 'coalesce(amount, 0.0)')
1✔
222
    ft_price = get_column_def(cols_order, 'ft_price', 'coalesce(price, 0.0)')
1✔
223

224
    # sqlite does not support literals for booleans
225
    with engine.begin() as connection:
1✔
226
        connection.execute(text(f"""
1✔
227
            insert into orders (id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
228
            status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
229
            stop_price, order_date, order_filled_date, order_update_date, ft_fee_base, funding_fee,
230
            ft_amount, ft_price
231
            )
232
            select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
233
            status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
234
            cost, {stop_price} stop_price, order_date, order_filled_date,
235
            order_update_date, {ft_fee_base} ft_fee_base, {funding_fee} funding_fee,
236
            {ft_amount} ft_amount, {ft_price} ft_price
237
            from {table_back_name}
238
            """))
239

240

241
def migrate_pairlocks_table(
1✔
242
        decl_base, inspector, engine,
243
        pairlock_back_name: str, cols: List):
244

245
    # Schema migration necessary
246
    with engine.begin() as connection:
1✔
247
        connection.execute(text(f"alter table pairlocks rename to {pairlock_back_name}"))
1✔
248

249
    drop_index_on_table(engine, inspector, pairlock_back_name)
1✔
250

251
    side = get_column_def(cols, 'side', "'*'")
1✔
252

253
    # let SQLAlchemy create the schema as required
254
    decl_base.metadata.create_all(engine)
1✔
255
    # Copy data back - following the correct schema
256
    with engine.begin() as connection:
1✔
257
        connection.execute(text(f"""insert into pairlocks
1✔
258
        (id, pair, side, reason, lock_time,
259
         lock_end_time, active)
260
        select id, pair, {side} side, reason, lock_time,
261
         lock_end_time, active
262
        from {pairlock_back_name}
263
        """))
264

265

266
def set_sqlite_to_wal(engine):
1✔
267
    if engine.name == 'sqlite' and str(engine.url) != 'sqlite://':
1✔
268
        # Set Mode to
269
        with engine.begin() as connection:
1✔
270
            connection.execute(text("PRAGMA journal_mode=wal"))
1✔
271

272

273
def fix_old_dry_orders(engine):
1✔
274
    with engine.begin() as connection:
1✔
275
        stmt = update(Order).where(
1✔
276
            Order.ft_is_open.is_(True),
277
            tuple_(Order.ft_trade_id, Order.order_id).not_in(
278
                select(
279
                    Trade.id, Trade.stoploss_order_id
280
                ).where(Trade.stoploss_order_id.is_not(None))
281
                  ),
282
            Order.ft_order_side == 'stoploss',
283
            Order.order_id.like('dry%'),
284

285
        ).values(ft_is_open=False)
286
        connection.execute(stmt)
1✔
287

288
        stmt = update(Order).where(
1✔
289
            Order.ft_is_open.is_(True),
290
            tuple_(Order.ft_trade_id, Order.order_id).not_in(
291
                select(
292
                    Trade.id, Trade.open_order_id
293
                ).where(Trade.open_order_id.is_not(None))
294
                  ),
295
            Order.ft_order_side != 'stoploss',
296
            Order.order_id.like('dry%')
297

298
        ).values(ft_is_open=False)
299
        connection.execute(stmt)
1✔
300

301

302
def check_migrate(engine, decl_base, previous_tables) -> None:
1✔
303
    """
304
    Checks if migration is necessary and migrates if necessary
305
    """
306
    inspector = inspect(engine)
1✔
307

308
    cols_trades = inspector.get_columns('trades')
1✔
309
    cols_orders = inspector.get_columns('orders')
1✔
310
    cols_pairlocks = inspector.get_columns('pairlocks')
1✔
311
    tabs = get_table_names_for_table(inspector, 'trades')
1✔
312
    table_back_name = get_backup_name(tabs, 'trades_bak')
1✔
313
    order_tabs = get_table_names_for_table(inspector, 'orders')
1✔
314
    order_table_bak_name = get_backup_name(order_tabs, 'orders_bak')
1✔
315
    pairlock_tabs = get_table_names_for_table(inspector, 'pairlocks')
1✔
316
    pairlock_table_bak_name = get_backup_name(pairlock_tabs, 'pairlocks_bak')
1✔
317

318
    # Check if migration necessary
319
    # Migrates both trades and orders table!
320
    # if ('orders' not in previous_tables
321
    # or not has_column(cols_orders, 'funding_fee')):
322
    migrating = False
1✔
323
    # if not has_column(cols_orders, 'ft_price'):
324
    if not has_column(cols_trades, 'is_stop_loss_trailing'):
1✔
325
        migrating = True
1✔
326
        logger.info(f"Running database migration for trades - "
1✔
327
                    f"backup: {table_back_name}, {order_table_bak_name}")
328
        migrate_trades_and_orders_table(
1✔
329
            decl_base, inspector, engine, table_back_name, cols_trades,
330
            order_table_bak_name, cols_orders)
331

332
    if not has_column(cols_pairlocks, 'side'):
1✔
333
        migrating = True
1✔
334
        logger.info(f"Running database migration for pairlocks - "
1✔
335
                    f"backup: {pairlock_table_bak_name}")
336

337
        migrate_pairlocks_table(
1✔
338
            decl_base, inspector, engine, pairlock_table_bak_name, cols_pairlocks
339
        )
340
    if 'orders' not in previous_tables and 'trades' in previous_tables:
1✔
341
        raise OperationalException(
1✔
342
            "Your database seems to be very old. "
343
            "Please update to freqtrade 2022.3 to migrate this database or "
344
            "start with a fresh database.")
345

346
    set_sqlite_to_wal(engine)
1✔
347
    fix_old_dry_orders(engine)
1✔
348

349
    if migrating:
1✔
350
        logger.info("Database migration finished.")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc