Skip to content

Trade DNA Evolution

Purpose

Trade DNA is the per-player evolutionary loop that makes ARIA's trading recommendations get better with use. Every time a player executes a trade, ARIA matches it to a ARIATradingPattern (a pattern genome), updates the pattern's performance metrics, and — once the pattern has been used enough times — either mutates a failing pattern or spawns offspring from a successful one. Over time, each player's pattern population converges toward strategies that work for that player's trading style.

This is a closed, per-player evolution: no cross-player gene sharing. Patterns are isolated by player_id like all ARIA data.

Inputs

What triggers this: - Trade completion event → evolve_trading_pattern(player_id, trade_result, db). - New unmatched trade → implicit _create_trading_pattern to seed a new genome. - Ad-hoc analysis call → get_evolved_patterns for UI surfacing.

State read: - ARIATradingPattern rows for the player (player_id partition). - trade_result payload from the trading service: {commodity, action, quantity, profit, time, sector_id, station_id, pattern_id?}. - Player.aria_consciousness_level (gates which patterns are visible / used by ARIA).

Process

Pattern as DNA

Each ARIATradingPattern row is a single genome:

id            (UUID)
player_id     (UUID, partition key)
pattern_id    (string, hash of DNA — stable across saves)
pattern_type  (arbitrage | bulk_trading | high_value | speculation | general)
pattern_dna   (JSONB)
generation    (int)
parent_pattern (string, parent pattern_id or NULL)
mutations     (JSONB list)
times_used, success_rate, average_profit, best_profit, worst_loss
fitness_score, survival_probability
discovered_at, last_used, evolved_at

pattern_dna schema (target):

{
  "commodity": "EQUIPMENT",
  "action": "buy" | "sell",
  "time_preference": 14,         // 0–23 hour-of-day bias
  "quantity_range": [min, max],
  "risk_tolerance": 0.5,         // 0–1
  "sector_affinity": ["sector_id", ...],
  "minimum_margin": 0.05         // required profit margin to trigger
}

pattern_id is generated by hashing the DNA — sha256(json.dumps(pattern_dna, sort_keys=True))[:16]. Stable across reads; changes when DNA is mutated.

Pattern matching

When a new trade completes, ARIA tries to attribute it to an existing pattern:

  1. If trade_result.pattern_id is set (because ARIA suggested the trade), use it directly.
  2. Otherwise, search the player's patterns for a DNA-compatible match (same commodity + action + overlapping quantity range + matching time_preference within ±2 hours).
  3. If no match, create a new pattern via _create_trading_pattern and use that.

Performance update

pattern.times_used += 1
pattern.last_used = now

if profit > 0:
    pattern.success_rate = (success_rate * (n-1) + 1) / n
    pattern.average_profit = (avg_profit * (n-1) + profit) / n
    pattern.best_profit = max(best_profit, profit)
else:
    pattern.success_rate = (success_rate * (n-1)) / n
    pattern.worst_loss = min(worst_loss, profit)

pattern.fitness_score = calculate_fitness(pattern)

Fitness function

fitness = 0.4 * success_rate
        + 0.4 * normalize(average_profit, cap=1000)
        + 0.2 * risk_component(worst_loss)

where:
  normalize(profit, cap) = min(profit/cap, 1.0) if profit > 0 else 0
  risk_component(worst_loss) =
      0                                   if worst_loss < -1000
      (1 + worst_loss / 1000) * 1.0       otherwise (worst_loss is negative)

fitness clamped to [0, 1]

Components (target spec): - 40% success rate — does it win? - 40% average profit (normalised against a 1000-credit reference) — does it win big? - 20% risk (penalises catastrophic losses) — does it lose disastrously?

Evolution decision

After fitness update, if times_used ≥ 10:

if fitness < 0.3:
    mutate(pattern)            # failing, try variants
elif fitness > 0.7:
    spawn_offspring(pattern)   # winning, multiply variants
# else: pattern is in stable middle ground; leave it

A pattern is only evaluated for evolution after enough samples (≥10 trades) to make fitness reliable.

Mutation rules

_mutate_pattern(pattern) modifies the DNA in place:

mutated_dna = pattern.pattern_dna.copy()

if "risk_tolerance" in dna:
    dna["risk_tolerance"] *= (1 + U(-0.2, 0.2))   # ±20% adjustment

if "time_preference" in dna:
    dna["time_preference"] = (dna["time_preference"] + randint(-2, 2)) % 24

if "minimum_margin" in dna:
    dna["minimum_margin"] *= (1 + U(-0.1, 0.1))   # smaller adjustment

pattern.pattern_dna = mutated_dna
pattern.generation += 1
pattern.evolved_at = now
pattern.mutations.append({generation, timestamp, changes})

