Skip to content

Commit

Permalink
feat: support for tracking contract transactions
Browse files Browse the repository at this point in the history
I also renamed Transaction.creditChange to creditsChange

This also adds an accounting script (which is how I learned we were
missing this information).
  • Loading branch information
eseidel committed Oct 8, 2023
1 parent b2d32f0 commit 818d5a8
Show file tree
Hide file tree
Showing 15 changed files with 392 additions and 43 deletions.
72 changes: 72 additions & 0 deletions packages/cli/bin/accounting.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'package:cli/cli.dart';
import 'package:cli/printing.dart';
import 'package:db/transaction.dart';

String describeTransaction(Transaction t) {
return '${t.timestamp} ${t.tradeSymbol} ${t.quantity} ${t.tradeType} '
'${t.shipSymbol} ${t.waypointSymbol} ${t.creditsChange}';
}

void reconcile(List<Transaction> transactions) {
final startingCredits = transactions.first.agentCredits;
var credits = startingCredits;
// Skip the first transaction, since agentCredits already includes the
// credits change from that transaction.
for (final t in transactions.skip(1)) {
credits += t.creditsChange;
if (credits != t.agentCredits) {
logger
..warn('Credits $credits does not match '
'agentCredits ${t.agentCredits} ')
..info(describeTransaction(t));
credits = t.agentCredits;
}
}
}

Future<void> command(FileSystem fs, ArgResults argResults) async {
const lookback = Duration(minutes: 60);
final db = await defaultDatabase();
final startTime = DateTime.timestamp().subtract(lookback);
final transactions = (await transactionsAfter(db, startTime)).toList();

final lastCredits = transactions.last.agentCredits;
final firstCredits = transactions.first.agentCredits;
final creditDiff = lastCredits - firstCredits;
final diffSign = creditDiff.isNegative ? '' : '+';
logger.info(
'$diffSign${creditsString(creditDiff)} '
'over ${approximateDuration(lookback)} '
'now ${creditsString(lastCredits)}',
);
final transactionCount = transactions.length;
logger.info('$transactionCount transactions');

// Add up the credits change from all transactions.
// The diff should not include the first transaction, since agentCredits
// is the credits *after* that transaction occured.
final computedDiff =
transactions.skip(1).fold(0, (sum, t) => sum + t.creditsChange);
if (computedDiff != creditDiff) {
logger.warn(
'Computed diff $computedDiff does not match '
'actual diff $creditDiff',
);
reconcile(transactions);
}
// Print the counts by transaction type.
final counts = <AccountingType, int>{};
for (final t in transactions) {
counts[t.accounting] = (counts[t.accounting] ?? 0) + 1;
}
for (final type in AccountingType.values) {
final count = counts[type] ?? 0;
logger.info('$count $type');
}

await db.close();
}

