aboutsummaryrefslogblamecommitdiff
path: root/tests/test_portfolio.py
blob: f2f639470c0bbf880916d16f5d0583b59392357e (plain) (tree)
1
2
3
4
5
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785



                     
                                                      






















                                                                                 





                                                                                                                    
                                                      


















































































































                                                                               


                                                                                        























                                                                               


                                                                                         
















                                                                                

                                                                                      

                                                                                                      

                                                                                      

                                                                                                     

                                                                                     




















                                                                        



















                                                                            









































































































































































































































































                                                                                                                     


















                                                                         








































































































                                                                                                                                  
                                                      










































































                                                                                                                                     
                                                      






































































































































                                                                                                 
                                              

                                                                   


                                                      
 

                                                     



                                                 
                                                               





























                                                                                                 

                                                                                                               







































































































                                                                                                    
                                                          









































































































































































































































































































































































                                                                                                                                                                                     
                                                                                   












































































































                                                                                                
                                                                                   






















































































































































































































































































































                                                                                                
                                                      











                                                          
                                                                                              




































                                                                                                                               
                                                                                               





                                                          
                                                      













                                                                   
                                                                                           






































































































































































































































                                                                                                                           
from .helper import *
import portfolio
import datetime

@unittest.skipUnless("unit" in limits, "Unit skipped")
class ComputationTest(WebMockTestCase):
    def test_compute_value(self):
        compute = mock.Mock()
        portfolio.Computation.compute_value("foo", "buy", compute_value=compute)
        compute.assert_called_with("foo", "ask")

        compute.reset_mock()
        portfolio.Computation.compute_value("foo", "sell", compute_value=compute)
        compute.assert_called_with("foo", "bid")

        compute.reset_mock()
        portfolio.Computation.compute_value("foo", "ask", compute_value=compute)
        compute.assert_called_with("foo", "ask")

        compute.reset_mock()
        portfolio.Computation.compute_value("foo", "bid", compute_value=compute)
        compute.assert_called_with("foo", "bid")

        compute.reset_mock()
        portfolio.Computation.computations["test"] = compute
        portfolio.Computation.compute_value("foo", "bid", compute_value="test")
        compute.assert_called_with("foo", "bid")

    def test_eat_several(self):
        self.m.ccxt.fetch_nth_order_book.return_value = D("0.00001275")

        self.assertEqual(D("0.00001275"), portfolio.Computation.eat_several(self.m)({ "symbol": "BTC/HUC" }, "ask"))