The pattern row is updated in place. The pattern_id does not rotate on mutation — same row, evolved DNA. (Target spec: rotate pattern_id on substantial mutation; track lineage via parent_pattern.)

Offspring rules

_create_pattern_offspring(player_id, parent) creates a new row:

offspring_dna = parent.pattern_dna.copy()
for key, value in offspring_dna.items():
    if isinstance(value, (int, float)):
        offspring_dna[key] *= (1 + U(-0.05, 0.05))   # ±5% variation

new_pattern = ARIATradingPattern(
    player_id = player_id,
    pattern_id = sha256(offspring_dna)[:16],
    pattern_type = parent.pattern_type,
    pattern_dna = offspring_dna,
    generation = parent.generation + 1,
    parent_pattern = parent.pattern_id,
    discovered_at = now
)

The parent persists; the offspring is a smaller variation that competes with the parent for future trades.

Selection pressure

ARIA's recommender selects which pattern to apply when suggesting a trade:

weighted_random_choice(
  patterns,
  weight = pattern.fitness_score ^ 2
)

Fitter patterns are quadratically more likely to be picked. Low-fitness patterns occasionally still get used (exploration), keeping the population diverse.

survival_probability is the long-term retention chance — patterns with survival_probability < 0.1 are pruned during a periodic GC pass (target: weekly per-player).

Convergence detection

A pattern population has converged when: - Top 3 patterns by fitness have fitness > 0.6. - The top patterns share lineage (same parent_pattern ancestor) — indicating selection has narrowed. - New trades match existing patterns ≥ 90% of the time.

On convergence, ARIA reports the player's "trading style" — a label derived from the dominant pattern_type. This surfaces in the player profile.

Outputs / state changes

Per trade completion: - One ARIATradingPattern row updated (matched pattern's metrics). - Possibly one row updated DNA (mutation). - Possibly one new row inserted (offspring or first-time pattern). - aria.patterns_evolved counter incremented in service. - pattern_evolved event emitted (target spec).

Per GC pass: - Low-survival rows deleted. - Convergence label written to Player.settings["aria_trading_style"] (target spec).

Invariants

  1. pattern.success_rate ∈ [0, 1].
  2. pattern.fitness_score ∈ [0, 1].
  3. pattern.generation ≥ 1; offspring's generation = parent.generation + 1.
  4. pattern.parent_pattern is null for genesis patterns; otherwise references an existing pattern_id for the same player.
  5. pattern_id is stable for the lifetime of a row (target). When DNA mutates substantially, a new row should be spawned instead.
  6. Pattern data is strictly per-player — no cross-player gene flow. Enforced by player_id filter on every query.
  7. pattern.times_used is monotonic non-decreasing.
  8. pattern.average_profit math invariant: average_profit * times_used == sum_of_profits (within rounding).
  9. Mutation only triggers when times_used ≥ 10 AND fitness < 0.3.
  10. Offspring only triggers when times_used ≥ 10 AND fitness > 0.7.

Failure modes

Mode Target handling
Trade event with no matching pattern and no pattern_id Create a genesis pattern via _create_trading_pattern; mark generation=1, no parent.
Two simultaneous trades for same player+pattern Race on metric update; last-write-wins is acceptable (transient — fitness recomputes on next trade).
DNA hash collision Vanishingly unlikely (sha256 prefix); guarded by per-player uniqueness constraint on (player_id, pattern_id).
Pattern row corrupted (DNA invalid JSON) Skip and log; remove on next GC.
Player pattern population grows unbounded Periodic GC prunes patterns with survival_probability < 0.1 AND times_used > 50 AND fitness < 0.3.
Negative worst_loss overflow Bounded — risk_component zero-floors at -1000.
Trade reverted post-evolution Trade reversal is atomic at the trading service level; pattern updates that fired do not roll back. Treated as accepted noise.
Pattern matched but DNA shape changed (schema migration) Migration adds defaults to old rows; matchers tolerate missing keys.
ARIA service unreachable Trade completion does not block on pattern evolution — async fire-and-forget via task queue.

Source map

Concern Path (target)
Pattern model services/gameserver/src/models/aria_personal_intelligence.py (ARIATradingPattern)
Pattern service services/gameserver/src/services/aria_personal_intelligence_service.py (evolve_trading_pattern, _mutate_pattern, _create_pattern_offspring, _calculate_pattern_fitness)
Trade event source services/gameserver/src/services/trading_service.py
Recommender (selection pressure) services/gameserver/src/services/ai_trading_service.py
Convergence labelling services/gameserver/src/services/aria_personal_intelligence_service.py (target — convergence detector)
Per-player GC services/gameserver/src/services/aria_pattern_gc.py (target — not yet split out)