void main(List<String> args) {
runOffline(args, command);
}
2 changes: 1 addition & 1 deletion packages/cli/bin/earning_per_ship.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TransactionSummary {

bool get isEmpty => transactions.isEmpty;

int get creditDiff => transactions.fold(0, (m, t) => m + t.creditChange);
int get creditDiff => transactions.fold(0, (m, t) => m + t.creditsChange);
Duration get duration => transactions.isEmpty
? Duration.zero
: transactions.last.timestamp.difference(transactions.first.timestamp);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/bin/list_transactions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:db/transaction.dart';

String describeTransaction(Transaction t) {
return '${t.timestamp} ${t.tradeSymbol} ${t.quantity} ${t.tradeType} '
'${t.shipSymbol} ${t.waypointSymbol} ${t.creditChange} ${t.accounting}';
'${t.shipSymbol} ${t.waypointSymbol} ${t.creditsChange} ${t.accounting}';
}

Future<void> command(FileSystem fs, ArgResults argResults) async {
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/bin/recent_deals.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ void main(List<String> args) async {

String describeTransaction(Transaction t) {
return '${t.timestamp} ${t.tradeSymbol} ${t.quantity} ${t.tradeType} '
'${t.shipSymbol} ${t.waypointSymbol} ${t.creditChange}';
'${t.shipSymbol} ${t.waypointSymbol} ${t.creditsChange}';
}

class SyntheticDeal {
Expand Down Expand Up @@ -46,19 +46,19 @@ class SyntheticDeal {
int get units => goodsBuys.fold<int>(0, (sum, t) => sum + t.quantity);

int get costOfGoodsSold =>
goodsBuys.fold<int>(0, (sum, t) => sum + t.creditChange);
goodsBuys.fold<int>(0, (sum, t) => sum + t.creditsChange);

int get revenue => transactions
.where((t) => t.tradeType == MarketTransactionTypeEnum.SELL)
.fold<int>(0, (sum, t) => sum + t.creditChange);
.fold<int>(0, (sum, t) => sum + t.creditsChange);

int get operatingExpenses => transactions
.where(
(t) =>
t.tradeType == MarketTransactionTypeEnum.PURCHASE &&
t.accounting == AccountingType.fuel,
)
.fold<int>(0, (sum, t) => sum + t.creditChange);
.fold<int>(0, (sum, t) => sum + t.creditsChange);

int get profit => revenue + costOfGoodsSold + operatingExpenses;

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/lib/behavior/jobs/buy_job.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ Future<JobResult> doBuyJob(
ship,
'Purchased ${transaction.quantity} ${transaction.tradeSymbol} '
'@ ${transaction.perUnitPrice} '
'${creditsString(transaction.creditChange)}',
'${creditsString(transaction.creditsChange)}',
);
}
jobAssert(
Expand Down
87 changes: 73 additions & 14 deletions packages/cli/lib/behavior/trader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ Future<DateTime?> _handleAtSourceWithDeal(
'Purchased ${transaction.quantity} ${transaction.tradeSymbol} '
'@ ${transaction.perUnitPrice} (expected '
'${creditsString(nextExpectedPrice)}) = '
'${creditsString(transaction.creditChange)}',
'${creditsString(transaction.creditsChange)}',
);
}
}
Expand Down Expand Up @@ -240,6 +240,7 @@ Future<DateTime?> _handleArbitrageDealAtDestination(
/// Handle contract deal at destination.
Future<DateTime?> _handleContractDealAtDestination(
Api api,
Database db,
CentralCommand centralCommand,
Caches caches,
Ship ship,
Expand All @@ -252,6 +253,8 @@ Future<DateTime?> _handleContractDealAtDestination(
final neededGood = contract!.goodNeeded(costedDeal.tradeSymbol);
final maybeResponse = await _deliverContractGoodsIfPossible(
api,
db,
caches.agent,
caches.contracts,
caches.ships,
ship,
Expand All @@ -263,24 +266,51 @@ Future<DateTime?> _handleContractDealAtDestination(
// decide next loop if we need to do more.
state.isComplete = true;

if (maybeResponse != null) {
// Update our cargo counts after fulfilling the contract.
ship.cargo = maybeResponse.cargo;
// If we've delivered enough, complete the contract.
if (maybeResponse.contract.goodNeeded(contractGood)!.amountNeeded <= 0) {
final response = await api.contracts.fulfillContract(contract.id);
final data = response!.data;
caches.agent.agent = data.agent;
caches.contracts.updateContract(data.contract);
shipInfo(ship, 'Contract complete!');
return null;
}
// If we've delivered enough, complete the contract.
if (maybeResponse != null &&
maybeResponse.contract.goodNeeded(contractGood)!.amountNeeded <= 0) {
await _completeContract(
api,
db,
caches,
ship,
maybeResponse.contract,
);
return null;
}
return null;
}

Future<void> _completeContract(
Api api,
Database db,
Caches caches,
Ship ship,
Contract contract,
) async {
final response = await api.contracts.fulfillContract(contract.id);
final data = response!.data;
caches.agent.agent = data.agent;
caches.contracts.updateContract(data.contract);

final contactTransaction = ContractTransaction.fulfillment(
contract: contract,
shipSymbol: ship.shipSymbol,
waypointSymbol: ship.waypointSymbol,
timestamp: DateTime.timestamp(),
);
final transaction = Transaction.fromContractTransaction(
contactTransaction,
caches.agent.agent.credits,
);
await db.insertTransaction(transaction);
shipInfo(ship, 'Contract complete!');
}

Future<DeliverContract200ResponseData?> _deliverContractGoodsIfPossible(
Api api,
Database db,
AgentCache agentCache,
ContractCache contractCache,
ShipCache shipCache,
Ship ship,
Expand All @@ -301,6 +331,7 @@ Future<DeliverContract200ResponseData?> _deliverContractGoodsIfPossible(
return null;
}

final unitsBefore = ship.countUnits(goods.tradeSymbolObject);
// And we have the desired cargo.
final response = await deliverContract(
api,
Expand All @@ -319,6 +350,24 @@ Future<DeliverContract200ResponseData?> _deliverContractGoodsIfPossible(
'${deliver.unitsFulfilled}/${deliver.unitsRequired}, '
'${approximateDuration(contract.timeUntilDeadline)} to deadline',
);

// Update our cargo counts after delivering the contract goods.
final unitsAfter = ship.countUnits(goods.tradeSymbolObject);
final unitsDelivered = unitsAfter - unitsBefore;

// Record the delivery transaction.
final contactTransaction = ContractTransaction.delivery(
contract: contract,
shipSymbol: ship.shipSymbol,
waypointSymbol: ship.waypointSymbol,
unitsDelivered: unitsDelivered,
timestamp: DateTime.timestamp(),
);
final transaction = Transaction.fromContractTransaction(
contactTransaction,
agentCache.agent.credits,
);
await db.insertTransaction(transaction);
return response;
}

Expand Down Expand Up @@ -388,6 +437,7 @@ Future<DateTime?> _handleAtDestinationWithDeal(
if (costedDeal.isContractDeal) {
return _handleContractDealAtDestination(
api,
db,
centralCommand,
caches,
ship,
Expand Down Expand Up @@ -500,6 +550,7 @@ String describeExpectedContractProfit(
/// Accepts contracts for us if needed.
Future<DateTime?> acceptContractsIfNeeded(
Api api,
Database db,
ContractCache contractCache,
MarketPrices marketPrices,
AgentCache agentCache,
Expand All @@ -515,7 +566,14 @@ Future<DateTime?> acceptContractsIfNeeded(
return null;
}
for (final contract in contractCache.unacceptedContracts) {
await acceptContractAndLog(api, contractCache, agentCache, contract);
await acceptContractAndLog(
api,
db,
contractCache,
agentCache,
ship,
contract,
);
}
return null;
}
Expand Down Expand Up @@ -689,6 +747,7 @@ Future<DateTime?> advanceTrader(
if (centralCommand.isContractTradingEnabled) {
await acceptContractsIfNeeded(
api,
db,
caches.contracts,
caches.marketPrices,
caches.agent,
Expand Down
26 changes: 21 additions & 5 deletions packages/cli/lib/net/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ Future<PurchaseShip201ResponseData> purchaseShipAndLog(
shipErr(ship, 'Bought ship: ${result.ship.symbol}');
final transaction = Transaction.fromShipyardTransaction(
result.transaction,
// purchaseShip updated the agentCache
agentCache.agent.credits,
ship.shipSymbol,
);
Expand Down Expand Up @@ -456,19 +457,34 @@ Future<Contract> negotiateContractAndLog(
/// Accept [contract] and log.
Future<AcceptContract200ResponseData> acceptContractAndLog(
Api api,
Database db,
ContractCache contractCache,
AgentCache agentCache,
Ship ship,
Contract contract,
) async {
final response = await api.contracts.acceptContract(contract.id);
final data = response!.data;
agentCache.agent = data.agent;
contractCache.updateContract(data.contract);
logger
..info('Accepted: ${contractDescription(contract)}.')
..info(
'received ${creditsString(contract.terms.payment.onAccepted)}',
);
shipInfo(ship, 'Accepted: ${contractDescription(contract)}.');
shipInfo(
ship,
'received ${creditsString(contract.terms.payment.onAccepted)}',
);

final contactTransaction = ContractTransaction.accept(
contract: contract,
shipSymbol: ship.shipSymbol,
waypointSymbol: ship.waypointSymbol,
timestamp: DateTime.timestamp(),
);
final transaction = Transaction.fromContractTransaction(
contactTransaction,
agentCache.agent.credits,
);
await db.insertTransaction(transaction);

return data;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/cli/lib/trading.dart
Original file line number Diff line number Diff line change
Expand Up @@ -240,23 +240,23 @@ extension CostedDealPrediction on CostedDeal {
int get actualRevenue {
return transactions
.where((t) => t.tradeType == MarketTransactionTypeEnum.SELL)
.fold(0, (a, b) => a + b.creditChange);
.fold(0, (a, b) => a + b.creditsChange);
}

/// The actual cost of goods sold.
int get actualCostOfGoodsSold {
return transactions
.where((t) => t.tradeType == MarketTransactionTypeEnum.PURCHASE)
.where((t) => t.accounting == AccountingType.goods)
.fold(0, (a, b) => a + -b.creditChange);
.fold(0, (a, b) => a + -b.creditsChange);
}

/// The actual operational expenses of the deal.
int get actualOperationalExpenses {
return transactions
.where((t) => t.tradeType == MarketTransactionTypeEnum.PURCHASE)
.where((t) => t.accounting == AccountingType.fuel)
.fold(0, (a, b) => a + -b.creditChange);
.fold(0, (a, b) => a + -b.creditsChange);
}

/// The actual profit of the deal.
Expand Down
Loading

0 comments on commit 818d5a8

Please sign in to comment.