@unittest.skipUnless("unit" in limits, "Unit skipped")
class TradeTest(WebMockTestCase):

    def test_values_assertion(self):
        value_from = portfolio.Amount("BTC", "1.0")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
        self.assertEqual("BTC", trade.base_currency)
        self.assertEqual("ETH", trade.currency)
        self.assertEqual(self.m, trade.market)

        with self.assertRaises(AssertionError):
            portfolio.Trade(value_from, -value_to, "ETH", self.m)
        with self.assertRaises(AssertionError):
            portfolio.Trade(value_from, value_to, "ETC", self.m)
        with self.assertRaises(AssertionError):
            value_from.currency = "ETH"
            portfolio.Trade(value_from, value_to, "ETH", self.m)
        value_from.currency = "BTC"
        with self.assertRaises(AssertionError):
            value_from2 = portfolio.Amount("BTC", "1.0")
            portfolio.Trade(value_from2, value_to, "ETH", self.m)

        value_from = portfolio.Amount("BTC", 0)
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
        self.assertEqual(0, trade.value_from.linked_to)

    def test_action(self):
        value_from = portfolio.Amount("BTC", "1.0")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        self.assertIsNone(trade.action)

        value_from = portfolio.Amount("BTC", "1.0")
        value_from.linked_to = portfolio.Amount("BTC", "1.0")
        value_to = portfolio.Amount("BTC", "2.0")
        trade = portfolio.Trade(value_from, value_to, "BTC", self.m)

        self.assertIsNone(trade.action)

        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        self.assertEqual("acquire", trade.action)

        value_from = portfolio.Amount("BTC", "0")
        value_from.linked_to = portfolio.Amount("ETH", "0")
        value_to = portfolio.Amount("BTC", "-1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        self.assertEqual("acquire", trade.action)

    def test_order_action(self):
        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        trade.inverted = False
        self.assertEqual("buy", trade.order_action())
        trade.inverted = True
        self.assertEqual("sell", trade.order_action())

        value_from = portfolio.Amount("BTC", "0")
        value_from.linked_to = portfolio.Amount("ETH", "0")
        value_to = portfolio.Amount("BTC", "-1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        trade.inverted = False
        self.assertEqual("sell", trade.order_action())
        trade.inverted = True
        self.assertEqual("buy", trade.order_action())

    def test_trade_type(self):
        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        self.assertEqual("long", trade.trade_type)

        value_from = portfolio.Amount("BTC", "0")
        value_from.linked_to = portfolio.Amount("ETH", "0")
        value_to = portfolio.Amount("BTC", "-1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        self.assertEqual("short", trade.trade_type)

    def test_is_fullfiled(self):
        with self.subTest(inverted=False):
            value_from = portfolio.Amount("BTC", "0.5")
            value_from.linked_to = portfolio.Amount("ETH", "10.0")
            value_to = portfolio.Amount("BTC", "1.0")
            trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

            order1 = mock.Mock()
            order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")

            order2 = mock.Mock()
            order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
            trade.orders.append(order1)
            trade.orders.append(order2)

            self.assertFalse(trade.is_fullfiled)

            order3 = mock.Mock()
            order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
            trade.orders.append(order3)

            self.assertTrue(trade.is_fullfiled)

            order1.filled_amount.assert_called_with(in_base_currency=True, refetch=True)
            order2.filled_amount.assert_called_with(in_base_currency=True, refetch=True)
            order3.filled_amount.assert_called_with(in_base_currency=True, refetch=True)

        with self.subTest(inverted=True):
            value_from = portfolio.Amount("BTC", "0.5")
            value_from.linked_to = portfolio.Amount("USDT", "1000.0")
            value_to = portfolio.Amount("BTC", "1.0")
            trade = portfolio.Trade(value_from, value_to, "USDT", self.m)
            trade.inverted = True

            order1 = mock.Mock()
            order1.filled_amount.return_value = portfolio.Amount("BTC", "0.3")

            order2 = mock.Mock()
            order2.filled_amount.return_value = portfolio.Amount("BTC", "0.01")
            trade.orders.append(order1)
            trade.orders.append(order2)

            self.assertFalse(trade.is_fullfiled)

            order3 = mock.Mock()
            order3.filled_amount.return_value = portfolio.Amount("BTC", "0.19")
            trade.orders.append(order3)

            self.assertTrue(trade.is_fullfiled)

            order1.filled_amount.assert_called_with(in_base_currency=False, refetch=True)
            order2.filled_amount.assert_called_with(in_base_currency=False, refetch=True)
            order3.filled_amount.assert_called_with(in_base_currency=False, refetch=True)


    def test_filled_amount(self):
        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        order1 = mock.Mock()
        order1.filled_amount.return_value = portfolio.Amount("ETH", "0.3")

        order2 = mock.Mock()
        order2.filled_amount.return_value = portfolio.Amount("ETH", "0.01")
        trade.orders.append(order1)
        trade.orders.append(order2)

        self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount())
        order1.filled_amount.assert_called_with(in_base_currency=False, refetch=False)
        order2.filled_amount.assert_called_with(in_base_currency=False, refetch=False)

        self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=False))
        order1.filled_amount.assert_called_with(in_base_currency=False, refetch=False)
        order2.filled_amount.assert_called_with(in_base_currency=False, refetch=False)

        self.assertEqual(portfolio.Amount("ETH", "0.31"), trade.filled_amount(in_base_currency=True))
        order1.filled_amount.assert_called_with(in_base_currency=True, refetch=False)
        order2.filled_amount.assert_called_with(in_base_currency=True, refetch=False)

    @mock.patch.object(portfolio.Computation, "compute_value")
    @mock.patch.object(portfolio.Trade, "filled_amount")
    @mock.patch.object(portfolio, "Order")
    def test_prepare_order(self, Order, filled_amount, compute_value):
        Order.return_value = "Order"

        with self.subTest(desc="Nothing to do"):
            value_from = portfolio.Amount("BTC", "10")
            value_from.rate = D("0.1")
            value_from.linked_to = portfolio.Amount("FOO", "100")
            value_to = portfolio.Amount("BTC", "10")
            trade = portfolio.Trade(value_from, value_to, "FOO", self.m)

            trade.prepare_order()

            filled_amount.assert_not_called()
            compute_value.assert_not_called()
            self.assertEqual(0, len(trade.orders))
            Order.assert_not_called()

        self.m.get_ticker.return_value = None
        with self.subTest(desc="Unknown ticker"):
            filled_amount.return_value = portfolio.Amount("BTC", "3")
            compute_value.return_value = D("0.125")

            value_from = portfolio.Amount("BTC", "1")
            value_from.rate = D("0.1")
            value_from.linked_to = portfolio.Amount("FOO", "10")
            value_to = portfolio.Amount("BTC", "10")
            trade = portfolio.Trade(value_from, value_to, "FOO", self.m)

            trade.prepare_order()

            filled_amount.assert_not_called()
            compute_value.assert_not_called()
            self.assertEqual(0, len(trade.orders))
            Order.assert_not_called()
            self.m.report.log_error.assert_called_once_with('prepare_order',
                    message='Unknown ticker FOO/BTC')

        self.m.get_ticker.return_value = { "inverted": False }
        with self.subTest(desc="Already filled"):
            filled_amount.return_value = portfolio.Amount("FOO", "100")
            compute_value.return_value = D("0.125")

            value_from = portfolio.Amount("BTC", "10")
            value_from.rate = D("0.1")
            value_from.linked_to = portfolio.Amount("FOO", "100")
            value_to = portfolio.Amount("BTC", "0")
            trade = portfolio.Trade(value_from, value_to, "FOO", self.m)

            trade.prepare_order()

            filled_amount.assert_called_with(in_base_currency=False)
            compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
            self.assertEqual(0, len(trade.orders))
            self.m.report.log_error.assert_called_with("prepare_order", message=mock.ANY)
            Order.assert_not_called()

        with self.subTest(action="dispose", inverted=False):
            filled_amount.return_value = portfolio.Amount("FOO", "60")
            compute_value.return_value = D("0.125")

            value_from = portfolio.Amount("BTC", "10")
            value_from.rate = D("0.1")
            value_from.linked_to = portfolio.Amount("FOO", "100")
            value_to = portfolio.Amount("BTC", "1")
            trade = portfolio.Trade(value_from, value_to, "FOO", self.m)

            trade.prepare_order()

            filled_amount.assert_called_with(in_base_currency=False)
            compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
            self.assertEqual(1, len(trade.orders))
            Order.assert_called_with("sell", portfolio.Amount("FOO", 30),
                    D("0.125"), "BTC", "long", self.m,
                    trade, close_if_possible=False)

        with self.subTest(action="dispose", inverted=False, close_if_possible=True):
            filled_amount.return_value = portfolio.Amount("FOO", "60")
            compute_value.return_value = D("0.125")

            value_from = portfolio.Amount("BTC", "10")
            value_from.rate = D("0.1")
            value_from.linked_to = portfolio.Amount("FOO", "100")
            value_to = portfolio.Amount("BTC", "1")
            trade = portfolio.Trade(value_from, value_to, "FOO", self.m)

            trade.prepare_order(close_if_possible=True)

            filled_amount.assert_called_with(in_base_currency=False)
            compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
            self.assertEqual(1, len(trade.orders))
            Order.assert_called_with("sell", portfolio.Amount("FOO", 30),
                    D("0.125"), "BTC", "long", self.m,
                    trade, close_if_possible=True)

        with self.subTest(action="acquire", inverted=False):
            filled_amount.return_value = portfolio.Amount("BTC", "3")
            compute_value.return_value = D("0.125")

            value_from = portfolio.Amount("BTC", "1")
            value_from.rate = D("0.1")
            value_from.linked_to = portfolio.Amount("FOO", "10")
            value_to = portfolio.Amount("BTC", "10")
            trade = portfolio.Trade(value_from, value_to, "FOO", self.m)

            trade.prepare_order()

            filled_amount.assert_called_with(in_base_currency=True)
            compute_value.assert_called_with(self.m.get_ticker.return_value, "buy", compute_value="default")
            self.assertEqual(1, len(trade.orders))

            Order.assert_called_with("buy", portfolio.Amount("FOO", 48),
                D("0.125"), "BTC", "long", self.m,
                trade, close_if_possible=False)

        with self.subTest(close_if_possible=True):
            filled_amount.return_value = portfolio.Amount("FOO", "0")
            compute_value.return_value = D("0.125")

            value_from = portfolio.Amount("BTC", "10")
            value_from.rate = D("0.1")
            value_from.linked_to = portfolio.Amount("FOO", "100")
            value_to = portfolio.Amount("BTC", "0")
            trade = portfolio.Trade(value_from, value_to, "FOO", self.m)

            trade.prepare_order()

            filled_amount.assert_called_with(in_base_currency=False)
            compute_value.assert_called_with(self.m.get_ticker.return_value, "sell", compute_value="default")
            self.assertEqual(1, len(trade.orders))
            Order.assert_called_with("sell", portfolio.Amount("FOO", 100),
                    D("0.125"), "BTC", "long", self.m,
                    trade, close_if_possible=True)

        self.m.get_ticker.return_value = { "inverted": True, "original": {} }
        with self.subTest(action="dispose", inverted=True):
            filled_amount.return_value = portfolio.Amount("FOO", "300")
            compute_value.return_value = D("125")

            value_from = portfolio.Amount("BTC", "10")
            value_from.rate = D("0.01")
            value_from.linked_to = portfolio.Amount("FOO", "1000")
            value_to = portfolio.Amount("BTC", "1")
            trade = portfolio.Trade(value_from, value_to, "FOO", self.m)

            trade.prepare_order(compute_value="foo")

            filled_amount.assert_called_with(in_base_currency=True)
            compute_value.assert_called_with(self.m.get_ticker.return_value["original"], "buy", compute_value="foo")
            self.assertEqual(1, len(trade.orders))
            Order.assert_called_with("buy", portfolio.Amount("BTC", D("4.8")),
                    D("125"), "FOO", "long", self.m,
                    trade, close_if_possible=False)

        with self.subTest(action="acquire", inverted=True):
            filled_amount.return_value = portfolio.Amount("BTC", "4")
            compute_value.return_value = D("125")

            value_from = portfolio.Amount("BTC", "1")
            value_from.rate = D("0.01")
            value_from.linked_to = portfolio.Amount("FOO", "100")
            value_to = portfolio.Amount("BTC", "10")
            trade = portfolio.Trade(value_from, value_to, "FOO", self.m)

            trade.prepare_order(compute_value="foo")

            filled_amount.assert_called_with(in_base_currency=False)
            compute_value.assert_called_with(self.m.get_ticker.return_value["original"], "sell", compute_value="foo")
            self.assertEqual(1, len(trade.orders))
            Order.assert_called_with("sell", portfolio.Amount("BTC", D("5")),
                    D("125"), "FOO", "long", self.m,
                    trade, close_if_possible=False)

    def test_tick_actions_recreate(self):
        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        self.assertEqual("average", trade.tick_actions_recreate(0))
        self.assertEqual("foo", trade.tick_actions_recreate(0, default="foo"))
        self.assertEqual("average", trade.tick_actions_recreate(1))
        self.assertEqual(trade.tick_actions[2][1], trade.tick_actions_recreate(2))
        self.assertEqual(trade.tick_actions[2][1], trade.tick_actions_recreate(3))
        self.assertEqual(trade.tick_actions[5][1], trade.tick_actions_recreate(5))
        self.assertEqual(trade.tick_actions[5][1], trade.tick_actions_recreate(6))
        self.assertEqual("default", trade.tick_actions_recreate(7))
        self.assertEqual("default", trade.tick_actions_recreate(8))

    @mock.patch.object(portfolio.Trade, "prepare_order")
    def test_update_order(self, prepare_order):
        order_mock = mock.Mock()
        new_order_mock = mock.Mock()

        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
        prepare_order.return_value = new_order_mock

        for i in [0, 1, 3, 4, 6]:
            with self.subTest(tick=i):
                trade.update_order(order_mock, i)
                order_mock.cancel.assert_not_called()
                new_order_mock.run.assert_not_called()
                self.m.report.log_order.assert_called_once_with(order_mock, i,
                        update="waiting", compute_value=None, new_order=None)

            order_mock.reset_mock()
            new_order_mock.reset_mock()
            trade.orders = []
            self.m.report.log_order.reset_mock()

        trade.update_order(order_mock, 2)
        order_mock.cancel.assert_called()
        new_order_mock.run.assert_called()
        prepare_order.assert_called()
        self.m.report.log_order.assert_called()
        self.assertEqual(2, self.m.report.log_order.call_count)
        calls = [
                mock.call(order_mock, 2, update="adjusting",
                    compute_value=mock.ANY,
                    new_order=new_order_mock),
                mock.call(order_mock, 2, new_order=new_order_mock),
                ]
        self.m.report.log_order.assert_has_calls(calls)

        order_mock.reset_mock()
        new_order_mock.reset_mock()
        trade.orders = []
        self.m.report.log_order.reset_mock()

        trade.update_order(order_mock, 5)
        order_mock.cancel.assert_called()
        new_order_mock.run.assert_called()
        prepare_order.assert_called()
        self.assertEqual(2, self.m.report.log_order.call_count)
        self.m.report.log_order.assert_called()
        calls = [
                mock.call(order_mock, 5, update="adjusting",
                    compute_value=mock.ANY,
                    new_order=new_order_mock),
                mock.call(order_mock, 5, new_order=new_order_mock),
                ]
        self.m.report.log_order.assert_has_calls(calls)

        order_mock.reset_mock()
        new_order_mock.reset_mock()
        trade.orders = []
        self.m.report.log_order.reset_mock()

        trade.update_order(order_mock, 7)
        order_mock.cancel.assert_called()
        new_order_mock.run.assert_called()
        prepare_order.assert_called_with(compute_value="default")
        self.m.report.log_order.assert_called()
        self.assertEqual(2, self.m.report.log_order.call_count)
        calls = [
                mock.call(order_mock, 7, update="market_fallback",
                    compute_value='default',
                    new_order=new_order_mock),
                mock.call(order_mock, 7, new_order=new_order_mock),
                ]
        self.m.report.log_order.assert_has_calls(calls)

        order_mock.reset_mock()
        new_order_mock.reset_mock()
        trade.orders = []
        self.m.report.log_order.reset_mock()

        for i in [10, 13, 16]:
            with self.subTest(tick=i):
                trade.update_order(order_mock, i)
                order_mock.cancel.assert_called()
                new_order_mock.run.assert_called()
                prepare_order.assert_called_with(compute_value="default")
                self.m.report.log_order.assert_called()
                self.assertEqual(2, self.m.report.log_order.call_count)
                calls = [
                        mock.call(order_mock, i, update="market_adjust",
                            compute_value='default',
                            new_order=new_order_mock),
                        mock.call(order_mock, i, new_order=new_order_mock),
                        ]
                self.m.report.log_order.assert_has_calls(calls)

            order_mock.reset_mock()
            new_order_mock.reset_mock()
            trade.orders = []
            self.m.report.log_order.reset_mock()

        for i in [8, 9, 11, 12]:
            with self.subTest(tick=i):
                trade.update_order(order_mock, i)
                order_mock.cancel.assert_not_called()
                new_order_mock.run.assert_not_called()
                self.m.report.log_order.assert_called_once_with(order_mock, i, update="waiting",
                        compute_value=None, new_order=None)

            order_mock.reset_mock()
            new_order_mock.reset_mock()
            trade.orders = []
            self.m.report.log_order.reset_mock()

        with self.subTest(tick=22):
            trade.update_order(order_mock, 22)
            order_mock.cancel.assert_called()
            new_order_mock.run.assert_called()
            prepare_order.assert_called_with(compute_value=mock.ANY)
            self.m.report.log_order.assert_called()
            self.assertEqual(2, self.m.report.log_order.call_count)
            calls = [
                    mock.call(order_mock, 22, update="market_adjust_eat",
                        compute_value=mock.ANY,
                        new_order=new_order_mock),
                    mock.call(order_mock, 22, new_order=new_order_mock),
                    ]
            self.m.report.log_order.assert_has_calls(calls)

        order_mock.reset_mock()
        new_order_mock.reset_mock()
        trade.orders = []
        self.m.report.log_order.reset_mock()

    def test_print_with_order(self):
        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        order_mock1 = mock.Mock()
        order_mock1.__repr__ = mock.Mock()
        order_mock1.__repr__.return_value = "Mock 1"
        order_mock2 = mock.Mock()
        order_mock2.__repr__ = mock.Mock()
        order_mock2.__repr__.return_value = "Mock 2"
        order_mock1.mouvements = []
        mouvement_mock1 = mock.Mock()
        mouvement_mock1.__repr__ = mock.Mock()
        mouvement_mock1.__repr__.return_value = "Mouvement 1"
        mouvement_mock2 = mock.Mock()
        mouvement_mock2.__repr__ = mock.Mock()
        mouvement_mock2.__repr__.return_value = "Mouvement 2"
        order_mock2.mouvements = [
                mouvement_mock1, mouvement_mock2
                ]
        trade.orders.append(order_mock1)
        trade.orders.append(order_mock2)

        with mock.patch.object(trade, "filled_amount") as filled:
            filled.return_value = portfolio.Amount("BTC", "0.1")

            trade.print_with_order()

            self.m.report.print_log.assert_called()
            calls = self.m.report.print_log.mock_calls
            self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(calls[0][1][0]))
            self.assertEqual("\tMock 1", str(calls[1][1][0]))
            self.assertEqual("\tMock 2", str(calls[2][1][0]))
            self.assertEqual("\t\tMouvement 1", str(calls[3][1][0]))
            self.assertEqual("\t\tMouvement 2", str(calls[4][1][0]))

            self.m.report.print_log.reset_mock()

            filled.return_value = portfolio.Amount("BTC", "0.5")
            trade.print_with_order()
            calls = self.m.report.print_log.mock_calls
            self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire ✔)", str(calls[0][1][0]))

            self.m.report.print_log.reset_mock()

            filled.return_value = portfolio.Amount("BTC", "0.1")
            trade.closed = True
            trade.print_with_order()
            calls = self.m.report.print_log.mock_calls
            self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire ❌)", str(calls[0][1][0]))

    def test_close(self):
        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)
        order1 = mock.Mock()
        trade.orders.append(order1)

        trade.close()

        self.assertEqual(True, trade.closed)
        order1.cancel.assert_called_once_with()

    def test_pending(self):
        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        trade.closed = True
        self.assertEqual(False, trade.pending)

        trade.closed = False
        self.assertEqual(True, trade.pending)

        order1 = mock.Mock()
        order1.filled_amount.return_value = portfolio.Amount("BTC", "0.5")
        trade.orders.append(order1)
        self.assertEqual(False, trade.pending)

    def test__repr(self):
        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        self.assertEqual("Trade(0.50000000 BTC [10.00000000 ETH] -> 1.00000000 BTC in ETH, acquire)", str(trade))

    def test_as_json(self):
        value_from = portfolio.Amount("BTC", "0.5")
        value_from.linked_to = portfolio.Amount("ETH", "10.0")
        value_to = portfolio.Amount("BTC", "1.0")
        trade = portfolio.Trade(value_from, value_to, "ETH", self.m)

        as_json = trade.as_json()
        self.assertEqual("acquire", as_json["action"])
        self.assertEqual(D("0.5"), as_json["from"])
        self.assertEqual(D("1.0"), as_json["to"])
        self.assertEqual("ETH", as_json["currency"])
        self.assertEqual("BTC", as_json["base_currency"])

