diff --git a/README.md b/README.md index ac4ce71f..3aa1a15b 100644 --- a/README.md +++ b/README.md @@ -131,98 +131,97 @@ For example if you run the [moving average example trading bot](./examples/backt you will get the following backtesting report: ```bash - :%%%#+- .=*#%%% Backtest report *%%%%%%%+------=*%%%%%%%- --------------------------- *%%%%%%%%%%%%%%%%%%%%%%%- Start date: 2023-08-24 00:00:00 .%%%%%%%%%%%%%%%%%%%%%%# End date: 2023-12-02 00:00:00 #%%%####%%%%%%%%**#%%%+ Number of days: 100 .:-+*%%%%- -+..#%%%+.+- +%%%#*=-: Number of runs: 1201 - .:-=*%%%%. += .%%# -+.-%%%%=-:.. Number of orders: 14 + .:-=*%%%%. += .%%# -+.-%%%%=-:.. Number of orders: 15 .:=+#%%%%%*###%%%%#*+#%%%%%%*+-: Initial balance: 400.0 - +%%%%%%%%%%%%%%%%%%%= Final balance: 417.8982 - :++ .=#%%%%%%%%%%%%%*- Total net gain: 15.4755 3.869% - :++: :+%%%%%%#-. Growth: 17.8982 4.475% + +%%%%%%%%%%%%%%%%%%%= Final balance: 453.07 + :++ .=#%%%%%%%%%%%%%*- Total net gain: 18.85 4.7% + :++: :+%%%%%%#-. Growth: 53.07 13.27% :++: .%%%%%#= Number of trades closed: 2 :++: .#%%%%%#*= Number of trades open(end of backtest): 2 :++- :%%%%%%%%%+= Percentage positive trades: 75.0% .++- -%%%%%%%%%%%+= Percentage negative trades: 25.0% - .++- .%%%%%%%%%%%%%+= Average trade size: 98.8050 EUR - .++- *%%%%%%%%%%%%%*+: Average trade duration: 11665.866590240556 hours + .++- .%%%%%%%%%%%%%+= Average trade size: 98.79 EUR + .++- *%%%%%%%%%%%%%*+: Average trade duration: 189.0 hours .++- %%%%%%%%%%%%%%#+= =++........:::%%%%%%%%%%%%%%*+- .=++++++++++**#%%%%%%%%%%%%%++. Positions overview ╭────────────┬──────────┬──────────────────────┬───────────────────────┬──────────────┬───────────────┬───────────────────────────┬────────────────┬───────────────╮ -│ Position │ Amount │ Pending buy amount │ Pending sell amount │ Cost (EUR) │ Value (EUR) │ Percentage of portfolio │ Growth (EUR) │ Growth_rate │ +│ Position │ Amount │ Pending buy amount │ Pending sell amount │ Cost (EUR) │ Value (EUR) │ Percentage of portfolio │ Growth (EUR) │ Growth_rate │ ├────────────┼──────────┼──────────────────────┼───────────────────────┼──────────────┼───────────────┼───────────────────────────┼────────────────┼───────────────┤ -│ EUR │ 218.062 │ 0 │ 0 │ 218.062 │ 218.062 │ 52.1806% │ 0 │ 0.0000% │ +│ EUR │ 253.09 │ 0 │ 0 │ 253.09 │ 253.09 EUR │ 55.86% │ 0.00 EUR │ 0.00% │ ├────────────┼──────────┼──────────────────────┼───────────────────────┼──────────────┼───────────────┼───────────────────────────┼────────────────┼───────────────┤ -│ BTC │ 0.0028 │ 0 │ 0 │ 97.4139 │ 99.7171 │ 23.8616% │ 2.3032 │ 2.3644% │ +│ BTC │ 0.0028 │ 0 │ 0 │ 97.34 │ 99.80 EUR │ 22.03% │ 2.46 EUR │ 2.52% │ ├────────────┼──────────┼──────────────────────┼───────────────────────┼──────────────┼───────────────┼───────────────────────────┼────────────────┼───────────────┤ -│ DOT │ 19.9084 │ 0 │ 0 │ 99.9999 │ 100.119 │ 23.9578% │ 0.1195 │ 0.1195% │ +│ DOT │ 19.9521 │ 0 │ 0 │ 100 │ 100.18 EUR │ 22.11% │ 0.18 EUR │ 0.18% │ ╰────────────┴──────────┴──────────────────────┴───────────────────────┴──────────────┴───────────────┴───────────────────────────┴────────────────┴───────────────╯ Trades overview -╭───────────────────┬────────────┬─────────────────────────────────┬─────────────────────┬────────────────────────────┬──────────────────────────┬────────────────────┬─────────────────────────────────╮ -│ Pair (Trade id) │ Status │ Net gain (EUR) │ Open date │ Close date │ Duration │ Open price (EUR) │ Close price's (EUR) │ -├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤ -│ BTC/EUR (1) │ CLOSED, TP │ 2.9820 (3.0460%) │ 2023-09-13 14:00:00 │ 2025-02-19 15:21:54.823674 │ 12601.365228798333 hours │ 24474.4 │ 25427.69, 25012.105 │ -├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤ -│ DOT/EUR (2) │ CLOSED, TP │ 9.3097 (9.3097%) │ 2023-10-30 04:00:00 │ 2025-02-19 15:22:02.227035 │ 11483.3672852875 hours │ 4.0565 │ 4.233, 4.377, 4.807499999999999 │ -├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤ -│ BTC/EUR (3) │ CLOSED │ -0.4248 (-0.4322%) │ 2023-11-06 14:00:00 │ 2025-02-19 15:21:59.823557 │ 11305.366617654721 hours │ 32761.8 │ 32620.225 │ -├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤ -│ BTC/EUR (4) │ CLOSED, TP │ 3.6086 (3.6364%) │ 2023-11-07 22:00:00 │ 2025-02-19 15:22:02.025198 │ 11273.367229221665 hours │ 33077.9 │ 34637.09, 33924.39 │ -├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤ -│ BTC/EUR (5) │ OPEN │ 2.3880 (2.4514%) (unrealized) │ 2023-11-29 12:00:00 │ │ 60.0 hours │ 34790.7 │ │ -├───────────────────┼────────────┼─────────────────────────────────┼─────────────────────┼────────────────────────────┼──────────────────────────┼────────────────────┼─────────────────────────────────┤ -│ DOT/EUR (6) │ OPEN │ -0.0398 (-0.0398%) (unrealized) │ 2023-11-30 18:00:00 │ │ 30.0 hours │ 5.023 │ │ -╰───────────────────┴────────────┴─────────────────────────────────┴─────────────────────┴────────────────────────────┴──────────────────────────┴────────────────────┴─────────────────────────────────╯ +╭───────────────────┬────────────────┬───────────────────────┬───────────────────────────┬──────────────────┬──────────────────┬─────────────┬────────────────────┬──────────────────────────────┬─────────────────────────────────╮ +│ Pair (Trade id) │ Status │ Amount (remaining) │ Net gain (EUR) │ Open date │ Close date │ Duration │ Open price (EUR) │ Close price's (EUR) │ High water mark │ +├───────────────────┼────────────────┼───────────────────────┼───────────────────────────┼──────────────────┼──────────────────┼─────────────┼────────────────────┼──────────────────────────────┼─────────────────────────────────┤ +│ BTC/EUR (1) │ CLOSED │ 0.0040 (0.0000) BTC │ 1.98 (2.02%) │ 2023-09-13 14:00 │ 2023-09-22 12:00 │ 214.0 hours │ 24490.4 │ 24984.93 │ 25703.77 EUR (2023-09-19 14:00) │ +├───────────────────┼────────────────┼───────────────────────┼───────────────────────────┼──────────────────┼──────────────────┼─────────────┼────────────────────┼──────────────────────────────┼─────────────────────────────────┤ +│ DOT/EUR (2) │ CLOSED, SL, TP │ 24.7463 (0.0000) DOT │ 13.53 (13.53%) │ 2023-10-30 04:00 │ 2023-11-15 02:00 │ 382.0 hours │ 4.04 │ 4.23, 4.38, 4.24, 4.25, 4.79 │ 5.45 EUR (2023-11-12 10:00) │ +├───────────────────┼────────────────┼───────────────────────┼───────────────────────────┼──────────────────┼──────────────────┼─────────────┼────────────────────┼──────────────────────────────┼─────────────────────────────────┤ +│ BTC/EUR (3) │ CLOSED │ 0.0030 (0.0000) BTC │ -0.20 (-0.20%) │ 2023-11-06 14:00 │ 2023-11-06 16:00 │ 2.0 hours │ 32691.5 │ 32625.87 │ 32625.87 EUR (2023-11-06 16:00) │ +├───────────────────┼────────────────┼───────────────────────┼───────────────────────────┼──────────────────┼──────────────────┼─────────────┼────────────────────┼──────────────────────────────┼─────────────────────────────────┤ +│ BTC/EUR (4) │ CLOSED, TP │ 0.0030 (0.0000) BTC │ 3.54 (3.56%) │ 2023-11-07 22:00 │ 2023-11-14 12:00 │ 158.0 hours │ 33126.6 │ 34746.64, 33865.42 │ 34967.12 EUR (2023-11-10 22:00) │ +├───────────────────┼────────────────┼───────────────────────┼───────────────────────────┼──────────────────┼──────────────────┼─────────────┼────────────────────┼──────────────────────────────┼─────────────────────────────────┤ +│ BTC/EUR (5) │ OPEN │ 0.0028 (0.0028) BTC │ 2.46 (2.52%) (unrealized) │ 2023-11-29 12:00 │ │ 60.0 hours │ 34765.9 │ │ 35679.63 EUR (2023-12-01 16:00) │ +├───────────────────┼────────────────┼───────────────────────┼───────────────────────────┼──────────────────┼──────────────────┼─────────────┼────────────────────┼──────────────────────────────┼─────────────────────────────────┤ +│ DOT/EUR (6) │ OPEN │ 19.9521 (19.9521) DOT │ 0.18 (0.18%) (unrealized) │ 2023-11-30 18:00 │ │ 30.0 hours │ 5.01 │ │ 5.05 EUR (2023-11-30 20:00) │ +╰───────────────────┴────────────────┴───────────────────────┴───────────────────────────┴──────────────────┴──────────────────┴─────────────┴────────────────────┴──────────────────────────────┴─────────────────────────────────╯ Stop losses overview -╭────────────────────┬───────────────┬──────────┬────────┬──────────────────────┬────────────────┬────────────────┬───────────────────┬──────────────┬─────────────┬───────────────╮ -│ Trade (Trade id) │ Status │ Active │ Type │ stop loss │ Open price │ Sell price's │ High water mark │ Percentage │ Size │ Sold amount │ -├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤ -│ BTC/EUR (1) │ NOT TRIGGERED │ True │ FIXED │ 23250.6847(5.0%) EUR │ 24474.4050 EUR │ None │ 24474.4 │ 50.0% │ 0.0020 BTC │ │ -├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤ -│ DOT/EUR (2) │ NOT TRIGGERED │ True │ FIXED │ 3.8537(5.0%) EUR │ 4.0565 EUR │ None │ 4.0565 │ 50.0% │ 12.3259 DOT │ │ -├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤ -│ BTC/EUR (3) │ NOT TRIGGERED │ True │ FIXED │ 31123.7242(5.0%) EUR │ 32761.8150 EUR │ None │ 32761.8 │ 50.0% │ 0.0015 BTC │ │ -├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤ -│ BTC/EUR (4) │ NOT TRIGGERED │ True │ FIXED │ 31423.9908(5.0%) EUR │ 33077.8850 EUR │ None │ 33077.9 │ 50.0% │ 0.0015 BTC │ │ -├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤ -│ BTC/EUR (5) │ NOT TRIGGERED │ True │ FIXED │ 33051.1460(5.0%) EUR │ 34790.6800 EUR │ None │ 34790.7 │ 50.0% │ 0.0014 BTC │ │ -├────────────────────┼───────────────┼──────────┼────────┼──────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────┤ -│ DOT/EUR (6) │ NOT TRIGGERED │ True │ FIXED │ 4.7718(5.0%) EUR │ 5.0230 EUR │ None │ 5.023 │ 50.0% │ 9.9542 DOT │ │ -╰────────────────────┴───────────────┴──────────┴────────┴──────────────────────┴────────────────┴────────────────┴───────────────────┴──────────────┴─────────────┴───────────────╯ +╭────────────────────┬───────────────┬──────────┬──────────┬─────────────────────────────────┬──────────────┬────────────────┬───────────────────────────────┬──────────────┬───────────┬───────────────╮ +│ Trade (Trade id) │ Status │ Active │ Type │ Stop Loss (Initial Stop Loss) │ Open price │ Sell price's │ High water mark │ Percentage │ Size │ Sold amount │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────┼──────────────┼────────────────┼───────────────────────────────┼──────────────┼───────────┼───────────────┤ +│ BTC/EUR (1) │ NOT TRIGGERED │ False │ TRAILING │ 24418.58 (23265.85) (5.0)% EUR │ 24490.37 EUR │ None │ 25703.77 EUR 2023-09-19 14:00 │ 50.0% │ 0.00 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────┼──────────────┼────────────────┼───────────────────────────────┼──────────────┼───────────┼───────────────┤ +│ DOT/EUR (2) │ TRIGGERED │ False │ TRAILING │ 4.28 (3.84) (5.0)% EUR │ 4.04 EUR │ 4.239,4.254 │ 4.51 EUR 2023-11-01 20:00 │ 50.0% │ 12.37 DOT │ 12.3732 DOT │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────┼──────────────┼────────────────┼───────────────────────────────┼──────────────┼───────────┼───────────────┤ +│ BTC/EUR (3) │ NOT TRIGGERED │ False │ TRAILING │ 31056.93 (31056.93) (5.0)% EUR │ 32691.51 EUR │ None │ 32691.51 EUR │ 50.0% │ 0.00 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────┼──────────────┼────────────────┼───────────────────────────────┼──────────────┼───────────┼───────────────┤ +│ BTC/EUR (4) │ NOT TRIGGERED │ False │ TRAILING │ 33218.76 (31470.27) (5.0)% EUR │ 33126.60 EUR │ None │ 34967.12 EUR 2023-11-10 22:00 │ 50.0% │ 0.00 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────┼──────────────┼────────────────┼───────────────────────────────┼──────────────┼───────────┼───────────────┤ +│ BTC/EUR (5) │ NOT TRIGGERED │ True │ TRAILING │ 33895.65 (33027.62) (5.0)% EUR │ 34765.92 EUR │ None │ 35679.63 EUR 2023-12-01 16:00 │ 50.0% │ 0.00 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────┼──────────────┼────────────────┼───────────────────────────────┼──────────────┼───────────┼───────────────┤ +│ DOT/EUR (6) │ NOT TRIGGERED │ True │ TRAILING │ 4.80 (4.76) (5.0)% EUR │ 5.01 EUR │ None │ 5.05 EUR 2023-11-30 20:00 │ 50.0% │ 9.98 DOT │ │ +╰────────────────────┴───────────────┴──────────┴──────────┴─────────────────────────────────┴──────────────┴────────────────┴───────────────────────────────┴──────────────┴───────────┴───────────────╯ Take profits overview -╭────────────────────┬───────────────┬──────────┬──────────┬───────────────────────┬────────────────┬────────────────┬───────────────────┬──────────────┬─────────────┬───────────────────╮ -│ Trade (Trade id) │ Status │ Active │ Type │ Take profit │ Open price │ Sell price's │ High water mark │ Percentage │ Size │ Sold amount │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ BTC/EUR (1) │ TRIGGERED │ False │ TRAILING │ 25698.1253(5.0)% EUR │ 24474.4050 EUR │ 25427.69 │ 25703.77 │ 50.0% │ 0.0020 BTC │ 0.002 │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ BTC/EUR (1) │ NOT TRIGGERED │ True │ TRAILING │ 26921.8455(10.0)% EUR │ 24474.4050 EUR │ None │ │ 20.0% │ 0.0008 BTC │ │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ DOT/EUR (2) │ TRIGGERED │ False │ TRAILING │ 5.1756(5.0)% EUR │ 4.0565 EUR │ 4.233 │ 5.448 │ 50.0% │ 12.3259 DOT │ 12.32585 │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ DOT/EUR (2) │ TRIGGERED │ False │ TRAILING │ 4.9032(10.0)% EUR │ 4.0565 EUR │ 4.377 │ 5.448 │ 20.0% │ 4.9303 DOT │ 4.930340000000001 │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ BTC/EUR (3) │ NOT TRIGGERED │ True │ TRAILING │ 34399.9057(5.0)% EUR │ 32761.8150 EUR │ None │ │ 50.0% │ 0.0015 BTC │ │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ BTC/EUR (3) │ NOT TRIGGERED │ True │ TRAILING │ 36037.9965(10.0)% EUR │ 32761.8150 EUR │ None │ │ 20.0% │ 0.0006 BTC │ │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ BTC/EUR (4) │ TRIGGERED │ False │ TRAILING │ 34731.7793(5.0)% EUR │ 33077.8850 EUR │ 34637.09 │ 34967.12 │ 50.0% │ 0.0015 BTC │ 0.0015 │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ BTC/EUR (4) │ NOT TRIGGERED │ True │ TRAILING │ 36385.6735(10.0)% EUR │ 33077.8850 EUR │ None │ │ 20.0% │ 0.0006 BTC │ │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ BTC/EUR (5) │ NOT TRIGGERED │ True │ TRAILING │ 36530.2140(5.0)% EUR │ 34790.6800 EUR │ None │ │ 50.0% │ 0.0014 BTC │ │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ BTC/EUR (5) │ NOT TRIGGERED │ True │ TRAILING │ 38269.7480(10.0)% EUR │ 34790.6800 EUR │ None │ │ 20.0% │ 0.0006 BTC │ │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ DOT/EUR (6) │ NOT TRIGGERED │ True │ TRAILING │ 5.2741(5.0)% EUR │ 5.0230 EUR │ None │ │ 50.0% │ 9.9542 DOT │ │ -├────────────────────┼───────────────┼──────────┼──────────┼───────────────────────┼────────────────┼────────────────┼───────────────────┼──────────────┼─────────────┼───────────────────┤ -│ DOT/EUR (6) │ NOT TRIGGERED │ True │ TRAILING │ 5.5253(10.0)% EUR │ 5.0230 EUR │ None │ │ 20.0% │ 3.9817 DOT │ │ -╰────────────────────┴───────────────┴──────────┴──────────┴───────────────────────┴────────────────┴────────────────┴───────────────────┴──────────────┴─────────────┴───────────────────╯ +╭────────────────────┬───────────────┬──────────┬──────────┬─────────────────────────────────────┬──────────────┬────────────────┬─────────────────────────────────┬──────────────┬─────────────┬───────────────╮ +│ Trade (Trade id) │ Status │ Active │ Type │ Take profit (Initial Take Profit) │ Open price │ Sell price's │ High water mark │ Percentage │ Size │ Sold amount │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ BTC/EUR (1) │ NOT TRIGGERED │ False │ TRAILING │ 25714.89 (25714.89) (5.0)% EUR │ 24490.37 EUR │ None │ │ 50.0% │ 0.0020 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ BTC/EUR (1) │ NOT TRIGGERED │ False │ TRAILING │ 26939.41 (26939.41) (10.0)% EUR │ 24490.37 EUR │ None │ │ 20.0% │ 0.0008 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ DOT/EUR (2) │ TRIGGERED │ False │ TRAILING │ 4.24 (4.24) (5.0)% EUR │ 4.04 EUR │ 4.233 │ 4.30 EUR (2023-10-31 00:00) │ 50.0% │ 12.3732 DOT │ 12.3732 DOT │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ DOT/EUR (2) │ TRIGGERED │ False │ TRAILING │ 4.45 (4.45) (10.0)% EUR │ 4.04 EUR │ 4.377 │ 4.51 EUR (2023-11-01 20:00) │ 20.0% │ 4.9493 DOT │ 4.9493 DOT │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ BTC/EUR (3) │ NOT TRIGGERED │ False │ TRAILING │ 34326.09 (34326.09) (5.0)% EUR │ 32691.51 EUR │ None │ │ 50.0% │ 0.0015 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ BTC/EUR (3) │ NOT TRIGGERED │ False │ TRAILING │ 35960.66 (35960.66) (10.0)% EUR │ 32691.51 EUR │ None │ │ 20.0% │ 0.0006 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ BTC/EUR (4) │ TRIGGERED │ False │ TRAILING │ 34782.93 (34782.93) (5.0)% EUR │ 33126.60 EUR │ 34746.64 │ 34967.12 EUR (2023-11-10 22:00) │ 50.0% │ 0.0015 BTC │ 0.0015 BTC │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ BTC/EUR (4) │ NOT TRIGGERED │ False │ TRAILING │ 36439.26 (36439.26) (10.0)% EUR │ 33126.60 EUR │ None │ │ 20.0% │ 0.0006 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ BTC/EUR (5) │ NOT TRIGGERED │ True │ TRAILING │ 36504.22 (36504.22) (5.0)% EUR │ 34765.92 EUR │ None │ │ 50.0% │ 0.0014 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ BTC/EUR (5) │ NOT TRIGGERED │ True │ TRAILING │ 38242.51 (38242.51) (10.0)% EUR │ 34765.92 EUR │ None │ │ 20.0% │ 0.0006 BTC │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ DOT/EUR (6) │ NOT TRIGGERED │ True │ TRAILING │ 5.26 (5.26) (5.0)% EUR │ 5.01 EUR │ None │ │ 50.0% │ 9.9761 DOT │ │ +├────────────────────┼───────────────┼──────────┼──────────┼─────────────────────────────────────┼──────────────┼────────────────┼─────────────────────────────────┼──────────────┼─────────────┼───────────────┤ +│ DOT/EUR (6) │ NOT TRIGGERED │ True │ TRAILING │ 5.51 (5.51) (10.0)% EUR │ 5.01 EUR │ None │ │ 20.0% │ 3.9904 DOT │ │ +╰────────────────────┴───────────────┴──────────┴──────────┴─────────────────────────────────────┴──────────────┴────────────────┴─────────────────────────────────┴──────────────┴─────────────┴───────────────╯ ``` ### Backtest experiments diff --git a/examples/backtest_example/run_backtest.py b/examples/backtest_example/run_backtest.py index 4fa7bfd7..254b99b0 100644 --- a/examples/backtest_example/run_backtest.py +++ b/examples/backtest_example/run_backtest.py @@ -1,4 +1,5 @@ import time +from datetime import datetime import logging.config from datetime import datetime, timedelta @@ -124,6 +125,7 @@ def apply_strategy(self, context: Context, market_data): trade = context.get_trade(order_id=order.id) context.add_stop_loss( trade=trade, + trade_risk_type="trailing", percentage=5, sell_percentage=50 ) diff --git a/investing_algorithm_framework/domain/models/trade/trade.py b/investing_algorithm_framework/domain/models/trade/trade.py index 4262e077..323423e0 100644 --- a/investing_algorithm_framework/domain/models/trade/trade.py +++ b/investing_algorithm_framework/domain/models/trade/trade.py @@ -59,7 +59,9 @@ def __init__( status, net_gain=0, last_reported_price=None, + last_reported_price_datetime=None, high_water_mark=None, + high_water_mark_datetime=None, updated_at=None, stop_losses=None, take_profits=None, @@ -76,12 +78,47 @@ def __init__( self.remaining = remaining self.net_gain = net_gain self.last_reported_price = last_reported_price + self.last_reported_price_datetime = last_reported_price_datetime self.high_water_mark = high_water_mark + self.high_water_mark_datetime = high_water_mark_datetime self.status = status self.updated_at = updated_at self.stop_losses = stop_losses self.take_profits = take_profits + def update(self, data): + + if "status" in data: + self.status = TradeStatus.from_value(data["status"]) + + if TradeStatus.CLOSED.equals(self.status): + + # Set all stop losses to inactive + if self.stop_losses is not None: + for stop_loss in self.stop_losses: + stop_loss.active = False + + # set all take profits to inactive + if self.take_profits is not None: + for take_profit in self.take_profits: + take_profit.active = False + + if "last_reported_price" in data: + self.last_reported_price = data["last_reported_price"] + + if self.high_water_mark is None: + self.high_water_mark = data["last_reported_price"] + self.high_water_mark_datetime = \ + data["last_reported_price_datetime"] + else: + + if data["last_reported_price"] > self.high_water_mark: + self.high_water_mark = data["last_reported_price"] + self.high_water_mark_datetime = \ + data["last_reported_price_datetime"] + + return super().update(data) + @property def closed_prices(self): return [ diff --git a/investing_algorithm_framework/domain/models/trade/trade_stop_loss.py b/investing_algorithm_framework/domain/models/trade/trade_stop_loss.py index 58697ded..b688bde5 100644 --- a/investing_algorithm_framework/domain/models/trade/trade_stop_loss.py +++ b/investing_algorithm_framework/domain/models/trade/trade_stop_loss.py @@ -55,13 +55,16 @@ def __init__( total_amount_trade: float, sell_percentage: float = 100, active: bool = True, - sell_prices: str = None + sell_prices: str = None, + sell_price_dates: str = None, + high_water_mark_date: str = None, ): self.trade_id = trade_id self.trade_risk_type = trade_risk_type self.percentage = percentage self.sell_percentage = sell_percentage self.high_water_mark = open_price + self.high_water_mark_date = high_water_mark_date self.open_price = open_price self.stop_loss_price = self.high_water_mark * \ (1 - (self.percentage / 100)) @@ -69,8 +72,9 @@ def __init__( self.sold_amount = 0 self.active = active self.sell_prices = sell_prices + self.sell_price_dates = sell_price_dates - def update_with_last_reported_price(self, current_price: float): + def update_with_last_reported_price(self, current_price: float, date): """ Function to update the take profit price based on the last reported price. @@ -88,6 +92,8 @@ def update_with_last_reported_price(self, current_price: float): if TradeRiskType.FIXED.equals(self.trade_risk_type): # Check if the current price is less than the high water mark + if current_price > self.high_water_mark: + self.high_water_mark = current_price return else: # Check if the current price is less than the stop loss price @@ -95,6 +101,7 @@ def update_with_last_reported_price(self, current_price: float): return elif current_price > self.high_water_mark: self.high_water_mark = current_price + self.high_water_mark_date = date self.stop_loss_price = self.high_water_mark * \ (1 - (self.percentage / 100)) diff --git a/investing_algorithm_framework/domain/models/trade/trade_take_profit.py b/investing_algorithm_framework/domain/models/trade/trade_take_profit.py index c9bb0f4b..c9aeeda4 100644 --- a/investing_algorithm_framework/domain/models/trade/trade_take_profit.py +++ b/investing_algorithm_framework/domain/models/trade/trade_take_profit.py @@ -55,13 +55,16 @@ def __init__( total_amount_trade: float, sell_percentage: float = 100, active: bool = True, - sell_prices: str = None + sell_prices: str = None, + sell_price_dates: str = None, + high_water_mark_date: str = None, ): self.trade_id = trade_id self.trade_risk_type = trade_risk_type self.percentage = percentage self.sell_percentage = sell_percentage self.high_water_mark = None + self.high_water_mark_date = high_water_mark_date self.open_price = open_price self.take_profit_price = open_price * \ (1 + (self.percentage / 100)) @@ -69,8 +72,9 @@ def __init__( self.sold_amount = 0 self.active = active self.sell_prices = sell_prices + self.sell_price_dates = sell_price_dates - def update_with_last_reported_price(self, current_price: float): + def update_with_last_reported_price(self, current_price: float, date): """ Function to update the take profit price based on the last reported price. @@ -85,6 +89,17 @@ def update_with_last_reported_price(self, current_price: float): # Do nothing for fixed take profit if TradeRiskType.FIXED.equals(self.trade_risk_type): + + if self.high_water_mark is not None: + if current_price > self.high_water_mark: + self.high_water_mark = current_price + self.high_water_mark_date = date + else: + if current_price >= self.take_profit_price: + self.high_water_mark = current_price + self.high_water_mark_date = date + return + return else: @@ -92,8 +107,10 @@ def update_with_last_reported_price(self, current_price: float): if current_price >= self.take_profit_price: self.high_water_mark = current_price + self.high_water_mark_date = date new_take_profit_price = self.high_water_mark * \ (1 - (self.percentage / 100)) + if self.take_profit_price <= new_take_profit_price: self.take_profit_price = new_take_profit_price @@ -106,6 +123,7 @@ def update_with_last_reported_price(self, current_price: float): # Increase the high water mark and take profit price elif current_price > self.high_water_mark: self.high_water_mark = current_price + self.high_water_mark_date = date new_take_profit_price = self.high_water_mark * \ (1 - (self.percentage / 100)) diff --git a/investing_algorithm_framework/domain/utils/backtesting.py b/investing_algorithm_framework/domain/utils/backtesting.py index 85b820e0..73cdad83 100644 --- a/investing_algorithm_framework/domain/utils/backtesting.py +++ b/investing_algorithm_framework/domain/utils/backtesting.py @@ -7,7 +7,7 @@ from tabulate import tabulate from investing_algorithm_framework.domain import DATETIME_FORMAT, \ - BacktestDateRange, TradeStatus, OrderSide + BacktestDateRange, TradeStatus, OrderSide, TradeRiskType from investing_algorithm_framework.domain.exceptions import \ OperationalException from investing_algorithm_framework.domain.models.backtesting import \ @@ -26,6 +26,20 @@ r"created-at_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}\.json$" ) +def format_date(date) -> str: + """ + Format the date to the format YYYY-MM-DD HH:MM:SS + """ + + if date is None: + return "" + + if isinstance(date, datetime): + return date.strftime("%Y-%m-%d %H:%M") + else: + date = datetime.strptime(date, "%Y-%m-%d %H:%M:%S") + return date.strftime("%Y-%m-%d %H:%M") + def is_positive(number) -> bool: """ Check if a number is positive. @@ -91,8 +105,11 @@ def pretty_print_growth_evaluation(reports, precision=4): def pretty_print_stop_losses( backtest_report, - precision=4, - triggered_only=False + triggered_only=False, + amount_precesion=4, + price_precision=2, + time_precision=1, + percentage_precision=0 ): print(f"{COLOR_YELLOW}Stop losses overview{COLOR_RESET}") stop_loss_table = {} @@ -101,7 +118,7 @@ def pretty_print_stop_losses( def get_sold_amount(stop_loss): if stop_loss["sold_amount"] > 0: - return float(stop_loss["sold_amount"]) + return f"{float(stop_loss['sold_amount']):.{amount_precesion}f} {stop_loss['target_symbol']}" return "" @@ -118,10 +135,21 @@ def get_status(stop_loss): def get_high_water_mark(stop_loss): if stop_loss["high_water_mark"] is not None: - return float(stop_loss["high_water_mark"]) + return f"{float(stop_loss['high_water_mark']):.{price_precision}f} {stop_loss['trading_symbol']} {format_date(stop_loss['high_water_mark_date'])}" return "" + def get_stop_loss_price(take_profit): + + if TradeRiskType.TRAILING.equals(take_profit["trade_risk_type"]): + initial_price = take_profit["open_price"] + percentage = take_profit["percentage"] + initial_stop_loss_price = \ + initial_price * (1 - (percentage / 100)) + return f"{float(take_profit['stop_loss_price']):.{price_precision}f} ({(initial_stop_loss_price):.{price_precision}f}) ({take_profit['percentage']})% {take_profit['trading_symbol']}" + else: + return f"{float(take_profit['stop_loss_price']):.{price_precision}f}({take_profit['percentage']})% {take_profit['trading_symbol']}" + if triggered_only: for trade in trades: @@ -138,6 +166,7 @@ def get_high_water_mark(stop_loss): "open_price": stop_loss.open_price, "sell_percentage": stop_loss.sell_percentage, "high_water_mark": stop_loss.high_water_mark, + "high_water_mark_date": stop_loss.high_water_mark_date, "stop_loss_price": stop_loss.stop_loss_price, "sell_amount": stop_loss.sell_amount, "sold_amount": stop_loss.sold_amount, @@ -163,6 +192,8 @@ def get_high_water_mark(stop_loss): "open_price": stop_loss.open_price, "sell_percentage": stop_loss.sell_percentage, "high_water_mark": stop_loss.high_water_mark, + "high_water_mark_date": \ + stop_loss.high_water_mark_date, "stop_loss_price": stop_loss.stop_loss_price, "sell_amount": stop_loss.sell_amount, "sold_amount": stop_loss.sold_amount, @@ -186,23 +217,23 @@ def get_high_water_mark(stop_loss): stop_loss_table["Type"] = [ f"{stop_loss['trade_risk_type']}" for stop_loss in selection ] - stop_loss_table["stop loss"] = [ - f"{float(stop_loss['stop_loss_price']):.{precision}f}({stop_loss['percentage']}%) {stop_loss['trading_symbol']}" for stop_loss in selection + stop_loss_table["Stop Loss (Initial Stop Loss)"] = [ + get_stop_loss_price(stop_loss) for stop_loss in selection ] stop_loss_table["Open price"] = [ - f"{float(stop_loss['open_price']):.{precision}f} {stop_loss['trading_symbol']}" for stop_loss in selection if stop_loss['open_price'] is not None + f"{float(stop_loss['open_price']):.{price_precision}f} {stop_loss['trading_symbol']}" for stop_loss in selection if stop_loss['open_price'] is not None ] stop_loss_table["Sell price's"] = [ f"{stop_loss['sell_prices']}" for stop_loss in selection ] stop_loss_table["High water mark"] = [ - get_high_water_mark(stop_loss) for stop_loss in selection + f"{get_high_water_mark(stop_loss)}" for stop_loss in selection ] stop_loss_table["Percentage"] = [ f"{float(stop_loss['sell_percentage'])}%" for stop_loss in selection ] stop_loss_table["Size"] = [ - f"{float(stop_loss['sell_amount']):.{precision}f} {stop_loss['target_symbol']}" for stop_loss in selection + f"{float(stop_loss['sell_amount']):.{price_precision}f} {stop_loss['target_symbol']}" for stop_loss in selection ] stop_loss_table["Sold amount"] = [ get_sold_amount(stop_loss) for stop_loss in selection @@ -211,7 +242,12 @@ def get_high_water_mark(stop_loss): def pretty_print_take_profits( - backtest_report, precision=4, triggered_only=False + backtest_report, + triggered_only=False, + amount_precesion=4, + price_precision=2, + time_precision=1, + percentage_precision=0 ): print(f"{COLOR_YELLOW}Take profits overview{COLOR_RESET}") take_profit_table = {} @@ -220,16 +256,27 @@ def pretty_print_take_profits( def get_high_water_mark(take_profit): if take_profit["high_water_mark"] is not None: - return float(take_profit["high_water_mark"]) + return f"{float(take_profit['high_water_mark']):.{price_precision}f} {take_profit['trading_symbol']} ({format_date(take_profit['high_water_mark_date'])})" return "" def get_sold_amount(take_profit): if take_profit["sold_amount"] > 0: - return float(take_profit["sold_amount"]) + return f"{float(take_profit['sold_amount']):.{amount_precesion}f} {take_profit['target_symbol']}" return "" + def get_take_profit_price(take_profit): + + if TradeRiskType.TRAILING.equals(take_profit["trade_risk_type"]): + initial_price = take_profit["open_price"] + percentage = take_profit["percentage"] + initial_take_profit_price = \ + initial_price * (1 + (percentage / 100)) + return f"{float(take_profit['take_profit_price']):.{price_precision}f} ({(initial_take_profit_price):.{price_precision}f}) ({take_profit['percentage']})% {take_profit['trading_symbol']}" + else: + return f"{float(take_profit['take_profit_price']):.{price_precision}f}({take_profit['percentage']})% {take_profit['trading_symbol']}" + def get_status(take_profit): if take_profit.sold_amount == 0: @@ -257,6 +304,8 @@ def get_status(take_profit): "open_price": take_profit.open_price, "sell_percentage": take_profit.sell_percentage, "high_water_mark": take_profit.high_water_mark, + "high_water_mark_date": \ + take_profit.high_water_mark_date, "take_profit_price": take_profit.take_profit_price, "sell_amount": take_profit.sell_amount, "sold_amount": take_profit.sold_amount, @@ -280,6 +329,8 @@ def get_status(take_profit): "open_price": take_profit.open_price, "sell_percentage": take_profit.sell_percentage, "high_water_mark": take_profit.high_water_mark, + "high_water_mark_date": \ + take_profit.high_water_mark_date, "take_profit_price": take_profit.take_profit_price, "sell_amount": take_profit.sell_amount, "sold_amount": take_profit.sold_amount, @@ -304,24 +355,24 @@ def get_status(take_profit): f"{stop_loss['trade_risk_type']}" for stop_loss in selection ] - take_profit_table["Take profit"] = [ - f"{float(take_profit['take_profit_price']):.{precision}f}({take_profit['percentage']})% {take_profit['trading_symbol']}" for take_profit in selection + take_profit_table["Take profit (Initial Take Profit)"] = [ + get_take_profit_price(stop_loss) for stop_loss in selection ] take_profit_table["Open price"] = [ - f"{float(stop_loss['open_price']):.{precision}f} {stop_loss['trading_symbol']}" for stop_loss in selection if stop_loss['open_price'] is not None + f"{float(stop_loss['open_price']):.{price_precision}f} {stop_loss['trading_symbol']}" for stop_loss in selection if stop_loss['open_price'] is not None ] take_profit_table["Sell price's"] = [ f"{stop_loss['sell_prices']}" for stop_loss in selection ] # Print nothing if high water mark is None take_profit_table["High water mark"] = [ - get_high_water_mark(stop_loss) for stop_loss in selection + f"{get_high_water_mark(stop_loss)}" for stop_loss in selection ] take_profit_table["Percentage"] = [ f"{float(stop_loss['sell_percentage'])}%" for stop_loss in selection ] take_profit_table["Size"] = [ - f"{float(stop_loss['sell_amount']):.{precision}f} {stop_loss['target_symbol']}" for stop_loss in selection + f"{float(stop_loss['sell_amount']):.{amount_precesion}f} {stop_loss['target_symbol']}" for stop_loss in selection ] take_profit_table["Sold amount"] = [ get_sold_amount(stop_loss) for stop_loss in selection @@ -491,7 +542,13 @@ def pretty_print_percentage_positive_trades( print(f"{COLOR_YELLOW}Most positive trades:{COLOR_RESET} {COLOR_GREEN}Algorithm {percentages.name} {float(percentages.percentage_positive_trades):.{precision}f}%{COLOR_RESET}") -def pretty_print_trades(backtest_report, precision=4): +def pretty_print_trades( + backtest_report, + amount_precesion=4, + price_precision=2, + time_precision=1, + percentage_precision=2 +): def get_status(trade): status = "OPEN" @@ -521,7 +578,7 @@ def get_close_prices(trade): if number_of_sell_orders > 0: text += ", " - text += f"{sell_order.price}" + text += f"{float(sell_order.price):.{price_precision}f}" number_of_sell_orders += 1 return text @@ -547,6 +604,12 @@ def has_triggered_stop_losses(trade): ] return len(triggered) > 0 + def get_high_water_mark(trade): + if trade.high_water_mark is not None: + return f"{float(trade.high_water_mark):.{price_precision}f} {trade.trading_symbol} ({format_date(trade.high_water_mark_datetime)})" + + return "" + print(f"{COLOR_YELLOW}Trades overview{COLOR_RESET}") trades_table = {} trades_table["Pair (Trade id)"] = [ @@ -556,38 +619,48 @@ def has_triggered_stop_losses(trade): trades_table["Status"] = [ get_status(trade) for trade in backtest_report.trades ] + trades_table["Amount (remaining)"] = [ + f"{float(trade.amount):.{amount_precesion}f} ({float(trade.remaining):.{amount_precesion}f}) {trade.target_symbol}" + for trade in backtest_report.trades + ] trades_table[f"Net gain ({backtest_report.trading_symbol})"] = [ - f"{float(trade.net_gain):.{precision}f}" + f"{float(trade.net_gain):.{price_precision}f}" for trade in backtest_report.trades ] trades_table["Open date"] = [ - trade.opened_at for trade in backtest_report.trades + trade.opened_at.strftime("%Y-%m-%d %H:%M") for trade in backtest_report.trades if trade.opened_at is not None ] trades_table["Close date"] = [ - trade.closed_at for trade in backtest_report.trades + trade.closed_at.strftime("%Y-%m-%d %H:%M") for trade in backtest_report.trades if trade.closed_at is not None ] trades_table["Duration"] = [ - f"{trade.duration} hours" for trade in backtest_report.trades + f"{trade.duration:.{time_precision}f} hours" for trade in backtest_report.trades ] # Add (unrealized) to the net gain if the trade is still open trades_table[f"Net gain ({backtest_report.trading_symbol})"] = [ - f"{float(trade.net_gain_absolute):.{precision}f} ({float(trade.net_gain_percentage):.{precision}f}%)" + (" (unrealized)" if not TradeStatus.CLOSED.equals(trade.status) else "") + f"{float(trade.net_gain_absolute):.{price_precision}f} ({float(trade.net_gain_percentage):.{percentage_precision}f}%)" + (" (unrealized)" if not TradeStatus.CLOSED.equals(trade.status) else "") for trade in backtest_report.trades ] trades_table[f"Open price ({backtest_report.trading_symbol})"] = [ - trade.open_price for trade in backtest_report.trades + f"{trade.open_price:.{price_precision}f}" for trade in backtest_report.trades ] trades_table[ f"Close price's ({backtest_report.trading_symbol})" ] = [ get_close_prices(trade) for trade in backtest_report.trades ] + trades_table["High water mark"] = [ + get_high_water_mark(trade) for trade in backtest_report.trades + ] print(tabulate(trades_table, headers="keys", tablefmt="rounded_grid")) def pretty_print_backtest_reports_evaluation( backtest_reports_evaluation: BacktestReportsEvaluation, - precision=4, + amount_precesion=4, + price_precision=2, + time_precision=1, + percentage_precision=2, backtest_date_range: BacktestDateRange = None ) -> None: """ @@ -666,7 +739,10 @@ def pretty_print_backtest( show_triggered_stop_losses_only=False, show_take_profits=True, show_triggered_take_profits_only=False, - precision=4 + amount_precesion=4, + price_precision=2, + time_precision=1, + percentage_precision=2 ): """ Pretty print the backtest report to the console. @@ -694,14 +770,14 @@ def pretty_print_backtest( .:-+*%%%%- {COLOR_PURPLE}-+..#{COLOR_RESET}%%%+.{COLOR_PURPLE}+- +{COLOR_RESET}%%%#*=-: {COLOR_YELLOW}Number of runs:{COLOR_RESET}{COLOR_GREEN} {backtest_report.number_of_runs}{COLOR_RESET} .:-=*%%%%. {COLOR_PURPLE}+={COLOR_RESET} .%%# {COLOR_PURPLE}-+.-{COLOR_RESET}%%%%=-:.. {COLOR_YELLOW}Number of orders:{COLOR_RESET}{COLOR_GREEN} {backtest_report.number_of_orders}{COLOR_RESET} .:=+#%%%%%*###%%%%#*+#%%%%%%*+-: {COLOR_YELLOW}Initial balance:{COLOR_RESET}{COLOR_GREEN} {backtest_report.initial_unallocated}{COLOR_RESET} - +%%%%%%%%%%%%%%%%%%%= {COLOR_YELLOW}Final balance:{COLOR_RESET}{COLOR_GREEN} {float(backtest_report.total_value):.{precision}f}{COLOR_RESET} - :++ .=#%%%%%%%%%%%%%*- {COLOR_YELLOW}Total net gain:{COLOR_RESET}{COLOR_GREEN} {float(backtest_report.total_net_gain):.{precision}f} {float(backtest_report.total_net_gain_percentage):.{precision}}%{COLOR_RESET} - :++: :+%%%%%%#-. {COLOR_YELLOW}Growth:{COLOR_RESET}{COLOR_GREEN} {float(backtest_report.growth):.{precision}f} {float(backtest_report.growth_rate):.{precision}}%{COLOR_RESET} + +%%%%%%%%%%%%%%%%%%%= {COLOR_YELLOW}Final balance:{COLOR_RESET}{COLOR_GREEN} {float(backtest_report.total_value):.{price_precision}f}{COLOR_RESET} + :++ .=#%%%%%%%%%%%%%*- {COLOR_YELLOW}Total net gain:{COLOR_RESET}{COLOR_GREEN} {float(backtest_report.total_net_gain):.{price_precision}f} {float(backtest_report.total_net_gain_percentage):.{percentage_precision}}%{COLOR_RESET} + :++: :+%%%%%%#-. {COLOR_YELLOW}Growth:{COLOR_RESET}{COLOR_GREEN} {float(backtest_report.growth):.{price_precision}f} {float(backtest_report.growth_rate):.{percentage_precision}f}%{COLOR_RESET} :++: .%%%%%#= {COLOR_YELLOW}Number of trades closed:{COLOR_RESET}{COLOR_GREEN} {backtest_report.number_of_trades_closed}{COLOR_RESET} :++: .#%%%%%#*= {COLOR_YELLOW}Number of trades open(end of backtest):{COLOR_RESET}{COLOR_GREEN} {backtest_report.number_of_trades_open}{COLOR_RESET} :++- :%%%%%%%%%+= {COLOR_YELLOW}Percentage positive trades:{COLOR_RESET}{COLOR_GREEN} {backtest_report.percentage_positive_trades}%{COLOR_RESET} .++- -%%%%%%%%%%%+= {COLOR_YELLOW}Percentage negative trades:{COLOR_RESET}{COLOR_GREEN} {backtest_report.percentage_negative_trades}%{COLOR_RESET} - .++- .%%%%%%%%%%%%%+= {COLOR_YELLOW}Average trade size:{COLOR_RESET}{COLOR_GREEN} {float(backtest_report.average_trade_size):.{precision}f} {backtest_report.trading_symbol}{COLOR_RESET} + .++- .%%%%%%%%%%%%%+= {COLOR_YELLOW}Average trade size:{COLOR_RESET}{COLOR_GREEN} {float(backtest_report.average_trade_size):.{price_precision}f} {backtest_report.trading_symbol}{COLOR_RESET} .++- *%%%%%%%%%%%%%*+: {COLOR_YELLOW}Average trade duration:{COLOR_RESET}{COLOR_GREEN} {backtest_report.average_trade_duration} hours{COLOR_RESET} .++- %%%%%%%%%%%%%%#+= =++........:::%%%%%%%%%%%%%%*+- @@ -718,35 +794,35 @@ def pretty_print_backtest( position.symbol for position in backtest_report.positions ] position_table["Amount"] = [ - f"{float(position.amount):.{precision}f}" for position in + f"{float(position.amount):.{amount_precesion}f}" for position in backtest_report.positions ] position_table["Pending buy amount"] = [ - f"{float(position.amount_pending_buy):.{precision}f}" + f"{float(position.amount_pending_buy):.{amount_precesion}f}" for position in backtest_report.positions ] position_table["Pending sell amount"] = [ - f"{float(position.amount_pending_sell):.{precision}f}" + f"{float(position.amount_pending_sell):.{amount_precesion}f}" for position in backtest_report.positions ] position_table[f"Cost ({backtest_report.trading_symbol})"] = [ - f"{float(position.cost):.{precision}f}" + f"{float(position.cost):.{price_precision}f}" for position in backtest_report.positions ] position_table[f"Value ({backtest_report.trading_symbol})"] = [ - f"{float(position.value):.{precision}f}" + f"{float(position.value):.{price_precision}f} {backtest_report.trading_symbol}" for position in backtest_report.positions ] position_table["Percentage of portfolio"] = [ - f"{float(position.percentage_of_portfolio):.{precision}f}%" + f"{float(position.percentage_of_portfolio):.{percentage_precision}f}%" for position in backtest_report.positions ] position_table[f"Growth ({backtest_report.trading_symbol})"] = [ - f"{float(position.growth):.{precision}f}" + f"{float(position.growth):.{price_precision}f} {backtest_report.trading_symbol}" for position in backtest_report.positions ] position_table["Growth_rate"] = [ - f"{float(position.growth_rate):.{precision}f}%" + f"{float(position.growth_rate):.{percentage_precision}f}%" for position in backtest_report.positions ] print( @@ -764,20 +840,32 @@ def has_triggered_stop_losses(trade): return len(triggered) > 0 if show_trades: - pretty_print_trades(backtest_report, precision=precision) + pretty_print_trades( + backtest_report, + amount_precesion=amount_precesion, + price_precision=price_precision, + time_precision=time_precision, + percentage_precision=percentage_precision + ) if show_stop_losses: pretty_print_stop_losses( backtest_report=backtest_report, - precision=precision, - triggered_only=show_triggered_stop_losses_only + triggered_only=show_triggered_stop_losses_only, + amount_precesion=amount_precesion, + price_precision=price_precision, + time_precision=time_precision, + percentage_precision=percentage_precision ) if show_take_profits: pretty_print_take_profits( backtest_report=backtest_report, - precision=precision, - triggered_only=show_triggered_take_profits_only + triggered_only=show_triggered_take_profits_only, + amount_precesion=amount_precesion, + price_precision=price_precision, + time_precision=time_precision, + percentage_precision=percentage_precision ) diff --git a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py index 406e7a3e..c71800dc 100644 --- a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +++ b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py @@ -410,6 +410,12 @@ def get_data( first_row_datetime = parser.parse(first_row["Datetime"][0]) + return { + "symbol": self.symbol, + "bid": float(first_row["Close"][0]), + "ask": float(first_row["Close"][0]), + "datetime": first_row_datetime, + } # Calculate the bid and ask price based on the high and low price return { "symbol": self.symbol, diff --git a/investing_algorithm_framework/infrastructure/models/trades/trade.py b/investing_algorithm_framework/infrastructure/models/trades/trade.py index 7907f304..7a258f90 100644 --- a/investing_algorithm_framework/infrastructure/models/trades/trade.py +++ b/investing_algorithm_framework/infrastructure/models/trades/trade.py @@ -32,6 +32,11 @@ class SQLTrade(Trade, SQLBaseModel, SQLAlchemyModelExtension): * remaining: float, the remaining amount of the trade * net_gain: float, the net gain of the trade * last_reported_price: float, the last reported price of the trade + * last_reported_price_datetime: datetime, the datetime when the last + reported price was reported + * high_water_mark: float, the high water mark of the trade + * high_water_mark_datetime: datetime, the datetime when the high water + mark was reported * created_at: datetime, the datetime when the trade was created * updated_at: datetime, the datetime when the trade was last updated * status: str, the status of the trade @@ -58,7 +63,9 @@ class SQLTrade(Trade, SQLBaseModel, SQLAlchemyModelExtension): net_gain = Column(Float, default=0) cost = Column(Float, default=0) last_reported_price = Column(Float, default=None) + last_reported_price_datetime = Column(DateTime, default=None) high_water_mark = Column(Float, default=None) + high_water_mark_datetime = Column(DateTime, default=None) updated_at = Column(DateTime, default=None) status = Column(String, default=TradeStatus.CREATED.value) # Stop losses should be actively loaded @@ -89,7 +96,9 @@ def __init__( net_gain=0, cost=0, last_reported_price=None, + last_reported_price_datetime=None, high_water_mark=None, + high_water_mark_datetime=None, sell_orders=[], stop_losses=[], take_profits=[], @@ -105,7 +114,9 @@ def __init__( self.net_gain = net_gain self.cost = cost self.last_reported_price = last_reported_price + self.last_reported_price_datetime = last_reported_price_datetime self.high_water_mark = high_water_mark + self.high_water_mark_datetime = high_water_mark_datetime self.opened_at = opened_at self.updated_at = updated_at self.status = status diff --git a/investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py b/investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py index 44e82079..eeef5a63 100644 --- a/investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py +++ b/investing_algorithm_framework/infrastructure/models/trades/trade_stop_loss.py @@ -31,8 +31,10 @@ class SQLTradeStopLoss(TradeStopLoss, SQLBaseModel, SQLAlchemyModelExtension): sell_percentage = Column(Float) open_price = Column(Float) high_water_mark = Column(Float) + high_water_mark_date = Column(String) stop_loss_price = Column(Float) sell_prices = Column(String) + sell_dates = Column(String) sell_amount = Column(Float) sold_amount = Column(Float) active = Column(Boolean) diff --git a/investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py b/investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py index d96be334..30c1f33b 100644 --- a/investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py +++ b/investing_algorithm_framework/infrastructure/models/trades/trade_take_profit.py @@ -32,8 +32,10 @@ class SQLTradeTakeProfit( sell_percentage = Column(Float) open_price = Column(Float) high_water_mark = Column(Float) + high_water_mark_date = Column(String) sell_prices = Column(String) take_profit_price = Column(Float) sell_amount = Column(Float) + sell_dates = Column(String) sold_amount = Column(Float) active = Column(Boolean) diff --git a/investing_algorithm_framework/services/order_service/order_backtest_service.py b/investing_algorithm_framework/services/order_service/order_backtest_service.py index 54e88e7e..56b443fc 100644 --- a/investing_algorithm_framework/services/order_service/order_backtest_service.py +++ b/investing_algorithm_framework/services/order_service/order_backtest_service.py @@ -36,10 +36,24 @@ def __init__( market_data_source_service def create(self, data, execute=True, validate=True, sync=True) -> Order: + """ + Override the create method to set the created_at and + updated_at attributes to the current backtest time. + + Args: + data (dict): Dictionary containing the order data + execute (bool): Flag to execute the order + validate (bool): Flag to validate the order + sync (bool): Flag to sync the order + + Returns: + Order: Created order object + """ config = self.configuration_service.get_config() # Make sure the created_at is set to the current backtest time data["created_at"] = config[BACKTESTING_INDEX_DATETIME] + data["updated_at"] = config[BACKTESTING_INDEX_DATETIME] # Call super to have standard behavior return super(OrderBacktestService, self)\ .create(data, execute, validate, sync) diff --git a/investing_algorithm_framework/services/trade_service/trade_service.py b/investing_algorithm_framework/services/trade_service/trade_service.py index badb3103..27688f28 100644 --- a/investing_algorithm_framework/services/trade_service/trade_service.py +++ b/investing_algorithm_framework/services/trade_service/trade_service.py @@ -1,9 +1,11 @@ import logging +from datetime import datetime, timezone from queue import PriorityQueue from investing_algorithm_framework.domain import OrderStatus, TradeStatus, \ Trade, OperationalException, TradeRiskType, PeekableQueue, OrderType, \ - OrderSide, MarketDataType + OrderSide, MarketDataType, Environment, ENVIRONMENT, \ + BACKTESTING_INDEX_DATETIME from investing_algorithm_framework.services.repository_service import \ RepositoryService @@ -224,6 +226,7 @@ def update(self, trade_id, data) -> Trade: Returns: Trade object """ + # Update the stop losses and take profits if last reported price # is updated if "last_reported_price" in data: @@ -233,17 +236,38 @@ def update(self, trade_id, data) -> Trade: take_profits = trade.take_profits to_be_saved_take_profits = [] + # Check if 'update_at' attribute is in data + + if 'last_reported_price_date' in data: + last_reported_price_date = data["last_reported_price_date"] + else: + + # Check if config environment has value BACKTEST + config = self.configuration_service.get_config() + environment = config[ENVIRONMENT] + + if Environment.BACKTEST.equals(environment): + last_reported_price_date = \ + config[BACKTESTING_INDEX_DATETIME] + else: + last_reported_price_date = \ + datetime.now(tz=timezone.utc) + for stop_loss in stop_losses: - stop_loss.update_with_last_reported_price( - data["last_reported_price"] - ) - to_be_saved_stop_losses.append(stop_loss) + + if stop_loss.active: + stop_loss.update_with_last_reported_price( + data["last_reported_price"], last_reported_price_date + ) + to_be_saved_stop_losses.append(stop_loss) for take_profit in take_profits: - take_profit.update_with_last_reported_price( - data["last_reported_price"] - ) - to_be_saved_take_profits.append(take_profit) + + if take_profit.active: + take_profit.update_with_last_reported_price( + data["last_reported_price"], last_reported_price_date + ) + to_be_saved_take_profits.append(take_profit) self.trade_stop_loss_repository\ .save_objects(to_be_saved_stop_losses) @@ -359,11 +383,6 @@ def create_order_metadata_with_trade_context( self._create_trade_metadata_with_sell_order(sell_order) else: - if trades is not None: - self._create_trade_metadata_with_sell_order_and_trades( - sell_order, trades - ) - if stop_losses is not None: self._create_stop_loss_metadata_with_sell_order( sell_order_id, stop_losses @@ -374,6 +393,11 @@ def create_order_metadata_with_trade_context( sell_order_id, take_profits ) + if trades is not None: + self._create_trade_metadata_with_sell_order_and_trades( + sell_order, trades + ) + # Retrieve all trades metadata objects order_metadatas = self.order_metadata_repository.get_all({ "order_id": sell_order_id @@ -602,6 +626,7 @@ def update_trades_with_market_data(self, market_data): last_row = data.tail(1) update_data = { "last_reported_price": last_row["Close"][0], + "last_reported_price_datetime": last_row["Datetime"][0], "updated_at": last_row["Datetime"][0] } self.update(open_trade.id, update_data) @@ -850,15 +875,15 @@ def get_triggered_take_profit_orders(self): if available_amount == 0: continue - for take_proft in open_trade.take_profits: + for take_profit in open_trade.take_profits: if ( - take_proft.active and - take_proft.has_triggered(open_trade.last_reported_price) + take_profit.active and + take_profit.has_triggered(open_trade.last_reported_price) ): - triggered_take_profits.append(take_proft) + triggered_take_profits.append(take_profit) - to_be_saved_take_profit_objects.append(take_proft) + to_be_saved_take_profit_objects.append(take_profit) if len(triggered_take_profits) > 0: take_profits_by_target_symbol[open_trade] = \ diff --git a/tests/app/algorithm/test_trade_price_update.py b/tests/app/algorithm/test_trade_price_update.py index b7e0dfe8..44caf49c 100644 --- a/tests/app/algorithm/test_trade_price_update.py +++ b/tests/app/algorithm/test_trade_price_update.py @@ -17,15 +17,6 @@ def apply_strategy(self, context, market_data): pass -class StrategyTwo(TradingStrategy): - time_unit = TimeUnit.SECOND - interval = 2 - market_data_sources = ["BTC/EUR-ohlcv", "BTC/EUR-ticker"] - - def apply_strategy(self, context, market_data): - pass - - class Test(TestCase): def setUp(self) -> None: @@ -103,7 +94,6 @@ def test_trade_recent_price_update(self): ) algorithm = Algorithm() algorithm.add_strategy(StrategyOne) - algorithm.add_strategy(StrategyTwo) app.add_algorithm(algorithm) app.set_config( "DATE_TIME", datetime(2023, 11, 2, 7, 59, tzinfo=timezone.utc) @@ -120,7 +110,6 @@ def test_trade_recent_price_update(self): strategy_orchestration_service = app.algorithm\ .strategy_orchestrator_service self.assertTrue(strategy_orchestration_service.has_run("StrategyOne")) - self.assertTrue(strategy_orchestration_service.has_run("StrategyTwo")) # Check that the last reported price is updated trade = app.context.get_trades()[0] diff --git a/tests/domain/models/trades/test_trade_take_profit.py b/tests/domain/models/trades/test_trade_take_profit.py index c2191f0c..0a7057bf 100644 --- a/tests/domain/models/trades/test_trade_take_profit.py +++ b/tests/domain/models/trades/test_trade_take_profit.py @@ -52,6 +52,16 @@ def test_is_triggered_default(self): self.assertTrue(take_profit.has_triggered(22)) def test_is_triggered_trailing(self): + """ + Test the trailing stop loss + + * Open price: 20 + * Percentage: 10% + * Sell percentage: 50% + + Initial take profit price: 22 + + """ take_profit = TradeTakeProfit( trade_id=1, trade_risk_type="trailing", diff --git a/tests/services/test_trade_service.py b/tests/services/test_trade_service.py index e4abd90b..13cd9d51 100644 --- a/tests/services/test_trade_service.py +++ b/tests/services/test_trade_service.py @@ -1,3 +1,4 @@ +from datetime import datetime from investing_algorithm_framework import PortfolioConfiguration, \ MarketCredential, OrderStatus, TradeStatus, TradeRiskType from tests.resources import TestBase @@ -1050,12 +1051,14 @@ def test_get_triggered_stop_loss_orders(self): trade_one_id, { "last_reported_price": 17, + "last_reported_price_datetime": datetime.now(), } ) trade_service.update( trade_two_id, { "last_reported_price": 7, + "last_reported_price_datetime": datetime.now(), } ) sell_order_data = trade_service.get_triggered_stop_loss_orders() @@ -1166,12 +1169,14 @@ def test_get_triggered_stop_loss_orders_with_unfilled_order(self): trade_one_id, { "last_reported_price": 17, + "last_reported_price_datetime": datetime.now(), } ) trade_service.update( trade_two_id, { "last_reported_price": 7, + "last_reported_price_datetime": datetime.now(), } ) sell_order_data = trade_service.get_triggered_stop_loss_orders() @@ -1292,12 +1297,14 @@ def test_get_triggered_stop_loss_orders_with_cancelled_order(self): trade_one_id, { "last_reported_price": 17, + "last_reported_price_datetime": datetime.now(), } ) trade_service.update( trade_two_id, { "last_reported_price": 7, + "last_reported_price_datetime": datetime.now(), } ) sell_order_data = trade_service.get_triggered_stop_loss_orders() @@ -1530,12 +1537,14 @@ def test_get_triggered_stop_loss_orders_with_partially_filled_orders(self): trade_one_id, { "last_reported_price": 17, + "last_reported_price_datetime": datetime.now(), } ) trade_service.update( trade_two_id, { "last_reported_price": 7, + "last_reported_price_datetime": datetime.now(), } ) sell_order_data = trade_service.get_triggered_stop_loss_orders() @@ -1685,12 +1694,13 @@ def test_get_triggered_take_profits_orders(self): trade_one_id, { "last_reported_price": 22, + "last_reported_price_datetime": datetime.now(), } ) trade_service.update( trade_two_id, { - "last_reported_price": 11, + "last_reported_price": 11, "last_reported_price_datetime": datetime.now(), } ) sell_order_data = trade_service.get_triggered_take_profit_orders() @@ -1711,12 +1721,14 @@ def test_get_triggered_take_profits_orders(self): trade_one_id, { "last_reported_price": 25, + "last_reported_price_datetime": datetime.now(), } ) trade_two = trade_service.update( trade_two_id, { "last_reported_price": 14, + "last_reported_price_datetime": datetime.now(), } ) @@ -1747,12 +1759,14 @@ def test_get_triggered_take_profits_orders(self): trade_one_id, { "last_reported_price": 22.4, + "last_reported_price_datetime": datetime.now(), } ) trade_two = trade_service.update( trade_two_id, { "last_reported_price": 12.5, + "last_reported_price_datetime": datetime.now(), } ) @@ -1876,12 +1890,14 @@ def test_get_triggered_take_profits_with_unfilled_order(self): trade_one_id, { "last_reported_price": 22, + "last_reported_price_datetime": datetime.now(), } ) trade_service.update( trade_two_id, { "last_reported_price": 11, + "last_reported_price_datetime": datetime.now(), } ) sell_order_data = trade_service.get_triggered_take_profit_orders() @@ -1902,12 +1918,12 @@ def test_get_triggered_take_profits_with_unfilled_order(self): for data in sell_order_data: order_service.create(data) - # Take profit 2 + # Take profit 2 take_profit_one = trade_take_profit_repository.get( take_profit_one.id ) self.assertEqual(22, take_profit_one.take_profit_price) - self.assertEqual(None, take_profit_one.high_water_mark) + self.assertEqual(22, take_profit_one.high_water_mark) self.assertEqual(10, take_profit_one.sold_amount) trade_one = self.app.container.trade_service().find( @@ -1921,12 +1937,14 @@ def test_get_triggered_take_profits_with_unfilled_order(self): trade_one_id, { "last_reported_price": 25, + "last_reported_price_datetime": datetime.now(), } ) trade_two = trade_service.update( trade_two_id, { "last_reported_price": 14, + "last_reported_price_datetime": datetime.now(), } ) @@ -1957,12 +1975,14 @@ def test_get_triggered_take_profits_with_unfilled_order(self): trade_one_id, { "last_reported_price": 22.4, + "last_reported_price_datetime": datetime.now(), } ) trade_two = trade_service.update( trade_two_id, { "last_reported_price": 12.5, + "last_reported_price_datetime": datetime.now(), } ) @@ -2089,12 +2109,14 @@ def test_get_triggered_take_profits_orders_with_cancelled_order(self): trade_one_id, { "last_reported_price": 22, + "last_reported_price_datetime": datetime.now(), } ) trade_service.update( trade_two_id, { "last_reported_price": 11, + "last_reported_price_datetime": datetime.now(), } ) sell_order_data = trade_service.get_triggered_take_profit_orders() @@ -2137,12 +2159,14 @@ def test_get_triggered_take_profits_orders_with_cancelled_order(self): trade_one_id, { "last_reported_price": 25, + "last_reported_price_datetime": datetime.now(), } ) trade_two = trade_service.update( trade_two_id, { "last_reported_price": 14, + "last_reported_price_datetime": datetime.now(), } ) @@ -2173,12 +2197,14 @@ def test_get_triggered_take_profits_orders_with_cancelled_order(self): trade_one_id, { "last_reported_price": 22.4, + "last_reported_price_datetime": datetime.now(), } ) trade_two = trade_service.update( trade_two_id, { "last_reported_price": 12.5, + "last_reported_price_datetime": datetime.now(), } ) @@ -2432,12 +2458,14 @@ def test_get_triggered_tp_orders_with_partially_filled_orders(self): trade_one_id, { "last_reported_price": 22, + "last_reported_price_datetime": datetime.now(), } ) trade_service.update( trade_two_id, { "last_reported_price": 11, + "last_reported_price_datetime": datetime.now(), } ) sell_order_data = trade_service.get_triggered_take_profit_orders() @@ -2480,12 +2508,14 @@ def test_get_triggered_tp_orders_with_partially_filled_orders(self): trade_one_id, { "last_reported_price": 25, + "last_reported_price_datetime": datetime.now(), } ) trade_two = trade_service.update( trade_two_id, { "last_reported_price": 14, + "last_reported_price_datetime": datetime.now(), } ) @@ -2516,8 +2546,426 @@ def test_get_triggered_tp_orders_with_partially_filled_orders(self): trade_one_id, { "last_reported_price": 22.4, + "last_reported_price_datetime": datetime.now(), } ) sell_order_data = trade_service.get_triggered_take_profit_orders() self.assertEqual(0, len(sell_order_data)) + + def test_deactivation_of_take_profits_when_stop_losses_are_triggered(self): + """ + Test for triggered stop loss orders: + + 1. Create a buy order for ADA with amount 20 at 20 EUR + 2. Create a stop loss with fixed percentage of 10 and + sell percentage 50 for the trade. This is a stop loss price + of 18 EUR + 3. Create a take profit with a trailing percentage of 10 and + sell percentage 25 for the trade. + + The first take profit will trigger at 22 EUR, and the second + take profit will set its high water mark and take profit price at 22 EUR, and only trigger if the price goes down from take profit price. + + 4. Create a buy order for DOT with amount 20 at 10 EUR + 5. Create a trailing stop loss with percentage of 10 and + sell percentage 25 for the trade. This is a stop loss price + initially set at 7 EUR + 6. Update the last reported price of ada to 17 EUR, triggering 2 + stop loss orders + 7. Update the last reported price of dot to 7 EUR, triggering 1 + stop loss order + 8. Check that the triggered stop loss orders are correct + + """ + order_service = self.app.container.order_service() + trade_take_profit_repository = self.app.container.\ + trade_take_profit_repository() + buy_order_one = order_service.create( + { + "target_symbol": "ADA", + "trading_symbol": "EUR", + "amount": 20, + "filled": 20, + "remaining": 0, + "order_side": "BUY", + "price": 20, + "order_type": "LIMIT", + "portfolio_id": 1, + "status": "CLOSED", + } + ) + + trade_service = self.app.container.trade_service() + trade_one = self.app.container.trade_service().find( + {"order_id": buy_order_one.id} + ) + trade_one_id = trade_one.id + stop_loss_one = trade_service.add_stop_loss( + trade_one, + 10, + "fixed", + sell_percentage=100, + ) + self.assertEqual(18, stop_loss_one.stop_loss_price) + take_profit_one = trade_service.add_take_profit( + trade_one, + 10, + "trailing", + sell_percentage=25, + ) + + # Update the last reported price of ada to 17 EUR, triggering 1 + # stop loss order + trade_service.update( + trade_one_id, + { + "last_reported_price": 17, + "last_reported_price_datetime": datetime.now(), + } + ) + sell_order_data = trade_service.get_triggered_stop_loss_orders() + self.assertEqual(1, len(sell_order_data)) + + for order_data in sell_order_data: + self.assertEqual("SELL", order_data["order_side"]) + self.assertEqual("EUR", order_data["trading_symbol"]) + self.assertEqual(1, order_data["portfolio_id"]) + self.assertEqual("LIMIT", order_data["order_type"]) + self.assertEqual(17, order_data["price"]) + self.assertEqual(20, order_data["amount"]) + self.assertEqual("ADA", order_data["target_symbol"]) + sell_order = order_service.create(order_data) + + # Trade should be closed + trade_one = trade_service.get(trade_one_id) + self.assertEqual(0, trade_one.remaining) + self.assertEqual(20, trade_one.amount) + self.assertEqual("CLOSED", trade_one.status) + + # All stop losses should be deactivated + for stop_loss in trade_one.stop_losses: + self.assertFalse(stop_loss.active) + + # All take profits should be deactivated + for take_profit in trade_one.take_profits: + self.assertFalse(take_profit.active) + + def test_deactivation_of_stop_losses_when_take_profits_are_triggered(self): + """ + Test for triggered stop loss orders: + + 1. Create a buy order for ADA with amount 20 at 20 EUR + 2. Create a stop loss with fixed percentage of 10 and + sell percentage 50 for the trade. This is a stop loss price + of 18 EUR + 3. Create a take profit with a trailing percentage of 10 and + sell percentage 25 for the trade. + + The first take profit will trigger at 22 EUR, and the second + take profit will set its high water mark and take profit price at 22 EUR, and only trigger if the price goes down from take profit price. + + 4. Create a buy order for DOT with amount 20 at 10 EUR + 5. Create a trailing stop loss with percentage of 10 and + sell percentage 25 for the trade. This is a stop loss price + initially set at 7 EUR + 6. Update the last reported price of ada to 17 EUR, triggering 2 + stop loss orders + 7. Update the last reported price of dot to 7 EUR, triggering 1 + stop loss order + 8. Check that the triggered stop loss orders are correct + + """ + order_service = self.app.container.order_service() + trade_take_profit_repository = self.app.container.\ + trade_take_profit_repository() + buy_order_one = order_service.create( + { + "target_symbol": "ADA", + "trading_symbol": "EUR", + "amount": 20, + "filled": 20, + "remaining": 0, + "order_side": "BUY", + "price": 20, + "order_type": "LIMIT", + "portfolio_id": 1, + "status": "CLOSED", + } + ) + + trade_service = self.app.container.trade_service() + trade_one = self.app.container.trade_service().find( + {"order_id": buy_order_one.id} + ) + trade_one_id = trade_one.id + take_profit_one = trade_service.add_take_profit( + trade_one, + 10, + "fixed", + sell_percentage=100, + ) + self.assertEqual(22, take_profit_one.take_profit_price) + stop_loss_one = trade_service.add_stop_loss( + trade_one, + 10, + "trailing", + sell_percentage=25, + ) + + # Update the last reported price of ada to 17 EUR, triggering 1 + # stop loss order + trade_service.update( + trade_one_id, + { + "last_reported_price": 22, + "last_reported_price_datetime": datetime.now(), + } + ) + sell_order_data = trade_service.get_triggered_take_profit_orders() + self.assertEqual(1, len(sell_order_data)) + + for order_data in sell_order_data: + self.assertEqual("SELL", order_data["order_side"]) + self.assertEqual("EUR", order_data["trading_symbol"]) + self.assertEqual(1, order_data["portfolio_id"]) + self.assertEqual("LIMIT", order_data["order_type"]) + self.assertEqual(22, order_data["price"]) + self.assertEqual(20, order_data["amount"]) + self.assertEqual("ADA", order_data["target_symbol"]) + sell_order = order_service.create(order_data) + + # Trade should be closed + trade_one = trade_service.get(trade_one_id) + self.assertEqual(0, trade_one.remaining) + self.assertEqual(20, trade_one.amount) + self.assertEqual("CLOSED", trade_one.status) + + # All stop losses should be deactivated + for stop_loss in trade_one.stop_losses: + self.assertFalse(stop_loss.active) + + # All take profits should be deactivated + for take_profit in trade_one.take_profits: + self.assertFalse(take_profit.active) + + def test_update_stop_losses_trailing_price_increase(self): + """ + Test if the stop losses are triggered correctly when the last reported + price is updated. This test will check for both fixed and trailing + stop losses if they are triggered correctly and also if the high water + mark is updated correctly. + + 1. Create a buy order for ADA with amount 20 at 20 EUR + 2. Create a stop loss with fixed percentage of 10 and + sell percentage 10 for the trade. This is a stop loss price + of 18 EUR + 3. Create a stop loss with trailing percentage of 10 and + sell percentage 25 for the trade. This is a stop loss price + initially set at 18 EUR + 4. Update the last reported price of ada to 21 EUR, triggering 0 + stop loss orders. The trailing stop loss should be updated to + 19.8 EUR. Both stop losses should have their high water mark + set to 21 EUR + 5. Update the last reported price of ada to 22 EUR. The trailing + stop loss should be updated to 20 EUR, and the fixed stop loss + should not be triggered. Both stop losses should have their + high water mark set to 22 EUR. + """ + order_service = self.app.container.order_service() + stop_loss_repository = self.app.container.\ + trade_stop_loss_repository() + buy_order_one = order_service.create( + { + "target_symbol": "ADA", + "trading_symbol": "EUR", + "amount": 20, + "filled": 20, + "remaining": 0, + "order_side": "BUY", + "price": 20, + "order_type": "LIMIT", + "portfolio_id": 1, + "status": "CLOSED", + } + ) + + trade_service = self.app.container.trade_service() + trade_one = self.app.container.trade_service().find( + {"order_id": buy_order_one.id} + ) + trade_one_id = trade_one.id + stop_loss_one = trade_service.add_stop_loss( + trade_one, + 10, + "fixed", + sell_percentage=25, + ) + self.assertEqual(18, stop_loss_one.stop_loss_price) + + # Create a stop loss with a trailing percentage of 10 and + # sell percentage 25 for the trade. + stop_loss_two = trade_service.add_stop_loss( + trade_one, + 10, + "trailing", + sell_percentage=25, + ) + self.assertEqual(18, stop_loss_two.stop_loss_price) + self.assertEqual(20, stop_loss_two.high_water_mark) + + # Update the last reported price of ada to 21 EUR, triggering 0 + # stop loss orders. The trailing stop loss should be updated to + # 18.9. Both stop losses should have their high water mark + # set to 21 EUR + + trade_service.update( + trade_one_id, + { + "last_reported_price": 21, + "last_reported_price_datetime": datetime.now(), + } + ) + + stop_loss_one = stop_loss_repository.get( + stop_loss_one.id + ) + self.assertEqual(18, stop_loss_one.stop_loss_price) + self.assertEqual(21, stop_loss_one.high_water_mark) + + stop_loss_two = stop_loss_repository.get( + stop_loss_two.id + ) + self.assertAlmostEqual(18.9, stop_loss_two.stop_loss_price) + self.assertEqual(21, stop_loss_two.high_water_mark) + + trade_service.update( + trade_one_id, + { + "last_reported_price": 22, + "last_reported_price_datetime": datetime.now(), + } + ) + + stop_loss_one = stop_loss_repository.get( + stop_loss_one.id + ) + self.assertEqual(18, stop_loss_one.stop_loss_price) + self.assertEqual(22, stop_loss_one.high_water_mark) + + stop_loss_two = stop_loss_repository.get( + stop_loss_two.id + ) + self.assertAlmostEqual(19.8, stop_loss_two.stop_loss_price) + self.assertEqual(22, stop_loss_two.high_water_mark) + + def test_update_latest_price_and_take_profits(self): + """ + Test if the stop losses are triggered correctly when the last reported + price is updated. This test will check for both fixed and trailing + stop losses if they are triggered correctly and also if the high water + mark is updated correctly. + + 1. Create a buy order for ADA with amount 20 at 20 EUR + 2. Create a stop loss with fixed percentage of 10 and + sell percentage 10 for the trade. This is a stop loss price + of 18 EUR + 3. Create a stop loss with trailing percentage of 10 and + sell percentage 25 for the trade. This is a stop loss price + initially set at 18 EUR + 4. Update the last reported price of ada to 21 EUR, triggering 0 + stop loss orders. The trailing stop loss should be updated to + 19.8 EUR. Both stop losses should have their high water mark + set to 21 EUR + 5. Update the last reported price of ada to 22 EUR. The trailing + stop loss should be updated to 20 EUR, and the fixed stop loss + should not be triggered. Both stop losses should have their + high water mark set to 22 EUR. + """ + order_service = self.app.container.order_service() + take_profit_repository = self.app.container.\ + trade_take_profit_repository() + buy_order_one = order_service.create( + { + "target_symbol": "ADA", + "trading_symbol": "EUR", + "amount": 20, + "filled": 20, + "remaining": 0, + "order_side": "BUY", + "price": 20, + "order_type": "LIMIT", + "portfolio_id": 1, + "status": "CLOSED", + } + ) + + trade_service = self.app.container.trade_service() + trade_one = self.app.container.trade_service().find( + {"order_id": buy_order_one.id} + ) + trade_one_id = trade_one.id + take_profit_one = trade_service.add_take_profit( + trade_one, + 10, + "fixed", + sell_percentage=25, + ) + self.assertEqual(22, take_profit_one.take_profit_price) + + # Create a stop loss with a trailing percentage of 10 and + # sell percentage 25 for the trade. + take_profit_two = trade_service.add_take_profit( + trade_one, + 10, + "trailing", + sell_percentage=25, + ) + self.assertEqual(22, take_profit_two.take_profit_price) + self.assertEqual(None, take_profit_two.high_water_mark) + + # Update the last reported price of ada to 21 EUR, triggering 0 + # stop loss orders. The trailing stop loss should be updated to + # 18.9. Both stop losses should have their high water mark + # set to 21 EUR + + trade_service.update( + trade_one_id, + { + "last_reported_price": 21, + "last_reported_price_datetime": datetime.now(), + } + ) + + take_profit_one = take_profit_repository.get( + take_profit_one.id + ) + self.assertEqual(22, take_profit_one.take_profit_price) + self.assertEqual(None, take_profit_one.high_water_mark) + + take_profit_two = take_profit_repository.get( + take_profit_two.id + ) + + self.assertAlmostEqual(22, take_profit_two.take_profit_price) + self.assertEqual(None, take_profit_two.high_water_mark) + + trade_service.update( + trade_one_id, + { + "last_reported_price": 25, + "last_reported_price_datetime": datetime.now(), + } + ) + + take_profit_one = take_profit_repository.get( + take_profit_one.id + ) + self.assertEqual(22, take_profit_one.take_profit_price) + self.assertEqual(25, take_profit_one.high_water_mark) + + take_profit_two = take_profit_repository.get( + take_profit_two.id + ) + self.assertAlmostEqual(22.5, take_profit_two.take_profit_price) + self.assertEqual(25, take_profit_two.high_water_mark)