Shariah-Compliant Portfolio Optimization: When Fuzzy Logic Meets Islamic Finance

Classical portfolio theory assumes probabilistic risk but Shariah compliance introduces hard binary constraints that break standard optimizers. A genetic algorithm approach using fuzzy semi-spreads offers a principled alternative. Here's why it matters, and how it works.

Modern portfolio theory has a Shariah problem.

Markowitz's mean-variance framework the foundation of quantitative portfolio construction since 1952 assumes that return uncertainty can be captured by a probability distribution, that risk is synonymous with variance, and that the optimizer is free to allocate to any asset in any proportion. None of these assumptions hold cleanly in an Islamic finance context.

The probability distribution assumption runs into a theological objection: in classical Islamic jurisprudence, gharar (excessive uncertainty) is prohibited. A model that treats future returns as draws from a known distribution and then maximises a risk-adjusted expected value of those draws is making a claim about the future that some scholars consider epistemically overconfident, if not outright problematic. More practically, the assumption that any asset can receive any allocation breaks immediately when you introduce Shariah screening: certain sectors are binary exclusions (riba-bearing instruments, alcohol, tobacco, weapons, entertainment), and certain financial ratios must fall within defined thresholds for a stock to qualify as halal at all.

The result is a combinatorial problem with hard binary constraints embedded inside a continuous optimization. Standard quadratic programming doesn't handle this gracefully. And the fuzzy semi-spread approach from genetic algorithm-based Shariah portfolio research offers something that standard approaches don't: a way to model return uncertainty without claiming to know the probability distribution, while still being able to enforce the binary constraints that Shariah compliance requires.

I want to walk through why this matters, how the mathematics works, and what it implies for quantitative Islamic finance more broadly.


The Problem with Probabilistic Risk in a Shariah Context

Standard mean-variance optimization works like this. You estimate expected returns μ and a covariance matrix Σ for a universe of assets. You then solve:

minimise   wᵀ Σ w
subject to wᵀ μ ≥ R_target
           Σᵢ wᵢ = 1
           wᵢ ≥ 0

The output is a set of portfolio weights w that minimise variance (proxy for risk) for a given target return.

The problems for Shariah-compliant portfolios are threefold:

1. The universe must be pre-screened. Before optimization runs, every asset must pass a Shariah filter sector exclusions (no conventional banks, no alcohol, etc.) and financial ratio screens (total debt to total assets < 33%, accounts receivable to total assets < 49%, etc.). This reduces the investable universe and makes it harder to achieve diversification.

2. Weights must respect sector constraints. Certain interpretations of Shariah require minimum allocations to productive sectors or prohibit concentration in specific areas. These are not smooth constraints they introduce discontinuities.

3. Return uncertainty is genuinely fuzzy, not probabilistic. For equities listed on Bursa Malaysia or the Gulf exchanges, historical return data is thin, volatile, and affected by regulatory, political, and religious factors that don't follow stationary distributions. Claiming to know μ and Σ with confidence is epistemically dubious. The Shariah-aware investor is, in a meaningful sense, operating under ambiguity rather than risk and ambiguity calls for a different mathematical treatment.


Fuzzy Numbers as a Model of Ambiguity

Fuzzy set theory, developed by Lotfi Zadeh in 1965, provides a mathematical framework for reasoning under ambiguity. Where probability assigns a single number P(x) representing the likelihood of an event, fuzzy logic assigns a membership function μ(x) ∈ [0, 1] representing the degree to which x belongs to a fuzzy set.

For portfolio returns, the key construct is a trapezoidal fuzzy number, which represents a return estimate as a range of possibilities with varying degrees of credibility:

R̃ = (a, b, c, d)

Where: - [b, c] is the core - the range of returns considered most plausible (membership = 1) - a is the pessimistic lower bound (membership = 0) - d is the optimistic upper bound (membership = 0) - The membership function rises linearly from a to b and falls linearly from c to d

Visually:

Membership
   1  |     ┌────┐
      |    /      \
      |   /        \
   0  |__/          \__
      a   b        c   d
              Return