@unittest.skipUnless("unit" in limits, "Unit skipped")
class BalanceTest(WebMockTestCase):
    def test_values(self):
        balance = portfolio.Balance("BTC", {
            "exchange_total": "0.65",
            "exchange_free": "0.35",
            "exchange_used": "0.30",
            "margin_total": "-10",
            "margin_borrowed": "10",
            "margin_available": "0",
            "margin_in_position": "0",
            "margin_position_type": "short",
            "margin_borrowed_base_currency": "USDT",
            "margin_liquidation_price": "1.20",
            "margin_pending_gain": "10",
            "margin_lending_fees": "0.4",
            "margin_borrowed_base_price": "0.15",
            })
        self.assertEqual(portfolio.D("0.65"), balance.exchange_total.value)
        self.assertEqual(portfolio.D("0.35"), balance.exchange_free.value)
        self.assertEqual(portfolio.D("0.30"), balance.exchange_used.value)
        self.assertEqual("BTC", balance.exchange_total.currency)
        self.assertEqual("BTC", balance.exchange_free.currency)
        self.assertEqual("BTC", balance.exchange_total.currency)

        self.assertEqual(portfolio.D("-10"), balance.margin_total.value)
        self.assertEqual(portfolio.D("10"), balance.margin_borrowed.value)
        self.assertEqual(portfolio.D("0"), balance.margin_available.value)
        self.assertEqual("BTC", balance.margin_total.currency)
        self.assertEqual("BTC", balance.margin_borrowed.currency)
        self.assertEqual("BTC", balance.margin_available.currency)

        self.assertEqual("BTC", balance.currency)

        self.assertEqual(portfolio.D("0.4"), balance.margin_lending_fees.value)
        self.assertEqual("USDT", balance.margin_lending_fees.currency)

    def test__repr(self):
        self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX])",
                repr(portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 })))
        balance = portfolio.Balance("BTX", { "exchange_total": 3,
            "exchange_used": 1, "exchange_free": 2 })
        self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance))

        balance = portfolio.Balance("BTX", { "exchange_total": 1, "exchange_used": 1})
        self.assertEqual("Balance(BTX Exch: [❌1.00000000 BTX])", repr(balance))

        balance = portfolio.Balance("BTX", { "margin_total": 3,
            "margin_in_position": 1, "margin_available": 2 })
        self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX + ❌1.00000000 BTX = 3.00000000 BTX])", repr(balance))

        balance = portfolio.Balance("BTX", { "margin_total": 2, "margin_available": 2 })
        self.assertEqual("Balance(BTX Margin: [✔2.00000000 BTX])", repr(balance))

        balance = portfolio.Balance("BTX", { "margin_total": -3,
            "margin_borrowed_base_price": D("0.1"),
            "margin_borrowed_base_currency": "BTC",
            "margin_lending_fees": D("0.002") })
        self.assertEqual("Balance(BTX Margin: [-3.00000000 BTX @@ 0.10000000 BTC/0.00200000 BTC])", repr(balance))

        balance = portfolio.Balance("BTX", { "margin_total": 1,
            "margin_in_position": 1, "exchange_free": 2, "exchange_total": 2})
        self.assertEqual("Balance(BTX Exch: [✔2.00000000 BTX] Margin: [❌1.00000000 BTX] Total: [0.00000000 BTX])", repr(balance))

    def test_as_json(self):
        balance = portfolio.Balance("BTX", { "exchange_free": 2, "exchange_total": 2 })
        as_json = balance.as_json()
        self.assertEqual(set(portfolio.Balance.base_keys), set(as_json.keys()))
        self.assertEqual(D(0), as_json["total"])
        self.assertEqual(D(2), as_json["exchange_total"])
        self.assertEqual(D(2), as_json["exchange_free"])
        self.assertEqual(D(0), as_json["exchange_used"])
        self.assertEqual(D(0), as_json["margin_total"])
        self.assertEqual(D(0), as_json["margin_available"])
        self.assertEqual(D(0), as_json["margin_borrowed"])

