diff --git a/notebook/porqua_ui.ipynb b/notebook/porqua_ui.ipynb
new file mode 100644
index 0000000..cdc7002
--- /dev/null
+++ b/notebook/porqua_ui.ipynb
@@ -0,0 +1,329 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "128a655d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "#Imports and path setup\n",
+ "import sys\n",
+ "import os\n",
+ "sys.path.insert(0, os.path.abspath('../src'))\n",
+ "\n",
+ "import io\n",
+ "import ipywidgets as widgets\n",
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "import plotly.graph_objects as go\n",
+ "\n",
+ "from optimization import MeanVariance, QEQW, LeastSquares, LAD\n",
+ "from optimization_data import OptimizationData\n",
+ "from constraints import Constraints"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "3f148839",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "9efc37a43e264d8398f501adef9cbf28",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "FileUpload(value=(), accept='.csv', description='Upload CSV')"
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "#File upload\n",
+ "uploader = widgets.FileUpload(\n",
+ " description='Upload CSV',\n",
+ " accept='.csv',\n",
+ " multiple=False\n",
+ ")\n",
+ "uploader"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "8f7540f8",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Loaded data: 6338 rows, 1 assets\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " NDDLWI | \n",
+ "
\n",
+ " \n",
+ " | Index | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 01-01-1999 | \n",
+ " -0.000072 | \n",
+ "
\n",
+ " \n",
+ " | 04-01-1999 | \n",
+ " 0.008686 | \n",
+ "
\n",
+ " \n",
+ " | 05-01-1999 | \n",
+ " 0.009618 | \n",
+ "
\n",
+ " \n",
+ " | 06-01-1999 | \n",
+ " 0.020803 | \n",
+ "
\n",
+ " \n",
+ " | 07-01-1999 | \n",
+ " -0.003080 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " NDDLWI\n",
+ "Index \n",
+ "01-01-1999 -0.000072\n",
+ "04-01-1999 0.008686\n",
+ "05-01-1999 0.009618\n",
+ "06-01-1999 0.020803\n",
+ "07-01-1999 -0.003080"
+ ]
+ },
+ "execution_count": 15,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "#Load and preview data\n",
+ "try:\n",
+ " uploaded_file = uploader.value[0]\n",
+ "except IndexError:\n",
+ " print(\"No file uploaded. Please upload a CSV file to proceed.\")\n",
+ " raise\n",
+ "content = uploaded_file['content']\n",
+ "df = pd.read_csv(io.BytesIO(content), index_col=0, parse_dates=True)\n",
+ "print(f\"Loaded data: {df.shape[0]} rows, {df.shape[1]} assets\")\n",
+ "df.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "51cb53e4",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "1e6c01f880e84e57a422c630b0af613b",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "VBox(children=(Dropdown(description='Strategy:', options=('Least Squares', 'Weighted Least Squares', 'LAD', 'M…"
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "#Optimisation controls\n",
+ "strategy_dropdown = widgets.Dropdown(\n",
+ " options=['Least Squares', 'Weighted Least Squares', 'LAD', 'Mean Variance'],\n",
+ " value='Least Squares',\n",
+ " description='Strategy:',\n",
+ " style={'description_width': 'initial'}\n",
+ ")\n",
+ "\n",
+ "n_assets_slider = widgets.IntSlider(\n",
+ " value=10,\n",
+ " min=2,\n",
+ " max=24,\n",
+ " step=1,\n",
+ " description='Number of Assets:',\n",
+ " style={'description_width': 'initial'}\n",
+ ")\n",
+ "\n",
+ "risk_aversion_slider = widgets.FloatSlider(\n",
+ " value=1.0,\n",
+ " min=0.1,\n",
+ " max=5.0,\n",
+ " step=0.1,\n",
+ " description='Risk Aversion:',\n",
+ " style={'description_width': 'initial'}\n",
+ ")\n",
+ "\n",
+ "controls = widgets.VBox([strategy_dropdown, n_assets_slider, risk_aversion_slider])\n",
+ "controls"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "3521b24e",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "d356443274274dfea207f5fe92007165",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "VBox(children=(Button(button_style='success', description='Run Optimisation', icon='check', style=ButtonStyle(…"
+ ]
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "#Run optimisation and visualise results\n",
+ "\n",
+ "STRATEGY_MAP = {\n",
+ " 'Least Squares': LeastSquares,\n",
+ " 'LAD': LAD,\n",
+ " 'Mean Variance': MeanVariance,\n",
+ " 'Weighted Least Squares': None # requires extra params, coming soon\n",
+ "}\n",
+ "\n",
+ "def plot_weights(weights_dict):\n",
+ " \"\"\"Render an interactive Plotly pie chart of portfolio weights.\"\"\"\n",
+ " filtered = {k: v for k, v in weights_dict.items() if v > 0.001}\n",
+ " fig = go.Figure(data=[go.Pie(\n",
+ " labels=list(filtered.keys()),\n",
+ " values=list(filtered.values()),\n",
+ " hole=0.3,\n",
+ " textinfo='label+percent'\n",
+ " )])\n",
+ " fig.update_layout(title='Portfolio Allocation', showlegend=True)\n",
+ " fig.show()\n",
+ "\n",
+ "def on_run_clicked(b):\n",
+ " with output:\n",
+ " output.clear_output()\n",
+ "\n",
+ " strategy_name = strategy_dropdown.value\n",
+ " StrategyClass = STRATEGY_MAP.get(strategy_name)\n",
+ "\n",
+ " if StrategyClass is None:\n",
+ " print(f\"'{strategy_name}' is not yet supported.\")\n",
+ " return\n",
+ "\n",
+ " print(f\"Running {strategy_name} optimisation...\")\n",
+ "\n",
+ " n = n_assets_slider.value\n",
+ " selection = list(df.columns[:n])\n",
+ "\n",
+ " opt_data = OptimizationData(\n",
+ " return_series=df[selection].iloc[1:].astype(float),\n",
+ " bm_series=df.iloc[1:, 0].astype(float)\n",
+ " )\n",
+ "\n",
+ " constraints = Constraints(selection=selection)\n",
+ " constraints.add_budget()\n",
+ " constraints.add_box(box_type='LongOnly')\n",
+ "\n",
+ " if strategy_name == 'Mean Variance':\n",
+ " model = StrategyClass(risk_aversion=risk_aversion_slider.value, constraints=constraints)\n",
+ " else:\n",
+ " model = StrategyClass(constraints=constraints)\n",
+ "\n",
+ " model.set_objective(opt_data)\n",
+ " model.solve()\n",
+ "\n",
+ " weights = model.results['weights']\n",
+ " print(\"\\nPortfolio Weights:\")\n",
+ " for asset, w in weights.items():\n",
+ " print(f\" {asset}: {w:.4f}\")\n",
+ "\n",
+ " plot_weights(weights)\n",
+ "\n",
+ "run_button = widgets.Button(\n",
+ " description='Run Optimisation',\n",
+ " button_style='success',\n",
+ " icon='check'\n",
+ ")\n",
+ "output = widgets.Output()\n",
+ "run_button.on_click(on_run_clicked)\n",
+ "widgets.VBox([run_button, output])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6789c89d",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": ".venv (3.10.5)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.5"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..90d94b9
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+numpy
+pandas
+scipy
+matplotlib
+qpsolvers
\ No newline at end of file