This is more honest than a point estimate. You're saying: "I believe the return will be somewhere between b and c with full confidence, could be as low as a or as high as d with diminishing confidence, and I make no claim beyond d or below a." No probability distribution required.


Fuzzy Semi-Spreads: Replacing Variance with Ambiguity-Aware Risk

The central innovation in the genetic algorithm approach to Shariah portfolio optimization is replacing variance (the standard risk measure) with fuzzy semi-spreads.

For a fuzzy return R̃ = (a, b, c, d), the semi-spreads are:

Left semi-spread:   lₛ = (b - a) / 2    ← downside ambiguity
Right semi-spread:  rₛ = (d - c) / 2    ← upside ambiguity

The expected value of the fuzzy return is:

E(R̃) = (a + b + c + d) / 4

The risk measure used is the left semi-spread lₛ capturing downside uncertainty specifically, rather than symmetric variance. This is analogous to semi-variance in classical finance (which only penalises downside volatility) but applied to fuzzy numbers rather than historical returns.

For a portfolio of n assets with weights w = (w₁, ..., wₙ) and fuzzy returns R̃ᵢ = (aᵢ, bᵢ, cᵢ, dᵢ):

Portfolio fuzzy return: R̃ₚ = Σᵢ wᵢ · R̃ᵢ
                             = (Σwᵢaᵢ, Σwᵢbᵢ, Σwᵢcᵢ, Σwᵢdᵢ)

Portfolio expected return: E(R̃ₚ) = Σᵢ wᵢ · E(R̃ᵢ)

Portfolio left semi-spread: lₛ(R̃ₚ) = Σᵢ wᵢ · lₛ(R̃ᵢ)

Note that both expected return and left semi-spread are linear in the weights which is computationally convenient, but the Shariah binary constraints mean standard linear/quadratic solvers still can't handle the full problem cleanly.


The Shariah Compliance Constraints

The full set of constraints in a Shariah-compliant fuzzy portfolio problem looks like this:

Binary screening constraints (hard, asset-level):

xᵢ ∈ {0, 1}    ← asset either included or excluded
xᵢ = 0         if asset fails any Shariah screen

Shariah screens typically include: - Sector exclusion: conventional finance, alcohol, pork, tobacco, weapons, adult entertainment - Debt ratio: total debt / total assets < 33% - Illiquid assets ratio: (cash + receivables) / total assets < 49% - Interest income ratio: interest income / total revenue < 5%

Allocation constraints (continuous, portfolio-level):