@unittest.skipUnless("unit" in limits, "Unit skipped")
class OrderTest(WebMockTestCase):
    def test_values(self):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", "market", "trade")
        self.assertEqual("buy", order.action)
        self.assertEqual(10, order.amount.value)
        self.assertEqual("ETH", order.amount.currency)
        self.assertEqual(D("0.1"), order.rate)
        self.assertEqual("BTC", order.base_currency)
        self.assertEqual("market", order.market)
        self.assertEqual("long", order.trade_type)
        self.assertEqual("pending", order.status)
        self.assertEqual("trade", order.trade)
        self.assertIsNone(order.id)
        self.assertFalse(order.close_if_possible)

    def test__repr(self):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", "market", "trade")
        self.assertEqual("Order(buy long 10.00000000 ETH at 0.1 BTC [pending])", repr(order))

        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", "market", "trade",
                close_if_possible=True)
        self.assertEqual("Order(buy long 10.00000000 ETH at 0.1 BTC [pending] ✂)", repr(order))

    def test_as_json(self):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", "market", "trade")
        mouvement_mock1 = mock.Mock()
        mouvement_mock1.as_json.return_value = 1
        mouvement_mock2 = mock.Mock()
        mouvement_mock2.as_json.return_value = 2

        order.mouvements = [mouvement_mock1, mouvement_mock2]
        as_json = order.as_json()
        self.assertEqual("buy", as_json["action"])
        self.assertEqual("long", as_json["trade_type"])
        self.assertEqual(10, as_json["amount"])
        self.assertEqual("ETH", as_json["currency"])
        self.assertEqual("BTC", as_json["base_currency"])
        self.assertEqual(D("0.1"), as_json["rate"])
        self.assertEqual("pending", as_json["status"])
        self.assertEqual(False, as_json["close_if_possible"])
        self.assertIsNone(as_json["id"])
        self.assertEqual([1, 2], as_json["mouvements"])

    def test_account(self):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", "market", "trade")
        self.assertEqual("exchange", order.account)

        order = portfolio.Order("sell", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "short", "market", "trade")
        self.assertEqual("margin", order.account)

    def test_pending(self):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", "market", "trade")
        self.assertTrue(order.pending)
        order.status = "open"
        self.assertFalse(order.pending)

    def test_open(self):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", "market", "trade")
        self.assertFalse(order.open)
        order.status = "open"
        self.assertTrue(order.open)

    def test_finished(self):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", "market", "trade")
        self.assertFalse(order.finished)
        order.status = "closed"
        self.assertTrue(order.finished)
        order.status = "canceled"
        self.assertTrue(order.finished)
        order.status = "error"
        self.assertTrue(order.finished)

    @mock.patch.object(portfolio.Order, "fetch")
    def test_cancel(self, fetch):
        with self.subTest(debug=True):
            self.m.debug = True
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.status = "open"

            order.cancel()
            self.m.ccxt.cancel_order.assert_not_called()
            self.m.report.log_debug_action.assert_called_once()
            self.m.report.log_debug_action.reset_mock()
            self.assertEqual("canceled", order.status)

        with self.subTest(desc="Nominal case"):
            self.m.debug = False
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.status = "open"
            order.id = 42

            order.cancel()
            self.m.ccxt.cancel_order.assert_called_with(42)
            fetch.assert_called_once_with()
            self.m.report.log_debug_action.assert_not_called()

        with self.subTest(exception=True):
            self.m.ccxt.cancel_order.side_effect = portfolio.OrderNotFound
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.status = "open"
            order.id = 42
            order.cancel()
            self.m.ccxt.cancel_order.assert_called_with(42)
            self.m.report.log_error.assert_called_once()

        self.m.reset_mock()
        with self.subTest(id=None):
            self.m.ccxt.cancel_order.side_effect = portfolio.OrderNotFound
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.status = "open"
            order.cancel()
            self.m.ccxt.cancel_order.assert_not_called()

        self.m.reset_mock()
        with self.subTest(open=False):
            self.m.ccxt.cancel_order.side_effect = portfolio.OrderNotFound
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.status = "closed"
            order.cancel()
            self.m.ccxt.cancel_order.assert_not_called()

    def test_mark_dust_amount_remaining(self):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", self.m, "trade")
        self.m.ccxt.is_dust_trade.return_value = False
        order.mark_dust_amount_remaining_order()
        self.assertEqual("pending", order.status)

        self.m.ccxt.is_dust_trade.return_value = True
        order.mark_dust_amount_remaining_order()
        self.assertEqual("pending", order.status)

        order.status = "open"
        order.mark_dust_amount_remaining_order()
        self.assertEqual("closed_dust_remaining", order.status)

    @mock.patch.object(portfolio.Order, "fetch")
    @mock.patch.object(portfolio.Order, "filled_amount", return_value=portfolio.Amount("ETH", 1))
    def test_remaining_amount(self, filled_amount, fetch):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", self.m, "trade")

        self.assertEqual(9, order.remaining_amount().value)

        order.status = "open"
        self.assertEqual(9, order.remaining_amount().value)

    @mock.patch.object(portfolio.Order, "fetch")
    def test_filled_amount(self, fetch):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", self.m, "trade")
        order.mouvements.append(portfolio.Mouvement("ETH", "BTC", {
            "tradeID": 42, "type": "buy", "fee": "0.0015",
            "date": "2017-12-30 12:00:12", "rate": "0.1",
            "amount": "3", "total": "0.3"
            }))
        order.mouvements.append(portfolio.Mouvement("ETH", "BTC", {
            "tradeID": 43, "type": "buy", "fee": "0.0015",
            "date": "2017-12-30 13:00:12", "rate": "0.2",
            "amount": "2", "total": "0.4"
            }))
        self.assertEqual(portfolio.Amount("ETH", 5), order.filled_amount())
        fetch.assert_not_called()
        order.status = "open"
        self.assertEqual(portfolio.Amount("ETH", 5), order.filled_amount(in_base_currency=False))
        fetch.assert_not_called()
        self.assertEqual(portfolio.Amount("ETH", 5), order.filled_amount(in_base_currency=False, refetch=True))
        fetch.assert_called_once()
        self.assertEqual(portfolio.Amount("BTC", "0.7"), order.filled_amount(in_base_currency=True))

    def test_fetch_mouvements(self):
        self.m.ccxt.privatePostReturnOrderTrades.return_value = [
                {
                    "tradeID": 42, "type": "buy", "fee": "0.0015",
                    "date": "2017-12-30 13:00:12", "rate": "0.1",
                    "amount": "3", "total": "0.3"
                    },
                {
                    "tradeID": 43, "type": "buy", "fee": "0.0015",
                    "date": "2017-12-30 12:00:12", "rate": "0.2",
                    "amount": "2", "total": "0.4"
                    }
            ]
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", self.m, "trade")
        order.id = 12
        order.mouvements = ["Foo", "Bar", "Baz"]

        order.fetch_mouvements()

        self.m.ccxt.privatePostReturnOrderTrades.assert_called_with({"orderNumber": 12})
        self.assertEqual(2, len(order.mouvements))
        self.assertEqual(43, order.mouvements[0].id)
        self.assertEqual(42, order.mouvements[1].id)

        self.m.ccxt.privatePostReturnOrderTrades.side_effect = portfolio.ExchangeError
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", self.m, "trade")
        order.fetch_mouvements()
        self.assertEqual(0, len(order.mouvements))

    def test_mark_finished_order(self):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "short", self.m, "trade",
                close_if_possible=True)
        order.status = "closed"
        self.m.debug = False

        order.mark_finished_order()
        self.m.ccxt.close_margin_position.assert_called_with("ETH", "BTC")
        self.m.ccxt.close_margin_position.reset_mock()

        order.status = "open"
        order.mark_finished_order()
        self.m.ccxt.close_margin_position.assert_not_called()

        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "short", self.m, "trade",
                close_if_possible=False)
        order.status = "closed"
        order.mark_finished_order()
        self.m.ccxt.close_margin_position.assert_not_called()

        order = portfolio.Order("sell", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "short", self.m, "trade",
                close_if_possible=True)
        order.status = "closed"
        order.mark_finished_order()
        self.m.ccxt.close_margin_position.assert_not_called()

        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", self.m, "trade",
                close_if_possible=True)
        order.status = "closed"
        order.mark_finished_order()
        self.m.ccxt.close_margin_position.assert_not_called()

        self.m.debug = True

        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "short", self.m, "trade",
                close_if_possible=True)
        order.status = "closed"

        order.mark_finished_order()
        self.m.ccxt.close_margin_position.assert_not_called()
        self.m.report.log_debug_action.assert_called_once()

    @mock.patch.object(portfolio.Order, "fetch_mouvements")
    @mock.patch.object(portfolio.Order, "mark_disappeared_order")
    @mock.patch.object(portfolio.Order, "mark_finished_order")
    def test_fetch(self, mark_finished_order, mark_disappeared_order, fetch_mouvements):
        order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                D("0.1"), "BTC", "long", self.m, "trade")
        order.id = 45
        with self.subTest(debug=True):
            self.m.debug = True
            order.fetch()
            self.m.report.log_debug_action.assert_called_once()
            self.m.report.log_debug_action.reset_mock()
            self.m.ccxt.fetch_order.assert_not_called()
            mark_finished_order.assert_not_called()
            mark_disappeared_order.assert_not_called()
            fetch_mouvements.assert_not_called()

        with self.subTest(debug=False):
            self.m.debug = False
            self.m.ccxt.fetch_order.return_value = {
                    "status": "foo",
                    "datetime": "timestamp"
                    }
            self.m.ccxt.is_dust_trade.return_value = False
            order.fetch()

            self.m.ccxt.fetch_order.assert_called_once_with(45)
            fetch_mouvements.assert_called_once()
            self.assertEqual("foo", order.status)
            self.assertEqual("timestamp", order.timestamp)
            self.assertEqual(1, len(order.results))
            self.m.report.log_debug_action.assert_not_called()
            mark_finished_order.assert_called_once()
            mark_disappeared_order.assert_called_once()

            mark_finished_order.reset_mock()
            with self.subTest(missing_order=True):
                self.m.ccxt.fetch_order.side_effect = [
                        portfolio.OrderNotCached,
                        ]
                order.fetch()
                self.assertEqual("closed_unknown", order.status)
                mark_finished_order.assert_called_once()

    def test_mark_disappeared_order(self):
        with self.subTest("Open order"):
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.id = 45
            order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
                "tradeID":21336541,
                "currencyPair":"BTC_XRP",
                "type":"sell",
                "rate":"0.00007013",
                "amount":"0.00000222",
                "total":"0.00000000",
                "fee":"0.00150000",
                "date":"2018-04-02 00:09:13"
                }))
            order.mark_disappeared_order()
            self.assertEqual("pending", order.status)

        with self.subTest("Non-zero amount"):
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.id = 45
            order.status = "closed"
            order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
                "tradeID":21336541,
                "currencyPair":"BTC_XRP",
                "type":"sell",
                "rate":"0.00007013",
                "amount":"0.00000222",
                "total":"0.00000010",
                "fee":"0.00150000",
                "date":"2018-04-02 00:09:13"
                }))
            order.mark_disappeared_order()
            self.assertEqual("closed", order.status)

        with self.subTest("Other mouvements"):
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.id = 45
            order.status = "closed"
            order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
                "tradeID":21336541,
                "currencyPair":"BTC_XRP",
                "type":"sell",
                "rate":"0.00007013",
                "amount":"0.00000222",
                "total":"0.00000001",
                "fee":"0.00150000",
                "date":"2018-04-02 00:09:13"
                }))
            order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
                "tradeID":21336541,
                "currencyPair":"BTC_XRP",
                "type":"sell",
                "rate":"0.00007013",
                "amount":"0.00000222",
                "total":"0.00000000",
                "fee":"0.00150000",
                "date":"2018-04-02 00:09:13"
                }))
            order.mark_disappeared_order()
            self.assertEqual("error_disappeared", order.status)

        with self.subTest("Order disappeared"):
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.id = 45
            order.status = "closed"
            order.mouvements.append(portfolio.Mouvement("XRP", "BTC", {
                "tradeID":21336541,
                "currencyPair":"BTC_XRP",
                "type":"sell",
                "rate":"0.00007013",
                "amount":"0.00000222",
                "total":"0.00000000",
                "fee":"0.00150000",
                "date":"2018-04-02 00:09:13"
                }))
            order.mark_disappeared_order()
            self.assertEqual("error_disappeared", order.status)

    @mock.patch.object(portfolio.Order, "fetch")
    def test_get_status(self, fetch):
        with self.subTest(debug=True):
            self.m.debug = True
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            self.assertEqual("pending", order.get_status())
            fetch.assert_not_called()
            self.m.report.log_debug_action.assert_called_once()

        with self.subTest(debug=False, finished=False):
            self.m.debug = False
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            def _fetch(order):
                def update_status():
                    order.status = "open"
                return update_status
            fetch.side_effect = _fetch(order)
            self.assertEqual("open", order.get_status())
            fetch.assert_called_once()

        fetch.reset_mock()
        with self.subTest(debug=False, finished=True):
            self.m.debug = False
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            def _fetch(order):
                def update_status():
                    order.status = "closed"
                return update_status
            fetch.side_effect = _fetch(order)
            self.assertEqual("closed", order.get_status())
            fetch.assert_called_once()

    def test_run(self):
        self.m.ccxt.order_precision.return_value = 4
        with self.subTest(debug=True):
            self.m.debug = True
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.run()
            self.m.ccxt.create_order.assert_not_called()
            self.m.report.log_debug_action.assert_called_with("market.ccxt.create_order('ETH/BTC', 'limit', 'buy', 10.0000, price=0.1, account=exchange)")
            self.assertEqual("open", order.status)
            self.assertEqual(1, len(order.results))
            self.assertEqual(-1, order.id)

        self.m.ccxt.create_order.reset_mock()
        with self.subTest(debug=False):
            self.m.debug = False
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            self.m.ccxt.create_order.return_value = { "id": 123 }
            order.run()
            self.m.ccxt.create_order.assert_called_once()
            self.assertEqual(1, len(order.results))
            self.assertEqual("open", order.status)

        self.m.ccxt.create_order.reset_mock()
        with self.subTest(exception=True):
            order = portfolio.Order("buy", portfolio.Amount("ETH", 10),
                    D("0.1"), "BTC", "long", self.m, "trade")
            self.m.ccxt.create_order.side_effect = Exception("bouh")
            order.run()
            self.m.ccxt.create_order.assert_called_once()
            self.assertEqual(0, len(order.results))
            self.assertEqual("error", order.status)
            self.m.report.log_error.assert_called_once()

        self.m.ccxt.create_order.reset_mock()
        with self.subTest(dust_amount_exception=True),\
                mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
            order = portfolio.Order("buy", portfolio.Amount("ETH", 0.001),
                    D("0.1"), "BTC", "long", self.m, "trade")
            self.m.ccxt.create_order.side_effect = portfolio.InvalidOrder
            order.run()
            self.m.ccxt.create_order.assert_called_once()
            self.assertEqual(0, len(order.results))
            self.assertEqual("closed", order.status)
            mark_finished_order.assert_called_once()

        self.m.ccxt.order_precision.return_value = 8
        self.m.ccxt.create_order.reset_mock()
        with self.subTest(insufficient_funds=True),\
                mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                    D("0.1"), "BTC", "long", self.m, "trade")
            self.m.ccxt.create_order.side_effect = [
                    portfolio.InsufficientFunds,
                    portfolio.InsufficientFunds,
                    portfolio.InsufficientFunds,
                    { "id": 123 },
                    ]
            order.run()
            self.m.ccxt.create_order.assert_has_calls([
                mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')),
                mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')),
                mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')),
                ])
            self.assertEqual(4, self.m.ccxt.create_order.call_count)
            self.assertEqual(1, len(order.results))
            self.assertEqual("open", order.status)
            self.assertEqual(4, order.tries)
            self.m.report.log_error.assert_called()
            self.assertEqual(4, self.m.report.log_error.call_count)

        self.m.ccxt.order_precision.return_value = 8
        self.m.ccxt.create_order.reset_mock()
        self.m.report.log_error.reset_mock()
        with self.subTest(insufficient_funds=True),\
                mock.patch.object(portfolio.Order, "mark_finished_order") as mark_finished_order:
            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                    D("0.1"), "BTC", "long", self.m, "trade")
            self.m.ccxt.create_order.side_effect = [
                    portfolio.InsufficientFunds,
                    portfolio.InsufficientFunds,
                    portfolio.InsufficientFunds,
                    portfolio.InsufficientFunds,
                    portfolio.InsufficientFunds,
                    ]
            order.run()
            self.m.ccxt.create_order.assert_has_calls([
                mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                mock.call('ETH/BTC', 'limit', 'buy', D('0.00099'), account='exchange', price=D('0.1')),
                mock.call('ETH/BTC', 'limit', 'buy', D('0.0009801'), account='exchange', price=D('0.1')),
                mock.call('ETH/BTC', 'limit', 'buy', D('0.00097029'), account='exchange', price=D('0.1')),
                mock.call('ETH/BTC', 'limit', 'buy', D('0.00096059'), account='exchange', price=D('0.1')),
                ])
            self.assertEqual(5, self.m.ccxt.create_order.call_count)
            self.assertEqual(0, len(order.results))
            self.assertEqual("error", order.status)
            self.assertEqual(5, order.tries)
            self.m.report.log_error.assert_called()
            self.assertEqual(5, self.m.report.log_error.call_count)
            self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00096060 ETH at 0.1 BTC [pending])", exception=mock.ANY)

        self.m.reset_mock()
        with self.subTest(invalid_nonce=True):
            with self.subTest(retry_success=True):
                order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                        D("0.1"), "BTC", "long", self.m, "trade")
                self.m.ccxt.create_order.side_effect = [
                        portfolio.InvalidNonce,
                        portfolio.InvalidNonce,
                        { "id": 123 },
                        ]
                order.run()
                self.m.ccxt.create_order.assert_has_calls([
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                ])
                self.assertEqual(3, self.m.ccxt.create_order.call_count)
                self.assertEqual(3, order.tries)
                self.m.report.log_error.assert_called()
                self.assertEqual(2, self.m.report.log_error.call_count)
                self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after invalid nonce", exception=mock.ANY)
                self.assertEqual(123, order.id)

            self.m.reset_mock()
            with self.subTest(retry_success=False):
                order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                        D("0.1"), "BTC", "long", self.m, "trade")
                self.m.ccxt.create_order.side_effect = [
                        portfolio.InvalidNonce,
                        portfolio.InvalidNonce,
                        portfolio.InvalidNonce,
                        portfolio.InvalidNonce,
                        portfolio.InvalidNonce,
                        ]
                order.run()
                self.assertEqual(5, self.m.ccxt.create_order.call_count)
                self.assertEqual(5, order.tries)
                self.m.report.log_error.assert_called()
                self.assertEqual(5, self.m.report.log_error.call_count)
                self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00100000 ETH at 0.1 BTC [pending]) after invalid nonce", exception=mock.ANY)
                self.assertEqual("error", order.status)

        self.m.reset_mock()
        with self.subTest(request_timeout=True):
            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                    D("0.1"), "BTC", "long", self.m, "trade")
            with self.subTest(retrieved=False), \
                    mock.patch.object(order, "retrieve_order") as retrieve:
                self.m.ccxt.create_order.side_effect = [
                        portfolio.RequestTimeout,
                        portfolio.RequestTimeout,
                        { "id": 123 },
                        ]
                retrieve.return_value = False
                order.run()
                self.m.ccxt.create_order.assert_has_calls([
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                ])
                self.assertEqual(3, self.m.ccxt.create_order.call_count)
                self.assertEqual(3, order.tries)
                self.m.report.log_error.assert_called()
                self.assertEqual(2, self.m.report.log_error.call_count)
                self.m.report.log_error.assert_called_with(mock.ANY, message="Retrying after timeout", exception=mock.ANY)
                self.assertEqual(123, order.id)

            self.m.reset_mock()
            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                    D("0.1"), "BTC", "long", self.m, "trade")
            with self.subTest(retrieved=True), \
                    mock.patch.object(order, "retrieve_order") as retrieve:
                self.m.ccxt.create_order.side_effect = [
                        portfolio.RequestTimeout,
                        ]
                def _retrieve():
                    order.results.append({"id": 123})
                    return True
                retrieve.side_effect = _retrieve
                order.run()
                self.m.ccxt.create_order.assert_has_calls([
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                ])
                self.assertEqual(1, self.m.ccxt.create_order.call_count)
                self.assertEqual(1, order.tries)
                self.m.report.log_error.assert_called()
                self.assertEqual(1, self.m.report.log_error.call_count)
                self.m.report.log_error.assert_called_with(mock.ANY, message="Timeout, found the order")
                self.assertEqual(123, order.id)

            self.m.reset_mock()
            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                    D("0.1"), "BTC", "long", self.m, "trade")
            with self.subTest(retrieved=False), \
                    mock.patch.object(order, "retrieve_order") as retrieve:
                self.m.ccxt.create_order.side_effect = [
                        portfolio.RequestTimeout,
                        portfolio.RequestTimeout,
                        portfolio.RequestTimeout,
                        portfolio.RequestTimeout,
                        portfolio.RequestTimeout,
                        ]
                retrieve.return_value = False
                order.run()
                self.m.ccxt.create_order.assert_has_calls([
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                    mock.call('ETH/BTC', 'limit', 'buy', D('0.0010'), account='exchange', price=D('0.1')),
                ])
                self.assertEqual(5, self.m.ccxt.create_order.call_count)
                self.assertEqual(5, order.tries)
                self.m.report.log_error.assert_called()
                self.assertEqual(5, self.m.report.log_error.call_count)
                self.m.report.log_error.assert_called_with(mock.ANY, message="Giving up Order(buy long 0.00100000 ETH at 0.1 BTC [pending]) after timeouts", exception=mock.ANY)
                self.assertEqual("error", order.status)

    def test_retrieve_order(self):
        with self.subTest(similar_open_order=True):
            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55, 0, tz(2))

            self.m.ccxt.order_precision.return_value = 8
            self.m.ccxt.fetch_orders.return_value = [
                    { # Wrong amount
                        'amount': 0.002, 'cost': 0.1,
                        'datetime': '2018-03-25T15:15:51.000Z',
                        'fee': None, 'filled': 0.0,
                        'id': '1',
                        'info': {
                            'amount': '0.002',
                            'date': '2018-03-25 15:15:51',
                            'margin': 0, 'orderNumber': '1',
                            'price': '0.1', 'rate': '0.1',
                            'side': 'buy', 'startingAmount': '0.002',
                            'status': 'open', 'total': '0.0002',
                            'type': 'limit'
                            },
                        'price': 0.1, 'remaining': 0.002, 'side': 'buy',
                        'status': 'open', 'symbol': 'ETH/BTC',
                        'timestamp': 1521990951000, 'trades': None,
                        'type': 'limit'
                        },
                    { # Margin
                        'amount': 0.001, 'cost': 0.1,
                        'datetime': '2018-03-25T15:15:51.000Z',
                        'fee': None, 'filled': 0.0,
                        'id': '2',
                        'info': {
                            'amount': '0.001',
                            'date': '2018-03-25 15:15:51',
                            'margin': 1, 'orderNumber': '2',
                            'price': '0.1', 'rate': '0.1',
                            'side': 'buy', 'startingAmount': '0.001',
                            'status': 'open', 'total': '0.0001',
                            'type': 'limit'
                            },
                        'price': 0.1, 'remaining': 0.001, 'side': 'buy',
                        'status': 'open', 'symbol': 'ETH/BTC',
                        'timestamp': 1521990951000, 'trades': None,
                        'type': 'limit'
                        },
                    { # selling
                        'amount': 0.001, 'cost': 0.1,
                        'datetime': '2018-03-25T15:15:51.000Z',
                        'fee': None, 'filled': 0.0,
                        'id': '3',
                        'info': {
                            'amount': '0.001',
                            'date': '2018-03-25 15:15:51',
                            'margin': 0, 'orderNumber': '3',
                            'price': '0.1', 'rate': '0.1',
                            'side': 'sell', 'startingAmount': '0.001',
                            'status': 'open', 'total': '0.0001',
                            'type': 'limit'
                            },
                        'price': 0.1, 'remaining': 0.001, 'side': 'sell',
                        'status': 'open', 'symbol': 'ETH/BTC',
                        'timestamp': 1521990951000, 'trades': None,
                        'type': 'limit'
                        },
                    { # Wrong rate
                        'amount': 0.001, 'cost': 0.15,
                        'datetime': '2018-03-25T15:15:51.000Z',
                        'fee': None, 'filled': 0.0,
                        'id': '4',
                        'info': {
                            'amount': '0.001',
                            'date': '2018-03-25 15:15:51',
                            'margin': 0, 'orderNumber': '4',
                            'price': '0.15', 'rate': '0.15',
                            'side': 'buy', 'startingAmount': '0.001',
                            'status': 'open', 'total': '0.0001',
                            'type': 'limit'
                            },
                        'price': 0.15, 'remaining': 0.001, 'side': 'buy',
                        'status': 'open', 'symbol': 'ETH/BTC',
                        'timestamp': 1521990951000, 'trades': None,
                        'type': 'limit'
                        },
                    { # All good
                        'amount': 0.001, 'cost': 0.1,
                        'datetime': '2018-03-25T15:15:51.000Z',
                        'fee': None, 'filled': 0.0,
                        'id': '5',
                        'info': {
                            'amount': '0.001',
                            'date': '2018-03-25 15:15:51',
                            'margin': 0, 'orderNumber': '1',
                            'price': '0.1', 'rate': '0.1',
                            'side': 'buy', 'startingAmount': '0.001',
                            'status': 'open', 'total': '0.0001',
                            'type': 'limit'
                            },
                        'price': 0.1, 'remaining': 0.001, 'side': 'buy',
                        'status': 'open', 'symbol': 'ETH/BTC',
                        'timestamp': 1521990951000, 'trades': None,
                        'type': 'limit'
                        }
                    ]
            result = order.retrieve_order()
            self.assertTrue(result)
            self.assertEqual('5', order.results[0]["id"])
            self.m.ccxt.fetch_my_trades.assert_not_called()
            self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750)

        self.m.reset_mock()
        with self.subTest(similar_open_order=False, past_trades=True):
            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55, 0, tz(2))

            self.m.ccxt.order_precision.return_value = 8
            self.m.ccxt.fetch_orders.return_value = []
            self.m.ccxt.fetch_my_trades.return_value = [
                    { # Wrong timestamp 1
                        'amount': 0.0006,
                        'cost': 0.00006,
                        'datetime': '2018-03-25T15:15:14.000Z',
                        'id': '1-1',
                        'info': {
                            'amount': '0.0006',
                            'category': 'exchange',
                            'date': '2018-03-25 15:15:14',
                            'fee': '0.00150000',
                            'globalTradeID': 1,
                            'orderNumber': '1',
                            'rate': '0.1',
                            'total': '0.00006',
                            'tradeID': '1-1',
                            'type': 'buy'
                            },
                        'order': '1',
                        'price': 0.1,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983714,
                        'type': 'limit'
                        },
                    { # Wrong timestamp 2
                        'amount': 0.0004,
                        'cost': 0.00004,
                        'datetime': '2018-03-25T15:16:54.000Z',
                        'id': '1-2',
                        'info': {
                            'amount': '0.0004',
                            'category': 'exchange',
                            'date': '2018-03-25 15:16:54',
                            'fee': '0.00150000',
                            'globalTradeID': 2,
                            'orderNumber': '1',
                            'rate': '0.1',
                            'total': '0.00004',
                            'tradeID': '1-2',
                            'type': 'buy'
                            },
                        'order': '1',
                        'price': 0.1,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983814,
                        'type': 'limit'
                        },
                    { # Wrong side 1
                        'amount': 0.0006,
                        'cost': 0.00006,
                        'datetime': '2018-03-25T15:15:54.000Z',
                        'id': '2-1',
                        'info': {
                            'amount': '0.0006',
                            'category': 'exchange',
                            'date': '2018-03-25 15:15:54',
                            'fee': '0.00150000',
                            'globalTradeID': 1,
                            'orderNumber': '2',
                            'rate': '0.1',
                            'total': '0.00006',
                            'tradeID': '2-1',
                            'type': 'sell'
                            },
                        'order': '2',
                        'price': 0.1,
                        'side': 'sell',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983754,
                        'type': 'limit'
                        },
                    { # Wrong side 2
                        'amount': 0.0004,
                        'cost': 0.00004,
                        'datetime': '2018-03-25T15:16:54.000Z',
                        'id': '2-2',
                        'info': {
                            'amount': '0.0004',
                            'category': 'exchange',
                            'date': '2018-03-25 15:16:54',
                            'fee': '0.00150000',
                            'globalTradeID': 2,
                            'orderNumber': '2',
                            'rate': '0.1',
                            'total': '0.00004',
                            'tradeID': '2-2',
                            'type': 'buy'
                            },
                        'order': '2',
                        'price': 0.1,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983814,
                        'type': 'limit'
                        },
                    { # Margin trade 1
                        'amount': 0.0006,
                        'cost': 0.00006,
                        'datetime': '2018-03-25T15:15:54.000Z',
                        'id': '3-1',
                        'info': {
                            'amount': '0.0006',
                            'category': 'marginTrade',
                            'date': '2018-03-25 15:15:54',
                            'fee': '0.00150000',
                            'globalTradeID': 1,
                            'orderNumber': '3',
                            'rate': '0.1',
                            'total': '0.00006',
                            'tradeID': '3-1',
                            'type': 'buy'
                            },
                        'order': '3',
                        'price': 0.1,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983754,
                        'type': 'limit'
                        },
                    { # Margin trade 2
                        'amount': 0.0004,
                        'cost': 0.00004,
                        'datetime': '2018-03-25T15:16:54.000Z',
                        'id': '3-2',
                        'info': {
                            'amount': '0.0004',
                            'category': 'marginTrade',
                            'date': '2018-03-25 15:16:54',
                            'fee': '0.00150000',
                            'globalTradeID': 2,
                            'orderNumber': '3',
                            'rate': '0.1',
                            'total': '0.00004',
                            'tradeID': '3-2',
                            'type': 'buy'
                            },
                        'order': '3',
                        'price': 0.1,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983814,
                        'type': 'limit'
                        },
                    { # Wrong amount 1
                        'amount': 0.0005,
                        'cost': 0.00005,
                        'datetime': '2018-03-25T15:15:54.000Z',
                        'id': '4-1',
                        'info': {
                            'amount': '0.0005',
                            'category': 'exchange',
                            'date': '2018-03-25 15:15:54',
                            'fee': '0.00150000',
                            'globalTradeID': 1,
                            'orderNumber': '4',
                            'rate': '0.1',
                            'total': '0.00005',
                            'tradeID': '4-1',
                            'type': 'buy'
                            },
                        'order': '4',
                        'price': 0.1,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983754,
                        'type': 'limit'
                        },
                    { # Wrong amount 2
                        'amount': 0.0004,
                        'cost': 0.00004,
                        'datetime': '2018-03-25T15:16:54.000Z',
                        'id': '4-2',
                        'info': {
                            'amount': '0.0004',
                            'category': 'exchange',
                            'date': '2018-03-25 15:16:54',
                            'fee': '0.00150000',
                            'globalTradeID': 2,
                            'orderNumber': '4',
                            'rate': '0.1',
                            'total': '0.00004',
                            'tradeID': '4-2',
                            'type': 'buy'
                            },
                        'order': '4',
                        'price': 0.1,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983814,
                        'type': 'limit'
                        },
                    { # Wrong price 1
                        'amount': 0.0006,
                        'cost': 0.000066,
                        'datetime': '2018-03-25T15:15:54.000Z',
                        'id': '5-1',
                        'info': {
                            'amount': '0.0006',
                            'category': 'exchange',
                            'date': '2018-03-25 15:15:54',
                            'fee': '0.00150000',
                            'globalTradeID': 1,
                            'orderNumber': '5',
                            'rate': '0.11',
                            'total': '0.000066',
                            'tradeID': '5-1',
                            'type': 'buy'
                            },
                        'order': '5',
                        'price': 0.11,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983754,
                        'type': 'limit'
                        },
                    { # Wrong price 2
                        'amount': 0.0004,
                        'cost': 0.00004,
                        'datetime': '2018-03-25T15:16:54.000Z',
                        'id': '5-2',
                        'info': {
                            'amount': '0.0004',
                            'category': 'exchange',
                            'date': '2018-03-25 15:16:54',
                            'fee': '0.00150000',
                            'globalTradeID': 2,
                            'orderNumber': '5',
                            'rate': '0.1',
                            'total': '0.00004',
                            'tradeID': '5-2',
                            'type': 'buy'
                            },
                        'order': '5',
                        'price': 0.1,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983814,
                        'type': 'limit'
                        },
                    { # All good 1
                        'amount': 0.0006,
                        'cost': 0.00006,
                        'datetime': '2018-03-25T15:15:54.000Z',
                        'id': '7-1',
                        'info': {
                            'amount': '0.0006',
                            'category': 'exchange',
                            'date': '2018-03-25 15:15:54',
                            'fee': '0.00150000',
                            'globalTradeID': 1,
                            'orderNumber': '7',
                            'rate': '0.1',
                            'total': '0.00006',
                            'tradeID': '7-1',
                            'type': 'buy'
                            },
                        'order': '7',
                        'price': 0.1,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983754,
                        'type': 'limit'
                        },
                    { # All good 2
                        'amount': 0.0004,
                        'cost': 0.000036,
                        'datetime': '2018-03-25T15:16:54.000Z',
                        'id': '7-2',
                        'info': {
                            'amount': '0.0004',
                            'category': 'exchange',
                            'date': '2018-03-25 15:16:54',
                            'fee': '0.00150000',
                            'globalTradeID': 2,
                            'orderNumber': '7',
                            'rate': '0.09',
                            'total': '0.000036',
                            'tradeID': '7-2',
                            'type': 'buy'
                            },
                        'order': '7',
                        'price': 0.09,
                        'side': 'buy',
                        'symbol': 'ETH/BTC',
                        'timestamp': 1521983814,
                        'type': 'limit'
                        },
                    ]

            result = order.retrieve_order()
            self.assertTrue(result)
            self.assertEqual('7', order.results[0]["id"])
            self.m.ccxt.fetch_orders.assert_called_once_with(symbol="ETH/BTC", since=1521983750)

        self.m.reset_mock()
        with self.subTest(similar_open_order=False, past_trades=False):
            order = portfolio.Order("buy", portfolio.Amount("ETH", "0.001"),
                    D("0.1"), "BTC", "long", self.m, "trade")
            order.start_date = datetime.datetime(2018, 3, 25, 15, 15, 55)

            self.m.ccxt.order_precision.return_value = 8
            self.m.ccxt.fetch_orders.return_value = []
            self.m.ccxt.fetch_my_trades.return_value = []
            result = order.retrieve_order()
            self.assertFalse(result)

