From 20eb62df43764a4645821b5eba182e00e9e3e746 Mon Sep 17 00:00:00 2001 From: Jeffrey Ward Date: Wed, 11 Feb 2026 20:34:31 -0500 Subject: [PATCH] Fixed formatting for return column. --- traderex/lib/core/bots/bot_persistence.ex | 5 +- traderex/lib/core/bots/bot_supervisor.ex | 6 +- traderex/lib/core/bots/dca_bot.ex | 132 +++++++++++++----- .../lib/traderex_web/live/dashboard_live.ex | 30 ++-- 4 files changed, 125 insertions(+), 48 deletions(-) diff --git a/traderex/lib/core/bots/bot_persistence.ex b/traderex/lib/core/bots/bot_persistence.ex index 3daf0e5..0905e01 100644 --- a/traderex/lib/core/bots/bot_persistence.ex +++ b/traderex/lib/core/bots/bot_persistence.ex @@ -96,7 +96,10 @@ defmodule Core.Bots.BotPersistence do case Core.Bots.BotSupervisor.start_bot(symbol, restore_state: bot_state) do {:ok, pid} -> - Logger.info("Restored bot for #{symbol} with #{bot_state["shares"]} shares at avg $#{bot_state["avg_price"]}") + Logger.info( + "Restored bot for #{symbol} with #{bot_state["shares"]} shares at avg $#{bot_state["avg_price"]}" + ) + {:ok, pid} {:error, {:already_started, pid}} -> diff --git a/traderex/lib/core/bots/bot_supervisor.ex b/traderex/lib/core/bots/bot_supervisor.ex index 0868621..f8878a5 100644 --- a/traderex/lib/core/bots/bot_supervisor.ex +++ b/traderex/lib/core/bots/bot_supervisor.ex @@ -52,7 +52,11 @@ defmodule Core.Bots.BotSupervisor do {:ok, :deferred} -> # Sell deferred until market opens - keep bot alive require Logger - Logger.info("Sell deferred for #{symbol} - bot will stop after selling when market opens") + + Logger.info( + "Sell deferred for #{symbol} - bot will stop after selling when market opens" + ) + false {:error, reason} -> diff --git a/traderex/lib/core/bots/dca_bot.ex b/traderex/lib/core/bots/dca_bot.ex index 58d0458..7b839f4 100644 --- a/traderex/lib/core/bots/dca_bot.ex +++ b/traderex/lib/core/bots/dca_bot.ex @@ -58,9 +58,12 @@ defmodule Core.Bots.DCABot do use GenServer require Logger - @tick_interval 60_000 # 1 minute - @drop_threshold 0.03 # 3% price drop triggers auto-buy - @profit_threshold 0.20 # 20% total return triggers auto-sell + # 1 minute + @tick_interval 60_000 + # 3% price drop triggers auto-buy + @drop_threshold 0.03 + # 20% total return triggers auto-sell + @profit_threshold 0.20 # Public API @@ -169,7 +172,9 @@ defmodule Core.Bots.DCABot do return_pct = restore_state["return_pct"] || 0.0 last_purchase_price = restore_state["last_purchase_price"] - Logger.info("DCABot RESTORED for #{symbol}: #{shares} shares @ avg $#{Float.round(avg_price, 2)} | Return: #{Float.round(return_pct, 2)}%") + Logger.info( + "DCABot RESTORED for #{symbol}: #{shares} shares @ avg $#{Float.round(avg_price, 2)} | Return: #{Float.round(return_pct, 2)}%" + ) %{ symbol: symbol, @@ -271,8 +276,22 @@ defmodule Core.Bots.DCABot do end @impl true - def handle_call(:position, _from, %{symbol: symbol, bot_shares: bot_shares, bot_cost_basis: bot_cost_basis, current_return_pct: current_return_pct, current_pl_dollars: current_pl_dollars, last_price: last_price, last_purchase_price: last_purchase_price, sell_and_stop_pending: sell_and_stop_pending} = state) do - next_buy_price = if last_purchase_price, do: last_purchase_price * (1 - @drop_threshold), else: nil + def handle_call( + :position, + _from, + %{ + symbol: symbol, + bot_shares: bot_shares, + bot_cost_basis: bot_cost_basis, + current_return_pct: current_return_pct, + current_pl_dollars: current_pl_dollars, + last_price: last_price, + last_purchase_price: last_purchase_price, + sell_and_stop_pending: sell_and_stop_pending + } = state + ) do + next_buy_price = + if last_purchase_price, do: last_purchase_price * (1 - @drop_threshold), else: nil position_info = %{ symbol: symbol, @@ -324,7 +343,14 @@ defmodule Core.Bots.DCABot do Process.send_after(self(), :tick, @tick_interval) end - defp check_price(%{symbol: symbol, last_purchase_price: last_purchase_price, bot_shares: bot_shares, bot_cost_basis: bot_cost_basis} = state) do + defp check_price( + %{ + symbol: symbol, + last_purchase_price: last_purchase_price, + bot_shares: bot_shares, + bot_cost_basis: bot_cost_basis + } = state + ) do case Core.Client.get_quotes(symbol) do {:ok, [quote | _]} -> price = quote.last @@ -343,12 +369,20 @@ defmodule Core.Bots.DCABot do # Log price and bot position status if bot_shares > 0 do avg_cost = Float.round(bot_cost_basis / bot_shares, 2) - Logger.info("#{symbol} price: $#{price} | Bot: #{bot_shares} shares @ avg $#{avg_cost} | Return: #{return_pct}%") + + Logger.info( + "#{symbol} price: $#{price} | Bot: #{bot_shares} shares @ avg $#{avg_cost} | Return: #{return_pct}%" + ) else Logger.info("#{symbol} current price: $#{price} | Bot: no position") end - state = %{state | last_price: price, current_return_pct: return_pct, current_pl_dollars: pl_dollars} + state = %{ + state + | last_price: price, + current_return_pct: return_pct, + current_pl_dollars: pl_dollars + } # Check if we should auto-sell (position up 20%) state = maybe_auto_sell(state, price) @@ -368,12 +402,16 @@ defmodule Core.Bots.DCABot do end end - defp maybe_auto_buy(state, current_price, last_purchase_price) when is_number(last_purchase_price) do + defp maybe_auto_buy(state, current_price, last_purchase_price) + when is_number(last_purchase_price) do price_drop = (last_purchase_price - current_price) / last_purchase_price if price_drop >= @drop_threshold do drop_percent = Float.round(price_drop * 100, 2) - Logger.info("Price dropped #{drop_percent}% from last purchase ($#{last_purchase_price}). Auto-buying...") + + Logger.info( + "Price dropped #{drop_percent}% from last purchase ($#{last_purchase_price}). Auto-buying..." + ) case execute_buy(state, :auto) do {:ok, _order, purchase_price, shares} -> @@ -392,7 +430,10 @@ defmodule Core.Bots.DCABot do state end - defp maybe_auto_sell(%{bot_shares: bot_shares, bot_cost_basis: bot_cost_basis} = state, current_price) do + defp maybe_auto_sell( + %{bot_shares: bot_shares, bot_cost_basis: bot_cost_basis} = state, + current_price + ) do if bot_shares > 0 && bot_cost_basis > 0 do current_value = bot_shares * current_price total_return = (current_value - bot_cost_basis) / bot_cost_basis @@ -400,11 +441,17 @@ defmodule Core.Bots.DCABot do if total_return >= @profit_threshold do profit_percent = Float.round(total_return * 100, 2) profit_amount = Float.round(current_value - bot_cost_basis, 2) - Logger.info("Bot position up #{profit_percent}% (profit: $#{profit_amount}). Auto-selling #{bot_shares} shares...") + + Logger.info( + "Bot position up #{profit_percent}% (profit: $#{profit_amount}). Auto-selling #{bot_shares} shares..." + ) case execute_sell(state, bot_shares) do {:ok, _order} -> - Logger.info("Successfully sold all bot shares. Re-establishing position with 5% of account...") + Logger.info( + "Successfully sold all bot shares. Re-establishing position with 5% of account..." + ) + state = reset_bot_position(state) # Re-establish position immediately @@ -442,16 +489,23 @@ defmodule Core.Bots.DCABot do end end - defp update_bot_position(%{bot_shares: bot_shares, bot_cost_basis: bot_cost_basis} = state, shares, price) do + defp update_bot_position( + %{bot_shares: bot_shares, bot_cost_basis: bot_cost_basis} = state, + shares, + price + ) do new_shares = bot_shares + shares - new_cost_basis = bot_cost_basis + (shares * price) + new_cost_basis = bot_cost_basis + shares * price - Logger.info("Bot position updated: #{new_shares} total shares, cost basis: $#{Float.round(new_cost_basis, 2)}") + Logger.info( + "Bot position updated: #{new_shares} total shares, cost basis: $#{Float.round(new_cost_basis, 2)}" + ) - new_state = %{state | - bot_shares: new_shares, - bot_cost_basis: new_cost_basis, - last_purchase_price: price + new_state = %{ + state + | bot_shares: new_shares, + bot_cost_basis: new_cost_basis, + last_purchase_price: price } # Save state after position update @@ -461,12 +515,13 @@ defmodule Core.Bots.DCABot do end defp reset_bot_position(state) do - new_state = %{state | - bot_shares: 0, - bot_cost_basis: 0.0, - last_purchase_price: nil, - current_return_pct: 0.0, - current_pl_dollars: 0.0 + new_state = %{ + state + | bot_shares: 0, + bot_cost_basis: 0.0, + last_purchase_price: nil, + current_return_pct: 0.0, + current_pl_dollars: 0.0 } # Save state after reset @@ -495,8 +550,14 @@ defmodule Core.Bots.DCABot do true -> "Manual buy" end - Logger.info("#{trigger_type}: Purchasing power: $#{Float.round(buying_power, 2)} | Using 5%: $#{Float.round(cash_amount, 2)}") - Logger.info("#{trigger_type} FILLED: #{actual_shares} shares of #{symbol} at $#{actual_price} (total: $#{Float.round(total_cost, 2)})") + Logger.info( + "#{trigger_type}: Purchasing power: $#{Float.round(buying_power, 2)} | Using 5%: $#{Float.round(cash_amount, 2)}" + ) + + Logger.info( + "#{trigger_type} FILLED: #{actual_shares} shares of #{symbol} at $#{actual_price} (total: $#{Float.round(total_cost, 2)})" + ) + {:ok, filled_order, actual_price, actual_shares} else {:error, reason} = error -> @@ -528,7 +589,8 @@ defmodule Core.Bots.DCABot do end end - defp get_buying_power(%{margin: %{stock_buying_power: buying_power}}) when is_number(buying_power) do + defp get_buying_power(%{margin: %{stock_buying_power: buying_power}}) + when is_number(buying_power) do buying_power end @@ -604,13 +666,19 @@ defmodule Core.Bots.DCABot do {:error, {:order_rejected, order.status}} Core.Order.pending?(order) && retries > 0 -> - Logger.debug("Order #{order_id} for #{symbol} still pending (#{order.status}), retrying...") + Logger.debug( + "Order #{order_id} for #{symbol} still pending (#{order.status}), retrying..." + ) + # Wait 500ms before checking again Process.sleep(500) verify_order_filled(account_id, order_id, symbol, retries - 1) true -> - Logger.error("Order #{order_id} for #{symbol} timed out or unknown status: #{order.status}") + Logger.error( + "Order #{order_id} for #{symbol} timed out or unknown status: #{order.status}" + ) + {:error, {:order_timeout, order.status}} end diff --git a/traderex/lib/traderex_web/live/dashboard_live.ex b/traderex/lib/traderex_web/live/dashboard_live.ex index 6c5e313..8ae892f 100644 --- a/traderex/lib/traderex_web/live/dashboard_live.ex +++ b/traderex/lib/traderex_web/live/dashboard_live.ex @@ -42,10 +42,12 @@ defmodule TraderexWeb.DashboardLive do } bots = Core.Bots.BotSupervisor.list_bots() - market_clock = case Core.Client.get_market_clock() do - {:ok, clock} -> clock - {:error, _} -> nil - end + + market_clock = + case Core.Client.get_market_clock() do + {:ok, clock} -> clock + {:error, _} -> nil + end socket = case results do @@ -317,12 +319,12 @@ defmodule TraderexWeb.DashboardLive do <:col :let={bot} label="Return">
- - {format_percent_simple(bot.return_pct)} - {format_currency(Map.get(bot, :pl_dollars, 0))} + + ({format_percent_simple(bot.return_pct)}) +
<:col :let={bot} label="Actions"> @@ -386,8 +388,7 @@ defmodule TraderexWeb.DashboardLive do phx-value-sell="false" class="btn btn-warning flex-1" > - <.icon name="hero-pause" class="size-5" /> - Keep Shares & Stop Bot + <.icon name="hero-pause" class="size-5" /> Keep Shares & Stop Bot