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:
- If
trade_result.pattern_idis set (because ARIA suggested the trade), use it directly. - Otherwise, search the player's patterns for a DNA-compatible match (same commodity + action + overlapping quantity range + matching time_preference within ±2 hours).
- If no match, create a new pattern via
_create_trading_patternand 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¶
pattern.success_rate ∈ [0, 1].pattern.fitness_score ∈ [0, 1].pattern.generation ≥ 1; offspring's generation = parent.generation + 1.pattern.parent_patternis null for genesis patterns; otherwise references an existing pattern_id for the same player.pattern_idis stable for the lifetime of a row (target). When DNA mutates substantially, a new row should be spawned instead.- Pattern data is strictly per-player — no cross-player gene flow. Enforced by
player_idfilter on every query. pattern.times_usedis monotonic non-decreasing.pattern.average_profitmath invariant:average_profit * times_used == sum_of_profits(within rounding).- Mutation only triggers when
times_used ≥ 10ANDfitness < 0.3. - Offspring only triggers when
times_used ≥ 10ANDfitness > 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) |
Related¶
aria-dialogue.md— ARIA personality referencing the trading style label.market-pricing.md— what the patterns predict.../FEATURES/gameplay/aria-companion.md— player-facing description.../FEATURES/economy/trading.md— trade event source.