@unittest.skipUnless("unit" in limits, "Unit skipped")
class MouvementTest(WebMockTestCase):
    def test_values(self):
        mouvement = portfolio.Mouvement("ETH", "BTC", {
            "tradeID": 42, "type": "buy", "fee": "0.0015",
            "date": "2017-12-30 12:00:12", "rate": "0.1",
            "amount": "10", "total": "1"
            })
        self.assertEqual("ETH", mouvement.currency)
        self.assertEqual("BTC", mouvement.base_currency)
        self.assertEqual(42, mouvement.id)
        self.assertEqual("buy", mouvement.action)
        self.assertEqual(D("0.0015"), mouvement.fee_rate)
        self.assertEqual(portfolio.datetime.datetime(2017, 12, 30, 12, 0, 12), mouvement.date)
        self.assertEqual(D("0.1"), mouvement.rate)
        self.assertEqual(portfolio.Amount("ETH", "10"), mouvement.total)
        self.assertEqual(portfolio.Amount("BTC", "1"), mouvement.total_in_base)

        mouvement = portfolio.Mouvement("ETH", "BTC", { "foo": "bar" })
        self.assertIsNone(mouvement.date)
        self.assertIsNone(mouvement.id)
        self.assertIsNone(mouvement.action)
        self.assertEqual(-1, mouvement.fee_rate)
        self.assertEqual(0, mouvement.rate)
        self.assertEqual(portfolio.Amount("ETH", 0), mouvement.total)
        self.assertEqual(portfolio.Amount("BTC", 0), mouvement.total_in_base)

    def test__repr(self):
        mouvement = portfolio.Mouvement("ETH", "BTC", {
            "tradeID": 42, "type": "buy", "fee": "0.0015",
            "date": "2017-12-30 12:00:12", "rate": "0.1",
            "amount": "10", "total": "1"
            })
        self.assertEqual("Mouvement(2017-12-30 12:00:12 ; buy 10.00000000 ETH (1.00000000 BTC) fee: 0.1500%)", repr(mouvement))

        mouvement = portfolio.Mouvement("ETH", "BTC", {
            "tradeID": 42, "type": "buy",
            "date": "garbage", "rate": "0.1",
            "amount": "10", "total": "1"
            })
        self.assertEqual("Mouvement(No date ; buy 10.00000000 ETH (1.00000000 BTC))", repr(mouvement))

    def test_as_json(self):
        mouvement = portfolio.Mouvement("ETH", "BTC", {
            "tradeID": 42, "type": "buy", "fee": "0.0015",
            "date": "2017-12-30 12:00:12", "rate": "0.1",
            "amount": "10", "total": "1"
            })
        as_json = mouvement.as_json()

        self.assertEqual(D("0.0015"), as_json["fee_rate"])
        self.assertEqual(portfolio.datetime.datetime(2017, 12, 30, 12, 0, 12), as_json["date"])
        self.assertEqual("buy", as_json["action"])
        self.assertEqual(D("10"), as_json["total"])
        self.assertEqual(D("1"), as_json["total_in_base"])
        self.assertEqual("BTC", as_json["base_currency"])
        self.assertEqual("ETH", as_json["currency"])

