diff --git a/lib/tradex/instruments.ex b/lib/tradex/instruments.ex
new file mode 100644
index 0000000..39770a2
--- /dev/null
+++ b/lib/tradex/instruments.ex
@@ -0,0 +1,104 @@
+defmodule Tradex.Instruments do
+ @moduledoc """
+ The Instruments context.
+ """
+
+ import Ecto.Query, warn: false
+
+ alias Tradex.Instruments.Security
+ alias Tradex.Repo
+
+ @doc """
+ Returns the list of securities.
+
+ ## Examples
+
+ iex> list_securities()
+ [%Security{}, ...]
+
+ """
+ def list_securities do
+ Repo.all(Security)
+ end
+
+ @doc """
+ Gets a single security.
+
+ Raises `Ecto.NoResultsError` if the Security does not exist.
+
+ ## Examples
+
+ iex> get_security!(123)
+ %Security{}
+
+ iex> get_security!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_security!(id), do: Repo.get!(Security, id)
+
+ @doc """
+ Creates a security.
+
+ ## Examples
+
+ iex> create_security(%{field: value})
+ {:ok, %Security{}}
+
+ iex> create_security(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_security(attrs \\ %{}) do
+ %Security{}
+ |> Security.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a security.
+
+ ## Examples
+
+ iex> update_security(security, %{field: new_value})
+ {:ok, %Security{}}
+
+ iex> update_security(security, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_security(%Security{} = security, attrs) do
+ security
+ |> Security.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a security.
+
+ ## Examples
+
+ iex> delete_security(security)
+ {:ok, %Security{}}
+
+ iex> delete_security(security)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_security(%Security{} = security) do
+ Repo.delete(security)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking security changes.
+
+ ## Examples
+
+ iex> change_security(security)
+ %Ecto.Changeset{data: %Security{}}
+
+ """
+ def change_security(%Security{} = security, attrs \\ %{}) do
+ Security.changeset(security, attrs)
+ end
+end
diff --git a/lib/tradex/instruments/historical_quote.ex b/lib/tradex/instruments/historical_quote.ex
new file mode 100644
index 0000000..26daf0e
--- /dev/null
+++ b/lib/tradex/instruments/historical_quote.ex
@@ -0,0 +1,18 @@
+defmodule Tradex.Instruments.HistoricalQuote do
+ @moduledoc false
+ use Tradex.Schema
+
+ schema "historical_quotes" do
+ field :date, :date
+ field :open, :decimal
+ field :high, :decimal
+ field :low, :decimal
+ field :close, :decimal
+ field :adj_close, :decimal
+ field :volume, :integer
+
+ belongs_to :security, Tradex.Instruments.Security
+
+ timestamps(type: :utc_datetime)
+ end
+end
diff --git a/lib/tradex/instruments/security.ex b/lib/tradex/instruments/security.ex
new file mode 100644
index 0000000..ea0fca3
--- /dev/null
+++ b/lib/tradex/instruments/security.ex
@@ -0,0 +1,24 @@
+defmodule Tradex.Instruments.Security do
+ @moduledoc false
+ use Tradex.Schema
+
+ import Ecto.Changeset
+
+ schema "securities" do
+ field :name, :string
+ field :ticker, :string
+ field :wkn, :string
+ field :isin, :string
+ field :expiration_date, :date
+ field :strike_price, :decimal
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(security, attrs) do
+ security
+ |> cast(attrs, [:name, :ticker, :wkn, :isin, :expiration_date, :strike_price])
+ |> validate_required([:name, :ticker, :wkn, :isin, :expiration_date, :strike_price])
+ end
+end
diff --git a/lib/tradex_web/controllers/security_controller.ex b/lib/tradex_web/controllers/security_controller.ex
new file mode 100644
index 0000000..ba17aa8
--- /dev/null
+++ b/lib/tradex_web/controllers/security_controller.ex
@@ -0,0 +1,62 @@
+defmodule TradexWeb.SecurityController do
+ use TradexWeb, :controller
+
+ alias Tradex.Instruments
+ alias Tradex.Instruments.Security
+
+ def index(conn, _params) do
+ securities = Instruments.list_securities()
+ render(conn, :index, securities: securities)
+ end
+
+ def new(conn, _params) do
+ changeset = Instruments.change_security(%Security{})
+ render(conn, :new, changeset: changeset)
+ end
+
+ def create(conn, %{"security" => security_params}) do
+ case Instruments.create_security(security_params) do
+ {:ok, security} ->
+ conn
+ |> put_flash(:info, "Security created successfully.")
+ |> redirect(to: ~p"/securities/#{security}")
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ render(conn, :new, changeset: changeset)
+ end
+ end
+
+ def show(conn, %{"id" => id}) do
+ security = Instruments.get_security!(id)
+ render(conn, :show, security: security)
+ end
+
+ def edit(conn, %{"id" => id}) do
+ security = Instruments.get_security!(id)
+ changeset = Instruments.change_security(security)
+ render(conn, :edit, security: security, changeset: changeset)
+ end
+
+ def update(conn, %{"id" => id, "security" => security_params}) do
+ security = Instruments.get_security!(id)
+
+ case Instruments.update_security(security, security_params) do
+ {:ok, security} ->
+ conn
+ |> put_flash(:info, "Security updated successfully.")
+ |> redirect(to: ~p"/securities/#{security}")
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ render(conn, :edit, security: security, changeset: changeset)
+ end
+ end
+
+ def delete(conn, %{"id" => id}) do
+ security = Instruments.get_security!(id)
+ {:ok, _security} = Instruments.delete_security(security)
+
+ conn
+ |> put_flash(:info, "Security deleted successfully.")
+ |> redirect(to: ~p"/securities")
+ end
+end
diff --git a/lib/tradex_web/controllers/security_html.ex b/lib/tradex_web/controllers/security_html.ex
new file mode 100644
index 0000000..0502649
--- /dev/null
+++ b/lib/tradex_web/controllers/security_html.ex
@@ -0,0 +1,13 @@
+defmodule TradexWeb.SecurityHTML do
+ use TradexWeb, :html
+
+ embed_templates "security_html/*"
+
+ @doc """
+ Renders a security form.
+ """
+ attr :changeset, Ecto.Changeset, required: true
+ attr :action, :string, required: true
+
+ def security_form(assigns)
+end
diff --git a/lib/tradex_web/controllers/security_html/edit.html.heex b/lib/tradex_web/controllers/security_html/edit.html.heex
new file mode 100644
index 0000000..bfdc5e5
--- /dev/null
+++ b/lib/tradex_web/controllers/security_html/edit.html.heex
@@ -0,0 +1,8 @@
+<.header>
+ Edit Security {@security.id}
+ <:subtitle>Use this form to manage security records in your database.
+
+
+<.security_form changeset={@changeset} action={~p"/securities/#{@security}"} />
+
+<.back navigate={~p"/securities"}>Back to securities
diff --git a/lib/tradex_web/controllers/security_html/index.html.heex b/lib/tradex_web/controllers/security_html/index.html.heex
new file mode 100644
index 0000000..72ee1b8
--- /dev/null
+++ b/lib/tradex_web/controllers/security_html/index.html.heex
@@ -0,0 +1,28 @@
+<.header>
+ Listing Securities
+ <:actions>
+ <.link href={~p"/securities/new"}>
+ <.button>New Security
+
+
+
+
+<.table id="securities" rows={@securities} row_click={&JS.navigate(~p"/securities/#{&1}")}>
+ <:col :let={security} label="Name">{security.name}
+ <:col :let={security} label="Ticker">{security.ticker}
+ <:col :let={security} label="Wkn">{security.wkn}
+ <:col :let={security} label="Isin">{security.isin}
+ <:col :let={security} label="Expiration date">{security.expiration_date}
+ <:col :let={security} label="Strike price">{security.strike_price}
+ <:action :let={security}>
+
+ <.link navigate={~p"/securities/#{security}"}>Show
+
+ <.link navigate={~p"/securities/#{security}/edit"}>Edit
+
+ <:action :let={security}>
+ <.link href={~p"/securities/#{security}"} method="delete" data-confirm="Are you sure?">
+ Delete
+
+
+
diff --git a/lib/tradex_web/controllers/security_html/new.html.heex b/lib/tradex_web/controllers/security_html/new.html.heex
new file mode 100644
index 0000000..2e57255
--- /dev/null
+++ b/lib/tradex_web/controllers/security_html/new.html.heex
@@ -0,0 +1,8 @@
+<.header>
+ New Security
+ <:subtitle>Use this form to manage security records in your database.
+
+
+<.security_form changeset={@changeset} action={~p"/securities"} />
+
+<.back navigate={~p"/securities"}>Back to securities
diff --git a/lib/tradex_web/controllers/security_html/security_form.html.heex b/lib/tradex_web/controllers/security_html/security_form.html.heex
new file mode 100644
index 0000000..570dbad
--- /dev/null
+++ b/lib/tradex_web/controllers/security_html/security_form.html.heex
@@ -0,0 +1,14 @@
+<.simple_form :let={f} for={@changeset} action={@action}>
+ <.error :if={@changeset.action}>
+ Oops, something went wrong! Please check the errors below.
+
+ <.input field={f[:name]} type="text" label="Name" />
+ <.input field={f[:ticker]} type="text" label="Ticker" />
+ <.input field={f[:wkn]} type="text" label="Wkn" />
+ <.input field={f[:isin]} type="text" label="Isin" />
+ <.input field={f[:expiration_date]} type="date" label="Expiration date" />
+ <.input field={f[:strike_price]} type="number" label="Strike price" step="any" />
+ <:actions>
+ <.button>Save Security
+
+
diff --git a/lib/tradex_web/controllers/security_html/show.html.heex b/lib/tradex_web/controllers/security_html/show.html.heex
new file mode 100644
index 0000000..83512de
--- /dev/null
+++ b/lib/tradex_web/controllers/security_html/show.html.heex
@@ -0,0 +1,20 @@
+<.header>
+ Security {@security.id}
+ <:subtitle>This is a security record from your database.
+ <:actions>
+ <.link href={~p"/securities/#{@security}/edit"}>
+ <.button>Edit security
+
+
+
+
+<.list>
+ <:item title="Name">{@security.name}
+ <:item title="Ticker">{@security.ticker}
+ <:item title="Wkn">{@security.wkn}
+ <:item title="Isin">{@security.isin}
+ <:item title="Expiration date">{@security.expiration_date}
+ <:item title="Strike price">{@security.strike_price}
+
+
+<.back navigate={~p"/securities"}>Back to securities
diff --git a/lib/tradex_web/router.ex b/lib/tradex_web/router.ex
index 1d8dd79..9e1974c 100644
--- a/lib/tradex_web/router.ex
+++ b/lib/tradex_web/router.ex
@@ -22,6 +22,7 @@ defmodule TradexWeb.Router do
pipe_through :browser
get "/", PageController, :home
+ resources "/securities", SecurityController
end
# Other scopes may use custom stacks.
diff --git a/priv/repo/migrations/20250127154056_add_securities_table.exs b/priv/repo/migrations/20250128134037_create_securities.exs
similarity index 70%
rename from priv/repo/migrations/20250127154056_add_securities_table.exs
rename to priv/repo/migrations/20250128134037_create_securities.exs
index a2dca71..3bf03b5 100644
--- a/priv/repo/migrations/20250127154056_add_securities_table.exs
+++ b/priv/repo/migrations/20250128134037_create_securities.exs
@@ -1,10 +1,10 @@
-defmodule Tradex.Repo.Migrations.AddSecuritiesTable do
+defmodule Tradex.Repo.Migrations.CreateSecurities do
use Ecto.Migration
def change do
create table(:securities, primary_key: false) do
add :id, :binary_id, primary_key: true
- add :name, :string, null: false
+ add :name, :string
add :ticker, :string
add :wkn, :string
add :isin, :string
@@ -13,7 +13,5 @@ defmodule Tradex.Repo.Migrations.AddSecuritiesTable do
timestamps(type: :utc_datetime)
end
-
- create unique_index(:securities, [:name])
end
end
diff --git a/priv/repo/migrations/20250128102025_add_historical_quotes_table.exs b/priv/repo/migrations/20250128142025_add_historical_quotes_table.exs
similarity index 100%
rename from priv/repo/migrations/20250128102025_add_historical_quotes_table.exs
rename to priv/repo/migrations/20250128142025_add_historical_quotes_table.exs
diff --git a/test/support/fixtures/instruments_fixtures.ex b/test/support/fixtures/instruments_fixtures.ex
new file mode 100644
index 0000000..6778e83
--- /dev/null
+++ b/test/support/fixtures/instruments_fixtures.ex
@@ -0,0 +1,25 @@
+defmodule Tradex.InstrumentsFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `Tradex.Instruments` context.
+ """
+
+ @doc """
+ Generate a security.
+ """
+ def security_fixture(attrs \\ %{}) do
+ {:ok, security} =
+ attrs
+ |> Enum.into(%{
+ expiration_date: ~D[2025-01-27],
+ isin: "some isin",
+ name: "some name",
+ strike_price: "120.5",
+ ticker: "some ticker",
+ wkn: "some wkn"
+ })
+ |> Tradex.Instruments.create_security()
+
+ security
+ end
+end
diff --git a/test/tradex/instruments_test.exs b/test/tradex/instruments_test.exs
new file mode 100644
index 0000000..68a1b93
--- /dev/null
+++ b/test/tradex/instruments_test.exs
@@ -0,0 +1,84 @@
+defmodule Tradex.InstrumentsTest do
+ use Tradex.DataCase
+
+ alias Tradex.Instruments
+
+ describe "securities" do
+ import Tradex.InstrumentsFixtures
+
+ alias Tradex.Instruments.Security
+
+ @invalid_attrs %{name: nil, ticker: nil, wkn: nil, isin: nil, expiration_date: nil, strike_price: nil}
+
+ test "list_securities/0 returns all securities" do
+ security = security_fixture()
+ assert Instruments.list_securities() == [security]
+ end
+
+ test "get_security!/1 returns the security with given id" do
+ security = security_fixture()
+ assert Instruments.get_security!(security.id) == security
+ end
+
+ test "create_security/1 with valid data creates a security" do
+ valid_attrs = %{
+ name: "some name",
+ ticker: "some ticker",
+ wkn: "some wkn",
+ isin: "some isin",
+ expiration_date: ~D[2025-01-27],
+ strike_price: "120.5"
+ }
+
+ assert {:ok, %Security{} = security} = Instruments.create_security(valid_attrs)
+ assert security.name == "some name"
+ assert security.ticker == "some ticker"
+ assert security.wkn == "some wkn"
+ assert security.isin == "some isin"
+ assert security.expiration_date == ~D[2025-01-27]
+ assert security.strike_price == Decimal.new("120.5")
+ end
+
+ test "create_security/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Instruments.create_security(@invalid_attrs)
+ end
+
+ test "update_security/2 with valid data updates the security" do
+ security = security_fixture()
+
+ update_attrs = %{
+ name: "some updated name",
+ ticker: "some updated ticker",
+ wkn: "some updated wkn",
+ isin: "some updated isin",
+ expiration_date: ~D[2025-01-28],
+ strike_price: "456.7"
+ }
+
+ assert {:ok, %Security{} = security} = Instruments.update_security(security, update_attrs)
+ assert security.name == "some updated name"
+ assert security.ticker == "some updated ticker"
+ assert security.wkn == "some updated wkn"
+ assert security.isin == "some updated isin"
+ assert security.expiration_date == ~D[2025-01-28]
+ assert security.strike_price == Decimal.new("456.7")
+ end
+
+ test "update_security/2 with invalid data returns error changeset" do
+ security = security_fixture()
+ assert {:error, %Ecto.Changeset{}} = Instruments.update_security(security, @invalid_attrs)
+ assert security == Instruments.get_security!(security.id)
+ end
+
+ test "delete_security/1 deletes the security" do
+ security = security_fixture()
+ assert {:ok, %Security{}} = Instruments.delete_security(security)
+ assert_raise Ecto.NoResultsError, fn -> Instruments.get_security!(security.id) end
+ end
+
+ test "change_security/1 returns a security changeset" do
+ security = security_fixture()
+ assert %Ecto.Changeset{} = Instruments.change_security(security)
+ end
+ end
+end
diff --git a/test/tradex_web/controllers/security_controller_test.exs b/test/tradex_web/controllers/security_controller_test.exs
new file mode 100644
index 0000000..dc31461
--- /dev/null
+++ b/test/tradex_web/controllers/security_controller_test.exs
@@ -0,0 +1,98 @@
+defmodule TradexWeb.SecurityControllerTest do
+ use TradexWeb.ConnCase
+
+ import Tradex.InstrumentsFixtures
+
+ @create_attrs %{
+ name: "some name",
+ ticker: "some ticker",
+ wkn: "some wkn",
+ isin: "some isin",
+ expiration_date: ~D[2025-01-27],
+ strike_price: "120.5"
+ }
+ @update_attrs %{
+ name: "some updated name",
+ ticker: "some updated ticker",
+ wkn: "some updated wkn",
+ isin: "some updated isin",
+ expiration_date: ~D[2025-01-28],
+ strike_price: "456.7"
+ }
+ @invalid_attrs %{name: nil, ticker: nil, wkn: nil, isin: nil, expiration_date: nil, strike_price: nil}
+
+ describe "index" do
+ test "lists all securities", %{conn: conn} do
+ conn = get(conn, ~p"/securities")
+ assert html_response(conn, 200) =~ "Listing Securities"
+ end
+ end
+
+ describe "new security" do
+ test "renders form", %{conn: conn} do
+ conn = get(conn, ~p"/securities/new")
+ assert html_response(conn, 200) =~ "New Security"
+ end
+ end
+
+ describe "create security" do
+ test "redirects to show when data is valid", %{conn: conn} do
+ conn = post(conn, ~p"/securities", security: @create_attrs)
+
+ assert %{id: id} = redirected_params(conn)
+ assert redirected_to(conn) == ~p"/securities/#{id}"
+
+ conn = get(conn, ~p"/securities/#{id}")
+ assert html_response(conn, 200) =~ "Security #{id}"
+ end
+
+ test "renders errors when data is invalid", %{conn: conn} do
+ conn = post(conn, ~p"/securities", security: @invalid_attrs)
+ assert html_response(conn, 200) =~ "New Security"
+ end
+ end
+
+ describe "edit security" do
+ setup [:create_security]
+
+ test "renders form for editing chosen security", %{conn: conn, security: security} do
+ conn = get(conn, ~p"/securities/#{security}/edit")
+ assert html_response(conn, 200) =~ "Edit Security"
+ end
+ end
+
+ describe "update security" do
+ setup [:create_security]
+
+ test "redirects when data is valid", %{conn: conn, security: security} do
+ conn = put(conn, ~p"/securities/#{security}", security: @update_attrs)
+ assert redirected_to(conn) == ~p"/securities/#{security}"
+
+ conn = get(conn, ~p"/securities/#{security}")
+ assert html_response(conn, 200) =~ "some updated name"
+ end
+
+ test "renders errors when data is invalid", %{conn: conn, security: security} do
+ conn = put(conn, ~p"/securities/#{security}", security: @invalid_attrs)
+ assert html_response(conn, 200) =~ "Edit Security"
+ end
+ end
+
+ describe "delete security" do
+ setup [:create_security]
+
+ test "deletes chosen security", %{conn: conn, security: security} do
+ conn = delete(conn, ~p"/securities/#{security}")
+ assert redirected_to(conn) == ~p"/securities"
+
+ assert_error_sent 404, fn ->
+ get(conn, ~p"/securities/#{security}")
+ end
+ end
+ end
+
+ defp create_security(_) do
+ security = security_fixture()
+ %{security: security}
+ end
+end