Fixed formatting for return column.

This commit is contained in:
2026-02-11 20:34:31 -05:00
parent 0e154f4ced
commit 20eb62df43
4 changed files with 125 additions and 48 deletions

View File

@@ -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}} ->

View File

@@ -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} ->

View File

@@ -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

View File

@@ -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>
<:col :let={bot} label="Return">
<div class="flex flex-col">
<span class={return_color(bot.return_pct)}>
{format_percent_simple(bot.return_pct)}
</span>
<span class={["text-sm", pl_color(Map.get(bot, :pl_dollars, 0))]}>
{format_currency(Map.get(bot, :pl_dollars, 0))}
</span>
<span class={return_color(bot.return_pct)}>
({format_percent_simple(bot.return_pct)})
</span>
</div>
</:col>
<: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
</button>
<button
@@ -396,8 +397,7 @@ defmodule TraderexWeb.DashboardLive do
phx-value-sell="true"
class="btn btn-error flex-1"
>
<.icon name="hero-arrow-trending-down" class="size-5" />
Sell All & Stop Bot
<.icon name="hero-arrow-trending-down" class="size-5" /> Sell All & Stop Bot
</button>
<button phx-click="close_stop_bot_modal" class="btn btn-ghost">
@@ -419,8 +419,7 @@ defmodule TraderexWeb.DashboardLive do
phx-value-sell="false"
class="btn btn-error"
>
<.icon name="hero-stop" class="size-5" />
Stop Bot
<.icon name="hero-stop" class="size-5" /> Stop Bot
</button>
<button phx-click="close_stop_bot_modal" class="btn btn-ghost">
@@ -585,7 +584,10 @@ defmodule TraderexWeb.DashboardLive do
market_status_class(@clock.state)
]}>
<span class="relative flex h-2 w-2">
<span :if={@clock.state == "open"} class="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75">
<span
:if={@clock.state == "open"}
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75"
>
</span>
<span class={[
"relative inline-flex rounded-full h-2 w-2",