{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 投资组合选择优化问题\n",
    "\n",
    "**类别：** 非线性优化\n",
    "\n",
    "使用 OptAgent 的 Python 接口描述变量、约束与目标。\n",
    "\n",
    "问题与原始示例来源：[Hexaly Code Templates](https://www.hexaly.com/templates/portfolio-selection-optimization-problem)。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 问题描述\n",
    "\n",
    "**在投资组合选择优化问题** 中，我们必须在一组股票中分配投资组合。每只股票的收益和风险协方差矩阵已知。问题在于决定投资组合中投入到每只股票的比例。我们必须使用全部投资组合，并达到给定的预期利润。问题的目标是最小化与所选投资相关的风险。该问题是对 [马科维茨投资组合选择优化问题](https://en.wikipedia.org/wiki/Markowitz_model) 的简化。\n",
    "\n",
    "### 建模要点\n",
    "\n",
    "- 添加 [浮点决策变量](https://optagent.pages.dev/guide/modeling/) 来建模投入到每只股票的投资组合比例\n",
    "- 使用 [非线性算子](https://optagent.pages.dev/guide/modeling/) 来计算风险\n",
    "- 了解 OptAgent 的建模方式：[区分决策变量与中间表达式](https://optagent.pages.dev/guide/modeling/)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 数据\n",
    "\n",
    "数据文件的格式如下：\n",
    "\n",
    "- 第一行：预期利润（占投资组合的百分比）\n",
    "- 第二行：股票数量\n",
    "- 接下来若干行：表示每对股票之间平衡风险的协方差矩阵\n",
    "- 最后一行：每只股票的价格变化\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 建模思路\n",
    "\n",
    "投资组合选择优化问题的 OptAgent 模型使用浮点决策变量，表示投入到每只股票的投资组合比例。由于我们必须投入全部投资组合，我们约束这些比例之和等于 1。\n",
    "\n",
    "投资组合的收益是所有股票利润的加权和，其中权重是分配到每只股票的投资组合比例。我们将其约束为大于或等于预期利润。\n",
    "\n",
    "问题的目标是最小化风险。投资组合风险是一个凸二次函数，通过对每对股票的投资比例与对应协方差的乘积求和得到。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Python 实现\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pathlib import Path\n",
    "\n",
    "from optagent import OptModel, solve\n",
    "\n",
    "\n",
    "def read_instance(filename):\n",
    "    lines = Path(filename).read_text(encoding=\"utf-8\").splitlines()\n",
    "\n",
    "    first_line = lines[0].split()\n",
    "\n",
    "    # Expected profit, in percentage of the portfolio\n",
    "    expected_profit = float(first_line[0])\n",
    "\n",
    "    second_line = lines[2].split()\n",
    "\n",
    "    # Number of stocks\n",
    "    nb_stocks = int(second_line[0])\n",
    "\n",
    "    # Covariance among the stocks\n",
    "    sigma_stocks = [[0 for i in range(nb_stocks)] for j in range(nb_stocks)]\n",
    "    for s in range(nb_stocks):\n",
    "        line = lines[s+4].split()\n",
    "        for t in range(nb_stocks):\n",
    "            sigma_stocks[s][t] = float(line[t])\n",
    "\n",
    "    # Variation of the price of each stock\n",
    "    delta_stock = [0 for i in range(nb_stocks)]\n",
    "    line = lines[nb_stocks+5].split()\n",
    "    for s in range(nb_stocks):\n",
    "        delta_stock[s] = float(line[s])\n",
    "\n",
    "    return expected_profit, nb_stocks, sigma_stocks, delta_stock\n",
    "\n",
    "\n",
    "def main(instance_file, output_file=None, time_limit=60):\n",
    "    expected_profit, nb_stocks, sigma_stocks, delta_stock = read_instance(\n",
    "        instance_file)\n",
    "\n",
    "    model = OptModel()\n",
    "\n",
    "        # Proportion of the portfolio invested in each stock\n",
    "    portfolio_stock = [\n",
    "        model.float(0, 1)\n",
    "        for s in range(nb_stocks)\n",
    "    ]\n",
    "\n",
    "        # Risk of the portfolio\n",
    "    risk = model.sum(\n",
    "        portfolio_stock[s] * portfolio_stock[t] * sigma_stocks[s][t]\n",
    "        for t in range(nb_stocks)\n",
    "        for s in range(nb_stocks)\n",
    "    )\n",
    "\n",
    "        # Return of the portfolio in percentage\n",
    "    profit = model.sum(\n",
    "        portfolio_stock[s] * delta_stock[s] for s in range(nb_stocks)\n",
    "    )\n",
    "\n",
    "        # All the portfolio is used\n",
    "    model.constraint(\n",
    "        model.sum(portfolio_stock[s] for s in range(nb_stocks)) == 1.0,\n",
    "    )\n",
    "\n",
    "        # The profit is at least the expected profit\n",
    "    model.constraint(profit >= expected_profit)\n",
    "\n",
    "        # Minimize the risk\n",
    "    model.minimize(risk)\n",
    "    solution = solve(model, time_limit_s=float(time_limit))\n",
    "    if not solution.feasible:\n",
    "        print(f\"No feasible portfolio found; Status = {solution.feasible}\")\n",
    "        return solution\n",
    "\n",
    "    lines = [\n",
    "        f\"Stock {s + 1}: {portfolio_stock[s].value * 100:.1f}%\"\n",
    "        for s in range(nb_stocks)\n",
    "    ]\n",
    "    lines.append(f\"Profit: {profit.value:.4f}%\")\n",
    "    result_text = \"\\n\".join(lines)\n",
    "    print(result_text)\n",
    "    if output_file is not None:\n",
    "        Path(output_file).write_text(result_text + \"\\n\", encoding=\"utf-8\")\n",
    "    return solution\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 本地运行\n",
    "\n",
    "在 notebook 所在目录执行以下 cell，即可调用一个投资组合实例。\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "INSTANCE_DIR = Path.cwd() / \"instances\"\n",
    "print(\"Instances:\", INSTANCE_DIR)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "solution_portfolio = main(INSTANCE_DIR / \"small_01.txt\", time_limit=1)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