@unittest.skipUnless("unit" in limits, "Unit skipped")
class AmountTest(WebMockTestCase):
    def test_values(self):
        amount = portfolio.Amount("BTC", "0.65")
        self.assertEqual(D("0.65"), amount.value)
        self.assertEqual("BTC", amount.currency)

    def test_in_currency(self):
        amount = portfolio.Amount("ETC", 10)

        self.assertEqual(amount, amount.in_currency("ETC", self.m))

        with self.subTest(desc="no ticker for currency"):
            self.m.get_ticker.return_value = None

            self.assertEqual(portfolio.Amount("ETH", 0), amount.in_currency("ETH", self.m))

        with self.subTest(desc="nominal case"):
            self.m.get_ticker.return_value = {
                    "bid": D("0.2"),
                    "ask": D("0.4"),
                    "average": D("0.3"),
                    "foo": "bar",
                    }
            converted_amount = amount.in_currency("ETH", self.m)

            self.assertEqual(D("3.0"), converted_amount.value)
            self.assertEqual("ETH", converted_amount.currency)
            self.assertEqual(amount, converted_amount.linked_to)
            self.assertEqual("bar", converted_amount.ticker["foo"])

            converted_amount = amount.in_currency("ETH", self.m, action="bid", compute_value="default")
            self.assertEqual(D("2"), converted_amount.value)

            converted_amount = amount.in_currency("ETH", self.m, compute_value="ask")
            self.assertEqual(D("4"), converted_amount.value)

        converted_amount = amount.in_currency("ETH", self.m, rate=D("0.02"))
        self.assertEqual(D("0.2"), converted_amount.value)

    def test__round(self):
        amount = portfolio.Amount("BAR", portfolio.D("1.23456789876"))
        self.assertEqual(D("1.23456789"), round(amount).value)
        self.assertEqual(D("1.23"), round(amount, 2).value)

    def test__abs(self):
        amount = portfolio.Amount("SC", -120)
        self.assertEqual(120, abs(amount).value)
        self.assertEqual("SC", abs(amount).currency)

        amount = portfolio.Amount("SC", 10)
        self.assertEqual(10, abs(amount).value)
        self.assertEqual("SC", abs(amount).currency)

    def test__add(self):
        amount1 = portfolio.Amount("XVG", "12.9")
        amount2 = portfolio.Amount("XVG", "13.1")

        self.assertEqual(26, (amount1 + amount2).value)
        self.assertEqual("XVG", (amount1 + amount2).currency)

        amount3 = portfolio.Amount("ETH", "1.6")
        with self.assertRaises(Exception):
            amount1 + amount3

        amount4 = portfolio.Amount("ETH", 0.0)
        self.assertEqual(amount1, amount1 + amount4)

        self.assertEqual(amount1, amount1 + 0)

    def test__radd(self):
        amount = portfolio.Amount("XVG", "12.9")

        self.assertEqual(amount, 0 + amount)
        with self.assertRaises(Exception):
            4 + amount

    def test__sub(self):
        amount1 = portfolio.Amount("XVG", "13.3")
        amount2 = portfolio.Amount("XVG", "13.1")

        self.assertEqual(D("0.2"), (amount1 - amount2).value)
        self.assertEqual("XVG", (amount1 - amount2).currency)

        amount3 = portfolio.Amount("ETH", "1.6")
        with self.assertRaises(Exception):
            amount1 - amount3

        amount4 = portfolio.Amount("ETH", 0.0)
        self.assertEqual(amount1, amount1 - amount4)

    def test__rsub(self):
        amount = portfolio.Amount("ETH", "1.6")
        with self.assertRaises(Exception):
            3 - amount

        self.assertEqual(portfolio.Amount("ETH", "-1.6"), 0-amount)

    def test__mul(self):
        amount = portfolio.Amount("XEM", 11)

        self.assertEqual(D("38.5"), (amount * D("3.5")).value)
        self.assertEqual(D("33"), (amount * 3).value)

        with self.assertRaises(Exception):
            amount * amount

    def test__rmul(self):
        amount = portfolio.Amount("XEM", 11)

        self.assertEqual(D("38.5"), (D("3.5") * amount).value)
        self.assertEqual(D("33"), (3 * amount).value)

    def test__floordiv(self):
        amount = portfolio.Amount("XEM", 11)

        self.assertEqual(D("5.5"), (amount / 2).value)
        self.assertEqual(D("4.4"), (amount / D("2.5")).value)

        with self.assertRaises(Exception):
            amount / amount

    def test__truediv(self):
        amount = portfolio.Amount("XEM", 11)

        self.assertEqual(D("5.5"), (amount / 2).value)
        self.assertEqual(D("4.4"), (amount / D("2.5")).value)

    def test__lt(self):
        amount1 = portfolio.Amount("BTD", 11.3)
        amount2 = portfolio.Amount("BTD", 13.1)

        self.assertTrue(amount1 < amount2)
        self.assertFalse(amount2 < amount1)
        self.assertFalse(amount1 < amount1)

        amount3 = portfolio.Amount("BTC", 1.6)
        with self.assertRaises(Exception):
            amount1 < amount3

    def test__le(self):
        amount1 = portfolio.Amount("BTD", 11.3)
        amount2 = portfolio.Amount("BTD", 13.1)

        self.assertTrue(amount1 <= amount2)
        self.assertFalse(amount2 <= amount1)
        self.assertTrue(amount1 <= amount1)

        amount3 = portfolio.Amount("BTC", 1.6)
        with self.assertRaises(Exception):
            amount1 <= amount3

    def test__gt(self):
        amount1 = portfolio.Amount("BTD", 11.3)
        amount2 = portfolio.Amount("BTD", 13.1)

        self.assertTrue(amount2 > amount1)
        self.assertFalse(amount1 > amount2)
        self.assertFalse(amount1 > amount1)

        amount3 = portfolio.Amount("BTC", 1.6)
        with self.assertRaises(Exception):
            amount3 > amount1

    def test__ge(self):
        amount1 = portfolio.Amount("BTD", 11.3)
        amount2 = portfolio.Amount("BTD", 13.1)

        self.assertTrue(amount2 >= amount1)
        self.assertFalse(amount1 >= amount2)
        self.assertTrue(amount1 >= amount1)

        amount3 = portfolio.Amount("BTC", 1.6)
        with self.assertRaises(Exception):
            amount3 >= amount1

    def test__eq(self):
        amount1 = portfolio.Amount("BTD", 11.3)
        amount2 = portfolio.Amount("BTD", 13.1)
        amount3 = portfolio.Amount("BTD", 11.3)

        self.assertFalse(amount1 == amount2)
        self.assertFalse(amount2 == amount1)
        self.assertTrue(amount1 == amount3)
        self.assertFalse(amount2 == 0)

        amount4 = portfolio.Amount("BTC", 1.6)
        with self.assertRaises(Exception):
            amount1 == amount4

        amount5 = portfolio.Amount("BTD", 0)
        self.assertTrue(amount5 == 0)

    def test__ne(self):
        amount1 = portfolio.Amount("BTD", 11.3)
        amount2 = portfolio.Amount("BTD", 13.1)
        amount3 = portfolio.Amount("BTD", 11.3)

        self.assertTrue(amount1 != amount2)
        self.assertTrue(amount2 != amount1)
        self.assertFalse(amount1 != amount3)
        self.assertTrue(amount2 != 0)

        amount4 = portfolio.Amount("BTC", 1.6)
        with self.assertRaises(Exception):
            amount1 != amount4

        amount5 = portfolio.Amount("BTD", 0)
        self.assertFalse(amount5 != 0)

    def test__neg(self):
        amount1 = portfolio.Amount("BTD", "11.3")

        self.assertEqual(portfolio.D("-11.3"), (-amount1).value)

    def test__str(self):
        amount1 = portfolio.Amount("BTX", 32)
        self.assertEqual("32.00000000 BTX", str(amount1))

        amount2 = portfolio.Amount("USDT", 12000)
        amount1.linked_to = amount2
        self.assertEqual("32.00000000 BTX [12000.00000000 USDT]", str(amount1))

    def test__repr(self):
        amount1 = portfolio.Amount("BTX", 32)
        self.assertEqual("Amount(32.00000000 BTX)", repr(amount1))

        amount2 = portfolio.Amount("USDT", 12000)
        amount1.linked_to = amount2
        self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT))", repr(amount1))

        amount3 = portfolio.Amount("BTC", 0.1)
        amount2.linked_to = amount3
        self.assertEqual("Amount(32.00000000 BTX -> Amount(12000.00000000 USDT -> Amount(0.10000000 BTC)))", repr(amount1))

    def test_as_json(self):
        amount = portfolio.Amount("BTX", 32)
        self.assertEqual({"currency": "BTX", "value": D("32")}, amount.as_json())

        amount = portfolio.Amount("BTX", "1E-10")
        self.assertEqual({"currency": "BTX", "value": D("0")}, amount.as_json())

        amount = portfolio.Amount("BTX", "1E-5")
        self.assertEqual({"currency": "BTX", "value": D("0.00001")}, amount.as_json())
        self.assertEqual("0.00001", str(amount.as_json()["value"]))