Σᵢ wᵢ = 1          (fully invested)
wᵢ ≥ 0              (no short selling - prohibited under Shariah)
wᵢ ≤ wᵢ_max         (concentration limits)
wᵢ = 0 if xᵢ = 0   (can't hold excluded assets)

Objective:

maximise   E(R̃ₚ) - λ · lₛ(R̃ₚ)

where λ is the investor's risk aversion parameter.

The coupling of binary xᵢ variables with continuous wᵢ variables makes this a mixed-integer programming problem NP-hard in general. This is why a genetic algorithm is the natural solver.


Why Genetic Algorithms

A genetic algorithm (GA) treats the portfolio selection problem as an evolutionary search. A chromosome encodes both the binary selection vector x and the continuous weight vector w:

Chromosome: [x₁, x₂, ..., xₙ | w₁, w₂, ..., wₙ]
             ←── binary part ──→ ←─ continuous part ─→

The algorithm:

  1. Initialise a population of random chromosomes (respecting Shariah screens any chromosome with xᵢ = 1 for a screened-out asset is immediately repaired)
  2. Evaluate fitness compute E(R̃ₚ) λ · lₛ(R̃ₚ) for each chromosome
  3. Select tournament or roulette selection biased toward higher fitness
  4. Crossover combine binary and continuous segments from two parents
  5. Mutate flip binary bits, perturb continuous weights (with renormalization)
  6. Repair enforce Shariah constraints and weight normalization after mutation
  7. Repeat for N generations

The repair step is critical. After crossover and mutation, a chromosome may violate the Shariah binary constraints or produce weights that don't sum to 1. A dedicated repair operator: - Sets wᵢ = 0 for any asset where xᵢ = 0 - Renormalizes remaining weights to sum to 1 - Checks financial ratios for all selected assets and flips xᵢ = 0 for violators


Python Implementation Sketch

import numpy as np
from dataclasses import dataclass

@dataclass
class FuzzyReturn:
    """Trapezoidal fuzzy number (a, b, c, d)."""
    a: float  # pessimistic lower bound
    b: float  # lower core
    c: float  # upper core
    d: float  # optimistic upper bound

    @property
    def expected_value(self) -> float:
        return (self.a + self.b + self.c + self.d) / 4

    @property
    def left_semi_spread(self) -> float:
        return (self.b - self.a) / 2

    @property
    def right_semi_spread(self) -> float:
        return (self.d - self.c) / 2


def portfolio_fitness(
    weights: np.ndarray,
    selection: np.ndarray,     # binary: 1 = included, 0 = excluded
    fuzzy_returns: list[FuzzyReturn],
    risk_aversion: float = 1.0,
) -> float:
    """
    Compute portfolio fitness: E(R̃ₚ) - λ · lₛ(R̃ₚ)
    Higher is better.
    """
    w = weights * selection   # zero out excluded assets
    if w.sum() == 0:
        return -np.inf
    w = w / w.sum()           # renormalize

    exp_returns = np.array([r.expected_value for r in fuzzy_returns])
    semi_spreads = np.array([r.left_semi_spread for r in fuzzy_returns])

    portfolio_return = np.dot(w, exp_returns)
    portfolio_risk   = np.dot(w, semi_spreads)

    return portfolio_return - risk_aversion * portfolio_risk


def shariah_screen(
    asset_idx: int,
    debt_ratio: float,
    interest_income_ratio: float,
    illiquid_ratio: float,
    sector: str,
    excluded_sectors: list[str],
) -> bool:
    """Returns True if asset passes Shariah screen."""
    if sector in excluded_sectors:
        return False
    if debt_ratio >= 0.33:
        return False
    if interest_income_ratio >= 0.05:
        return False
    if illiquid_ratio >= 0.49:
        return False
    return True


def repair_chromosome(
    weights: np.ndarray,
    selection: np.ndarray,
    shariah_eligible: np.ndarray,  # pre-computed eligibility
) -> tuple[np.ndarray, np.ndarray]:
    """
    Enforce Shariah constraints and renormalize weights.
    """
    # Force exclude non-eligible assets
    selection = selection * shariah_eligible

    # Zero weights of excluded assets
    weights = weights * selection

    # Renormalize
    total = weights.sum()
    if total > 0:
        weights = weights / total
    else:
        # Fallback: equal weight eligible assets
        eligible = shariah_eligible.astype(float)
        weights = eligible / eligible.sum()
        selection = shariah_eligible.copy()

    return weights, selection


class SharıahGA:
    """
    Genetic Algorithm for Shariah-compliant fuzzy portfolio optimization.
    """
    def __init__(
        self,
        fuzzy_returns: list[FuzzyReturn],
        shariah_eligible: np.ndarray,
        risk_aversion: float = 1.0,
        population_size: int = 100,
        n_generations: int = 200,
        crossover_rate: float = 0.8,
        mutation_rate: float = 0.05,
        seed: int = 42,
    ):
        self.fuzzy_returns   = fuzzy_returns
        self.eligible        = shariah_eligible
        self.lam             = risk_aversion
        self.pop_size        = population_size
        self.n_gen           = n_generations
        self.cx_rate         = crossover_rate
        self.mut_rate        = mutation_rate
        self.n_assets        = len(fuzzy_returns)
        self.rng             = np.random.default_rng(seed)

    def _init_population(self):
        """Generate initial population respecting Shariah eligibility."""
        population = []
        for _ in range(self.pop_size):
            # Random binary selection (only from eligible assets)
            sel = self.rng.integers(0, 2, self.n_assets) * self.eligible
            # Random weights
            w = self.rng.dirichlet(np.ones(self.n_assets))
            w, sel = repair_chromosome(w, sel, self.eligible)
            population.append((w, sel))
        return population

    def _fitness(self, w, sel):
        return portfolio_fitness(w, sel, self.fuzzy_returns, self.lam)

    def _tournament_select(self, population, fitness_scores, k=3):
        """Tournament selection."""
        idx = self.rng.choice(len(population), k, replace=False)
        best = idx[np.argmax([fitness_scores[i] for i in idx])]
        return population[best]

    def _crossover(self, parent1, parent2):
        """Single-point crossover on both binary and weight segments."""
        point = self.rng.integers(1, self.n_assets)
        w1, s1 = parent1
        w2, s2 = parent2

        child_w = np.concatenate([w1[:point], w2[point:]])
        child_s = np.concatenate([s1[:point], s2[point:]])

        return repair_chromosome(child_w, child_s, self.eligible)

    def _mutate(self, w, sel):
        """Bit-flip mutation on selection, Gaussian perturbation on weights."""
        for i in range(self.n_assets):
            if self.rng.random() < self.mut_rate and self.eligible[i]:
                sel[i] = 1 - sel[i]   # flip
        # Perturb weights
        noise = self.rng.normal(0, 0.05, self.n_assets)
        w = np.maximum(w + noise, 0)
        return repair_chromosome(w, sel, self.eligible)

    def optimise(self) -> tuple[np.ndarray, np.ndarray, float]:
        """Run GA and return (best_weights, best_selection, best_fitness)."""
        population = self._init_population()
        best_w, best_sel = population[0]
        best_fit = self._fitness(best_w, best_sel)

        for gen in range(self.n_gen):
            fitness_scores = [self._fitness(w, s) for w, s in population]

            # Track best
            gen_best_idx = np.argmax(fitness_scores)
            if fitness_scores[gen_best_idx] > best_fit:
                best_fit = fitness_scores[gen_best_idx]
                best_w, best_sel = population[gen_best_idx]

            # Build next generation
            next_pop = [population[gen_best_idx]]  # elitism: keep best
            while len(next_pop) < self.pop_size:
                p1 = self._tournament_select(population, fitness_scores)
                p2 = self._tournament_select(population, fitness_scores)

                if self.rng.random() < self.cx_rate:
                    child_w, child_sel = self._crossover(p1, p2)
                else:
                    child_w, child_sel = p1

                child_w, child_sel = self._mutate(child_w.copy(), child_sel.copy())
                next_pop.append((child_w, child_sel))

            population = next_pop

            if (gen + 1) % 50 == 0:
                print(f"  Gen {gen+1:3d}/{self.n_gen} - best fitness: {best_fit:.4f}")

        return best_w, best_sel, best_fit

Running It

# Example: 5-asset Shariah-eligible universe
# Fuzzy returns estimated from analyst range estimates
fuzzy_returns = [
    FuzzyReturn(0.04, 0.07, 0.11, 0.15),   # MAYBANK (screened - conventional bank, excluded)
    FuzzyReturn(0.06, 0.09, 0.13, 0.18),   # TENAGA
    FuzzyReturn(0.05, 0.08, 0.12, 0.16),   # PETRONAS CHEMICALS
    FuzzyReturn(0.03, 0.06, 0.09, 0.13),   # DIALOG GROUP
    FuzzyReturn(0.07, 0.10, 0.14, 0.20),   # HARTALEGA
]

# Pre-computed Shariah eligibility (0 = excluded, 1 = eligible)
shariah_eligible = np.array([0, 1, 1, 1, 1])  # MAYBANK excluded (riba)

ga = SharıahGA(
    fuzzy_returns=fuzzy_returns,
    shariah_eligible=shariah_eligible,
    risk_aversion=1.5,
    population_size=150,
    n_generations=300,
)

best_weights, best_selection, best_fitness = ga.optimise()

print(f"\nOptimal Shariah Portfolio:")
assets = ["MAYBANK", "TENAGA", "PETCHEM", "DIALOG", "HARTALEGA"]
for i, (asset, w, sel) in enumerate(zip(assets, best_weights, best_selection)):
    status = "EXCLUDED" if sel == 0 else f"{w*100:.1f}%"
    ev = fuzzy_returns[i].expected_value
    lss = fuzzy_returns[i].left_semi_spread
    print(f"  {asset:<15} {status:<12} E(R̃)={ev:.3f}  lₛ={lss:.3f}")

print(f"\nPortfolio E(R̃ₚ):        {sum(best_weights[i]*fuzzy_returns[i].expected_value for i in range(5)):.4f}")
print(f"Portfolio lₛ(R̃ₚ):       {sum(best_weights[i]*fuzzy_returns[i].left_semi_spread for i in range(5)):.4f}")
print(f"Fitness (E - λ·lₛ):     {best_fitness:.4f}")

Why This Is More Honest Than Standard Mean-Variance

The fuzzy approach forces you to be explicit about what you actually know and don't know.

When you estimate R̃ = (0.06, 0.09, 0.13, 0.18) for a stock, you're saying: "I'm confident the annual return is somewhere between 9% and 13%, I think it could be as low as 6% or as high as 18%, and I won't pretend to have a view beyond that." There's no claim that this follows a normal distribution. There's no pretense that a three-year historical covariance matrix reliably predicts future co-movements.

The left semi-spread lₛ = (b a) / 2 captures only downside ambiguity. This is both more Islamic in spirit (the prohibition on gharar is specifically about uncertainty that disadvantages the counterparty downside risk) and more practically conservative.

Standard mean-variance optimization is symmetric in its risk treatment it penalises upside volatility as much as downside. A Shariah-aware investor who is genuinely uncertain about the future but optimistic about a halal business shouldn't be penalised for that optimism in the same way they should be penalised for downside exposure.


Implications for Takaful and Islamic Fund Management

In the Malaysian context where takaful operators are required to maintain Shariah-compliant investment portfolios under the Islamic Financial Services Act 2013 and BNM's guidelines the practical application of this framework has direct relevance.

Takaful funds invest participants' tabarru' contributions into a portfolio that must: 1. Be fully Shariah-screened (enforced by the Shariah Supervisory Board) 2. Achieve sufficient returns to cover claims and expenses 3. Maintain adequate reserves against adverse loss scenarios

The fuzzy semi-spread approach maps naturally onto this requirement structure. The left semi-spread is a conservative estimate of downside ambiguity in investment returns exactly the kind of risk measure a takaful operator should use when sizing reserves against investment shortfalls. The genetic algorithm handles the Shariah binary constraints cleanly without requiring a custom MILP solver.

What I find intellectually compelling about this approach is that it doesn't try to fit Islamic finance into a Western quantitative framework with constraints bolted on. It starts from the epistemological position that the future is genuinely uncertain in ways that probability distributions don't capture well a position that is both mathematically defensible and theologically coherent in the Islamic tradition.

That's rare in quantitative finance. Most "Islamic" financial products are conventional instruments with Shariah labels. A portfolio optimization framework built on fuzzy logic instead of probabilistic risk is, in a meaningful sense, Islamic not just in its outputs but in its foundations.


What Comes Next

The framework described here is a starting point. Obvious extensions include:

Multi-period optimization. The current framework is single-period. A multi-period fuzzy portfolio model would incorporate the evolution of fuzzy return estimates over time relevant for takaful funds with long-duration liabilities.

Zakat-adjusted returns. A truly Islamic portfolio model should account for zakat obligations on investment gains. This adjusts the effective return distribution and could be incorporated as a linear reduction in expected fuzzy return.

ESG integration. Shariah screening overlaps significantly with ESG criteria. A combined Shariah-ESG screen with fuzzy return estimates for green sukuk and Islamic REITs would be a natural next step for Malaysian institutional portfolios.

Calibration from analyst ranges. The trapezoidal fuzzy numbers need to be estimated from real data. Analyst forecast ranges (low/base/high) map directly onto (a, b, c, d) making this more practical than it might appear. Bloomberg consensus estimates already provide exactly the range structure that fuzzy returns require.

The quantitative Islamic finance literature is thinner than it should be. Papers like this one which take the epistemological constraints of Islamic finance seriously rather than treating Shariah compliance as just another constraint set are exactly the kind of work that deserves wider attention from practitioners.


This post draws on the framework from "A Genetic Algorithm Approach to Shariah-Compliant Portfolio Optimization in a Fuzzy Environment." The Python implementation is original. All financial examples are illustrative not investment advice.